为什么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的接口,请在评论区说出你的理解,大家共同讨论提高。

相关推荐
monkey_meng11 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss19 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己19 分钟前
UE5运行时创建slate窗口
java·服务器·ue5
2401_8532757340 分钟前
ArrayList 源码分析
java·开发语言
爪哇学长44 分钟前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
大鲤余1 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
她说彩礼65万1 小时前
Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
后端·asp.net·mvc
MoFe11 小时前
【.net core】【sqlsugar】字符串拼接+内容去重
java·开发语言·.netcore
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
_江南一点雨1 小时前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端