深入源码剖析为什么JDK动态代理仅能代理接口?

Java技术 潘老师 9个月前 (08-11) 261 ℃ (0) 扫码查看

你有没有思考过:为什么JDK动态代理仅能代理接口?这个问题已被广泛讨论,Java面试也会经常被问到,这里我们通过实际案例结合源码分析进行了深入研究。

网上解答

在网络上浏览了多种解答,总结起来大致如下:

1)JDK动态代理基于接口实现,使用Proxy创建代理对象时,需要指定一个接口列表,这些接口定义了代理对象应实现的方法,成为代理对象的类型。

2)由于Java的单继承限制,JDK动态代理已继承Proxy类,无法同时继承被代理的类,只能通过实现接口来实现代理。

这些解释在我看来都是正确的,尽管表述有些模糊。我为了更好地理解,亲自编写了示例代码,并查看生成的代理类,从中加深了对这些概念的理解。

案例说明

实际开发中,动态代理常用于为多个方法添加统一的增强逻辑,而不改动原始代码。值得强调的是这里的“多个”,因为在我看来,若只需增强一个方法,静态代理同样能够实现不改动原代码的效果。我们之所以选择动态代理,是因为它具有动态性,能够为多个方法添加增强逻辑,甚至是尚未存在的方法,将来也能适用增强逻辑。

以下是案例的大致步骤:

1)定义接口UserService,并定义了两个方法。

public interface UserService {

    /**
     * 根据用户ID获取用户姓名
     * @param id 用户id
     * @return 用户姓名
     */
    String getNameById(Long id);

    /**
     * 获取所有的用户名列表
     * @return 所有的用户名列表
     */
    List<String> getAllUserNameList();
}

2)实现接口UserServiceImpl,实现了上述两个方法。

public class UserServiceImpl implements UserService {

    @Override
    public String getNameById(Long id) {
        System.out.println("invoke getNameById return foo");
        return "foo";
    }

    @Override
    public List<String> getAllUserNameList() {
        System.out.println("invoke getAllUserNameList return list");
        return Arrays.asList("foo", "bar");
    }
}

3)编写一个实现java.lang.reflect.InvocationHandler的类,在其中为方法前后添加增强逻辑。

public class MyHandler implements InvocationHandler {

    private Object target;

    public MyHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("before");
    }

    private void after() {
        System.out.println("after");
    }
}

4)在调用处使用Proxy.newProxyInstance来创建代理类,调用UserService接口中的两个方法。

public static void main(String[] args) {
    // 保存自动生成的动态代理的类
    System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
    // 1. 创建被代理的对象,UserService接口的实现类
    UserServiceImpl userServiceImpl = new UserServiceImpl();
    // 2. 获取对应的 ClassLoader
    ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
    // 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService,
    Class[] interfaces = userServiceImpl.getClass().getInterfaces();
    // 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
    MyHandler myHandler = new MyHandler(userServiceImpl);
    UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, myHandler);
    // 调用代理类的方法
    proxy.getNameById(1L);
    proxy.getAllUserNameList();
}

源码解析

UserService和UserServiceImpl是初始接口和实现类。重点分析MyHandler和Main。

MyHandler

MyHandler是java.lang.reflect.InvocationHandler的实现。在其接口中,关键是invoke方法。在invoke方法中,我们使用method.invoke(target, args)来调用原始接口。我们可以在其前后执行自定义增强操作,如日志记录、参数分支逻辑、耗时计算等。

Main

这是调用部分,重要的代码是创建代理对象:UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, myHandler);。我们创建了代理对象,用一个原始接口UserService的引用proxy来接收。随后,调用proxy的方法实际上是调用了代理对象的方法,从而进入增强逻辑。

上述内容是对动态代理使用代码的解释。但要回答问题,关键在于动态生成的代理类源代码。在Main中,我们通过System.getProperties().put(“jdk.proxy.ProxyGenerator.saveGeneratedFiles”, “true”);来保存生成的代理类文件。让我们关注生成的文件,它位于原工程的jdk/proxy1目录下。如下图所示:

$Proxy0类的源码

