Spring4学习回顾之路11-AOP
Srping的核心除了之前讲到的IOC/DI之外,还有一个AOP(Aspect Oriented Programming:面向切面编程):通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要的功能: 日志记录,性能统计,安全控制,事务处理,异常处理等等。
在介绍AOP之前,先介绍动态代理;代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及何时将方法调用转到原始对象上。
案例:基于接口实现的动态代理:JDK动态代理
首先定义一个接口,用于运算的Operation.java
package com.lql.proxy; /** * @author: lql * @date: 2019.10.29 * Description: */ public interface Operation { //加法 int add(int i,int j); //减法 int sub(int i,int j); }
再来定义实现类OperationImpl.java
package com.lql.proxy; /** * @author: lql * @date: 2019.10.29 * Description: */ public class OperationImpl implements Operation { @Override public int add(int i, int j) { return 0; } @Override public int sub(int i, int j) { return 0; } }
现在的需求就是用硬编码的形式添加日志,传统的做法是:
package com.lql.proxy; /** * @author: lql * @date: 2019.10.29 * Description: */ public class OperationImpl implements Operation { @Override public int add(int i, int j) { System.out.println("两数相加的参数为:" + i + "+" + j); int result = i+j; System.out.println("两数相加的结果为:" + result); return result; } @Override public int sub(int i, int j) { System.out.println("两数相减的参数为:" + i + "-" + j); int result = i-j; System.out.println("两数相减的结果为:" + result); return result; } }
测试代码和结果为:
package com.lql.proxy; /** * @author: lql * @date: 2019.10.29 * Description: */ public class Test { public static void main(String[] args) { Operation op = new OperationImpl(); System.out.println(op.add(1, 2)); System.out.println(op.sub(4,2)); } } 两数相加的参数为:1+2 两数相加的结果为:3 3 两数相减的参数为:4-2 两数相减的结果为:2 2
显然这么操作是不合理,但是JDK的动态代理就能很好的解决这个问题;如下所示:
还原最初版本的OperationImplProxy.java
package com.lql.proxy; /** * @author: lql * @date: 2019.10.29 * Description: */ public class OperationImplProxy implements Operation { @Override public int add(int i, int j) { int result = i+j; return result; } @Override public int sub(int i, int j) { int result = i-j; return result; } }
编写代理类OperationProxy.java:
package com.lql.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; /** * @author: lql * @date: 2019.10.29 * Description: * Created with IntelliJ IDEA */ public class OperationProxy { //代理对象 private Operation target; public OperationProxy(Operation target) { this.target = target; } //获取代理对象 public Operation getInstance() { Operation proxy = null; ClassLoader loader = target.getClass().getClassLoader(); Class[] clazz= target.getClass().getInterfaces(); InvocationHandler in = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String name = method.getName(); System.out.println("方法名:" + name + ",参数:" + Arrays.asList(args)); Object result = method.invoke(target, args); System.out.println("结果:" + result); return result; } }; proxy = (Operation) Proxy.newProxyInstance(loader, clazz, in); return proxy; } }
测试类和结果:
package com.lql.proxy; /** * @author: lql * @date: 2019.10.29 * Description: */ public class Test { public static void main(String[] args) { // Operation op = new OperationImpl(); // System.out.println(op.add(1, 2)); // System.out.println(op.sub(4,2)); Operation target = new OperationImplProxy(); Operation operation = new OperationProxy(target).getInstance(); System.out.println(operation.add(1, 2)); System.out.println(operation.sub(4, 2)); } } 方法名:add,参数:[1, 2] 结果:3 3 方法名:sub,参数:[4, 2] 结果:2 2
至此JDK的动态代理demo完成了,这里就不详细介绍每一步了。回归正题:AOP:面向切面编程,相较于传统的OOP(面向对象编程)来说是个补充;说到AOP那就不得不提及它的相关术语了:
-切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
-通知(advice): 切面必须要完成的功能
-目标(target) : 被通知的对象
-代理(Proxy) :向目标对象应用通知之后创建的对象
-连接点(Joinpoint): 程序执行的某个特定位置
-切点(pointcut):每个类都拥有多个连接点
关于AOP的基础配置先出个总图有个印象:
在Spring2.0以上的版本中,可以使用基于AspectJ注解或者基于XML配置的AOP;其中AspectJ是Java社区里最完整流行的AOP框架。
一:基于注解方式:
①:前置通知:
先导入需要的jar包
建立Spring配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!--配置自动扫描的包--> <context:component-scan base-package="com.lql.spring07"/> <!--使AspectJ注解起作用,自动为匹配的类生成代理对象--> <aop:aspectj-autoproxy/> </beans>
建立接口
package com.lql.spring07; /** * @author: lql * @date: 2019.10.29 * Description: */ public interface Operation { //加法 int add(int i, int j); //减法 int sub(int i, int j); }
建立接口实现
package com.lql.spring07; import com.lql.proxy.Operation; import org.springframework.stereotype.Component; /** * @author: lql * @date: 2019.10.29 * Description: */ @Component public class OperationImpl implements Operation { @Override public int add(int i, int j) { int result = i+j; return result; } @Override public int sub(int i, int j) { int result = i-j; return result; } }
测试类和结果
package com.lql.spring07; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author: lql * @date: 2019.10.29 * Description: */ public class Test { public static void main(String[] args) { ApplicationContext app = new ClassPathXmlApplicationContext("aop.xml"); OperationImpl bean = app.getBean("operationImpl", OperationImpl.class); System.out.println(bean.add(2, 2)); System.out.println(bean.sub(4, 2)); } } 4 2
这样是没有问题的,现在开始给程序增加日志功能,也就是完成之前动态代理的活;
首先是需要定义切面,并把该切面放入IOC容器中,定义如下:先简单完成,后完善功能,具体语法稍后解释!
package com.lql.spring07; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; /** * @author: lql * @date: 2019.10.29 * Description: */ @Component @Aspect public class LogAspect { //声明该方法是个前置通知,在目标方法执行前执行 @Before(value = "execution(* com.lql.spring07.*.*(..))") public void before() { System.out.println("方法前被调用"); } }
再次执行测试类结果:
方法前被调用 4 方法前被调用 2
接下来完善日志信息:
package com.lql.spring07; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * @author: lql * @date: 2019.10.29 * Description: */ @Component @Aspect public class LogAspect { //声明该方法是个前置通知,在目标方法执行前执行 @Before(value = "execution(* com.lql.spring07.*.*(..))") public void before(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("方法名称: " + methodName + ",参数为 : " + args); } }
结果如下:
方法名称: add,参数为 : [2, 2] 4 方法名称: sub,参数为 : [4, 2] 2
关于@Before(value = "execution(* com.lql.spring07.*.*(..))")后面中的一大串其实就是AspectJ的表达式,具体使用(如果看不懂可以直接写完整的方法):
-里面的组成: execution(修饰符匹配 ? 返回值类型匹配操作类型匹配 ? 名称匹配(参数匹配)抛出异常匹配);
*.*描述的是这个包中所有类中所有的方法,不想全部可以细分
(..)描述的是参数
com.lql.spring07是切入点匹配的包名称,如果多层则可以使用..代替比如:com.lql..service.spring07
第一个*则表示该方法的返回值,任意数据类型,当然前面也能增加访问修饰符public或者private
②:后置通知:
在之前的代码中新增后置通知:
package com.lql.spring07; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * @author: lql * @date: 2019.10.29 * Description: */ @Component @Aspect public class LogAspect { //声明该方法是个前置通知,在目标方法执行前执行 @Before(value = "execution(* com.lql.spring07.*.*(..))") public void before(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("方法名称: " + methodName + ",参数为 : " + args); } //后置通知:方法执行后执行,无论是否执行完毕 @After("execution(* com.lql.spring07.*.*(..))") public void after(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("方法名称: " + methodName + "执行完毕"); } }
执行结果为:
方法名称: add,参数为 : [2, 2] 方法名称: add执行完毕 4 方法名称: sub,参数为 : [4, 2] 方法名称: sub执行完毕 2
需要注意的是:在后置通知的时候还不能获取目标方法的处理结果!
③:返回通知:
在原先的LogAspect中继续追加返回通知
package com.lql.spring07; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * @author: lql * @date: 2019.10.29 * Description: */ @Component @Aspect public class LogAspect { //声明该方法是个前置通知,在目标方法执行前执行 @Before(value = "execution(* com.lql.spring07.*.*(..))") public void before(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("方法名称: " + methodName + ",参数为 : " + args); } //后置通知:方法执行后执行,无论是否执行完毕 @After("execution(* com.lql.spring07.*.*(..))") public void after(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("方法名称: " + methodName + "执行完毕"); } /** * 返回通知:在方法正常结束时执行的代码 * @param joinPoint 连接点,可以获取详细信息 * @param result 接收到的返回值 */ @AfterReturning(value = "execution(* com.lql.spring07.*.*(..))",returning = "result") public void afterReturn(JoinPoint joinPoint,Object result) { String name = joinPoint.getSignature().getName(); System.out.println(name +"()计算结果为:" + result); } }
执行结果:
方法名称: add,参数为 : [2, 2] 方法名称: add执行完毕 add()计算结果为:4 4 方法名称: sub,参数为 : [4, 2] 方法名称: sub执行完毕 sub()计算结果为:2 2
④:异常通知:
先修改OperationImpl的sub(),手动抛出一个异常
@Override public int sub(int i, int j) { int result = i-j; throw new RuntimeException(); }
修改LogAspect:增加异常通知
package com.lql.spring07; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * @author: lql * @date: 2019.10.29 * Description: */ @Component @Aspect public class LogAspect { //声明该方法是个前置通知,在目标方法执行前执行 @Before(value = "execution(* com.lql.spring07.*.*(..))") public void before(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("方法名称: " + methodName + ",参数为 : " + args); } //后置通知:方法执行后执行,无论是否执行完毕 @After("execution(* com.lql.spring07.*.*(..))") public void after(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("方法名称: " + methodName + "执行完毕"); } /** * 返回通知:在方法正常结束时执行的代码 * @param joinPoint 连接点,可以获取详细信息 * @param result 接收到的返回值 */ @AfterReturning(value = "execution(* com.lql.spring07.*.*(..))",returning = "result") public void afterReturn(JoinPoint joinPoint,Object result) { String name = joinPoint.getSignature().getName(); System.out.println(name +"()计算结果为:" + result); } /** * 异常通知:在目标方法出现异常时会执行,可以访问到异常对象,也能指定具体的异常后执行代码 * @param joinPoint 连接点,可以获取详细信息 * @param e 捕获异常信息 */ @AfterThrowing(value = "execution(* com.lql.spring07.*.*(..))",throwing = "e") public void afterThrowing(JoinPoint joinPoint,Exception e) { String name = joinPoint.getSignature().getName(); System.out.println(name +"()异常为 :" +e); } }
执行结果为:
方法名称: add,参数为 : [2, 2] 方法名称: add执行完毕 add()计算结果为:4 4 方法名称: sub,参数为 : [4, 2] 方法名称: sub执行完毕 sub()异常为 :java.lang.RuntimeException Exception in thread "main" java.lang.RuntimeException...
⑤:环绕通知:
环绕通知几乎能干上述四种通知,比较全能,就相当于动态代理的全过程,但是不一定是最好的。为了观赏性,取消上述定义的手工抛异常,修改LogAspect.java,取消之前定义好的所有通知,只留下环绕通知:
package com.lql.spring07; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * @author: lql * @date: 2019.10.29 * Description: */ @Component @Aspect public class LogAspect { /** * 环绕通知:需要携带ProceedingJoinPoint类型的参数,环绕通知类似于动态代理的全过程 * * @param join 可以决定是否执行目标方法 * 环绕通知必须由返回值,返回值即为目标方法的返回值 */ @Around("execution(* com.lql.spring07.*.*(..))") public Object around(ProceedingJoinPoint join) { Object result = null; String methodName = join.getSignature().getName(); try { //前置通知 System.out.println(methodName + "()方法,参数为:" + Arrays.asList(join.getArgs())); //执行目标方法 result = join.proceed(); //返回通知 System.out.println(methodName + "()方法返回值为:" + result); } catch (Throwable throwable) { throwable.printStackTrace(); //异常通知 System.out.println(methodName + "()方法出现的异常为:" + throwable); } //后置通知 System.out.println(methodName + "()方法执行完毕"); return result; } }
执行结果:
add()方法,参数为:[2, 2] add()方法返回值为:4 add()方法执行完毕 4 sub()方法,参数为:[4, 2] sub()方法返回值为:2 sub()方法执行完毕 2
如果有异常,那么结果为:
至此,五大通知就告一段落了,我们也可以给切面定义优先级,如果有多个切面,可以在每个切面上使用@Order来标明优先级,比如这样
@Order后面的数值越小,优先级就越高!
所以上述完整的切面代码如下所示:
package com.lql.spring07; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * @author: lql * @date: 2019.10.29 * Description: */ @Order(2) @Component @Aspect public class LogAspect { //声明该方法是个前置通知,在目标方法执行前执行 @Before(value = "execution(* com.lql.spring07.*.*(..))") public void before(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("方法名称: " + methodName + ",参数为 : " + args); } //后置通知:方法执行后执行,无论是否执行完毕 @After("execution(* com.lql.spring07.*.*(..))") public void after(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("方法名称: " + methodName + "执行完毕"); } /** * 返回通知:在方法正常结束时执行的代码 * @param joinPoint 连接点,可以获取详细信息 * @param result 接收到的返回值 */ @AfterReturning(value = "execution(* com.lql.spring07.*.*(..))",returning = "result") public void afterReturn(JoinPoint joinPoint,Object result) { String name = joinPoint.getSignature().getName(); System.out.println(name +"()计算结果为:" + result); } /** * 异常通知:在目标方法出现异常时会执行,可以访问到异常对象,也能指定具体的异常后执行代码 * @param joinPoint 连接点,可以获取详细信息 * @param e 捕获异常信息 */ @AfterThrowing(value = "execution(* com.lql.spring07.*.*(..))",throwing = "e") public void afterThrowing(JoinPoint joinPoint,Exception e) { String name = joinPoint.getSignature().getName(); System.out.println(name +"()异常为 :" +e); } /** * 环绕通知:需要携带ProceedingJoinPoint类型的参数,环绕通知类似于动态代理的全过程 * * @param join 可以决定是否执行目标方法 * 环绕通知必须由返回值,返回值即为目标方法的返回值 */ @Around("execution(* com.lql.spring07.*.*(..))") public Object around(ProceedingJoinPoint join) { Object result = null; String methodName = join.getSignature().getName(); try { //前置通知 System.out.println(methodName + "()方法,参数为:" + Arrays.asList(join.getArgs())); //执行目标方法 result = join.proceed(); //返回通知 System.out.println(methodName + "()方法返回值为:" + result); } catch (Throwable throwable) { throwable.printStackTrace(); //异常通知 System.out.println(methodName + "()方法出现的异常为:" + throwable); } //后置通知 System.out.println(methodName + "()方法执行完毕"); return result; } }
不难看出上面用粗体标识的代码太过重复,以后维护也不容易,所以有个重用切点的概念!使用@PointCut来声明切入点表达式,后面的其他通知使用到的时候直接引用方法名即可,定义如下:
@Pointcut(value ="execution(* com.lql.spring07.*.*(..))") public void ref(){}
更换后完整的代码:
package com.lql.spring07; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * @author: lql * @date: 2019.10.29 * Description: */ @Order(2) @Component @Aspect public class LogAspect { @Pointcut(value ="execution(* com.lql.spring07.*.*(..))") public void ref(){} //声明该方法是个前置通知,在目标方法执行前执行 @Before(value = "ref()") public void before(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("方法名称: " + methodName + ",参数为 : " + args); } //后置通知:方法执行后执行,无论是否执行完毕 @After("ref()") public void after(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("方法名称: " + methodName + "执行完毕"); } /** * 返回通知:在方法正常结束时执行的代码 * @param joinPoint 连接点,可以获取详细信息 * @param result 接收到的返回值 */ @AfterReturning(value = "ref()",returning = "result") public void afterReturn(JoinPoint joinPoint,Object result) { String name = joinPoint.getSignature().getName(); System.out.println(name +"()计算结果为:" + result); } /** * 异常通知:在目标方法出现异常时会执行,可以访问到异常对象,也能指定具体的异常后执行代码 * @param joinPoint 连接点,可以获取详细信息 * @param e 捕获异常信息 */ @AfterThrowing(value = "ref()",throwing = "e") public void afterThrowing(JoinPoint joinPoint,Exception e) { String name = joinPoint.getSignature().getName(); System.out.println(name +"()异常为 :" +e); } /** * 环绕通知:需要携带ProceedingJoinPoint类型的参数,环绕通知类似于动态代理的全过程 * * @param join 可以决定是否执行目标方法 * 环绕通知必须由返回值,返回值即为目标方法的返回值 */ @Around("ref()") public Object around(ProceedingJoinPoint join) { Object result = null; String methodName = join.getSignature().getName(); try { //前置通知 System.out.println(methodName + "()方法,参数为:" + Arrays.asList(join.getArgs())); //执行目标方法 result = join.proceed(); //返回通知 System.out.println(methodName + "()方法返回值为:" + result); } catch (Throwable throwable) { throwable.printStackTrace(); //异常通知 System.out.println(methodName + "()方法出现的异常为:" + throwable); } //后置通知 System.out.println(methodName + "()方法执行完毕"); return result; } }
二:基于配置文件的方式配置AOP
首先统统还原为最原始的样子,去掉所有的注解!xml配置如下
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!--配置bean--> <bean id="operationImpl" class="com.lql.spring08.OperationImpl"></bean> <!--配置切面的Bean--> <bean id="logAspect" class="com.lql.spring08.LogAspect"></bean> <!--配置AOP--> <aop:config> <!--配置切点表达式--> <aop:pointcut id="pointcut" expression="execution(* com.lql.spring08.*.*(..))"/> <!--配置切面及通知--> <aop:aspect ref="logAspect" order="2"> <aop:before method="before" pointcut-ref="pointcut"></aop:before> <aop:after method="after" pointcut-ref="pointcut"></aop:after> <aop:after-returning method="afterReturn" pointcut-ref="pointcut" returning="result"></aop:after-returning> <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"></aop:after-throwing> </aop:aspect> <!--可以定义多个切面,如这样--> <aop:aspect ref="logAspect2" order="1"> <!--省略。。。--> </aop:aspect> </aop:config> </beans>
运行结果如:
方法名称: add,参数为 : [2, 2]
方法名称: add执行完毕
add()计算结果为:4
4
方法名称: sub,参数为 : [4, 2]
方法名称: sub执行完毕
sub()计算结果为:2
2