spring AOP使用和注意事项
本项目研究spring的AOP
官方参考文档
https://docs.spring.io/spring/docs/5.2.3.RELEASE/spring-framework-reference/core.html#aop
项目说明
(本文是一个测试项目的 README.md 文件,如果找不到代码,那就是没有提供代码,懒得上传GitHub)
例子1
com.wyf.test.aopspring.aophello.example01.AopTest
:说明不能不启动spring容器进行测试com.wyf.test.aopspring.aophello.example01.Guess
:猜测了@Before、@After、@AfterReturning、@AfterThrowing、@Around 这些注解的拦截先后顺序,提供了伪代码com.wyf.test.aopspring.aophello.example01.LogAspect01
:例子1,说明了Aspect、Pointcut、@Before、@After、@AfterReturning、@AfterThrowing、@Around 的用法,以及这些注解拦截的先后顺序com.wyf.test.aopspring.aophello.example01.LogAspect01SpringTest
:是spring test的测试类,启动spring容器并测试
例子2
com.wyf.test.aopspring.aophello.example02.LogAspect02
:演示了Pointcut的写法。
例子3
com.wyf.test.aopspring.aophello.example03.LogAspect03SpringTest
:演示public、private、protected、default、static的方法能否得到拦截(除了private和static都可以)
例子4
com.wyf.test.aopspring.aophello.example04.LogAspect04SpringTest
:演示如果类是final的,是否会被拦截(结果是spring容器无法启动,因为启动时需要产生子类的bean)
例子5
com.wyf.test.aopspring.aophello.example05.LogAspect05
:演示Pointcut表达式的多种写法
例子99
com.wyf.test.aopspring.aophello.example99.ControllerAspect
:演示了常用的拦截Controller的入参的aop的写法,详细参考附录
使用AOP步骤
- 添加依赖
<!-- AOP,Springboot默认未引入,需要自行引入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 定义 aspect、pointcut、advice
- 新建一个 aspect(切面)的java类,即用 @Aspect 注解的类;aspect 类要让 spring 容器能发现,所以要增加 @Component 注解
- 在 aspect 类里定义 pointcut(切入点),即新增一个方法用 @Pointcut 注解,它是一个定义要拦截的规则。可以定义多个@Pointcut
- 在 aspect 类里定义 advice(通知),即Aspect类里定义怎么拦截的方法(如 @Before、@After、@Around、@AfterReturning、@AfterThrowing 所注释的方法就叫做advice方法)
使用AOP中的注意实现和细节
- @Before、@After、@AfterReturning、@AfterThrowing、@Around 这些注解的拦截先后顺序、异常情况时的细节,参考附录
com.wyf.test.aopspring.aophello.example01.Guess
- 常见的pointcut的写法,见附录
com.wyf.test.aopspring.aophello.example02.LogAspect02
- 如果目标类是final的(Calculator),是否会被拦截? 不能。结果导致spring容器无法启动,因为启动时需要产生目标类的子类的bean用于动态代理。需要注释掉以免导致其他spring test启动不了。
- 静态方法无法使用aop? 是的,因为需要动态代理而静态方法。
- protected、default、private方法能否使用aop?
protected和default可以,private无法调用,但是通过反射强制调用私有方法,表名是不行的。使用反射调用public、protected、default是可以触发aop拦截的 - aspect可以写多个Pointcut吗? 可以
- 我们可能会遇到将Pointcut表达式直接写在advice的注解里(即@Before…),或者让advice的注解引用@Pointcut的方法。参考附录
com.wyf.test.aopspring.aophello.example05.LogAspect05
补充:AOP的概念
- 切面(ASPECT):横切关注点被模块化的特殊对象。即,它是一个类。 ====>即 @Aspect 所注解的类
- 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。 ====>即 @Aspect 所注解的类 里, @Before、@After、@Around、@AfterReturning、@AfterThrowing 所注解的方法
- 目标(Target):被通知对象。 ===>即类似
Calculator02_00
这些类的对象 - 代理(Proxy):向目标对象应用通知之后创建的对象。 =====> 即
Calculator02_00
这些类,会产生子类作为它的代理对象 - 切入点(PointCut):切面通知执行的“地点”的定义。 ===>即 @Aspect 所注解的类 里, @Pointcut 所注解的方法
- 连接点(JointPoint):与切入点匹配的执行点。
注意aop的拦截,跟javax.servlet.Filter或者spring的interceptor是不一样的,它们需要http请求才能拦截,而aop的拦截并不需要aop,只要spring容器启动起来,普通的junit测试的调用即可拦截
附录
附录1:com.wyf.test.aopspring.aophello.example02.LogAspect02
@Aspect
@Component
public class LogAspect02 {
/**
* 具体的某个类,的某个方法
*/
@Pointcut("execution(* com.wyf.test.aopspring.aophello.example02.method.Calculator02_00.div2(..))")
public void pointCutOneMethod() {
}
/**
* 具体的某个类,的所有方法
*/
@Pointcut("execution(* com.wyf.test.aopspring.aophello.example02.sub.Calculator02_01.*(..))")
public void pointCutOneClassAllMethod() {
}
/**
* 某个包下的所有类,不包括子目录,的所有方法
*/
@Pointcut("execution(* com.wyf.test.aopspring.aophello.example02.sub2.*.*(..))")
public void pointCutAllClassExceptSubDir() {
}
/**
* 某个包下的所有类,包括子目录,的所有方法
*/
@Pointcut("execution(* com.wyf.test.aopspring.aophello.example02.sub2..*.*(..))")
public void pointCutPackageIncludeSubDir() {
}
/**
* 任何目录下的,类上有@RestController或@Controller,方法带有@GetMapping、@PostMapping
* 、@DeleteMapping、@PutMapping、@RequestMapping任一注解的则拦截(用于拦截Controller方法)
* <p>
* 注意:仅方法有注解,类上没注解的,不拦截
*/
@Pointcut("!execution(* com.wyf.test.aopspring.aophello.example02.HealthCheckController.*(..)) &&" +
"((@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)) " +
"&& (@annotation(org.springframework.web.bind.annotation.GetMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.RequestMapping)))")
public void pointCutControllerMethod3() {
}
/**
* 拦截用指定注解的方法(这个注解单独写在类上面,并不会自动实现该类所有方法被拦截)
*/
@Pointcut("@annotation(com.wyf.test.aopspring.aophello.example02.MyAnnotaion)")
public void pointCutSomeAnnotation() {
}
@Before("pointCutControllerMethod3()")
public void before(JoinPoint joinPoint) {
System.out.println("target:" + joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName());
}
}
附录2:com.wyf.test.aopspring.aophello.example01.Guess
/**
* 这是一个猜测advice的源码的类,为什么@Before、@After、@AfterReturning、@AfterThrowing、@Around 会有那样的执行顺序? 下面是猜测
*
* @author Stone
* @version V1.0.0
* @date 2020/2/14
*/
public class Guess {
/**
* <pre>
* 【建议直接看伪代码更加简单!!!】
* 1、当调用目标方法时,不是直接调用真正的实现,而是其代理方法
* 2、代理方法最终会调用这里的 entrance 方法
* 3、调用 @Around 所修饰的方法
* 3.1 先执行proceed()之前的代码 `around before`
* 3.2 执行proceed()方法
* 3.2.1 执行 @Before (有定义@Before的话)
* 3.2.2 执行实际的目标方法
* 3.2.3 执行 @After(有的话),无论目标方法是否有抛出异常在finally里,所以一定会执行
* 3.3 proceed()是否抛出异常
* 3.3.1 否
* 3.3.1.1 执行 proceed()之后的代码 `around after`
* 3.3.1.2 执行 @AfterReturning(有的话)。【结束】
* 3.3.2 是
* 3.3.2.1 不执行proceed()之后的代码 `around after`
* 3.3.2.1 执行 @AfterThrowing,并继续往上抛出异常。【结束】
* </pre>
*/
void entrance() {
boolean isException = false;
Throwable throwable = null;
try {
// 当然不是直接调用,估计是通过反射进行调用,这里为了简化写成直接调用
around();
} catch (Throwable t) {
isException = true;
throwable = t;
}
if (!isException) {
// 执行 @AfterReturning(有的话)
} else {
// 执行 @AfterThrowing(有的话)
throw new RuntimeException(throwable);
}
}
/**
* 这是 @Around 所修饰的方法
*/
void around() {
System.out.println("around before");
proceed();
System.out.println("around after");
}
void proceed() {
// 执行 @Before(有的话)
try {
// 执行实际的目标方法
} finally {
// 执行 @After(有的话)
}
}
}
附录3:com.wyf.test.aopspring.aophello.example05.LogAspect05
@Aspect
@Component
public class LogAspect05 {
/**
* 指定具体的某个类,
*/
@Pointcut("execution(* com.wyf.test.aopspring.aophello.example05.Calculator05_1.*(..))")
public void pointCut() {
}
/**
* 可以引用@Pointcut的方法,也可以直接将表达式写在字串里
*
* @param joinPoint
*/
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("@Before,target:" + joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName());
}
/**
* 可以引用@Pointcut的方法,也可以直接将表达式写在字串里
*
* @param joinPoint
*/
@After("execution(* com.wyf.test.aopspring.aophello.example05.Calculator05_2.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("@Before,target:" + joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName());
}
}
附录4:com.wyf.test.aopspring.aophello.example99.ControllerAspect
package com.wyf.test.aopspring.aophello.example99;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
/**
* 拦截控制器的方法
*
* @author Strone
* @version V1.0.0
* @date 2020/2/14
*/
@Aspect
@Component
@Slf4j
public class ControllerAspect {
/**
* 响应头的名字,值是请求的唯一ID;同时也是日志的全局ID,这么设置:%X{RequestId},
* 例如:<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{RequestId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
*/
private static final String REQUEST_ID = "RequestId";
/**
* 格式化JSON的工具。线程安全。
*/
private static ObjectMapper objectMapper = new ObjectMapper();
static {
// 如果json字符串中有新增的字段,但实体类中不存在的该字段,不报错。默认true
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 日期格式指定格式
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
/**
* 任何目录下的,类上有@RestController或@Controller,方法带有@GetMapping、@PostMapping
* 、@DeleteMapping、@PutMapping、@RequestMapping任一注解的则拦截,除HealthCheckController外,除HealthCheckController外(用于拦截Controller方法)
* <p>
* 注意:仅方法有注解,类上没注解的,不拦截
*/
@Pointcut("!execution(* com.wyf.test.aopspring.aophello.example02.HealthCheckController.*(..)) &&" +
"((@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)) " +
"&& (@annotation(org.springframework.web.bind.annotation.GetMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.RequestMapping)))")
public void pointCutControllerMethod() {
}
/**
* 拦截请求,打印请求的参数,方便DEBUG
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around(value = "pointCutControllerMethod()")
public Object aroundRestApi(ProceedingJoinPoint joinPoint) throws Throwable {
long beginTime = System.currentTimeMillis();
// 防止非http请求方式进行调用导致异常
HttpServletRequest req = null;
HttpServletResponse resp = null;
String requestId = null;
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
req = attributes.getRequest();
resp = attributes.getResponse();
// 将该请求ID写入响应头,方便查找到该条日志
requestId = UUID.randomUUID().toString();
resp.setHeader(REQUEST_ID, requestId);
} catch (Exception e) {
}
// 请求执行
boolean isSuccess = true;
try {
// 每个请求拥有唯一的请求ID
MDC.put(REQUEST_ID, requestId);
// 执行实际方法
return joinPoint.proceed();
} catch (Throwable e) {
isSuccess = false;
// 需要注意以下@ControllerAdvice或@RestControllerAdvice是否能囊括这里抛出的异常并将其封装
// (实际测试似乎不需要加@Order默认就能包揽这里抛出的异常
throw e;
} finally {
MDC.clear();
Long endTime = System.currentTimeMillis();
log.info("IP:{}, RequestId:{}, Method:{}, ContentType:{}, Url:{}, Param:{}, Time:{}, Status:{}",
req == null ? null : req.getRemoteAddr(),
requestId,
req == null ? null : req.getMethod(),
req.getContentType(),
req == null ? null : req.getRequestURL(),
getRequestParams(joinPoint),
// getRequestParams(req),// 这种方式对于application/json,请求体里的参数无法获取
endTime - beginTime,
isSuccess ? "success" : "fail");
}
}
/**
* 获取请求参数
*
* @param joinPoint 上下文
* @return 请求参数
*/
private String getRequestParams(JoinPoint joinPoint) {
try {
Object[] args = joinPoint.getArgs();
Map<String, Object> parameters = new LinkedHashMap<>();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 将自己写在Controller里的HttpServletRequest和HttpServletResponse等排除掉。常用的是这些,要排除掉以免某些类无法json化导致异常
for (int i = 0; i < method.getParameters().length; i++) {
if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse) {
continue;
}
parameters.put(method.getParameters()[i].getName(), args[i]);
}
return objectMapper.writeValueAsString(parameters);
} catch (Exception e) {
log.error("OBTAIN_PARAM_ERROR", e);
return "OBTAIN_PARAM_ERROR";
}
}
/**
* 获取请求参数。弊端:对于application/json,请求体里的参数无法获取。不予采用
*
* @param req
* @return
*/
@Deprecated
private String getRequestParams(HttpServletRequest req) {
try {
if (req == null) {
return null;
}
return objectMapper.writeValueAsString(req.getParameterMap());
} catch (Exception e) {
log.error("OBTAIN_PARAM_ERROR", e);
return "OBTAIN_PARAM_ERROR";
}
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/135285.html