基于注解的 Spring AOP 开发
基本概念
定义切入点函数 1 2 3 4 5 @After (value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))" ) public void after () { System.out.println("最终通知...." ); }
还可采用 @PointCut 关键字定义切入点表达式:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Pointcut ("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))" )private void myPointcut () {}@After (value="myPointcut()" )public void afterDemo () { System.out.println("最终通知...." ); }
切入点指示符 通配符 在定义匹配表达式时,通配符几乎随处可见如 *
, ..
, +
,它们的含义如下:
..
:匹配方法定义中任意数量的参数,此外还匹配类定义中任意数量包
1 2 3 4 execution(public * *(..)) within(com.zejian.dao..*)
1 2 within(com.zejian.dao.DaoUser+)
1 2 3 4 within(com.zejian.service..*) execution(* set*(int ))
类型签名表达式 为了方便类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字。其语法格式如下:
type name 则使用包名或者类名替换即可。
1 2 3 4 5 6 7 8 9 10 11 @Pointcut ("within(com.zejian.dao..*)" )@Pointcut ("within(com.zejian.dao.UserDaoImpl)" )@Pointcut ("within(com.zejian.dao.UserDaoImpl+)" )@Pointcut ("within(com.zejian.dao.UserDao+)" )
方法签名表达式 如果想根据方法签名进行过滤,关键字 execution 可以帮到我们,语法表达式如下
1 2 3 4 5 execution(<scope> <return -type> <fully-qualified-class -name >.*(parameters ))
对于给定的作用域、返回值类型、完全限定类名以及参数匹配的方法将会应用切点函数指定的通知,这里给出模型案例:
1 2 3 4 5 6 7 8 9 10 11 @Pointcut ("execution(* com.zejian.dao.UserDaoImpl.*(..))" )@Pointcut ("execution(public * com.zejian.dao.UserDaoImpl.*(..))" )@Pointcut ("execution(public int com.zejian.dao.UserDaoImpl.*(..))" )@Pointcut ("execution(public * com.zejian.dao.UserDaoImpl.*(int , ..))" )
其他指示符
this:用于匹配当前 AOP 代理对象类型的执行方法;请注意是 AOP代理对象 的类型匹配,这样就可能包括引入接口也类型匹配
1 2 3 @Pointcut ("this(com.zejian.spring.springAop.dao.UserDao)" )private void myPointcut2 () {}
bean:Spring AOP 扩展的,AspectJ没有对应指示符,用于匹配特定名称的 Bean 对象的执行方法
1 2 3 @Pointcut ("bean(*Service)" )private void myPointcut1 () {}
target:用于匹配当前目标对象类型的执行方法
1 2 3 @Pointcut ("target(com.zejian.spring.springAop.dao.UserDao)" )private void myPointcut3 () {}
@within:用于匹配所有持有指定注解类型内的方法;请注意与 within 是有区别的,within是用于匹配指定类型内的方法执行;
1 2 3 @Pointcut ("@within(com.zejian.spring.annotation.MarkerAnnotation)" )private void myPointcut4 () {}
@annotation(com.zejian.spring.MarkerMethodAnnotation) : 根据所应用的注解进行方法过滤
1 2 3 @Pointcut ("@annotation(com.zejian.spring.annotation.MarkerAnnotation)" )private void myPointcut5 () {}
切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或者&&、||、!),如下一个简单例子:
1 2 3 4 5 6 7 @Pointcut ("target(com.zejian.spring.springAop.dao.UserDao) !within(com.zejian.dao..*)" )private void myPointcut6 () {}@Pointcut ("target(com.zejian.spring.springAop.dao.UserDao)&&execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))" )private void myPointcut7 () {}
5 种通知函数 通知主要分5种类型,分别是前置通知、后置通知、异常通知、最终通知以及环绕通知,下面分别介绍。
前置通知 @Before
前置通知通过 @Before 注解进行标注,并可直接传入切点表达式的值,该通知在目标函数执行前执行,JoinPoint 是 Spring 提供的静态变量,通过它,可以获取目标对象的信息,如类名称,方法参数,方法名等,该参数可选。
1 2 3 4 5 6 7 8 @Before ("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))" )public void before (JoinPoint joinPoint) { System.out.println("我是前置通知" ); }
后置通知 @AfterReturning
通过 @AfterReturning 标注,该函数在目标函数执行完成后执行,并可以获取到目标函数最终的返回值 returnVal,当目标函数没有返回值时,returnVal 将返回 null。
必须通过returning = “returnVal”注明参数的名称而且必须与通知函数的参数名称相同。
1 2 3 4 5 6 7 @AfterReturning (value="execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))" )public void AfterReturning () { System.out.println("我是后置通知..." ); }
1 2 3 4 5 6 7 8 @AfterReturning (value="execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))" ,returning = "returnVal" )public void AfterReturning (JoinPoint joinPoint,Object returnVal) { System.out.println("我是后置通知...returnVal+" +returnVal); }
异常通知 @AfterThrowing
该通知只有在异常时才会被出发,并由 throwing 来声明一个接受异常信息的变量,同样异常通知也用于 Joinpoing 参数,需要时可以加上。
1 2 3 4 5 6 7 8 @AfterThrowing (value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))" ,throwing = "e" )public void afterThrowable (Throwable e) { System.out.println("出现异常:msg=" +e.getMessage()); }
最终通知 @After
该通知有点类似于 finally 代码块,只要应用了,无论什么情况下都会执行。
1 2 3 4 5 6 7 8 @After ("execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))" )public void after (JoinPoint joinPoint) { System.out.println("最终通知...." ); }
环绕通知 @Around
环绕通知既可在目标方法前执行也可以在目标方法后执行,更重要的是环绕通知可以控制目标方法是否指向执行,但即使如此,我们应该尽量以最简单的方式满足需求,在仅需目标方法前执行时,使用前置通知而非环绕通知。
第一个参数必须是 ProceedingJoinPoint,通过该对象的 proceed() 方法来执行目标函数,proceed() 的返回值就是环绕通知的返回值。
1 2 3 4 5 6 7 8 @Around ("execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))" )public Object around (ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("我是环绕通知前...." ); Object obj= (Object) joinPoint.proceed(); System.out.println("我是环绕通知后...." ); return obj; }
通知传递参数 在 Spring AOP 中,除了 execution 和 bean 指示符不能传递参数给通知方法,其他指示符都可以将匹配的方法相应参数或对象 自动传递给通知方法。获取到匹配的方法参数后通过 argNames
属性指定参数名。如下,args(param)、argNames=”param”、before(int param)这三个参数命名必须保持一致 。
1 2 3 4 @Before (value="args(param)" , argNames="param" ) public void before (int param) { System.out.println("param:" + param); }
也可直接使用 args 指示符不带 argNames 声明参数,如下:
1 2 3 4 5 @Before ("execution(public * com.zejian..*.addUser(..)) && args(userId,..)" ) public void before (int userId) { System.out.println("userId:" + userId); }
args(userId,..)表达式表示:只匹配那些至少接收一个参数而且传入的类型必须与userId一致的方法,传递的参数可以简单类型或者对象,只有参数和目标方法也匹配时才有会有值传递进来。
Aspect 优先级 如果有多个通知需要在同一个切点函数指定的过滤目标方法上执行,在所有前置通知函数中,优先级最高的通知函数将会被先执行,在所有后置通知函数中,优先级最高的通知函数将会最后执行。
新建名为 aspectdemo 的工程,首先引入核心依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > </dependency >
添加启动类
1 2 3 4 5 6 7 8 @SpringBootApplication public class AspectdemoApplication { public static void main (String[] args) { SpringApplication.run(AspectdemoApplication.class , args ) ; } }
开启 AspectJAutoProxy ,添加配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.hoo.aspectdemo.config;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @EnableAspectJAutoProxy public class AspectJConfig {}
添加测试用 Controller
1 2 3 4 5 6 7 @RestController public class TestController { @GetMapping ("/order" ) public String testOrder () { return "success" ; } }
首先添加优先级较高的切面
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 package com.hoo.aspectdemo.aspect;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.core.Ordered;import org.springframework.stereotype.Component;@Aspect @Component public class AspectOrderZero implements Ordered { @Override public int getOrder () { return 0 ; } @Pointcut ("execution(* com.hoo.aspectdemo.controller.TestController.testOrder())" ) public void pointCut () { } @Before ("pointCut()" ) public void beforeOne () { System.out.println("前置通知 ..优先级 0 .. 执行顺序 1" ); } @Before ("pointCut()" ) public void beforeTwo () { System.out.println("前置通知 ..优先级 0 .. 执行顺序 2" ); } @AfterReturning ("pointCut()" ) public void afterReturningThree () { System.out.println("后置通知 ..优先级 0 .. 执行顺序 3" ); } @AfterReturning ("pointCut()" ) public void afterReturningFour () { System.out.println("后置通知 ..优先级 0 .. 执行顺序 4" ); } }
然后添加优先级较低的切面
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 package com.hoo.aspectdemo.aspect;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.core.Ordered;import org.springframework.stereotype.Component;@Aspect @Component public class AspectOrderTwo implements Ordered { @Override public int getOrder () { return 2 ; } @Pointcut ("execution(* com.hoo.aspectdemo.controller.TestController.testOrder())" ) public void pointCut () { } @Before ("pointCut()" ) public void beforeOne () { System.out.println("前置通知 ..优先级 2 .. 执行顺序 1" ); } @Before ("pointCut()" ) public void beforeTwo () { System.out.println("前置通知 ..优先级 2 .. 执行顺序 2" ); } @AfterReturning ("pointCut()" ) public void afterReturningThree () { System.out.println("后置通知 ..优先级 2 .. 执行顺序 3" ); } @AfterReturning ("pointCut()" ) public void afterReturningFour () { System.out.println("后置通知 ..优先级 2 .. 执行顺序 4" ); } }
运行,即可在控制台看到如下信息:
1 2 3 4 5 6 7 8 前置通知 ..优先级 0 .. 执行顺序 1 前置通知 ..优先级 0 .. 执行顺序 2 前置通知 ..优先级 2 .. 执行顺序 1 前置通知 ..优先级 2 .. 执行顺序 2 后置通知 ..优先级 2 .. 执行顺序 3 后置通知 ..优先级 2 .. 执行顺序 4 后置通知 ..优先级 0 .. 执行顺序 3 后置通知 ..优先级 0 .. 执行顺序 4
总结:在同一个切面中定义多个通知响应同一个切点函数,执行顺序为声明顺序;如果在不同的切面中定义多个通知响应同一个切点,进入时则优先级高的切面类中的通知函数优先执行,退出时则最后执行。
Spring AOP 简单应用场景
性能监控
首先我们定义用于测试的controller,并模拟这个接口需要 5s 来执行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.hoo.aspectdemo.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class TestController2 { @GetMapping ("/monitor" ) public String testMonitor () throws InterruptedException { Thread.sleep(5 ); return "success" ; } }
然后定义性能监控信息类 MonitorTime
1 2 3 4 5 6 7 8 9 10 11 12 package com.hoo.aspectdemo.aspect;import java.util.Date;public class MonitorTime { private String className; private String methodName; private Date logTime; private long consumeTime; }
然后定义一个监控的切面类:
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 package com.hoo.aspectdemo.aspect;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.util.Date;@Aspect @Component public class TimerAspect { @Pointcut ("within(com.hoo.aspectdemo.controller..*)" ) public void pointCut () { } @Around ("pointCut()" ) public Object logTimer (ProceedingJoinPoint joinPoint) throws Throwable { MonitorTime monitor = new MonitorTime(); String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); monitor.setClassName(className); monitor.setMethodName(methodName); monitor.setLogTime(new Date()); long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long time = System.currentTimeMillis() - start; monitor.setConsumeTime(time); System.out.println(monitor); return result; } }
启动程序,访问http://localhost:8080/monitor,查看控制台,打印出如下信息:
1 MonitorTime{className='com.hoo.aspectdemo.controller.TestController2' , methodName='testMonitor' , logTime=Mon Apr 12 17 :08 :53 CST 2021 , consumeTime=11 }
异常监控
首先定义异常信息类
1 2 3 4 5 6 7 8 9 10 11 12 package com.hoo.aspectdemo.aspect;import java.util.Date;public class ExceptionInfo { private String className; private String methodName; private Date logTime; private String message; }
定义自己的异常类
1 2 3 4 5 6 7 8 package com.hoo.aspectdemo.exception;public class MyException extends Exception { public MyException (String message) { super (message); } }
在 TestController2 中添加测试函数
1 2 3 4 5 6 7 8 @GetMapping ("/monitor/{mode}" ) public String testMonitor2 (@PathVariable Integer mode) throws MyException { if (mode == 0 ){ return "success" ; }else { throw new MyException(" testMonitor() 方法出错了!" ); } }
然后定义异常处理的切面
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 package com.hoo.aspectdemo.aspect;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.util.Date;@Aspect @Component public class ExceptionMonitorAspect { @Pointcut ("within(com.hoo.aspectdemo.controller..*)" ) void exceptionMethod () { } @Around ("exceptionMethod()" ) public Object monitorMethods (ProceedingJoinPoint joinPoint) { try { return joinPoint.proceed(); } catch (Throwable e) { ExceptionInfo info = new ExceptionInfo(); info.setClassName(joinPoint.getTarget().getClass().getName()); info.setMethodName(joinPoint.getSignature().getName()); info.setLogTime(new Date()); info.setMessage(e.getMessage()); System.out.println(info); return e.getMessage(); } } }
浏览器访问 http://localhost:8080/monitor/0 ,可以看到返回 ”success“信息,并在控制台可以看到访问时间日志:
1 MonitorTime{className='com.hoo.aspectdemo.controller.TestController2', methodName='testMonitor2', logTime=Mon Apr 12 21:46:30 CST 2021, consumeTime=11}
浏览器访问 http://localhost:8080/monitor/1 ,可以看到返回 ”testMonitor() 方法出错了!“信息,并在控制台可以看到访问时间日志:
1 ExceptionInfo{className='com.hoo.aspectdemo.controller.TestController2', methodName='testMonitor2', logTime=Mon Apr 12 21:47:15 CST 2021, message=' testMonitor() 方法出错了!'}
Spring AOP 实现原理概要 对于织入过程,一般分为动态织入和静态织入,动态织入在运行时动态地将要增强的代码织入到目标类中,这往往是通过动态代理技术完成的,如 Java JDK 的动态代理(底层通过反射实现)或者 CGLIB 的动态代理(底层通过继承实现)。
AspectJ 采用静态织入的方式。它在编译器使用 acj 编译器把 aspect 类编译成 class 字节码后,在 java 目标类编译时织入,先编译 aspect 类再编译目标类。
JDK 动态代理 首先看一个简单的例子,声明一个 ExInterface 接口,利用 JDK 动态代理技术在 execute() 方法前后加入权限验证和日志记录。
1 2 3 4 public interface ExInterface { void execute () ; }
1 2 3 4 5 6 7 public class A implements ExInterface { @Override public void execute () { System.out.println("---- A.execute() ----" ); } }
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 import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class JDKProxy implements InvocationHandler { private A target; public JDKProxy (A target) { this .target = target; } public ExInterface createProxy () { return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this ); } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if ("execute" .equals(method.getName())) { System.out.println("---- 鉴权 ----" ); Object result = method.invoke(target, args); System.out.println("---- 记录日志 ----" ); return result; }else if ("delete" .equals(method.getName())){ } return method.invoke(target,args); } }
1 2 3 4 5 6 7 8 9 10 11 12 class Test { public static void main (String[] args) { A a = new A(); JDKProxy jdkProxy = new JDKProxy(a); ExInterface proxy = jdkProxy.createProxy(); proxy.execute(); } }
1 2 3 4 ---- 鉴权 ---- ---- A.execute() ---- ---- 记录日志 ----
在 A 的 execute() 方法中没有调用任何权限和日志的 代码,也没有直接操作 a 对象,只是调用了 proxy 代理对象的方法,最终的结果却是预期的,这就是动态代理技术。动态代理的底层通过反射来实现,只要拿到 A 类的 class 文件和 A 类的实现接口,很自然就可以生成相同接口的代理类并调用 a 对象的方法。
实现 Java 动态代理是先决条件为:目标对象必须带接口 ,如果类的接口是 ExInterface,通过该接口,动态代理可以创建与 A 类类型相同的代理对象。
用JDK动态代理,被代理类(目标对象,如A类),必须已有实现接口如(ExInterface),因为JDK提供的Proxy类将通过目标对象的类加载器ClassLoader和Interface,以及句柄(Callback)创建与A类拥有相同接口的代理对象proxy,该代理对象将拥有接口ExInterface中的所有方法。同时,代理类必须实现一个类似回调函数的InvocationHandler接口并重写该接口中的invoke方法,当调用proxy的每个方法(如案例中的proxy#execute())时,invoke方法将被调用,利用该特性,可以在invoke方法中对目标对象(被代理对象如A)方法执行的前后动态添加其他外围业务操作,此时无需触及目标对象的任何代码,也就实现了外围业务的操作与目标对象(被代理对象如A)完全解耦合的目的。当然缺点也很明显需要拥有接口,这也就有了后来的CGLIB动态代理。
通过CGLIB动态代理实现上述功能并不要求目标对象拥有接口类,实际上CGLIB动态代理是通过继承的方式实现的,因此可以减少没必要的接口,下面直接通过简单案例协助理解。
首先引入 asm-8.0.1.jar 和 cglib-3.3.0.jar ,也可使用 maven 下载。
1 2 3 4 5 6 public class A { public void execute () { System.out.println("---- A.execute() ----" ); } }
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 import net.sf.cglib.proxy.Enhancer;import net.sf.cglib.proxy.MethodInterceptor;import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class CGLibProxy implements MethodInterceptor { private A target; public CGLibProxy (A target) { super (); this .target = target; } public A createProxy () { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(this ); return (A) enhancer.create(); } @Override public Object intercept (Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { if ("execute" .equals(method.getName())){ System.out.println("---- 鉴权 ----" ); Object result = proxy.invokeSuper(obj,args); System.out.println("---- 日志 ----" ); return result; }else if ("delete" .equals(method.getName())){ return proxy.invokeSuper(obj, args); } return proxy.invokeSuper(obj,args); } }
1 2 3 4 5 6 7 8 9 10 11 public class CGLibTest { public static void main (String[] args) { A a = new A(); CGLibProxy cgLibProxy = new CGLibProxy(a); A proxy = cgLibProxy.createProxy(); proxy.execute(); } }
1 2 3 4 ---- 鉴权 ---- ---- A.execute() ---- ---- 日志 ----
被代理的类无需接口即可实现动态代理,而CGLibProxy代理类需要实现一个方法拦截器接口MethodInterceptor并重写intercept方法,类似JDK动态代理的InvocationHandler接口,也是理解为回调函数,同理每次调用代理对象的方法时,intercept方法都会被调用,利用该方法便可以在运行时对方法执行前后进行动态增强。关于代理对象创建则通过Enhancer类来设置的,Enhancer是一个用于产生代理对象的类,作用类似JDK的Proxy类,因为CGLib底层是通过继承实现的动态代理,因此需要传递目标对象(如A)的Class,同时需要设置一个回调函数对调用方法进行拦截并进行相应处理,最后通过create()创建目标对象(如A)的代理对象,运行结果与前面的JDK动态代理效果相同。
总结 Spring AOP内部已都实现了这两种技术,Spring AOP 在使用时机上也进行自动化调整,当有接口时会自动选择JDK动态代理技术,如果没有则选择CGLIB技术,当然Spring AOP的底层实现并没有这么简单,为更简便生成代理对象,Spring AOP 内部实现了一个专注于生成代理对象的工厂类,这样就避免了大量的手动编码,这点也是十分人性化的,但最核心的还是动态代理技术。从性能上来说,Spring AOP 虽然无需特殊编译器协助,但性能上并不优于AspectJ的静态织入。
参考