8.Spring系列之AOP
Posted 飘来荡去
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了8.Spring系列之AOP相关的知识,希望对你有一定的参考价值。
一、什么是AOP?
AOP是面向切面编程(Aspect-Oriented Programming),它是一种新的方法论,是对传统的面向对象编程的一种补充,更具体的说是在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。
二、需求
从AOP角度来看,我们开发过程中有哪些现有的需求可以改造成以Spring AOP来实现?以下举个简单例子:
首先,定义一个计算器接口
public interface Calculator { // 加法接口 int add(int x,int y); // 减法接口 int sub(int x,int y); }
接着,定义一个计算器实现接口,并在其中加入操作日志
@Service public class CalculatorImpl implements Calculator{ @Override public int add(int x, int y) { System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y)); int z = x + y; System.out.println(String.format("接口执行结果,z=%s", z)); return z; } @Override public int sub(int x, int y) { System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y)); int z = x - y; System.out.println(String.format("接口执行结果,z=%s", z)); return z; } }
然后,在IOC容器上添加注解扫描包
<?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: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/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!-- 开启注解扫描包 --> <context:component-scan base-package="com.spring"></context:component-scan> </beans>
最后,写个测试方法
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator cal = ctx.getBean(Calculator.class); cal.add(20, 10); cal.sub(20, 10); /** * 执行结果: * 接口接收参数,x=20,y=10 * 接口执行结果,z=30 * 接口接收参数,x=20,y=10 * 接口执行结果,z=10 */ } }
我们在每个接口执行真正的业务之前都添加了日志输出,在真正的业务执行之后也添加了日志输出,看着很简单,但如果一个接口内几百个乃至上千个接口呢,那么就得写好多日志;再者,如果你拼命的把日志写好了,发现里面有个错别字或者不符合规范,又
得重新改几百个乃至几千个日志输出的信息。
解决方案:
1.使用java动态代理来完成这个事情
2.使用Spring的AOP面向切面编程
三、动态代理
首先,我们先将service上的打印日志输出注释掉,引入动态代理类:
public class CalculatorProxy { // 要代理的对象(注意:代理的是接口) private Calculator target; // 初始化 public CalculatorProxy(Calculator target) { super(); this.target = target; } // 返回代理对象 public Calculator getLoggingProxy(){ Calculator proxy = null; ClassLoader loader = target.getClass().getClassLoader(); Class[] interfaces = new Class[]{Calculator.class}; InvocationHandler handler = new InvocationHandler() { /** * proxy: 代理对象。 一般不使用该对象 * method: 正在被调用的方法 * args: 调用方法传入的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args)throws Throwable { String methodName = method.getName(); //打印日志 System.out.println(String.format("接口接收参数,x=%s,y=%s", Arrays.asList(args).get(0),Arrays.asList(args).get(1))); //调用目标方法 Object result = null; result = method.invoke(target, args); //打印日志 int res = 0; if("add".equals(methodName)) { res = Integer.parseInt(Arrays.asList(args).get(0).toString()) + Integer.parseInt(Arrays.asList(args).get(1).toString()); }else { res = Integer.parseInt(Arrays.asList(args).get(0).toString()) - Integer.parseInt(Arrays.asList(args).get(1).toString()); } System.out.println(String.format("接口执行结果,z=%s", res)); return result; } }; /** * loader: 代理对象使用的类加载器。 * interfaces: 指定代理对象的类型. 即代理代理对象中可以有哪些方法. * h: 当具体调用代理对象的方法时, 应该如何进行响应, 实际上就是调用 InvocationHandler 的 invoke 方法 */ proxy = (Calculator) Proxy.newProxyInstance(loader, interfaces, handler); return proxy; } }
测试动态代理日志输出:
public class Main { public static void main(String[] args) { // 被代理对象,是一个接口实现类,即哪个实现类被代理 Calculator calculator = new CalculatorImpl(); // 返回代理对象 calculator = new CalculatorProxy(calculator).getLoggingProxy(); calculator.add(20, 10); calculator.sub(20, 10); /** * 执行结果: * 接口接收参数,x=20,y=10 * 接口执行结果,z=30 * 接口接收参数,x=20,y=10 * 接口执行结果,z=10 */ } }
四、AOP
1.我们必须要清楚的几个概念:
aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar
其次,定义一个日志切面,并且创建一个前置通知方法:
// 声明为一个切面 @Aspect // 交给IOC容器管理 @Component public class CalculatorAspectLogging { /** * 让这个方法知道该方法在哪些类的哪些方法开始之前执行 * 声明该方法是一个前置通知 * 表达式为:public 返回值 包名 接口名 参数类型,参数只需要类型 * 参数:JoinPoint为连接点,可以获取参数 */ @Before("execution(public int com.spring.service.Calculator.add(int, int))") public void beforeMethod(JoinPoint joinPoint) { // 获取方法名称 String methodName = joinPoint.getSignature().getName(); //获取方法参数 List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println(String.format("接口方法:%s接收参数,x=%s,y=%s", methodName,args.get(0),args.get(1))); } }
表达式到对应的接口方法复制即可:
接着,在IOC容器中配置扫描表,以及开启AOP注解扫描:
<?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:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" 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-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd"> <!-- 开启注解扫描包 --> <context:component-scan base-package="com.spring"></context:component-scan> <!-- 开启AOP注解扫描,让AOP注解生效 --> <!-- 使切面注解起作用 --> <!-- 当我们调用一个目标方法,而那个目标方法跟我们标注的通知注解相匹配的时候,aop框架自动的为目标方法所在的类生成一个代理对象 --> <!-- 在目标方法执行之前,先执行有前置通知注解标注的方法 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
最后,测试AOP前置通知:
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10); //执行结果:接口方法:add接收参数,x=20,y=10 } }
这样,我们前置通知就可以了,但是怎么实现方法调用前输出日志,调用后也输出日志呢?我们还需要添加一个后置通知
// 声明为一个切面 @Aspect // 交给IOC容器管理 @Component public class CalculatorAspectLogging { /** * 让这个方法知道该方法在哪些类的哪些方法开始之前执行 * 声明该方法是一个前置通知 * 表达式为:public 返回值 包名 接口名 参数类型,参数只需要类型 * 参数:JoinPoint为连接点,可以获取参数 */ @Before("execution(public int com.spring.service.Calculator.add(int, int))") public void beforeMethod(JoinPoint joinPoint) { // 获取方法名称 String methodName = joinPoint.getSignature().getName(); //获取方法参数 List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println(String.format("接口方法:%s接收参数,x=%s,y=%s", methodName,args.get(0),args.get(1))); } /** * 声明该方法是后置通知 * @param joinPoint */ @After("execution(public int com.spring.service.Calculator.add(int, int))") public void afterMethod(JoinPoint joinPoint) { int res = 0; // 获取方法名称 String methodName = joinPoint.getSignature().getName(); //获取方法参数 List<Object> args = Arrays.asList(joinPoint.getArgs()); if("add".equals(methodName)) { res = Integer.parseInt(String.valueOf(args.get(0))) + Integer.parseInt(String.valueOf(args.get(1))); }else { res = Integer.parseInt(String.valueOf(args.get(0))) - Integer.parseInt(String.valueOf(args.get(1))); } System.out.println(String.format("接口方法:%s执行结果,z=%s", methodName,res)); } }
再次运行测试方法:
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10); /** * 执行结果: * 接口方法:add接收参数,x=20,y=10 * 接口方法:add执行结果,z=30 */ } }
这样,就大功告成 ! 但是我们不是要在所有方法调用之前和调用之后都织入通知么?只需要修改通知表达式,我们将表达式修改为:
@Before("execution(public int com.spring.service.Calculator.*(int, int))") @After("execution(public int com.spring.service.Calculator.*(int, int))") *表示在com.spring.service.Calculator包下的所有类都匹配的意思
再次执行测试程序:
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10); calculator.sub(20, 10);//记得该方法要添加调用 /** * 执行结果: * 接口方法:add接收参数,x=20,y=10 * 接口方法:add执行结果,z=30 * 接口方法:sub接收参数,x=20,y=10 * 接口方法:sub执行结果,z=10 */ } }
3.AOP表达式的语法规则
方法签名 AspectJ 切入点表达式: •最典型的切入点表达式时根据方法的签名来匹配各种方法: –execution * com.spring.Calculator.*(..): 匹配 Calculator 中声明的所有方法,第一个 * 代表任意修饰符及任意返回值,第二个 * 代表任意方法, 参数括号内的 .. 匹配任意数量的参数, 若目标类与接口与该切面在同一个包中, 可以省略包名. –execution * com.spring.*.*(..):跟上方不同的是这里匹配的是spring包下的所有类以及所有方法 –execution public * Calculator.*(..): 匹配 Calculator 接口的所有公有方法. –execution public double Calculator.*(..): 匹配 Calculator 中返回 double 类型数值的方法 –execution public double Calculator.*(double, ..): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数 –execution public double Calculator.*(double, double): 匹配参数类型为 double, double 类型的方法.
另外,还有比较复杂的合并表达式,在 AspectJ 中, 切入点表达式可以通过操作符 &&, ||, ! 结合起来:
@Pointcut("execution(* *.add*(int, ..)) || execution(* *.sub(int, ..))")
4.通知类型
通过前面的案例,我们大概了解了下前置通知和后置通知,其实AspectJ为我们提供了5种类型的通知注解,分别是:前置通知、后置通知、返回通知、异常通知、环绕通知。
①.后置通知
是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候;
②.返回通知
无论连接点是正常返回还是抛出异常,后置通知都会执行,如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知
返回通知是在方法正常结束受执行的代码
返回通知是可以访问到方法返回值的
③.异常通知
只在连接点抛出异常时才执行异常通知
将 throwing 属性添加到 @AfterThrowing 注解中,也可以访问连接点抛出的异常,Throwable 是所有错误和异常类的超类,所以在异常通知方法可以捕获到任何错误和异常
④.环绕通知
环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点,甚至可以控制是否执行连接点
对于环绕通知来说,连接点的参数类型必须是 ProceedingJoinPoint,它是 JoinPoint 的子接口,允许控制何时执行,是否执行连接点
在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法,如果忘记这样做就会导致通知被执行了,但目标方法没有被执行
注意:
- 环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed(); 的返回值,否则会出现空指针异常
- 环绕通知类似于动态代理的全过程
- ProceedingJoinPoint类型的参数可以决定是否执行目标方法,且环绕通知必须有返回值,返回值即为目标方法的返回值。
概念性的东西,看了这些都不太明白,通过下面的说明就能清楚了解上方5个通知分别的作用了 !
我们都知道Spring的事务机制(尚未整理,后面几个章节详细说明)是基于AOP实现的,在JDBC中,每次我们操作业务,都使用try...catch包围起来,当业务方法抛出异常,可以及时捕获异常并且手动回滚,所以我们大概能知道使用AOP实现事务的机制大概为:
try { System.out.println("这是前置通知"); //执行真正的业务方法 System.out.println("这是返回通知"); } catch (Exception e) { //抛出异常捕获并且回滚 System.out.println("这是异常通知"); } System.out.println("这是后置通知");
现在,不同的通知类型功能就清晰许多了。
5.通知类型案例
说明:前置通知和后置通知前面已经涉及,不再给出案例,以下直接给出切面和测试结果代码,接口等以上方为例 !
①.返回通知
/** * 返回通知 * 在方法正常结束后执行的通知 * 可以访问到方法的返回值 * @param joinPoint */ @AfterReturning(value = "execution(public int com.spring.service.Calculator.*(int, int))",returning="res") public void afterReturnningMethod(JoinPoint joinPoint,Object res) { String methodName = joinPoint.getSignature().getName(); System.out.println(String.format("接口方法:%s返回通知返回结果:%s", methodName,res)); }
测试返回通知:
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10); calculator.sub(20, 10); /** * 执行结果: * 接口方法:add接收参数,x=20,y=10 // 前置通知 * 接口方法:add执行结果,z=30 // 后置通知 * 接口方法:add返回通知返回结果:30 // 返回通知 * 接口方法:sub接收参数,x=20,y=10 // 前置通知 * 接口方法:sub执行结果,z=10 // 后置通知 * 接口方法:sub返回通知返回结果:10 // 返回通知 */ } }
②.异常通知
首先,新增一个除法方法
public interface Calculator { int add(int x,int y); int sub(int x,int y); int div(int x,int y); }
其次,实现这个接口方法
@Service public class CalculatorImpl implements Calculator{ @Override public int add(int x, int y) { //System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y)); int z = x + y; //System.out.println(String.format("接口执行结果,z=%s", z)); return z; } @Override public int sub(int x, int y) { //System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y)); int z = x - y; //System.out.println(String.format("接口执行结果,z=%s", z)); return z; } @Override public int div(int x, int y) { int z = x / y; return z; } }
再者,添加异常通知
/** * 在目标方法出现异常时会执行的通知 * 可以访问到异常对象且可以指定在出现特定异常时在执行通知 * @param joinPoint * @param e */ @AfterThrowing(value = "execution(public int com.spring.service.Calculator.*(int, int))",throwing = "e") public void afterThrowingMethod(JoinPoint joinPoint,Exception e) { String methodName = joinPoint.getSignature().getName(); System.out.println(String.format("接口方法:%s返回通知返回结果:%s", methodName,e)); }
最后,测试异常通知
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10); calculator.sub(20, 10); calculator.div(10, 0); /** *执行结果: *接口方法:add接收参数,x=20,y=10 *接口方法:add执行结果,z=30 *接口方法:add返回通知返回结果:30 *接口方法:sub接收参数,x=20,y=10 *接口方法:sub执行结果,z=10 *接口方法:sub返回通知返回结果:10 *接口方法:div接收参数,x=10,y=0 *接口方法:div执行结果,z=10 *接口方法:div异常通知返回结果:java.lang.ArithmeticException: / by zero */ } }
注意:在我们执行calculator.div(10,0)之后,出现了异常,而返回通知是正常执行才有返回结果,div这个方法并没有返回通知的日志。
同时,我们可以指定某个异常时才会打印异常日志:
/** * 在目标方法出现异常时会执行的通知 * 可以访问到异常对象且可以指定在出现特定异常时在执行通知 * @param joinPoint * @param e */ @AfterThrowing(value = "execution(public int com.spring.service.Calculator.*(int, int))",throwing = "e") public void afterThrowingMethod(JoinPoint joinPoint,NullPointerException e) { //这里指定了空指针才会执行该通知 String methodName = joinPoint.getSignature().getName(); System.out.println(String.format("接口方法:%s异常通知返回结果:%s", methodName,e)); }
再次执行测试代码
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10); calculator.sub(20, 10); calculator.div(10, 0); /** *执行结果: *接口方法:add接收参数,x=20,y=10 *接口方法:add执行结果,z=30 *接口方法:add返回通知返回结果:30 *接口方法:sub接收参数,x=20,y=10 *接口方法:sub执行结果,z=10 *接口方法:sub返回通知返回结果:10 *接口方法:div接收参数,x=10,y=0 *接口方法:div执行结果,z=10 *Exception in thread "main" java.lang.ArithmeticException: / by zero 这里不是异常通知日志,现在的异常通知只有当NullPointExcetpion才会执行 */ } }
③.环绕通知
为了区别,我们先把所有的通知去掉,只测试环绕通知:
首先,添加通知
/** * 环绕通知必须携带ProceedingJoinPoint类型的参数 * 环绕通知相当于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法 * 环绕通知必须有返回值,返回值是被代理方法即目标方法的返回值 * @param joinPoint */ @Around("execution(public int com.spring.service.Calculator.*(int, int))") public Object aroundMethod(ProceedingJoinPoint joinPoint) { System.out.println("这是环绕通知"); return 1111; }
测试环绕通知
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10); calculator.sub(20, 10); calculator.div(10, 10); /** * 执行后输出: * 这是环绕通知 * 这是环绕通知 * 这是环绕通知 */ } }
ok,在调用目标方法时环绕通知已经可以起作用了。那么难道这就是环绕通知?这只是环绕通知的简单测试,环绕通知可以整合所有的通知,如下:
// 声明为一个切面 @Aspect // 交给IOC容器管理 @Component public class CalculatorAspectLogging { /** * 环绕通知必须携带ProceedingJoinPoint类型的参数 * 环绕通知相当于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法 * 环绕通知必须有返回值,返回值是被代理方法即目标方法的返回值 * @param joinPoint */ @Around("execution(public int com.spring.service.Calculator.*(int, int))") public Object aroundMethod(ProceedingJoinPoint joinPoint) { Object res = null; String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); try { // 前置通知 System.out.println(String.format("接口方法:%s[前置通知]接收参数,x=%s,y=%s", methodName,args.get(0),args.get(1))); // 执行目标方法(即接口内的add、sub、div方法) res = joinPoint.proceed(); // 返回通知 System.out.println(String.format("接口方法:%s[返回通知]返回结果,z=%s", methodName,Integer.parseInt(String.valueOf(args.get(0)))+Integer.parseInt(String.valueOf(args.get(1))))); return res; } catch (Throwable e) { // 异常通知 System.out.println(String.format("接口方法:%s[异常通知]返回异常结果:%s", methodName,e)); } // 后置通知 System.out.println(String.format("接口方法:%s[后置通知]返回结果,res=%s", methodName,res)); return res; } }
测试环绕通知
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10); calculator.sub(20, 10); calculator.div(10, 10); /** * 接口方法:add[前置通知]接收参数,x=20,y=10 * 接口方法:add[返回通知]返回结果,z=30 * 接口方法:sub[前置通知]接收参数,x=20,y=10 * 接口方法:sub[返回通知]返回结果,z=30 * 接口方法:div[前置通知]接收参数,x=10,y=10 * 接口方法:div[返回通知]返回结果,z=20 */ } }
这里结果没有异常通知和后置通知,所以我们把div除法方法设置为可以抛出异常:
calculator.div(10, 0);
再次执行测试方法:
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator calculator = (Calculator) ctx.getBean(Calculator.class); calculator.add(20, 10战五渣系列之八(绝杀AOP)8 -- 深入使用Spring -- 4... Spring的AOP
Spring读源码系列之AOP--01---aop基本概念扫盲---上