面向切面编程-进一步掌握动态代理
代理模式是GoF提出的23种设计模式中最为经典的模式之一,代理模式是对象的结构模式,它给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。简单的说,代理对象可以完成比原对象更多的职责,当需要为原对象添加横切关注功能时,就可以使用原对象的代理对象。我们在打开Office系列的Word文档时,如果文档中有插图,当文档刚加载时,文档中的插图都只是一个虚框占位符,等用户真正翻到某页要查看该图片时,才会真正加载这张图,这其实就是对代理模式的使用,代替真正图片的虚框就是一个虚拟代理;Hibernate的load方法也是返回一个虚拟代理对象,等用户真正需要访问对象的属性时,才向数据库发出SQL语句获得真实对象。代理模式如下所示:
据图所示,我们需要在客户端(调用者)调用对象之前产生一个代理对象,而这个代理对象需要和目标对象(真实对象)建立代理关系,所以代理必须分为两个步骤:
- 代理对象和真实对象建立代理关系。
- 实现代理对象的代理逻辑方法。
代理可分为静态代理和动态代理,下面用一个找枪手代考的例子演示代理模式的使用:
/** * 参考人员接口(抽象主题角色) */ public interface Candidate { /** * 答题 */ public void answerTheQuestions(); }
抽象主题角色:声明了真实主题和代理主题的共同接口,这样一来在任何可以使用真实主题的地方都可以是使用代理主题
/** * 学渣(真实主题角色) */ public class SlackerStudent implements Candidate { private String name; // 姓名 public SlackerStudent (String name) { this.name = name; } @Override public void answerTheQuestions() { // 学渣只能写出自己的名字不会答题 System.out.println("姓名: " + name); } }
** * 枪手(代理主题角色) */ public class Gunman implements Candidate { private Candidate target; // 被代理对象 public Gunman(Candidate target) { this.target = target; } @Override public void answerTheQuestions() { // 枪手要写上代考的学生的姓名 target.answerTheQuestions(); // 枪手要帮助懒学生答题并交卷 System.out.println("奋笔疾书正确答案"); System.out.println("交卷"); } }
public class ProxyTest{ public static void main(String[] args) { var c = new Gunman(new SlackerStudent ("peppa")); c.answerTheQuestions(); } }
- 代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
- 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。
从JDK 1.3开始,Java提供了动态代理技术,允许开发者在运行时创建接口的代理实例,在java中有多种动态代理技术,比如JDK、CGLib、Javassist、ASM,其中最常用的动态代理技术有两种:一种是JDK动态代理,这是JDK自带的功能;一种是CGLib,这是第三方提供的一个技术。目前,Spring常用JDK和CGLib,而mybatis还使用了javassist,无论哪种代理技术,他们的理念都是相似的。
在JDK动态代理中,要实现代理逻辑类必须去实现 java.lang.reflect.InvocationHandler接口,它里面定义了一个invoke方法,并提供接口数组用于下挂代理对象,如下代码所示:
public class JDKProxyFactory implements InvocationHandler{ //目标对象 private Object target; /** * 建立代理对象和目标对象的代理关系,并返回代理对象 * @param target 目标对象 * @return 代理对象 */ public Object createTargetProxyInstance(Object target) { this.target = target; /* * 第1个参数为类加载器:采用了target本身的类加载器 * 第2个参数为把生成的动态代理对象下挂在哪些接口下,这个写法表示放在target实现的接口下 * 第3个表示实现方法逻辑的代理类,this表示当前对象,它必须实现InvocationHandler的invoke方法 */ return Proxy.newProxyInstance(this.target.getClass().getClassLoader(), this.target.getClass().getInterfaces(), this); } /** * 代理方法逻辑 * @param proxy 代理对象 * @param method 当前调度方法 * @param args 当前方法参数 * @return 代理结果返回 * @throws Throwable 异常 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object ret = null; System.out.println("进入代理逻辑方法,在调度真实对象之前的服务,比如记录日志"); ret = method.invoke(target, args); System.out.println("调度真实对象之后的服务"); return ret; } }
使用Java的动态代理有一个局限性就是代理的类必须要实现接口,虽然面向接口编程是每个优秀的Java程序都知道的规则,但现实往往不尽如人意,对于没有实现接口的类如何为其生成代理呢?继承!继承是最经典的扩展已有代码能力的手段,虽然继承常常被初学者滥用,但继承也常常被进阶的程序员忽视。CGLib采用非常底层的字节码生成技术,通过为一个类创建子类来生成代理,它弥补了Java动态代理的不足,因此Spring中动态代理和CGLib都是创建代理的重要手段,对于实现了接口的类就用动态代理为其生成代理类,而没有实现接口的类就用CGLib通过继承的方式为其创建代理。
public class CGLibProxyFactory implements MethodInterceptor { //目标对象 private Object target; /** * 生成CGLib代理对象 * @param target 目标对象 * @return 目标对象的CGLib代理对象 */ public Object createProxyInstance(Object target) { this.target = target; // 增强类对象 var enhancer = new Enhancer(); //设置增强类型 enhancer.setSuperclass(target.getClass()); //定义代理逻辑对象为当前对象,要求当前对象实现MethodInterceptor接口 enhancer.setCallback(this); //生成并返回代理对象 return enhancer.create(); } /** * 代理逻辑方法 * @param proxy 代理对象 * @param method 方法 * @param args 方法参数 * @param methodProxy方法 代理 * @return 代理逻辑返回 * @throws Throwable 异常 */ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object ret = null; if (权限判断) {//环绕通知(可以决定方法是否被调用(权限拦截)) try { System.out.println("开启事务"); // 前置通知 //反射调用真实方法 ret = method.invoke(target, args); //ret = methodProxy.invokeSuper(proxy, args); System.out.println("提交事务");// 后置通知 } catch (Exception e) { System.out.println("回滚事务");// 例外通知 } finally { System.out.println("释放资源,最终操作"); // 最终通知 } } return ret; } }
由于 JDK 8 中有关反射相关的功能自从 JDK 9 开始就已经被限制了,为了兼容原先的版本,需要在运行项目时添加 --add-opens java.base/java.lang=ALL-UNNAMED
选项来开启这种默认不被允许的行为
Spring AOP中相关概念
连接点:程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring仅支持方法的连接点。
切点:如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。
增强:增强是织入到目标类连接点上的一段程序代码。Spring提供的增强接口都是带方位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。很多资料上将增强译为“通知”,这明显是个词不达意的翻译,让很多程序员困惑了许久。
通知:切面开启后,切面的方法。它根据在代理对象真实方法调用前、后的顺序和逻辑区分。
- 前置通知(before): 在连接点之前执行的通知
- 后置通知(afterReturning): 方法执行后执行
- 最终通知(after): 方法执行后,无论异常与否都会被执行的通知
- 异常通知(afterThrowing): 在方法抛出异常之后执行
- 环绕通知(around):在动态代理中,它可以取代当前被拦截对象的方法,提供回调原有被拦截对象的方法。诸如权限拦截等
引介:引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过引介功能,可以动态的为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
织入:织入是将增强添加到目标类具体连接点上的过程,AOP有三种织入方式:
- 编译期织入:需要特殊的Java编译期(例如AspectJ)
- 类装载期织入:要求使用特殊的类加载器
- 动态代理织入:在运行时为目标类生成代理实现增强
切面:切面是由切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义。
横切性关注点
通过对系统需求和实现的识别,我们可以将模块中的这些关注点分为:核心关注点和横切关注点。对于核心关注点而言,通常来说,实现这些关注点的模块是相互独立的,他们分别完成了系统需要的商业逻辑,这些逻辑与具体的业务需求有关。而对于日志、安全、持久化等关注点而言,他们却是商业逻辑模块所共同需要的,这些逻辑分布于核心关注点的各处。在AOP中,诸如这些模块,都称为横切关注点。应用AOP的横切技术,关键就是要实现对关注点的识别。