public final class $Proxy0 extends Proxy implements UserService {
    private static final Method m0;
    private static final Method m1;
    private static final Method m2;
    private static final Method m3;
    private static final Method m4;

    public $Proxy0(InvocationHandler var1) {
        super(var1);
    }

    public final int hashCode() {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final boolean equals(Object var1) {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String getNameById(Long var1) {
        try {
            return (String)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final List getAllUserNameList() {
        try {
            return (List)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("cn.pdf.UserService").getMethod("getNameById", Class.forName("java.lang.Long"));
            m4 = Class.forName("cn.pdf.UserService").getMethod("getAllUserNameList");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }

    private static MethodHandles.Lookup proxyClassLookup(MethodHandles.Lookup var0) throws IllegalAccessException {
        if (var0.lookupClass() == Proxy.class && var0.hasFullPrivilegeAccess()) {
            return MethodHandles.lookup();
        } else {
            throw new IllegalAccessException(var0.toString());
        }
    }
}

可以观察到,在这个类中,最初定义了5个final的Method对象m0~m4,它们分别表示原始类的5个方法。其中,hashCode、equals和toString方法是从Object类继承而来的,而getNameById和getAllUserNameList是我们自己定义的方法。

在这个类的静态代码块中,对这5个Method对象进行了赋值,以供这个类的5个方法分别使用。这个类的5个方法的核心是调用了super.h.invoke方法。因为这个$Proxy0类是继承自java.lang.reflect.Proxy类,所以这里的super.h自然就是java.lang.reflect.Proxy类的h属性。如果有需要,我们可以打开这个类并查看其内容。 结合上图,可以看出在上述的代码中,h是一个InvocationHandler类型的对象。这个值是在Proxy的构造方法中传入的,实际上就是在调用Proxy.newProxyInstance时传入的myHandler对象。您可以参考源码来进一步了解相关细节。

综合解答

通过之前的示例和源码解读,我们对JDK动态代理的过程有了初步的了解,包括使用的方法、自动生成的源码以及Proxy.newProxyInstance方法的调用等。现在我们回到最初的问题:为什么JDK动态代理只能代理接口?

我认为可以从以下几个方面来解答这个问题:

  1. 动态代理的目的是对多个方法进行增强,在使用代理对象时,我们需要能够获取到代理对象并使用原始对象的类型来接收。如果我们使用UserService类型的对象proxy来接收代理对象,这样我们才能调用原始的方法。否则,如果我们无法使用原始对象接收代理对象,那我们必须使用代理对象的类型来接收,但是事先我们并不知道自动生成的代理对象的具体类型。如果我们事先知道代理对象的类型,那么就变成了静态代理的情况。
  2. 如果要使用原始对象的类型来接收代理对象,Java中有两种方法:继承原始对象成为子类,或者实现原始对象的接口。JDK动态代理自动生成的类$Proxy0继承了java.lang.reflect.Proxy类,由于Java的单继承特性,这里没有机会再去继承被代理类。
  3. 因此,只剩下第二种方案,即实现原始对象的接口。由于要实现原始对象的接口,所以被代理的对象只能是接口,而不能是类。
  4. 我们可以观察到,关键点在于JDK动态代理自动生成的代理类继承自java.lang.reflect.Proxy类。如果不是直接继承java.lang.reflect.Proxy类,而是设计一个类似java.lang.reflect.Proxy类的接口,然后实现这个接口,就可以实现对被代理类的继承。仔细查看java.lang.reflect.Proxy类的内容,可以发现设计成java.lang.reflect.Proxy接口也没有什么问题,特别是在Java支持接口中默认方法实现之后,这样的设计也并不困难。

综上所述,JDK动态代理自动生成的类继承自java.lang.reflect.Proxy类可能只是出于历史原因,当时的技术方案设计就是这样。而且我们通常倡导面向接口编程,在大多数情况下,动态代理只支持接口并没有问题。然而,总有例外情况,正因为对类的动态代理的需求,才出现了像cglib这样的库,它可以支持对类的动态代理,并且获得了广泛的应用。

 


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/java/7914.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】