springboot-aop
AOP(面向切面编程)是Spring的两大核心功能之一,功能非常强大,为解耦提供了非常优秀的解决方案。
现在就以springboot中aop的使用来了解一下如何使用aop。
写几个简单的Spring RESTful服务接口方法,实现方法前面或后面打印日志。
AOP术语定义
Spring的AOP中有几个重要概念搞清楚就行
- 执行点(Executepoint) - 类初始化,方法调用。
- 连接点(Joinpoint) - 执行点+方位的组合,可确定Joinpoint,比如类开始初始化前,类初始化后,方法调用前,方法调用后。
- 切点(Pointcut) - 在众多执行点中,定位感兴趣的执行点。Executepoint相当于数据库表中的记录,而Pointcut相当于查询条件。
- 增强(Advice) - 织入到目标类连接点上的一段程序代码。除了一段程序代码外,还拥有执行点的方位信息。
- 目标对象(Target) - 增强逻辑的织入目标类
- 引介(Introduction) - 一种特殊的增强(advice),它为类添加一些额外的属性和方法,动态为业务类添加其他接口的实现逻辑,让业务类成为这个接口的实现类。
- 代理(Proxy) - 一个类被AOP织入后,产生一个结果类,它便是融合了原类和增强逻辑的代理类。
- 切面(Aspect) - 切面由切点(Pointcut)和增强(Advice/Introduction)组成,既包括横切逻辑定义,也包括连接点定义。
AOP工作重点:
- 如何通过切点(Pointcut)和增强(Advice)定位到连接点(Jointpoint)上;
- 如何在增强(Advice)中编写切面的代码。
添加maven依赖
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 <parent> 6 <groupId>org.springframework.boot</groupId> 7 <artifactId>spring-boot-starter-parent</artifactId> 8 <version>2.1.3.RELEASE</version> 9 <relativePath/> <!-- lookup parent from repository --> 10 </parent> 11 <groupId>com.code</groupId> 12 <artifactId>springboot-aop</artifactId> 13 <version>0.0.1-SNAPSHOT</version> 14 <name>springboot-aop</name> 15 <description>springboot-aop</description> 16 17 <properties> 18 <java.version>1.8</java.version> 19 </properties> 20 21 <dependencies> 22 <dependency> 23 <groupId>org.springframework.boot</groupId> 24 <artifactId>spring-boot-starter</artifactId> 25 </dependency> 26 27 <dependency> 28 <groupId>org.springframework.boot</groupId> 29 <artifactId>spring-boot-starter-test</artifactId> 30 <scope>test</scope> 31 </dependency> 32 <dependency> 33 <groupId>org.springframework.boot</groupId> 34 <artifactId>spring-boot-starter-web</artifactId> 35 <exclusions> 36 <exclusion> 37 <groupId>org.springframework.boot</groupId> 38 <artifactId>spring-boot-starter-tomcat</artifactId> 39 </exclusion> 40 </exclusions> 41 </dependency> 42 <dependency> 43 <groupId>org.springframework.boot</groupId> 44 <artifactId>spring-boot-starter-jetty</artifactId> 45 </dependency> 46 <dependency> 47 <groupId>org.springframework.boot</groupId> 48 <artifactId>spring-boot-starter-aop</artifactId> 49 </dependency> 50 </dependencies> 51 52 <build> 53 <plugins> 54 <plugin> 55 <groupId>org.springframework.boot</groupId> 56 <artifactId>spring-boot-maven-plugin</artifactId> 57 </plugin> 58 </plugins> 59 </build> 60 61 </project>
配置application.yml
server: port: 8092
创建Controller
没有结果返回的示例:
@RestController public class UserController { @RequestMapping("/first") public Object first() { return "first controller"; } @RequestMapping("/doError") public Object error() { return 1 / 0; } }
创建切面类
1 /** 2 * 日志切面 3 */ 4 @Aspect 5 @Component 6 public class LogAspect { 7 8 @Pointcut("execution(public * com.code.aop.controller.*.*(..))") 9 public void webLog(){} 10 11 @Before("webLog()") 12 public void deBefore(JoinPoint joinPoint) throws Throwable { 13 // 接收到请求,记录请求内容 14 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 15 HttpServletRequest request = attributes.getRequest(); 16 // 记录下请求内容 17 System.out.println("URL : " + request.getRequestURL().toString()); 18 System.out.println("HTTP_METHOD : " + request.getMethod()); 19 System.out.println("IP : " + request.getRemoteAddr()); 20 System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); 21 System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs())); 22 23 } 24 25 @AfterReturning(returning = "ret", pointcut = "webLog()") 26 public void doAfterReturning(Object ret) throws Throwable { 27 // 处理完请求,返回内容 28 System.out.println("方法的返回值 : " + ret); 29 } 30 31 //后置异常通知 32 @AfterThrowing("webLog()") 33 public void throwss(JoinPoint jp){ 34 System.out.println("方法异常时执行....."); 35 } 36 37 //后置最终通知,final增强,不管是抛出异常或者正常退出都会执行 38 @After("webLog()") 39 public void after(JoinPoint jp){ 40 System.out.println("方法最后执行....."); 41 } 42 43 //环绕通知,环绕增强,相当于MethodInterceptor 44 @Around("webLog()") 45 public Object arround(ProceedingJoinPoint pjp) { 46 System.out.println("方法环绕start....."); 47 try { 48 Object o = pjp.proceed(); 49 System.out.println("方法环绕proceed,结果是 :" + o); 50 return o; 51 } catch (Throwable e) { 52 e.printStackTrace(); 53 return null; 54 } 55 } 56 }
测试效果
启动项目,执行如下mvn命令:
mvn spring-boot:run
模拟正常执行的情况,访问http://localhost:8092/first
,看控制台结果:
方法环绕start..... URL : http://localhost:8092/first HTTP_METHOD : GET IP : 0:0:0:0:0:0:0:1 CLASS_METHOD : com.code.aop.controller.UserController.first ARGS : [] 方法环绕proceed,结果是 :first controller 方法最后执行..... 方法的返回值 : first controller
模拟出现异常时的情况,访问http://localhost:8092/doError
,看控制台结果:
java.lang.ArithmeticException: / by zero at com.code.aop.controller.UserController.error(UserController.java:17) at com.code.aop.controller.UserController$$FastClassBySpringCGLIB$$db286cd9.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88) at com.code.aop.aspect.LogAspect.arround(LogAspect.java:60) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:47) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:55) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) at com.code.aop.controller.UserController$$EnhancerBySpringCGLIB$$b85974ee.error(<generated>) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897) at javax.servlet.http.HttpServlet.service(HttpServlet.java:645) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882) at javax.servlet.http.HttpServlet.service(HttpServlet.java:750) at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:867) at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1623) at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:214) at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610) at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610) at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610) at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540) at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146) at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548) at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132) at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257) at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1588) at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255) at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345) at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203) at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480) at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1557) at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201) at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247) at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144) at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132) at org.eclipse.jetty.server.Server.handle(Server.java:502) at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:364) at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260) at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305) at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103) at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118) at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765) at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683) at java.lang.Thread.run(Thread.java:748) 方法环绕start..... URL : http://localhost:8092/doError HTTP_METHOD : GET IP : 0:0:0:0:0:0:0:1 CLASS_METHOD : com.code.aop.controller.UserController.error ARGS : [] 方法最后执行..... 方法的返回值 : null
通过上面的简单的例子,可以看到aop的执行顺序。知道了顺序后,就可以在相应的位置做切面处理了。
切面注解说明
- @Aspect 作用是把当前类标识为一个切面供容器读取
- @Pointcut 定义切点,切点方法不用任何代码,返回值是void,重要的是条件表达式
- @Before 标识一个前置增强方法,相当于BeforeAdvice的功能
- @AfterReturning 后置增强,相当于AfterReturningAdvice,方法退出时执行
- @AfterThrowing 异常抛出增强,相当于ThrowsAdvice
- @After final增强,不管是抛出异常或者正常退出都会执行
- @Around 环绕增强,相当于MethodInterceptor
方法参数说明:
除了@Around外,每个方法里都可以加或者不加参数JoinPoint。
JoinPoint里包含了类名、被切面的方法名,参数等属性,可供读取使用。
@Around参数必须为ProceedingJoinPoint,pjp.proceed相应于执行被切面的方法。
@AfterReturning方法里,可以加returning = “xxx”,xxx即为在controller里方法的返回值,本例中的返回值是“first controller”。
@AfterThrowing方法里,可以加throwing = “XXX”,读取异常信息,如本例中可以改为:
1 //后置异常通知 2 @AfterThrowing(throwing = "ex", pointcut = "webLog()") 3 public void throwss(JoinPoint jp, Exception ex){ 4 System.out.println("方法异常时执行....."); 5 }
一般常用的有before和afterReturn组合,或者单独使用Around,即可获取方法开始前和结束后的切面。
关于切点PointCut
execution函数用于匹配方法执行的连接点,语法为:
execution(方法修饰符(可选) 返回类型 方法名 参数 异常模式(可选))
参数部分允许使用通配符:
-
- 匹配任意字符,但只能匹配一个元素
- .. 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用
-
- 必须跟在类名后面,如Horseman+,表示类本身和继承或扩展指定类的所有类
除了execution(),Spring中还支持其他多个函数,这里列出名称和简单介绍
- @annotation() 表示标注了指定注解的目标类方法
- args() 通过目标类方法的参数类型指定切点
- @args() 通过目标类参数的对象类型是否标注了指定注解指定切点
- within() 通过类名指定,它将匹配指定类型
- target() 通过类名指定,同时包含所有子类
- @within() 匹配标注了指定注解的类及其子类,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
- @target() 匹配标注了指定注解的类,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
逻辑运算符
表达式可由多个切点函数通过逻辑运算组成,与(&&)、 或(||)、 非(!)
比如execution(* chop(..)) && target(Horseman) 表示Horseman及其子类的chop方法
自定义注解
一般多用于某些特定的功能,我们来自定义一个注解:
1 @Target({ElementType.METHOD, ElementType.TYPE}) 2 @Retention(RetentionPolicy.RUNTIME) 3 public @interface UserAccess { 4 5 String desc() default "无信息"; 6 7 }
在Controller里加个方法:
@RequestMapping("/second") @UserAccess(desc = "second") public Object second() { return "second controller"; }
创建一个新的切面类:
1 @Component 2 @Aspect 3 public class UserAccessAspect { 4 5 @Pointcut(value = "@annotation(com.code.aop.annotation.UserAccess)") 6 public void access() { 7 8 } 9 10 @Before("access()") 11 public void deBefore(JoinPoint joinPoint) throws Throwable { 12 System.out.println("second before"); 13 } 14 15 @Around("@annotation(userAccess)") 16 public Object around(ProceedingJoinPoint pjp, UserAccess userAccess) { 17 //获取注解里的值 18 System.out.println("second around:" + userAccess.desc()); 19 try { 20 return pjp.proceed(); 21 } catch (Throwable throwable) { 22 throwable.printStackTrace(); 23 return null; 24 } 25 } 26 }
主要看一下@Around注解这里,如果需要获取在controller注解中赋给UserAccess的desc里的值,就需要这种写法,这样UserAccess参数就有值了。
执行:mvn spring-boot:run
启动项目,访问http://localhost:8092/second
,看控制台:
方法环绕start..... URL : http://localhost:8092/second HTTP_METHOD : GET IP : 0:0:0:0:0:0:0:1 CLASS_METHOD : com.code.aop.controller.UserController.second ARGS : [] second around:second second before 方法环绕proceed,结果是 :second controller 方法最后执行..... 方法的返回值 : second controller
通知结果可以看到,两个aop切面类都工作了,顺序如下:
spring aop就是一个同心圆,要执行的方法为圆心,最外层的order最小。从最外层按照AOP1、AOP2的顺序依次执行doAround方法,doBefore方法。然后执行method方法,最后按照AOP2、AOP1的顺序依次执行doAfter、doAfterReturn方法。也就是说对多个AOP来说,先before的,一定后after。
对于上面的例子就是,先外层的就是对所有controller的切面,内层就是自定义注解的。 那不同的切面,顺序怎么决定呢,尤其是同格式的切面处理,譬如两个execution的情况,那spring就是随机决定哪个在外哪个在内了。
所以大部分情况下,我们需要指定顺序,最简单的方式就是在Aspect切面类上加上@Order(1)注解即可,order越小最先执行,也就是位于最外层。像一些全局处理的就可以把order设小一点,具体到某个细节的就设大一点。