什么是代理模式
在介绍动态代理前,我们需要先搞清楚什么是代理模式。
代理模式是一种很常见的设计模式,简而言之就是通过创建一个代理对象,让调用方来访问代理对象,从而实现对真实对象的保护或增强。Spring AOP、Mockito测试框架生成的mock或spy对象都是代理模式的例子。
其实,代理并不是一个编程中才有的概念,生活中委托中介帮忙租房子,或者找律师打官司,都可以看作代理模式的应用。
下面是一个简单的代理模式的例子:
- 首先定义一个UserService接口
public interface UserService {
void createUser(String name);
}
- 定义一个真实对象类UserServiceImpl,实现UserService接口,在真实对象类中可以实现主要的业务逻辑
public class UserServiceImpl implements UserService{
@Override
public void createUser(String name) {
System.out.println("用户" + name + "已创建");
}
}
- 定义一个代理对象类UserServiceProxy,同样实现UserService接口,关键点在于它内部持有一个真实对象的引用,因此我们可以在其中调用真实对象的方法,但在调用前后可以增加一些附加逻辑
public class UserServiceProxy implements UserService{
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void createUser(String name) {
System.out.println("这里可以写调用目标方法前的逻辑");
target.createUser(name);
System.out.println("这里可以写调用目标方法后的逻辑");
}
}
这样我们就完成了代理模式的编写,下面可以写个测试类看看效果
public class UserServiceTest {
public static void main(String[] args) {
// 这里的proxy是代理对象,我们调用它的构造方法注入了一个UserServiceImpl对象,这才是真正的对象
UserService proxy = new UserServiceProxy(new UserServiceImpl());
proxy.createUser("Frank");
}
}
输出结果为
静态代理与动态代理
这种直接编写代码实现代理模式的写法也被称为静态代理。从上面的例子可以看出静态代理实现起来很简单,但存在灵活性不足的问题,即事先必须确定好代理方法要应用在哪些接口上面。
为了弥补这一不足,动态代理技术应运而生。
与静态代理需要先写代码然后编译成字节码运行的方式不同,动态代理在运行时生成字节码,这就可以让我们随心所欲地做更多事情。
JDK动态代理
JDK动态代理是Java原生的一种实现动态代理的方式,不过被代理的类需要有实现的接口。
假设我们还是想代理UserService接口,JDK动态代理的实现方式如下:
- 编写一个InvocationHandler的实现类,在invoke方法中实现代理逻辑
public class MyInvocationHandler implements InvocationHandler {
private UserService target;
public MyInvocationHandler(UserService target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 判断被调用的方法是不是createUser,如果不是不进行增强
if (method.getName().equals("createUser")) {
System.out.println("JDK动态代理执行中");
}
// 执行原方法
return method.invoke(target, args);
}
}
invoke方法接收三个参数,分别是代理对象,被调用的方法和调用参数,这里需要注意第一个参数是生成的代理对象,并不是要被代理的真实对象,所以在method.invoke方法中需要传入真实对象,否则会报错InvocationTargetException
- 使用Proxy.newProxyInstance方法动态创建一个代理对象,并在代理对象上调用createUser方法
public class UserServiceTest {
public static void main(String[] args) {
UserService userServiceProxy = (UserService) Proxy.newProxyInstance(UserService.class.getClassLoader(),
new Class[]{UserService.class},
new MyInvocationHandler(new UserServiceImpl()));
userServiceProxy.createUser("Frank");
}
}
运行结果如下:
Proxy.newProxyInstance方法也是接收三个参数,分别是类加载器,代理对象需要实现的接口(可传递多个),一个实现了InvocationHandler接口的对象。
该方法会返回一个代理对象,这个对象继承了java.lang.reflect.Proxy,并且实现了第二个参数中传入的接口。它还持有一个InvocationHandler对象的引用,每当调用它实现的接口的方法以及继承自Object类的hashCode, equals和toString方法时,都会转交为调用它持有的InvocationHandler对象的invoke方法,并传入上面介绍的invoke方法的三个参数。
另外需要注意三点:
- 生成的代理对象并没有继承被代理的真实类,因此如果强制转型会抛出ClassCastException
- 很多人说被代理的类一定要实现接口,其实这句话并不完全正确。因为Proxy.newProxyInstance即使第二个参数传入一个空数组也能正常返回代理对象,所以被代理的对象并不需要实现接口。不过此时会被代理的方法只有上面提到的继承自Object类的三个方法,那么为什么不直接采用Override的方式呢?所以我认为这句话从技术上来说是错的,但如果真的要代理一个没实现任何接口的类也没有意义。
- 因为JDK动态代理的类是在运行时生成的,所以默认不会保存字节码文件到文件系统中,如果希望将其保存下来可以执行这段代码来实现
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
关于cglib动态代理的内容,可以参考本人另一篇文章Java动态代理之cglib动态代理