Advice
在原方法基础之上,通过插入一段代码从而增强原方法的功能。
1 2 3 4 public int doWork (int a, int b) { int ret = a / b; return ret; }
增强原方法:在这个方法体的不同时机插入一段额外代码,让 doWork 方法的功能变得更强大,比如调用 doWork 方法前做日志记录。
环绕增强=前置增强+后置增强+异常增强+最终增强,上图看成一个整体就是一个环绕增强代码案例。
假设一个应用场景:需要对系统中的某些业务方法做事务管理。
1 2 3 4 5 public class AccountServiceImpl implements IAccountService { public void save () { } }
加上事务之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class AccountServiceImpl implements IAccountService { public void save () { try { } catch (Exception e) { } finally { } } }
每个业务方法都得处理事务管理操作。所以会出现问题,
责任不分离:业务方法只需要关心如何完成该业务功能,不关心事务管理/日志管理/权限管理等
代码结构重复:在开发中不要重复代码,重复就意味着维护成本增大。
责任分离和代理思想
代理的目的:不允许客户端直接访问真实对象,必须通过代理。客户端找代理,代理再找真实对象。
代理的特点:
代理对象完全包含真实对象,客户端使用的都是代理对象的方法,和真实对象没有直接关系。
代理模式的职责:把不是真实对象该做的事情从真实对象上撇开—职责清晰。
静态代理
程序运行前,就能看到代理类的字节码。可以看到代理类的定义。所以代理对象和真实对象的关系在运行之前就确定了。
模拟事务管理器,包含了事务开启、提交和回滚操作
这里的代码是基于文章 Spring入门中Spring集成Mybatis ,我们需要在增删改查的基础上,增强 service,添加事务方法又不濡染业务代码。因此,在增加功能的同时,我们不能去修改 AccountServiceImpl 的代码。
Test,我们的目的是调用代理的 gets 方法可以在完成以前 Service 功能的同事增加事务管理代码
1 2 3 4 5 6 7 8 9 10 @RunWith (SpringJUnit4ClassRunner.class)@ContextConfiguration public class ProxyApp { @Autowired private TransactionManagerProxy proxy; @Test public void testStaticProxy () throws Exception { proxy.gets().forEach(System.out::println); } }
事务管理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class TransactionManager { public void begin () { System.out.println("事务开始..." ); } public void commit () { System.out.println("提交事务..." ); } public void rollback () { System.out.println("回滚..." ); } public void close () { System.out.println("关闭资源..." ); } }
TransactionManagerProxy 静态代理类,用于增强 service,这里的两个属性不能忘记写 set 方法,因为使用 DI 注入数据是通过 set 方法注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class TransactionManagerProxy implements IAccountService { private IAccountService service; private TransactionManager manager; @Override public List<Account> gets () { manager.begin(); List<Account> gets = null ; try { gets = service.gets(); manager.commit(); } catch (Exception e) { e.printStackTrace(); manager.rollback(); } finally { manager.close(); } return gets; } }
只需要在 ProxyApp-context.xml 中配置 bean,让 Spring 注入数据即可。
1 2 3 4 5 6 7 <import resource ="classpath:applicationContext.xml" /> <bean id ="transactionManager" class ="cn.lizhaoloveit.proxy.TransactionManager" > </bean > <bean id ="transactionManagerProxy" class ="cn.lizhaoloveit.proxy.TransactionManagerProxy" > <property name ="service" ref ="accountService" > </property > <property name ="manager" ref ="transactionManager" > </property > </bean >
代理类 TransactionManagerProxy 需要被赋值2个属性
优点:业务类只需关注业务逻辑本身,保证了业务类的重用性,保护了真是对象 缺点:每一个真实对象都要创建一个代理对象,如果代理的方法很多则需要对每一种方法代理处理,如果接口增加一个方法,除了所有实现类要实现这个方法,代理类也要实现这个方法。
如果有一个代理类能代理所有的 Service 实现类来处理事务控制就好了
JDK 动态代理(有接口)
程序运行之后,通过反射等机制动态生成的,代理对象和真实对象的关系是在程序运行时期才确定的。
Java 是静态语言,需要编译,那么 Java 是如何实现动态的添加字节码并执行的呢。 我们编写的 Java 代码都是要被编译成字节码后才能放到 JVM 里执行,字节码(.class) 文件就是普通的二进制文件,它是通过 Java 编译器生成的。只要是文件就可以被改变,如果用特定的规则解析原有的字节码文件,对它进行修改或者干脆重新定义,就可以改变代码行为。
遵循 Java 编译系统组织.class文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,如此就完成了在代码中动态创建一个类的能力
JDK 动态代理 必须要求真实对象有接口
java.lang.reflect.Proxy 类,
反射包中有一个代理类,Java 动态代理机制生成的所有动态代理类的父类,提供了一组静态方法来为一组接口动态地创建代理类及其对象。主要方法:public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler hanlder)
方法职责:未指定接口生成动态代理类对象并返回,参数列表如下:
参数
说明
loader
类加载器,一般使用真实对象的类加载器即可
interfaces
代理类需要实现的接口
hanlder
代理对象如何做增强
java.lang.reflect.InvocationHandler 接口
表示需要做功能增强的规范接口public Object invoke(Object proxy, Method method, Object[] args)
方法职责:负责对动态代理类上的所有方法做增强(advice)操作,返回真是方法的执行结果,参数列表如下:
参数
说明
proxy
生成的代理对象
method
当前调用的真实方法对象
args
当前调用方法的实参
JDK 动态代理操作步骤
实现 InvocationHandler 接口,创建自己增强功能的增强类。
给 Proxy 类提供 ClassLoader 对象和代理接口类型数组,创建动态代理对象。
在增强类中实现具体增强操作
在使用时,需要与静态代理相同,在保护真实类型的前提下,增强真实类型的功能。因为需要动态获取 service 代理对象,因此会比静态多出一步获取代理对象的步骤。
1 2 3 4 5 6 7 8 9 10 11 12 @RunWith (SpringJUnit4ClassRunner.class)@ContextConfiguration public class JDKProxyApp { @Autowired private TransactionManagerAdvice advice; @Test public void testGets () throws Exception { IAccountService proxy = advice.getProxyObject(); proxy.gets().forEach(System.out::println); } }
事务管理类同静态代理,动态代理增强类代码如下:TransactionManagerAdvice.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class TransactionManagerAdvice implements InvocationHandler { private Object target; private TransactionManager manager; public <T> T getProxyObject () { return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this ); } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Object res = null ; manager.begin(); try { res = method.invoke(target, args); manager.commit(); } catch (Exception e) { e.printStackTrace(); manager.rollback(); } finally { manager.close(); } return res; } }
java.lang.reflect.Proxy 工具类会帮我们通过代理类需要实现的接口、类加载器和功能增强类需要实现的接口 这三个参数来动态生成一个字节码对象并返回。
在调用这个动态代理对象的 代理真实类接口的方法时,会去public Object invoke(Object proxy, Method method, Object[] args)
方法中找到 真实对象的 method 方法,并调用。就相当于我们通过动态代理类拦截到真实对象调用接口的方法,并对方法进行增强处理。做更多的事情。但是真实类没有丝毫改变。
最后在 JDKProxyApp-context.xml 中去给 TransactionManagerAdvice 的属性赋值即可。
1 2 3 4 5 6 7 <import resource ="classpath:applicationContext.xml" /> <bean id ="txManager" class ="cn.lizhaoloveit.jdkproxy.TransactionManager" > </bean > <bean id ="txAdvice" class ="cn.lizhaoloveit.jdkproxy.TransactionManagerAdvice" > <property name ="target" ref ="accountService" /> <property name ="manager" ref ="txManager" > </property > </bean >
JDK 动态代理原理
1 2 3 4 5 6 7 8 9 public class AccountServiceImpl implements IAccountService { private AccountMapper accountMapper; public List<Account> gets () { return accountMapper.gets(); } public void setAccountMapper (AccountMapper accountMapper) { this .accountMapper = accountMapper; } }
上述类生成动态代理类字节码经过反编译后会变成如下 java 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public final class AccountServiceProxy -2019-8-1 22-57-04 extends Proxy implements IAccountService private static Method m1 ;private static Method m2;private static Method m3;private static Method m0;public final boolean equals (Object paramObject) throws { try { return ((Boolean)this .h.invoke(this , m1, new Object[]{ paramObject })).booleanValue(); } catch (RuntimeException localRuntimeException) { throw localRuntimeException; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } public final void gets () throws { try { this .h.invoke(this , m4, new Object[0 ]); return ; } catch (RuntimeException localRuntimeException) { throw localRuntimeException; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } public final int hashCode () throws { try { return ((Integer)this .h.invoke(this , m0, null )).intValue(); } catch (RuntimeException localRuntimeException) { throw localRuntimeException; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } static { try { m1 = Class.forName("java.lang.Object" ).getMethod("equals" , new Class[]{Class.forName("java.lang.Object" )}); m2 = Class.forName("java.lang.Object" ).getMethod("toString" , new Class[0 ]); m0 = Class.forName("java.lang.Object" ).getMethod("hashCode" , new Class[0 ]); m4 = Class.forName("cn.lizhaoloveit.aop.service.IAccountService" ).getMethod("gets" , new Class[0 ]); } }
我们使用 proxy.gets()
实际上就是调用了 this.h.invoke(this, m4, new Object[0]);
this 是这个动态代理对象,h 就是 InvocationHandler 的实现类对象,也就是调用了实现类对象的 invoke(Object proxy, Method method, Object[] args)
方法,该 method 就是真实类的接口中的方法。
如果我们在 InvocationHandler 中的 invoke 方法中打印 Proxy 动态代理类实例,其实是调用其 toString方法,会发现栈溢出,因为 toString 方法是真实类接口的父类接口方法,调用 toString 实际上还会调用 method 为 toString 的 invoke 方法,于是不停的重复调用出现栈溢出。
CGlib 动态代理
没有接口的情况下,想要创建动态代理类,就只能选择继承真实类,然后通过覆盖真实类的方法去实现增强。
使用 CGlib Spring 内部已经集成了 CGLIB 库,而且为了和 JDK 动态增强保持一致,Spring 重写了 InvocationHandle 接口和相关组件,使之与 java.lang.reflect.InvocationHandler 接口保持回调的方法签名一致,这样在使用时,会发现除了生成动态代理类的方法会有区别,其余代码无需更改。
所以在使用时,只需要更改 TransactionManagerAdvice 类即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class TransactionManagerAdvice implements InvocationHandler { private Object target; private TransactionManager manager; public <T> T getProxyObject () { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(this ); return (T)enhancer.create(); } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Object res = null ; manager.begin(); try { res = method.invoke(target, args); manager.commit(); } catch (Exception e) { e.printStackTrace(); manager.rollback(); } finally { manager.close(); } return res; } }
注意:InvocationHandler接口 是 org.springframework.cglib.proxy.InvocationHandler 包下的接口。 Enhancer 是增强类,用于动态构建字节码对象 需要传入父类,类型和设置回调。
CGlib 原理
动态代理类代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 public class AccountServiceImpl $$EnhancerByCGLIB $$b78ee32f extends AccountServiceImpl implements Factory { private boolean CGLIB$BOUND; public static Object CGLIB$FACTORY_DATA; private static final ThreadLocal CGLIB$THREAD_CALLBACKS; private static final Callback[] CGLIB$STATIC_CALLBACKS; private InvocationHandler CGLIB$CALLBACK_0; private static Object CGLIB$CALLBACK_FILTER; private static final Method CGLIB$setAccountMapper$0 ; private static final Method CGLIB$gets$1 ; private static final Method CGLIB$equals$2 ; private static final Method CGLIB$toString$3 ; private static final Method CGLIB$hashCode$4 ; private static final Method CGLIB$clone$5 ; static void CGLIB$STATICHOOK1() { CGLIB$THREAD_CALLBACKS = new ThreadLocal(); CGLIB$setAccountMapper$0 = Class.forName("cn.lizhaoloveit.aop.service.impl.AccountServiceImpl" ).getDeclaredMethod("setAccountMapper" , new Class[] { Class.forName("cn.lizhaoloveit.aop.mapper.AccountMapper" ) }); CGLIB$gets$1 = Class.forName("cn.lizhaoloveit.aop.service.impl.AccountServiceImpl" ).getDeclaredMethod("gets" , new Class[0 ]); CGLIB$equals$2 = Class.forName("java.lang.Object" ).getDeclaredMethod("equals" , new Class[] { Class.forName("java.lang.Object" ) }); CGLIB$toString$3 = Class.forName("java.lang.Object" ).getDeclaredMethod("toString" , new Class[0 ]); CGLIB$hashCode$4 = Class.forName("java.lang.Object" ).getDeclaredMethod("hashCode" , new Class[0 ]); CGLIB$clone$5 = Class.forName("java.lang.Object" ).getDeclaredMethod("clone" , new Class[0 ]); } public final void setAccountMapper (AccountMapper paramAccountMapper) { try { if (this .CGLIB$CALLBACK_0 == null ) { this .CGLIB$CALLBACK_0; CGLIB$BIND_CALLBACKS(this ); } new Object[1 ][0 ] = paramAccountMapper; return ; } catch (RuntimeException|Error runtimeException) { throw null ; } catch (Throwable throwable) { throw new UndeclaredThrowableException(null ); } } public final List gets () { try { if (this .CGLIB$CALLBACK_0 == null ) { this .CGLIB$CALLBACK_0; CGLIB$BIND_CALLBACKS(this ); } return (List)this .CGLIB$CALLBACK_0.invoke(this , CGLIB$gets$1 , new Object[0 ]); } catch (RuntimeException|Error runtimeException) { throw null ; } catch (Throwable throwable) { throw new UndeclaredThrowableException(null ); } } public final boolean equals (Object paramObject) { try { if (this .CGLIB$CALLBACK_0 == null ) { this .CGLIB$CALLBACK_0; CGLIB$BIND_CALLBACKS(this ); } return ((Boolean)this .CGLIB$CALLBACK_0.invoke(this , CGLIB$equals$2 , new Object[] { paramObject })).booleanValue(); } catch (RuntimeException|Error runtimeException) { throw null ; } catch (Throwable throwable) { throw new UndeclaredThrowableException(null ); } } public final String toString () { try { if (this .CGLIB$CALLBACK_0 == null ) { this .CGLIB$CALLBACK_0; CGLIB$BIND_CALLBACKS(this ); } return (String)this .CGLIB$CALLBACK_0.invoke(this , CGLIB$toString$3 , new Object[0 ]); } catch (RuntimeException|Error runtimeException) { throw null ; } catch (Throwable throwable) { throw new UndeclaredThrowableException(null ); } } public final int hashCode () { try { if (this .CGLIB$CALLBACK_0 == null ) { this .CGLIB$CALLBACK_0; CGLIB$BIND_CALLBACKS(this ); } return ((Number)this .CGLIB$CALLBACK_0.invoke(this , CGLIB$hashCode$4 , new Object[0 ])).intValue(); } catch (RuntimeException|Error runtimeException) { throw null ; } catch (Throwable throwable) { throw new UndeclaredThrowableException(null ); } } protected final Object clone () throws CloneNotSupportedException { try { if (this .CGLIB$CALLBACK_0 == null ) { this .CGLIB$CALLBACK_0; CGLIB$BIND_CALLBACKS(this ); } return this .CGLIB$CALLBACK_0.invoke(this , CGLIB$clone$5 , new Object[0 ]); } catch (RuntimeException|Error|CloneNotSupportedException runtimeException) { throw null ; } catch (Throwable throwable) { throw new UndeclaredThrowableException(null ); } } public AccountServiceImpl$$EnhancerByCGLIB$$b78ee32f() { CGLIB$BIND_CALLBACKS(this ); } public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] paramArrayOfCallback) { CGLIB$THREAD_CALLBACKS.set(paramArrayOfCallback); } public static void CGLIB$SET_STATIC_CALLBACKS(Callback[] paramArrayOfCallback) { CGLIB$STATIC_CALLBACKS = paramArrayOfCallback; } private static final void CGLIB$BIND_CALLBACKS(Object paramObject) { AccountServiceImpl$$EnhancerByCGLIB$$b78ee32f accountServiceImpl$$EnhancerByCGLIB$$b78ee32f = (AccountServiceImpl$$EnhancerByCGLIB$$b78ee32f)paramObject; if (!accountServiceImpl$$EnhancerByCGLIB$$b78ee32f.CGLIB$BOUND) { accountServiceImpl$$EnhancerByCGLIB$$b78ee32f.CGLIB$BOUND = true ; if (CGLIB$THREAD_CALLBACKS.get() == null ) { CGLIB$THREAD_CALLBACKS.get(); if (CGLIB$STATIC_CALLBACKS == null ) { CGLIB$STATIC_CALLBACKS; return ; } } accountServiceImpl$$EnhancerByCGLIB$$b78ee32f.CGLIB$CALLBACK_0 = (InvocationHandler)(Callback[])CGLIB$THREAD_CALLBACKS.get()[0 ]; } } public Object newInstance (Callback[] paramArrayOfCallback) { CGLIB$SET_THREAD_CALLBACKS(paramArrayOfCallback); CGLIB$SET_THREAD_CALLBACKS(null ); return new AccountServiceImpl$$EnhancerByCGLIB$$b78ee32f(); } public Object newInstance (Callback paramCallback) { CGLIB$SET_THREAD_CALLBACKS(new Callback[] { paramCallback }); CGLIB$SET_THREAD_CALLBACKS(null ); return new AccountServiceImpl$$EnhancerByCGLIB$$b78ee32f(); } public Callback getCallback (int paramInt) { CGLIB$BIND_CALLBACKS(this ); switch (paramInt) { case 0 : } this .CGLIB$CALLBACK_0; return null ; } public void setCallback (int paramInt, Callback paramCallback) { switch (paramInt) { case 0 : this .CGLIB$CALLBACK_0 = (InvocationHandler)paramCallback; break ; } } public Callback[] getCallbacks() { CGLIB$BIND_CALLBACKS(this ); return new Callback[] { this .CGLIB$CALLBACK_0 }; } public void setCallbacks (Callback[] paramArrayOfCallback) { this .CGLIB$CALLBACK_0 = (InvocationHandler)paramArrayOfCallback[0 ]; } static { CGLIB$STATICHOOK1(); } }
可以看到,动态代理类继承了 AccountServiceImpl ,覆盖了所有的 public 修饰的方法,与JDK动态代理类类似,将 AccountServiceImpl 中的 public 方法全部作为成员变量记录下来,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void CGLIB$STATICHOOK1() { CGLIB$THREAD_CALLBACKS = new ThreadLocal(); CGLIB$setAccountMapper$0 = Class.forName("cn.lizhaoloveit.aop.service.impl.AccountServiceImpl" ).getDeclaredMethod("setAccountMapper" , new Class[] { Class.forName("cn.lizhaoloveit.aop.mapper.AccountMapper" ) }); CGLIB$gets$1 = Class.forName("cn.lizhaoloveit.aop.service.impl.AccountServiceImpl" ).getDeclaredMethod("gets" , new Class[0 ]); CGLIB$equals$2 = Class.forName("java.lang.Object" ).getDeclaredMethod("equals" , new Class[] { Class.forName("java.lang.Object" ) }); CGLIB$toString$3 = Class.forName("java.lang.Object" ).getDeclaredMethod("toString" , new Class[0 ]); CGLIB$hashCode$4 = Class.forName("java.lang.Object" ).getDeclaredMethod("hashCode" , new Class[0 ]); CGLIB$clone$5 = Class.forName("java.lang.Object" ).getDeclaredMethod("clone" , new Class[0 ]); } public final List gets () { try { if (this .CGLIB$CALLBACK_0 == null ) { this .CGLIB$CALLBACK_0; CGLIB$BIND_CALLBACKS(this ); } return (List)this .CGLIB$CALLBACK_0.invoke(this , CGLIB$gets$1 , new Object[0 ]); } catch (RuntimeException|Error runtimeException) { throw null ; } catch (Throwable throwable) { throw new UndeclaredThrowableException(null ); } }
当使用动态代理类调用 gets() 时,实际上调用了 (List)this.CGLIB$CALLBACK_0.invoke(this, CGLIB$gets$1, new Object[0]);
CGLIB$CALLBACK_0 就是 TransactionManagerAdvice,是 InvocationHandler 的实现类。 调用该方法就是调用 TransactionManagerAdvice 实现的 invoke 方法。并在方法内部对真实类的 gets() 方法进行增强。
总结: JDK 动态代理会拦截所有接口中定义的方法,在接口中增加了新的方法,不用修改代码也会被拦截。 用 CGlib,类型不能被final 修饰,只能拦截 非final 、非static 、非private 的方法。 动态代理的最小单位是类,所有类(接口)中的方法都会被拦截,如果只想拦截一部分方法,则需要在 invoke 中处理。 性能:Javassit > CGlib > JDK 真实对象实现了接口,选用 JDK,否则选用 CGlib、JDK(面向接口编程)
AOP
在很多业务中,需要在一些业务流程 中插入一些其他的模块功能。将业务流程当作水流,AOP 思想就是在水流的某个位置(切面)插入功能模块,同时,不影响正常的业务代码类的思想 。
开发中,为了给业务方法中增加日志记录,权限检查,事务控制等功能,我们需要修改业务代码,考虑到代码的重用性,使用 继承、组合关系消除重复代码。但无论是集成业务类的子类,还是引用业务类去完成功能,都无法做到通用性,必须对业务类有所影响。
此时,既不遵循开闭原则 ,也无法完全消除重复代码。
AOP 思想
我们把这些零散的存在于业务方法中的功能代码,称为横切面关注点,横切面关注点不属于业务范围,应该从业务代码中剥离。把多个业务方法的切面关注点封装到一个模块中称为一个切面。切面就是一个功能模块,通用的解决业务流中的某一个面上的问题。比如,应用中许多地方需要做日志记录,只需要插入日志切面即可。
AOP 把多个业务方法需要调用的代码封装到不同的模块中去(责任分离思想),使用动态代理机制来动态的增强业务功能,从而达到了代码的复用,提高了维护性。
核心概念
where 哪些方法,使用类的全限定名称 (Pointcut ,哪些包的哪些类中的哪些方法)
when 在方法体执行的时机:之前、之后、异常、最终、环绕 (Jointpoint )
what 做什么功能的增强:不同的增强功能使用不同的模块或类来封装 (Advice )
AOP 中有几个核心概念,必须掌握:
Joinpoint:连接点,程序执行过程中的特定位置,初始化前/后,方法调用前/后,方法抛出异常后,这些特定的位置称为连接点,在连接点可以插入代码,增强功能
Pointcut:切入点,开发中,不需要对应用中所有的连接点都做增强,切入点的作用就是缩小增强范围,比如那些包的哪些类中的那些方法,相当于切面在何处增强
Advice:增强,拦截到连接点后,需要做什么功能。 what
Aspect:切面,Pointcur + Advice。哪些方法中+在方法执行的什么时机+做什么增强
Target:真实类,被代理的目标
Weaving:把 Advice 加到 Target 上后,创建出 Proxy 对象的过程,编织。
Proxy:一个类被 AOP 织入增强后,产生的代理类。
Pointcut 语法
在指定 AOP 规范时,首先需要解决的一个问题就是怎么来表示切入点,需要在哪些方法上做增强?这是一个如何表示 Where 的问题
AspectJ 项目定义了 AOP 的语法,确定了如何去表达 where:(哪些包?哪些类?哪些方法?)
1 2 execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws -pattern?)
中文:
参数名
含义
modifiers-pattern
方法修饰符?
ret-type-pattern
返回类型
declaring-type-pattern
声明类型?(方法名的全限定名)
name-pattern
方法名
param-pattern
参数
throws-pattern
异常?
1 2 public static Class java.lang.Class.forName(String className) throws ClassNotFoundException --修饰符-------返回类型---------全限定名-------------参数------------异常---
表达式中可使用通配符:
*
表示匹配任意部分,不包括子包。
..
用于全限定名和方法参数中,分别表示子包和0-N个参数。
常见写法 1 excution(public * *(..))
所有的以 public 修饰的方法都会被增强
1 execution(* cn.wolfcode.wms.service.*.*(..))
表示:在 cn.wolfcode.wms.service 包下的方法会被增强
表示所有 setXxx 方法都会被增强
1 execution(* cn.wolfcode.wms.service.*Service.*(..))
表示 所有 cn.wolfcode.wms.service 包下的类名称以 Service 结尾的,所有方法都会被增强。
1 execution(* cn.wolfcode..service.*Service.*(..))
表示 所有以 cn.wolfcode 开头,以 service 包结尾下的 类名称以 Service 结尾的所有方法都会被增强
开发中的配置
Spring5 开始,在 spring-aop 中纳入了 AOP 联盟的 API,不需要拷贝 aopalliance-1.0.0.jar。
添加依赖:
1 2 3 4 5 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.8.13</version > </dependency >
AOP 配置三部曲:where-when-what
之前,我们自己来做 JDK 动态代理时,需要创建 Advice 增强类来获取动态代理对象,并接受回调。而使用 spring-aop,只需要增强功能模块,以及配置文件即可。让 spring-aop 帮我们完成动态代理增强功能。
配置文件中要阐述清楚 where-when-what
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:context ="http://www.springframework.org/schema/context" xmlns:aop ="http://www.springframework.org/schema/aop" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation =" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <import resource ="classpath:applicationContext.xml" /> <bean id ="transactionManager" class ="cn.lizhaoloveit.aopjdkproxy.TransactionManager" /> <aop:config proxy-target-class ="true" > <aop:aspect ref ="transactionManager" > <aop:pointcut expression ="execution(* cn.lizhaoloveit.aop.service.*Service.*(..))" id ="txPointcut" /> <aop:before method ="begin" pointcut-ref ="txPointcut" /> </aop:aspect > </aop:config > </beans >
首先,必须引入 schema/aop 约束,然后注入我们的增强功能模块 transactionManager
。
在 <aop:config>
的配置中,默认是使用 JDK动态代理,因为 spring 希望我们使用面向接口编程的思想。proxy-target-class="true"
该属性默认是 false 使用 JDK动态代理,改为 true 则使用 CGlib 动态代理。
在 aop 标签中,一个 aspect 代表一个功能模块,译文为切面。每个 <aop:aspect>
都会阐述清楚 在 哪个类中的哪个方法(where) 的 前置/后置/异常/环绕等(when) 时机插入哪个功能模块类的 什么方法(what) 具体为:
1 2 3 4 <aop:aspect ref ="transactionManager" > <aop:pointcut expression ="execution(* cn.lizhaoloveit.aop.service.*Service.*(..))" id ="txPointcut" /> <aop:before method ="begin" pointcut-ref ="txPointcut" /> </aop:aspect >
aspect : 表示一个切面 ref 属性表示功能增强模块类
pointcut : 表示切入点,也就是用 AspectJ语法表达了在哪个业务类,为了方便后续引用,给该切入点配置 id 属性。
<aop:before>...
等标签规定了时机,比如上面的例子,before 就是前置增强,也就是说框架会在业务代码调用该方法时拦截**(通过业务类的接口生成动态代理对象调用接口方法,动态代理再调用接口方法时,实际上调用的 InvocationHandler 实现类(也就是增强类)的 invoke方法,使得增强类通过实现接口的回调方法进行拦截)**并在业务类的方法调用前,调用增强模块的 begin 方法。
代码变得非常简洁:我们需要写的只有
业务类
增强功能模块
配置文件,把(where, when, what)串联起来 就可以在不修改任何业务代码的情况下,给业务流中间注入功能模块,维护只需要修改配置文件即可。提现了业务与功能模块分离,更好管理和维护。
各种增强时机
增强
备注
aop:before 前置增强
方法执行前执行增强 权限控制、记录调用日志
aop:after-returning 后置增强
在方法正常执行之后,执行增强(中间没遇到任何异常) 统计分析结果数据
aop:throwing 异常增强
在方法抛出异常catch时执行增强 日志记录方法获取异常信息,可以通过配置 throwing 来获得拦截到的异常信息
aop:after 最终增强
finally 里执行,通常用于释放资源
aop:around 环绕增强
最强大的一种增强类型,环绕增强可以在方法调用前/后完成自定义行为,环绕增强有两个要求:1.方法需要返回一个 Object,2.方法的第一个参数必须是 ProceedingJoinPoint 可以继续向下传递的连接点 缓存、性能日志、权限、事务管理
如果在增强方法中引用了没有的参数会报如下错误,例如在 close()
关闭资源中加入了 Throwable exception 参数,close(Throwable exception)
,aop:after 是没有 Throwable exception 引用的
1 Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut
TranscationManager.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class TransactionManager { public void begin (JoinPoint jp) { System.out.println("代理对象 :" + jp.getThis().getClass()); System.out.println("目标对象 :" + jp.getTarget().getClass()); System.out.println("被增强方法参数 :" + Arrays.toString(jp.getArgs())); System.out.println("当前连接点签名 :" + jp.getSignature()); System.out.println("当前连接点类型 :" + jp.getKind()); System.out.println("事务开始..." ); } public void commit (JoinPoint jp) { System.out.println("当前连接点签名 :" + jp.getSignature()); System.out.println("提交事务..." ); } public void rollback (JoinPoint jp, Throwable exception) { System.out.println("当前连接点签名 :" + jp.getSignature()); System.out.println("回滚..." + exception); } public void close () { System.out.println("关闭资源..." ); } public Object around (ProceedingJoinPoint pjp) { Object res = null ; begin(pjp); try { System.out.println("执行目标方法" ); res = pjp.proceed(); commit(pjp); } catch (Throwable e) { e.printStackTrace(); rollback(pjp, e); } finally { close(); } return res; } }
JDKProxyApp-context.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:context ="http://www.springframework.org/schema/context" xmlns:aop ="http://www.springframework.org/schema/aop" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation =" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <import resource ="classpath:applicationContext.xml" /> <bean id ="transactionManager" class ="cn.lizhaoloveit.aopjdkproxy.TransactionManager" /> <aop:config proxy-target-class ="false" > <aop:aspect ref ="transactionManager" > <aop:pointcut expression ="execution(* cn.lizhaoloveit.aop.service.*Service.*(..))" id ="txPointcut" /> <aop:before method ="begin" pointcut-ref ="txPointcut" /> <aop:after-returning method ="commit" pointcut-ref ="txPointcut" /> <aop:after-throwing method ="rollback" pointcut-ref ="txPointcut" throwing ="exception" /> <aop:after method ="close" pointcut-ref ="txPointcut" /> <aop:around method ="around" pointcut-ref ="txPointcut" /> </aop:aspect > </aop:config > </beans >
环绕增强=前置增强 + 后置增强 + 异常增强 + 最终增强,如果有环绕增强,则其余增强不需要写
配置增强的参数:
Spring AOP 提供 org.aspectj.lang.joinPoint 类,作为增强方法的第一个参数。
JoinPoint:提供访问当前被增强方法的真实对象、代理对象、方法参数等数据
ProceedingJoinPoint:JinPoint 子类,只用于环绕增强中,可以处理被增强方法。环绕增强中,调用真实类的业务方法。
异常增强获取异常信息:<aop:after-throwing method="rollback" pointcut-ref="txPointcut" throwing="exception"/>
,只需要做这个配置
1 2 3 4 public void rollback (JoinPoint jp, Throwable exception) { System.out.println("当前连接点签名 :" + jp.getSignature()); System.out.println("回滚..." + exception.getMessage()); }
如此可以拿到异常信息。
案例
在每次调用 service 方法之前,往数据库中插入一条数据,记录时间,哪一个类中的哪一个方法。
在做一个切面功能模块时,按照以下步骤:
先要将模块功能代码编写完毕,编写测试功能模块,并且测试通过。
编写模块功能使用类型,例如 TransactionManager.java 类,对外入口。
编写切面模块插入业务流程的测试模块。并测试完毕
日志录入功能其实并不复杂,就是对另一个表进行增删改查,首先建立一个表 record 表,设置字段,建立domain
1 2 3 4 5 6 7 public class Record { private Long id; private Date addTime; private String method; private String classQualifiedName; private String className; }
创建 Mybatis 需要的 RecordMapper.java 接口和 RecordMapper.xml,这里只需要 insert 方法即可。(略)
编写测试代码,并且测试通过
接下来,需要有一个类去完成功能增强,LogRecord.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class LogRecord { private RecordService recordService; public void setRecordService (RecordService recordService) { this .recordService = recordService; } public void record (JoinPoint jp) { System.out.println("插入日志到数据库" ); Class targetClass = jp.getTarget().getClass(); String className = targetClass.getSimpleName(); String classQualifiedName = targetClass.getName(); String method = jp.getSignature().getName(); Date addtime = new Date(); Record record = new Record(null , addtime, method, classQualifiedName, className); recordService.insertLog(record); } }
这个类的作用就是完成增强功能的代码,接下来就可以在配置文件中告诉 spring-aop 实现动态代理增强业务功能了,what 已经有了,还需要配置 where 和 when
1 2 3 4 5 6 7 8 9 10 <bean id ="logRecord" class ="cn.lizhaoloveit.logrecord.LogRecord" > <property name ="recordService" ref ="recordService" > </property > </bean > <aop:config > <aop:aspect ref ="logRecord" > <aop:pointcut expression ="execution(* cn.lizhaoloveit.aop.service.*Service.*(..))" id ="logPointcut" /> <aop:before method ="record" pointcut-ref ="logPointcut" /> </aop:aspect > </aop:config >
然后对业务功能进行测试,看是否已经插入切面。