AOP是Aspect-Oriented Programming,即为面向( 切面
)方面编程。在维基百科中的解释: Aspect
是一种新的 模块化机制
,用来描述分散在对象、类或函数中的 横切关注点
。从关注点中分离出横切关注点是面向切面的程序设计核心概念。 分离关注点
使得解决特定领域问题的代码从 业务逻辑
中独立出来,业务逻辑代码不需要再包含针对特定领域问题代码的调用,比如一些公用模块的日志、安全等代码。 代码通过切面抽离,更加整齐和清晰,将重复的代码抽取出来单独的进行维护,在需要使用的时候,统一调用这些公共模块的代码,这样一个类就是一个基本的模块,方便统一维护和扩展更新。 AOP就是为业务实现提供了切面注入的一种机制,将定义好的切面通过切入点(pointcut)在业务逻辑中进行绑定。比如SpringBoot微服务中的所有controller层需要对http请求进行一些常规日志的打印,如果每次在controller进行打印,代码就会冗余,如果说将这些公共代码进行封装,也需要每一个controller类进行调用,所以AOP出现的恰到好处,这时候引入AOP对http相关的日志逻辑进行统一管理编写代码,不需要controller层进行调用,只需要创建一个切面,并通过切入点绑定controller即可,下面的示例会讲到。
Spring AOP
切面(Aspect)
:是指横切多个对象的关注点的一个模块化,事务管理就是J2EE应用中横切关注点的很好示例。在Spring AOP中,切面通过常规类(基本模式方法)或者通过使用了注解 @Aspect
的常规类来实现。 连接点(Joint point)
:是指在程序执行期间的一个点,比如某个方法的执行或者是某个异常的处理。在Spring AOP中,一个连接点往往代表的是 一个方法执行
。 通知(Advice)
:是指切面在某个特殊连接点上执行的动作。通知有不同类型,包括 "around"
, "before"
和 "after"
通知。许多AOP框架包括Spring,将通知建模成一个拦截器,并且围绕连接点维持一个拦截器链。 切入点(Pointcut)
:是指匹配连接点的一个断言。通知是和一个切入点表达式关联的,并且在任何被切入点匹配的连接点上运行(举例,使用特定的名字执行某个方法)。AOP的核心就是 切入点表达式匹配连接点的思想
。Spring默认使用 AspectJ切入点表达式语言
引入(Introduction)
:代表了对一个类型额外的方法或者属性的声明。Spring AOP允许引入新接口到任何被通知对象(以及一个对应实现)。比如,可以使用一个引入去使一个bean实现 IsModified
接口,从而简化缓存机制。(在AspectJ社区中,一个引入也称为一个inter-type declaration类型间声明) 目标对象(Target object)
:是指被一个或多个切面通知的那个对象。也指被通知对象( "advised object"
),由于Spring AOP是通过运行时代理事项的,这个目标对象往往是一个 代理对象
。 AOP 代理(AOP proxy)
:是指通过AOP框架创建的对象,用来实现切面合约的(执行通知方法等等)。在Spring框架中,一个AOP代理是一个 JDK动态代理
或者是一个 CGLIB代理
。 织入(Weaving)
:将切面和其他应用类型或者对象连接起来,创骗一个被通知对象。这些可以在编译时(如使用AspectJ编译器)、加载时或者运行时完成。Spring AOP,比如其他纯Java AOP框架一般是在 运行时
完成织入。 前置通知(Before advice) 后置返回通知(After returning advice) 后置异常通知(After throwing advice) 后置(最终)通知(After(finally) advice) 环绕通知(Around advice)
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.14</version> </dependency> </dependencies> 复制代码
其中,关于aop相关的主要引入了支持切面编程的依赖: org.aspectj.aspectjweaver
和 org.aspectj.aspectjrt
的依赖。 aspectjweaver
是aspectj的织入包, aspectjrt
是aspectj的运行时包。
server: context-path: /demo/v1 port: 9000 复制代码
package com.example.andya.demo.controller; import org.springframework.web.bind.annotation.*; /** * @author Andya * @create 2020-04-12 10:36 */ @RestController @RequestMapping("/aopTest") public class AopController { @RequestMapping(value = "/sayHi/{name}", method = RequestMethod.GET) public String sayHi(@PathVariable(value = "name") String name) { return "hi, " + name; } } 复制代码
package com.example.andya.demo.aop; import com.alibaba.fastjson.JSON; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; /** * @author Andya * @create 2020-04-12 10:39 */ @Aspect @Component public class WebLogAspect { private Logger LOG = LoggerFactory.getLogger(WebLogAspect.class); ThreadLocal<Long> startTime = new ThreadLocal<>(); /** * 定义切入点,以controller下所有包的请求为切入点 */ @Pointcut("execution(public * com.example.andya.demo.controller..*.*(..))*") public void webLog(){ } /** *前置通知:在切入点之前执行的通知 * @param joinPoint * @throws Throwable */ @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { startTime.set(System.currentTimeMillis()); ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = servletRequestAttributes.getRequest(); //打印请求相关参数 LOG.info("========================================== Start =========================================="); LOG.info("URL:" + request.getRequestURL().toString()); LOG.info("HTTP_METHOD:" + request.getMethod()); //header第一种格式展示 Enumeration<String> enumeration = request.getHeaderNames(); Map<String, String> headerMap = new HashMap<>(); while (enumeration.hasMoreElements()) { String headerName = enumeration.nextElement(); headerMap.put(headerName, request.getHeader(headerName)); } String headerJsonStr = JSON.toJSONString(headerMap); if (headerJsonStr.length() > 0) { LOG.info("HTTP_HEADERS INFO IS: {}", headerJsonStr); } //header第二种格式展示 LOG.info("HTTP_HEADERS: "); Enumeration<?> enumeration1 = request.getHeaderNames(); while (enumeration1.hasMoreElements()) { String key = (String) enumeration1.nextElement(); String value = request.getHeader(key); LOG.info(" {}: {}", key, value); } LOG.info("IP:" + request.getRemoteAddr()); LOG.info("CLASS_METHOD:" + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); try { LOG.info("REQUEST BODY : [{}]", JSON.toJSONString(joinPoint.getArgs()[0])); // LOG.info("ARGS:{}", Arrays.toString(joinPoint.getArgs())); } catch (Exception e) { LOG.error("REQUEST BODY PARSE ERROR!"); } HttpSession session = (HttpSession) servletRequestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION); LOG.info("SESSION ID:" + session.getId()); } // /** // * 后置通知 // * @param ret // * @throws Throwable // */ // @AfterReturning(returning = "ret", pointcut = "webLog()") // public void doAfterReturning(Object ret) throws Throwable { // // 处理完请求,返回内容 // LOG.info("RESPONSE : " + ret); // LOG.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get())); // // } /** * 后置最终通知 * @throws Throwable */ @After("webLog()") public void doAfter() throws Throwable { LOG.info("=========================================== End ==========================================="); // 每个请求之间空一行 LOG.info(""); } /** * 环绕通知 * 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型 * @param proceedingJoinPoint * @return * @throws Throwable */ @Around("webLog()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); String resultStr = JSON.toJSONString(result); // 打印出参 LOG.info("RESPONSE ARGS : {}", resultStr); // 执行耗时 LOG.info("TIME-CONSUMING : {} ms", System.currentTimeMillis() - startTime); return result; } } 复制代码
其中:
@Aspect
注解是表示该类为切面类, @Component
注解是将切面类加入到Ioc容器中。 @Pointcut
定义切入点为整个controller包下的所有函数。 joinPoint.getArgs()
获取目标方法的参数信息。 joinPoint.getSignature()
获取通知的签名,并且通过 joinPoint.getSignature().getDeclaringTypeName()
获取 代理类
的名字, joinPoint.getSignature().getName()
获取 代理方法
的名字。 package com.example.andya.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } 复制代码
http://127.0.0.1:9000/demo/v1/aopTest/sayHi/andya
... ... ... ... ... ... 2020-04-12 15:21:31.737 INFO 18548 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 2020-04-12 15:21:31.782 INFO 18548 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 9000 (http) 2020-04-12 15:21:31.786 INFO 18548 --- [ main] com.example.andya.demo.DemoApplication : Started DemoApplication in 2.928 seconds (JVM running for 4.26) 2020-04-12 15:21:41.084 INFO 18548 --- [nio-9000-exec-2] o.a.c.c.C.[.[localhost].[/demo/v1] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2020-04-12 15:21:41.085 INFO 18548 --- [nio-9000-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2020-04-12 15:21:41.102 INFO 18548 --- [nio-9000-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 17 ms 2020-04-12 15:21:41.126 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : ========================================== Start ========================================== 2020-04-12 15:21:41.126 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : URL:http://127.0.0.1:9000/demo/v1/aopTest/sayHi/andya 2020-04-12 15:21:41.126 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : HTTP_METHOD:GET 2020-04-12 15:21:41.155 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : HTTP_HEADERS INFO IS: {"accept-language":"zh-CN","cookie":"JSESSIONID=1014BD34FFE9D2660CB47B282C63FA7D","host":"127.0.0.1:9000","connection":"Keep-Alive","accept-encoding":"gzip, deflate","accept":"text/html, application/xhtml+xml, image/jxr, */*","user-agent":"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"} 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : HTTP_HEADERS: 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : accept: text/html, application/xhtml+xml, image/jxr, */* 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : accept-language: zh-CN 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : accept-encoding: gzip, deflate 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : host: 127.0.0.1:9000 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : connection: Keep-Alive 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : cookie: JSESSIONID=1014BD34FFE9D2660CB47B282C63FA7D 2020-04-12 15:21:41.156 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : IP:127.0.0.1 2020-04-12 15:21:41.157 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : CLASS_METHOD:com.example.andya.demo.controller.AopController.sayHi 2020-04-12 15:21:41.157 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : REQUEST BODY : ["andya"] 2020-04-12 15:21:41.160 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : SESSION ID:0357B2624C73C5F79BC977AD628DB45F 2020-04-12 15:21:41.162 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : RESPONSE ARGS : "hi, andya" 2020-04-12 15:21:41.163 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : TIME-CONSUMING : 37 ms 2020-04-12 15:21:41.163 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : =========================================== End =========================================== 2020-04-12 15:21:41.163 INFO 18548 --- [nio-9000-exec-2] com.example.andya.demo.aop.WebLogAspect : 复制代码
参考书籍 《SPING技术内幕 深入解析SPRING架构与设计原理》 参考官网 Spring AOP官网