为什么JDK动态代理只能代理接口

近期在准备找一些新的工作机会,在网上看了一些面试常见问题,看看自己是否能比较好的回答。今天的这个问题:为什么JDK动态代理只能代理接口。这个问题看到次数挺多的,所以自己也深入研究了一下。

一些解答

在网上看了很多人的解答,大致有这么一些:

  • 是JDK动态代理是基于接口实现的,当你使用Proxy类创建代理对象时,你需要指定一个接口列表来表示代理对象所应该实现的接口,这些接口就成为代理对象的类型
  • java是单继承的,JDK动态代理继承了Proxy类,无法同时继承被代理类,只能去实现被代理接口

其实这些答案我理解都是对的,但是都说的比较模糊,没有完全说透,自己使用idea写了例子,看了下生成的代理类,加深一些理解。

使用例子

在实际的开发中,使用动态代理,目的大多是为了对多个方法添加统一的增强逻辑,且不对原始代码做入侵。这里强调了多个,因为在我理解,如果只是为了对一个方法做增强,使用静态代理也可以做到不对原始代码做入侵,实现同样的效果。我们使用动态代理,就是为了这里的动态,对多个方法做统一增强,甚至是暂时还没有的方法,未来添加进来,也可以走到增强逻辑。

这里的使用例子,大致分成这么几个步骤:

  1. 定义接口 UserService,并定义2个方法
  2. 定义接口实现 UserServiceImpl,并实现上述2个方法
  3. 定义java.lang.reflect.InvocationHandler的一个实现类,在这个实现类中,对方法前后进行增强
  4. 在调用处使用Proxy.newProxyInstance来创建代理类,调用UserService接口中的2个方法

下面是这些实际的代码:

UserService
java 复制代码
package cn.pdf;

import java.util.List;

public interface UserService {

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

    /**
     * 获取所有的用户名列表
     * @return 所有的用户名列表
     */
    List<String> getAllUserNameList();
}
UserServiceImpl
java 复制代码
package cn.pdf;

import java.util.Arrays;
import java.util.List;

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");
    }
}
MyHandler
java 复制代码
package cn.pdf;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

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");
    }
}
Main
scss 复制代码
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的实现类,这个InvocationHandler的接口中,重点就是这个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类的源码
java 复制代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package jdk.proxy1;

import cn.pdf.UserService;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;

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个方法,其中hashCodeequalstoString应该是原始类从Object继承过来的,getNameByIdgetAllUserNameList是我们自己定义的方法。在这个类的静态代码块中,对着5个Method对象进行了赋值,供这个类中的5个方法分别使用。这个类的5个方法,核心是调用了super.h.invoke方法。因为这个$Proxy0类是继承自java.lang.reflect.Proxy类,这里的super.h自然也就是java.lang.reflect.Proxy类的h属性,我们可以打开这个类看下。

从上面2个图片中,可以看到,这个h是一个InvocationHandler类型的对象,这个值是在Proxy的构造方法中传入的,其实就是在我们调用Proxy.newProxyInstance的时候传入的myHandler对象,如下图中的源码所示

我的解答

通过上面的示例和源码解读,我们已经大致明白了JDK动态代理的过程,包括使用的方法、自动生成的源码、Proxy.newProxyInstance方法的调用等。我们回到今天开始的问题:为什么JDK动态代理只能代理接口?

我理解可以从这些方面来解答:

  1. 动态代理是为了做多个方法的增强,而我们在使用的地方,必须可以获取到代理对象,且可以使用被代理对象的类型来接收,就如果上面我们使用UserService类型的对象proxy来接收这个代理对象,这样我们才能调用原始的方法,否则,我们无法用原始对象接收的话,我们必须使用这个代理对象的类型来接收,但是我们事先是不知道这个自动生成的代理对象的类型的,如果事先知道的话,这里其实就退化成了静态代理。
  2. 既然要使用原始的类型来接收,那么在java中有2种方法:继承原始对象成为子类,或者 实现原始对象接口。
  3. JDK动态 代理自动生成对的类$Proxy0继承了java.lang.reflect.Proxy类,由于java是单继承的,所以这里没有机会再去继承被代理类。
  4. 所以,这里只剩下第2种方案,实现原始对象的接口。既然要实现原始对象的接口,那么原始的被代理的,只能是接口,不可以是类。

我们可以看出来,关键点在于,JDK动态代理自动生成的代理类在设计的时候,继承了java.lang.reflect.Proxy类,如果这里不是直接继承java.lang.reflect.Proxy类,而是设计了一个类似java.lang.reflect.Proxy类的接口,然后去实现这个接口,那就可以做到继承被代理的类。仔细查看了java.lang.reflect.Proxy类的内容,好像可以设计成java.lang.reflect.Proxy接口也没什么问题,特别是java支持接口中方法的default实现之后,这里做成接口也没什么特别的困难。

所以,这里JDK动态代理自动生成的类继承自java.lang.reflect.Proxy类,可能只是历史原因,当时技术方案设计就是这样,而且我们一般提倡面向接口编程,大多数情况下,动态代理只支持接口并没有什么问题,不过凡事总有例外,也许正式因为对类的动态代理的需求,cglib这种可以支持对类动态代理的库,才获得的了比较广泛的应用。

感谢大家耐心读完,对JDK动态代理自动生成的类为什么设计成继承自java.lang.reflect.Proxy类,而不是实现xxxProxy的接口,请在评论区说出你的理解,大家共同讨论提高。

相关推荐
mazhimazhi几秒前
GC垃圾收集时,居然还有用户线程在奔跑
后端·面试
Python私教1 分钟前
基于 Requests 与 Ollama 的本地大模型交互全栈实践指南
后端
ypf52083 分钟前
Tortoise_orm与Aerich 迁移
后端
Victor3563 分钟前
Dubbo(78)Dubbo的集群容错机制是如何实现的?
后端
lizhongxuan4 分钟前
PgBackRest备份原理详解
后端
真是他4 分钟前
多继承出现的菱形继承问题
后端
Java技术小馆4 分钟前
SpringBoot中暗藏的设计模式
java·面试·架构
xiguolangzi5 分钟前
《springBoot3 中使用redis》
java
李菠菜8 分钟前
POST请求的三种编码及SpringBoot处理详解
spring boot·后端
李菠菜9 分钟前
浅谈Maven依赖传递中的optional和provided
后端·maven