代理设计模式

代理设计模式:在不改变原有对象的前提下,为该对象扩展一些功能,比如在对象的某个方法执行前后增加一些自定义的操作。
为了实现这一目标,我们通常要求代理类中也要有目标类中的方法,要想有目标类中的方法有两种方案

  • 接口方案:代理类实现跟目标类相同的接口
  • 继承方案:目标类没有实现接口,代理类继承目标类

后面我们对这两种方案进行代码实现。

代理的实现方案

静态代理(基本不使用,但对于理解动态代理很有帮助)

开发人员需要手动编写代理类(.java文件),基本上一个目标类就需要手动编写一个代理类,非常不灵活,比如接口如果新增方法,目标类和代理类都需要改动。

静态代理实现步骤:

  1. 定义一个接口及其实现类;
  2. 创建一个代理类同样实现这个接口
  3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

接口方案示例代码:

新建发送短信接口

  1. package com.lff.service;
  2. public interface SmsService {
  3. String send(String message);
  4. }

实现发送短信接口

  1. package com.lff.service;
  2. public class SmsServiceImpl implements SmsService{
  3. @Override
  4. public String send(String message) {
  5. System.out.println(message);
  6. return null;
  7. }
  8. }

新建代理类,代理类要拥有一个实现类作为属性

  1. package com.lff.service;
  2. public class SmsProxy implements SmsService{
  3. private SmsService smsService;
  4. public void setSmsService(SmsService smsService) {
  5. this.smsService = smsService;
  6. }
  7. public SmsProxy(){
  8. }
  9. @Override
  10. public String send(String message) {
  11. System.out.println("执行send方法----before操作!");
  12. smsService.send(message);
  13. System.out.println("执行send方法----after操作!");
  14. return null;
  15. }
  16. }

我们这里使用Spring进行创建对象依赖注入,Spring配置文件

  1. <bean id="smsProxy" class="com.lff.service.SmsProxy">
  2. <property name="smsService">
  3. <bean class="com.lff.service.SmsServiceImpl" />
  4. </property>
  5. </bean>

外界使用

  1. ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  2. SmsService proxy = (SmsService) context.getBean("smsProxy");
  3. proxy.send("娃哈哈");

这里我们并没有直接依赖SmsProxy,从context.getBean(“smsProxy”);获取的仍然是接口类型,也就是外界不知道SmsProxy代理类的存在。如果以后不想使用代理类,只需改配置文件,外界使用方式不需要改变、实现类也不需要改变,比如配置做如下配置,就可以去掉代理的功能。

  1. <bean id="smsProxy" class="com.lff.service.SmsServiceImpl"></bean>

这样就把代理类的功能去掉了,当然我们还是使用代理类。

打印日志

  1. 执行send方法----before操作!
  2. 娃哈哈
  3. 执行send方法----after操作!

我们已经实现该功能,在执行接口的实现类的send方法时,前后执行了一段代码。

继承方案示例代码

有时我们想给某一个类的方法增加额外的功能时,这个类可能没有实现接口。这时我们可以采用继承的方案。
发送短信类

  1. package com.lff.send;
  2. public class SmsService {
  3. public String send(String message) {
  4. System.out.println(message);
  5. return null;
  6. }
  7. }

代理类,此时代理类继承上面的类

  1. package com.lff.send;
  2. public class SmsProxy extends SmsService {
  3. public SmsProxy(){
  4. }
  5. @Override
  6. public String send(String message) {
  7. System.out.println("执行send方法----before操作!");
  8. super.send(message);
  9. System.out.println("执行send方法----after操作!");
  10. return null;
  11. }
  12. }

此时的代理类不需要拥有一个目标类作为属性。因为可以直接调用父类的方法。
XML配置

  1. <bean id="smsProxy" class="com.lff.send.SmsProxy" />

外界使用

  1. ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  2. SmsService proxy = (SmsService) context.getBean("smsProxy");
  3. proxy.send("娃哈哈");

打印日志

  1. 执行send方法----before操作!
  2. 娃哈哈
  3. 执行send方法----after操作!

这样也实现了对目标类中的方式增加额外功能的目的。


动态代理

由于上面的静态代理类实现起来比较麻烦,需要程序员手动的编写代理类,基本上有一个目标类,就需要编写一个代理类。为了解决这一问题,我们衍生出了动态代理技术,能够在程序运行中动态的生成字节码文件。
和上面静态代理类似,动态代理也有两种实现方案

  • 接口方案:代理类实现跟目标类相同的接口(使用JDK自带的方案)
  • 继承方案:目标类没有实现接口,代理类继承目标类(使用开源项目CGLib实现,Spring已经集成了CGLib)

动态代理接口方案(JDK自带)

在 Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。
Proxy 类中使用频率最高的方法是:newProxyInstance() ,这个方法主要用来生成一个代理对象。

  1. public static Object newProxyInstance(ClassLoader loader,
  2. Class<?>[] interfaces,
  3. InvocationHandler h)
  4. throws IllegalArgumentException
  5. {
  6. ......
  7. }

