Java学习之旅第三季-14:代理
在 Java 中,代理(Proxy)是一种设计模式,它通过创建一个代理对象来间接访问目标对象,从而在不修改目标对象代码的前提下,为其增加额外功能(如日志记录、权限校验、事务控制、性能监控等)。Java 中主要有两种代理方式:静态代理和动态代理。
14.1 静态代理
静态代理是指在编译期就已经确定代理类和目标类的关系,代理类是手动编写的,且与目标类实现相同的接口。
实现步骤如下:
- 定义一个接口(共同接口),声明目标类和代理类的共同方法。
- 实现目标类(被代理的类),重写接口方法。
- 实现代理类,同样实现接口,并在内部持有目标类的引用,在代理方法中调用目标方法,并添加额外逻辑。
先声明一个接口:
public interface MyInterface {int method(int num);
}
实现目标类:
public class MyTarget implements MyInterface {@Overridepublic int method(int num) {System.out.println("目标方法执行...");return num * num;}
}
实现代理类,代理类针对目标类中的方法增强了功能,主要是记录了参数及结果作为日志:
public class MyStaticProxy implements MyInterface {private MyTarget myTarget;public MyStaticProxy(MyTarget myTarget) {this.myTarget = myTarget;}@Overridepublic int method(int num) {System.out.println("传入的参数值:" + num);int result = myTarget.method(num);System.out.println("执行完成后的结果:" + result);return result;}
}
它们的关系如下:
使用代理类,此时需要将目标类的实例传入代理类中,本案例使用的是构造方法,当然也可以使用setter方法:
MyTarget myTarget = new MyTarget();
MyInterface myProxy = new MyStaticProxy(myTarget);
int result = myProxy.method(10);
System.out.println("最终结果:" + result);
运行之后,在目标类的方法执行前后都会相应的日志输出:
传入的参数值:10
目标方法执行...
执行完成后的结果:100
最终结果:100
静态代理的优点:
- 简单直观,易于理解和实现
静态代理的缺点:
- 代理类与目标类必须实现同一接口,代码冗余
- 若接口新增方法,代理类和目标类都需修改,维护成本高
- 每新增一个目标类,则需要对应实现一个代理类
14.2 动态代理
动态代理是指在运行时动态生成代理类,无需手动编写代理类代码,灵活性更高。Java 中主要通过 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口实现动态代理。一般将这种实现称为 JDK 动态代理;与之对应的还有第三方的动态代理实现,如:cglib(不支持新版本Java),Byte Buddy。
JDK中的代理有两个不同的场景:
- 只有接口,没有需要代理的目标类,可以"无中生有"提供功能,典型例子是MyBatis框架
- 有接口且代理的目标类实现了该接口,可以增强目标类的功能,典型例子是Spring声明式事务
不管哪种场景,要想产生动态代理,就需要使用Proxy的静态方法newProxyInstance,该方法的声明如下:
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h)
参数说明:
-
ClassLoader loader:用于定义代理类的类加载器,一般使用当前类的类加载
-
Class<?>[] interfaces:代理类要实现的接口列表
-
InvocationHandler h:用于将方法调用分发给的调用处理程序,这是一个接口,需要开发人员自行实现。其声明如下,它是一个函数式接口且带有一个静态方法:
public interface InvocationHandler{Object invoke(Object proxy, Method method, Object[] args) throws Throwable;static Object invokeDefault(Object proxy, Method method, Object... args) throws Throwable{} }
在调用代理对象的每个方法时,会调用其声明的方法invoke,该方法通常用于实现自定义逻辑。该方法的参数说明如下:
-
Object proxy:产生的代理对象
-
Method method:调用的方法
-
Object[] args:调用方法的参数
而 invoke方法返回的结果即为代理对象方法返回的接口。
-
场景一:只有接口,没有需要代理的目标类
还是以刚才声明的接口MyInterface为例,我声明一个方法用于获取MyInterface接口的代理对象:
public MyInterface getProxyOfMyInterface() {MyInterface proxy = (MyInterface) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{MyInterface.class}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) {// 自行实现逻辑if (args[0] instanceof Integer num) {return num * num *5;}return 0;}});return proxy;
}
上面的方法中最重要的就是invoke方法的实现,在其中并没有目标对象的存在,至于怎么实现接口中的方法完全可以有开发人员根据功能需求自由发挥。比如,在自己实现的逻辑是得到整型参数平方的5倍。
测试效果如下:
MyDynamicProxy myDynamicProxy = new MyDynamicProxy();
MyInterface proxy = myDynamicProxy.getProxyOfMyInterface();
System.out.println(proxy.method(10)); // 输出500
产生的代理对象如接口及Proxy的关系如下:
如果要代理的接口实现的逻辑是一样的,也可以泛型方法实现获取代理对象:
public <T> T getProxyWithGeneric(Class<T> c) {T proxy = (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{c}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) {if (args[0] instanceof Integer num) {return num * num * 5;}return 0;}});return proxy;
}
测试如下:
MyDynamicProxy myDynamicProxy = new MyDynamicProxy();
MyInterface proxy = myDynamicProxy.getProxyWithGeneric(MyInterface.class);
System.out.println(proxy.method(10));
场景二:有接口且代理的目标类实现了该接口
还是以刚才声明的接口MyInterface 为例,现在我声明一个实现了该接口的类MyTarget为例:
public class MyTarget implements MyInterface {@Overridepublic int method(int num) {System.out.println("目标方法执行...");return num * num;}
}
由于此时有目标类,就可以将其实例设置到类的属性中,然后声明方法产生其代理,下面是具体实现:
public class MyDynamicProxy {private MyInterface target;public MyDynamicProxy() {}public MyDynamicProxy(MyInterface target) {this.target = target;}public MyInterface getProxy() {return (MyInterface) Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Exception {System.out.println("前置处理");Object result = method.invoke(target, args); //调用目标对象的方法System.out.println("后置处理");return result;}});}
}
调用此方法:
MyDynamicProxy myDynamicProxy = new MyDynamicProxy(new MyTarget()); // 目标对象传入
MyInterface proxy = myDynamicProxy.getProxy();
System.out.println(proxy.method(10));
运行之后的结果:
前置处理
目标方法执行...
后置处理
100
可以看到在执行代理对象的方法时,会对目标对象的方法进行增强,本案例仅仅是在目标方法执行前后增加了信息的输出,实际开发中,可以有日志记录、权限校验、事务控制、性能监控等应用。
下图表明了代理类、目标类、接口以及Proxy类的关系:
从动态代理的两种情况来看,JDK动态代理中产生的代理类只是接口的实现,与目标类没有父子关系,所以在接收代理对象时只能使用接口,而不能使用目标类。
JDk中动态代理的核心原理总结如下:
Proxy.newProxyInstance() 在运行时生成一个实现了目标类所有接口的代理类字节码,并加载到 JVM 中。
代理对象调用任何方法时,都会转发到 InvocationHandler 的 invoke 方法,由其统一处理增强逻辑或目标方法调用。
动态代理的优点:
- 无需手动编写代理类,减少代码冗余。
- 一个代理处理器可代理多个目标类(只要实现接口),扩展性强。
动态代理的缺点:
- JDK 动态代理必须基于接口或实现了接口的类,否则无法使用 。
14.3 小结
Java代理模式分为静态代理和动态代理。静态代理需要手动编写代理类,实现与目标类相同的接口,在代理方法中调用目标方法并添加额外逻辑,优点是简单直观,缺点是代码冗余且维护成本高。动态代理通过Proxy类和InvocationHandler接口在运行时生成代理类,无需手动编写代理代码,支持两种场景:无目标类时自行实现接口方法逻辑;有目标类时增强其功能。动态代理灵活性更高,典型应用包括MyBatis和Spring声明式事务。