【设计模式】结构型模式其七: 代理模式(下) -> (动态代理)

59 阅读7分钟

动态代理

前一篇文章我们代理模式全都是静态的,为什么这么说?

静态代理的特点:

  1. 我们的接口方法的实现类是提前写好的
  2. 编译器将其加载为字节码编译期生成),然后jvm加载后才能使用
  3. 客户端调用时调用代理类,而不是原始接口。

相对而言,动态代理的特点是:

  • 运行期创建(编译)代理类
  • 无需为每个接口手动创建代理类
  • 通过反射实现方法调用

动态代理概述

  • 动态代理(Dynamic Proxy)可以让系统在运行时根据实际需要来动态创建代理类,让同一个代理类能够代理多个不同的真实主题类而且可以代理不同的方法
  • Java语言提供了对动态代理的支持,Java语言实现动态代理时需要用到位于java.lang.reflect包中的一些类

Proxy类

public static Object newProxyInstance(ClassLoader loader, Class <?>[] interfaces, InvocationHandler h):该方法用于返回一个动态创建的代理类的实例,

方法中第一个参数loader表示代理类的类加载器,这里可以看出它是会使用到反射的,拿到接口的classLoader便于读取传入接口的具体结构生成具体的代理类

第二个参数interfaces表示代理类所实现的接口列表(与真实主题类的接口列表一致),

第三个参数h表示所指派的调用处理程序类.

classLoader

类加载器(ClassLoader)是一个用来负责加载类定义的系统组件。

首先我们通过反射获得类的类加载器,动态代理中我们的类加载器该传什么呢?

  1. 最常见的是要实现的接口所在类的 ClassLoader 下面是原因:

    • 接口和它的实现类一般位于同一个 ClassLoader 下(第三方库使用时不一定哦)
  2. 代理对象所在类的 ClassLoader

我可以在Proxy.newInstance的加载器的当前类中新建一个对象,然后使用它的类加载器

// 这个MyClass与动态代理类位于同一个类加载器下
ClassLoader classLoader = MyClass.class.getClassLoader();

Object proxy = Proxy.newProxyInstance(
     classLoader,  
     ...
);  

3. 当前线程的上下文类加载器(context class loader)

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

Object proxy = Proxy.newProxyInstance(
     classLoader,  
     ... 
);

线程的上下文类加载器之所以能作为 classLoader 参数使用, 是因为它可以加载我们指定的接口类。(线程的上下文类加载器可以加载当前线程需要使用的所有类。)

最重要的是ClassLoader 能正确加载:

  • 传入的接口类
  • 以及动态生成的代理类

那又有一个问题,当我有多个接口实现,该传入哪个classloader?

多个接口需要实现动态代理时,应该传入一个可以加载全部接口的classloader:

  1. 如果这多个接口位于同一个classloader下, 如果是同一个jar包或classpath,那么可以传入任意一个接口的classloader:

因为这个classloader可以同时加载这两个接口

  1. 如果这多个接口位于不同classloader:
  • 创建一个自定义classloader, 来加载所有这些接口类到一个classloader中再使用:
ClassLoader customLoader = new URLClassLoader(); // 里面的参数为地址数组,传入要加载的类的文件路径
customLoader.loadClass(InterfaceA.class.getName()); // 加载类
customLoader.loadClass(InterfaceB.class.getName()); 

3. 执行上面的代码之后,该classloader就同时拥有这两接口了。

也可以使用系统已有的某个classloader,只要它能加载所有这些接口,比如:

线程的上下文类加载器只负责加载那些线程"可能需要"的类。
ClassLoader classLoader =  
   Thread.currentThread().getContextClassLoader();

Object proxy = Proxy.newProxyInstance(
     classLoader,
     ...
)        

可以根据具体情况,选择:

  • 接口共享的一个classloader
  • 自定义的能加载所有接口的classloader
  • 系统已有的能加载所有接口的classloder

再来一个问题,当实现多个接口时,返回的动态代理对象的类型是什么?

答案: 该动态代理对象$Proxy0实现了这多个接口,实现的类型更"接近泛型", 而不是具体的某一个类。

InvocationHandler接口

InvocationHandler接口是代理处理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用处理者(InvocationHandler接口的子类)。

public Object invoke(Object proxy, Method method, Object[] args)该方法用于处理对代理类实例的方法调用并返回相应的结果当一个代理实例中的业务方法被调用时将自动调用该方法invoke()方法包含三个参数,

其中第一个参数proxy表示代理类的实例
第二个参数method表示需要代理的方法
第三个参数args表示代理方法的参数数组

总而言之,客户端在调用动态代理对象的方法时,调用请求会将请求自动转发给InvocationHandler对象的invoke()方法,由invoke()方法来实现对请求的统一处理

注意一点,动态代理只是将你的请求转发给invoke()方法调用,真实业务逻辑还是需要手动写。