这个方法一共有 3 个参数:
loader :类加载器,用于加载代理对象。
interfaces : 被代理类实现的一些接口。
h : 实现了 InvocationHandler 接口的对象。
要实现动态代理的话,还必须需要实现InvocationHandler 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。

public interface InvocationHandler {

  1. /**
  2. * 当你使用代理对象调用方法的时候实际会调用到这个方法
  3. */
  4. public Object invoke(Object proxy, Method method, Object[] args)
  5. throws Throwable;

}
invoke() 方法有下面三个参数:
proxy :动态生成的代理类
method : 与代理类对象调用的方法相对应
args : 当前 method 方法的参数
也就是说:你通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

JDK 动态代理类使用步骤

  1. 定义一个接口及其实现类;
  2. 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  3. 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

示例代码
发送短信接口

  1. public interface SmsService {
  2. String send(String message);
  3. }

实现类

  1. public class SmsServiceImpl implements SmsService{
  2. @Override
  3. public String send(String message) {
  4. System.out.println(message);
  5. return null;
  6. }
  7. }

Spring XML配置,下面配置会创建上面的实现类对象

  1. <bean id="smsService" class="com.lff.service.SmsServiceImpl" />

外界使用

  1. ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  2. //拿到目标类对象
  3. SmsService target = (SmsService) context.getBean("smsService");
  4. //使用反射API创建代理对象
  5. SmsService proxyTarget = (SmsService) Proxy.newProxyInstance(getClass().getClassLoader(),
  6. target.getClass().getInterfaces(),
  7. new InvocationHandler() {//当调用目标方法时会来到这里
  8. @Override
  9. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  10. //proxy:代理对象
  11. //method:目标方法
  12. //args:目标参数
  13. System.out.println("执行send方法----before操作!");
  14. //调用目标对象的目标方法,也就是业务代码(发送短信代码)
  15. Object object = method.invoke(target,args);
  16. System.out.println("执行send方法----after操作!");
  17. return object;
  18. }
  19. }
  20. );
  21. //调用目标方法
  22. proxyTarget.send("娃哈哈");

打印日志

  1. 执行send方法----before操作!
  2. 娃哈哈
  3. 执行send方法----after操作!

这样也实现了对目标方法增加额外的功能。

动态代理继承方案

JDK自带的动态代理只能处理目标类实现了接口的场景。如果需要代理的目标类没有实现接口,就需要使用第三方库,最常用的就是CGLIB开源库。
CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

CGLIB动态代理使用步骤

  1. 定义一个类;
  2. 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
  3. 通过 Enhancer 类的 create()创建代理类;

Maven依赖,Spring已经集成依赖啦,这一步不需要

  1. <dependency>
  2. <groupId>cglib</groupId>
  3. <artifactId>cglib</artifactId>
  4. <version>3.3.0</version>
  5. </dependency>

发送短信类

  1. public class SmsService {
  2. public String send(String message) {
  3. System.out.println(message);
  4. return null;
  5. }
  6. }

自定义拦截器

  1. /**
  2. * 自定义MethodInterceptor
  3. */
  4. public class logMethodInterceptor implements MethodInterceptor {
  5. /**
  6. * @param o 代理对象(增强的对象)
  7. * @param method 被拦截的方法(需要增强的方法)
  8. * @param args 方法入参
  9. * @param methodProxy 用于调用原始方法
  10. */
  11. @Override
  12. public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
  13. //调用方法之前,我们可以添加自己的操作
  14. System.out.println("before method " + method.getName());
  15. Object object = methodProxy.invokeSuper(o, args);
  16. //调用方法之后,我们同样可以添加自己的操作
  17. System.out.println("after method " + method.getName());
  18. return object;
  19. }
  20. }

采用工厂模式创建代理类

  1. package com.lff.send;
  2. import org.springframework.cglib.proxy.Enhancer;
  3. public class CglibProxyFactory {
  4. public static Object getProxy(Class<?> clazz) {
  5. // 创建动态代理增强类
  6. Enhancer enhancer = new Enhancer();
  7. // 设置类加载器
  8. enhancer.setClassLoader(clazz.getClassLoader());
  9. // 设置被代理类
  10. enhancer.setSuperclass(clazz);
  11. // 设置方法拦截器
  12. enhancer.setCallback(new logMethodInterceptor());
  13. // 创建代理类
  14. return enhancer.create();
  15. }
  16. }

Spring XML配置

  1. <bean id="smsService" class="com.lff.send.SmsService" />

外界使用

  1. ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  2. //拿到目标类对象
  3. SmsService target = (SmsService) context.getBean("smsService");
  4. //获取代理对象
  5. SmsService smsServiceProxy = (SmsService) CglibProxyFactory.getProxy(target.getClass());
  6. //调用目标方法
  7. smsServiceProxy.send("娃哈哈");

打印日志

  1. before method send
  2. 娃哈哈
  3. after method send

JDK 动态代理和 CGLIB 动态代理对比

JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。Spring AOP 中会先判断目标类是否实现了接口,如果实现了接口就采用JDK方案,如果没有实现就采用CGLIB方案。