SpringBoot—集成AOP详解(面向切面编程Aspect)
AOP介绍
AOP概述
AOP是Aspect-Oriented Programming,即为面向(切面
)方面编程。在维基百科中的解释:Aspect
是一种新的模块化机制
,用来描述分散在对象、类或函数中的横切关注点
。从关注点中分离出横切关注点是面向切面的程序设计核心概念。分离关注点
使得解决特定领域问题的代码从业务逻辑
中独立出来,业务逻辑代码不需要再包含针对特定领域问题代码的调用,比如一些公用模块的日志、安全等代码。
代码通过切面抽离,更加整齐和清晰,将重复的代码抽取出来单独的进行维护,在需要使用的时候,统一调用这些公共模块的代码,这样一个类就是一个基本的模块,方便统一维护和扩展更新。
AOP就是为业务实现提供了切面注入的一种机制,将定义好的切面通过切入点(pointcut)在业务逻辑中进行绑定。比如SpringBoot微服务中的所有controller层需要对http请求进行一些常规日志的打印,如果每次在controller进行打印,代码就会冗余,如果说将这些公共代码进行封装,也需要每一个controller类进行调用,所以AOP出现的恰到好处,这时候引入AOP对http相关的日志逻辑进行统一管理编写代码,不需要controller层进行调用,只需要创建一个切面,并通过切入点绑定controller即可,下面的示例会讲到。
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框架一般是在运行时
完成织入。
AOP Advice相关术语
前置通知(Before advice)
:在一个连接点之前执行的通知。但这种通知不能阻止连接点的执行流程(除非它抛出一个异常)后置返回通知(After returning advice)
:在一个连接点正常完成后执行的通知(如,如果一个方法没有抛出异常的返回)后置异常通知(After throwing advice)
:在一个方法抛出一个异常退出时执行的通知。后置(最终)通知(After(finally) advice)
:在一个连接点退出时(不管是正常还是异常返回)执行的通知。环绕通知(Around advice)
:环绕一个连接点的通知,比如方法的调用。这是一个最强大的通知类型。环绕通知可以在方法调用之前和之后完成自定义的行为。也负责通过返回自己的返回值或者抛出异常这些方式,选择是否继续执行连接点或者简化被通知方法的执行。
SpringBoot中集成AOP
1)pom.xml引入
<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的运行时包。
2)配置文件
server:
context-path: /demo/v1
port: 9000
3)controller类
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;
}
}
4)AOP层类
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()
获取代理方法
的名字。
5)运行类
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);
}
}
6)访问url
7)运行结果
补充获取被切面方法入参,及返回值
@Slf4j
@Aspect
@Component
public class LanguageAspect {
@Around("execution(public * com.service.controller.*.*(..))")
public Object judgeShowData(ProceedingJoinPoint point) {
BaseResult baseResult = new BaseResult();
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); //在AOP中获取request
// HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); //获取response
Object[] args = point.getArgs();
Object object = point.proceed(args); //获取返回值
if (object instanceof BaseResult) {
baseResult = (BaseResult) object;
return baseResult;
} else {
return object;
}
} catch (Throwable ex) {
baseResult = BaseResult.error(ex.getMessage());
return baseResult;
}
}
}