案例学习

需求: 某软件公司欲为公司OA系统数据访问层DAO增加方法调用日志,记录每一个方法被调用的时间和调用结果,现使用动态代理进行设计和实现。

抽象主题对象

具体的接口

//抽象UserDAO:抽象主题角色
public interface AbstractUserDAO {
   public Boolean findUserById(String userId);
}
//抽象UserDAO:抽象主题角色
public interface AbstractUserDAO {
   public Boolean findUserById(String userId);
}

具体主题对象

//具体UserDAO类:真实角色
public class UserDAO implements AbstractUserDAO {
   public Boolean findUserById(String userId) {
      if (userId.equalsIgnoreCase("张无忌")) {
         System.out.println("查询ID为" + userId + "的用户信息成功!");
         return true;
      }
      else {
         System.out.println("查询ID为" + userId + "的用户信息失败!");
         return false;
      }
   }
}
//具体DocumentDAO类:真实角色
public class DocumentDAO implements AbstractDocumentDAO {
   public Boolean deleteDocumentById(String documentId) {
      if (documentId.equalsIgnoreCase("D001")) {
         System.out.println("删除ID为" + documentId + "的文档信息成功!");
         return true;
      }
      else {
         System.out.println("删除ID为" + documentId + "的文档信息失败!");
         return false;
      }
   }
}

自定义请求处理

该类可以通过构造方法将需要代理的主题对象传入处理器的内部,然后invoke()方法里调用传入方法的真实处理并添加额外处理。

//自定义请求处理程序类
public class DAOLogHandler implements InvocationHandler {
   private Calendar calendar;
   private Object object;

   public DAOLogHandler() {
   }

   //自定义有参构造函数,用于注入一个需要提供代理的真实主题对象
   public DAOLogHandler(Object object) {
      this.object = object;
   }

   //实现invoke()方法,调用在真实主题类中定义的方法
   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      beforeInvoke();
      Object result = method.invoke(object, args); //转发调用
      afterInvoke();
      return result;
   }

   //记录方法调用时间
   public void beforeInvoke(){
      calendar = new GregorianCalendar();
      int hour = calendar.get(Calendar.HOUR_OF_DAY);
      int minute = calendar.get(Calendar.MINUTE);
      int second = calendar.get(Calendar.SECOND);
      String time = hour + ":" + minute + ":" + second;
      System.out.println("调用时间:" + time);
   }

   public void afterInvoke(){
      System.out.println("方法调用结束!" );
   }
}

测试类

public class Client {
   public static void main(String args[]) {
      InvocationHandler handler;
      AbstractUserDAO userDAO = new UserDAO();
      handler = new DAOLogHandler(userDAO);
      
        //动态创建代理对象,用于代理一个AbstractUserDAO类型的真实主题对象
      AbstractUserDAO proxy = (AbstractUserDAO)Proxy.newProxyInstance(AbstractUserDAO. class.getClassLoader(), new Class[]{AbstractUserDAO.class}, handler);
       proxy.findUserById("张无忌"); //调用代理对象的业务方法
    
       System.out.println("------------------------------");
       
      AbstractDocumentDAO docDAO = new DocumentDAO();
      handler = new DAOLogHandler(docDAO);
      
      //动态创建代理对象,用于代理一个AbstractDocumentDAO类型的真实主题对象
      AbstractDocumentDAO proxy_new = (AbstractDocumentDAO)Proxy.newProxyInstance(AbstractDocumentDAO.class.getClassLoader(), new Class[]{AbstractDocumentDAO.class}, handler);
       proxy_new.deleteDocumentById("D002"); //调用代理对象的业务方法
   } 
}

输出及分析

调用时间:20:43:38
查询ID为张无忌的用户信息成功!
方法调用结束!
---------------------
调用时间:20:43:38
删除ID为D002的文档信息失败!
方法调用结束!

  1. 我们初始化InvocationHandler
  2. AbstractUserDao的实现传入InvocationHandler
  3. 然后创建动态代理对象
  4. 调用方法被代理到invoke()方法
  5. 调用invoke()方法来执行 前置任务 -> 实际任务 -> 具体任务

注意: Method类中也有invoke()方法, 它的参数是 (Object o, Object... args), 里面的Object对象为传入的具体实现代码。

动态代理适用范围

这个的适用环境很广, 任何需要拦截的都可以使用。

常见的如AOP编程为其编写切面代码,但是spring帮我写了,我一般是直接用所以spring怎么写的我不知道

知识补充

CGLIB实现动态代理和JDK动态代理不同。

JDK动态代理需要实现接口,并在InvocationHandler中对实现类方法做转发调用。

而CGLIB的实现方式是:

  1. 通过继承实现类来创建子类
  2. 重写父类的方法
  3. 在重写的方法中加入代理逻辑