Springboot使用AOP自定义Log注解



Springboot使用AOP自定义Log注解

1.基本介绍

根据静态编译时和运行时两种环境, aop可以分为 静态代理动态代理。静态代理主要涉及AspectJ, 动态代理主要涉及Spring AOP, CGLIB.

我们需要声明一个注解,那么就必须得了解几个常用的注解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Documented:指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。

@Retention(RetentionPolicy.RUNTIME):指明修饰的注解的生存周期,即会保留到哪个阶段。
RetentionPolicy的取值:
SOURCE:源码级别保留,编译后即丢弃。
CLASS: 编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值。
RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用。

@Target(ElementType.METHOD):指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里。
ElementType取值:
TYPE:类,接口或者枚举
FIELD:域,包含枚举常量
METHOD:方法
PARAMETER:参数
CONSTRUCTOR:构造方法
LOCAL_VARIABLE:局部变量
ANNOTATION_TYPE:注解类型
PACKAGE:包

例如如下我将自定义一个增强的日志功能。首先我们得在配置类上添加 @EnableAspectJAutoProxy注解开启注解版的AOP功能

声明自定义的注解Log

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
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {

/**
* 操作模块
* @return
*/
String title() default "my annotation for printing log";

/**
* 业务操作类型(enum):主要是select,insert,update,delete
*/
BusinessType businessType() default BusinessType.OTHER;

/**
* 操作人类别,默认后台操作用户
*/
public OperatorType operatorType() default OperatorType.MANAGE;

/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;

/**
* 日志等级:自己定,此处分为1-9
*/
int level() default 0;
}

然后再定义切面类

1
2
3
4
5
@Aspect
@Component
public class MyLogAspect {
//...
}

定义连接点:匹配cn.coderblue.studyaop.annotation.Log下所有方法。logPointCut()被AOP AspectJ定义为 切点签名方法,作用是使得 通知的注解可以通过这个切点签名方法连接到切点(比如使用@Around(“logPointCut”)),通过解释切点表达式找到需要 被切入的连接点

1
2
3
@Pointcut("@annotation(cn.coderblue.studyaop.annotation.Log)")
public void logPointCut() {
}

其中,切点指示符是切点定义的关键字,切点表达式以切点指示符开始,有以下9种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation

1、@annotation

这个指示器匹配那些有 指定注解的连接点,比如,我们可以新建一个这样的注解@Log:

1
2
@Pointcut("@annotation(cn.coderblue.studyaop.annotation.Log)")
public void logPointCut() {}

我们可以使用@Log注解标记哪些方法执行需要输出日志:

1
2
3
4
5
@Log(title = "自定义log注解", businessType = BusinessType.INSERT)
@RequestMapping("/log")
public String myAopAnnotation() {
return "success";
}

2、execution

execution是一种使用频率比较高比较主要的一种切点指示符,用来 匹配方法签名,方法签名使用 全限定名,包括访问修饰符(public/private/protected)、返回类型,包名、类名、方法名、参数,其中 返回类型,包名,类名,方法,参数是必须的,如下面代码片段所示:

1
@Pointcut("execution(public String cn.coderblue.studyaop.UserInfoController.findById(Long))")

上面的代码片段里的表达式精确地 匹配到UserInfoController类里的findById(Long)方法,但是这看起来不是很灵活。假设我们要匹配UserInfoController类的所有方法,这些方法可能会有不同的方法名,不同的返回值,不同的参数列表,为了达到这种效果,我们可以使用通配符。如下代码片段所示:

1
@Pointcut("execution(* cn.coderblue.studyaop.UserInfoController.*(..))")

第一个* 通配符:匹配所有返回值类型,
第二个cn.coderblue.studyaop.UserInfoController.*:匹配这个类里的所有方法,
第三个()括号: 表示参数列表
第四个括号里的用两个点号:表示匹配任意个参数,包括0个

参数指示符是一对括号所括的内容,用来匹配指定方法参数:

1
@Pointcut("execution(* *..find*(Long))")

这个切点匹配所有以find开头的方法,并且只一个Long类的参数。如果我们想要匹配一个有任意个参数,但是第一个参数必须是Long类的,我们这可使用下面这个切点表达式:

1
@Pointcut("execution(* *..find*(Long,..))")

3、within

1
@Pointcut("within(cn.coderblue.studyaop.mapper.UserInfoMapper)")

我们也可以使用within指示符来匹配某个包下面所有类的方法(包括子包下面的所有类方法),如下代码所示:

1
@Pointcut("within(cn.coderblue.studyaop.mapper..*)")

4、切点表达式组合

可以使用&&、||、!、三种运算符来组合切点表达式,表示与或非的关系。

1
2
3
4
5
6
7
8
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}

@Pointcut("execution(* *..create*(Long,..))")
public void firstLongParamMethods() {}

@Pointcut("repositoryMethods() && firstLongParamMethods()")
public void entityCreationMethods() {}

通过这个切点签名logPointCut()方法连接到切点,然后我们也可以做实际业务的功能处理

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
 /**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
handleLog(joinPoint, null, jsonResult);
}

/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e, null);
}

@After(value = "logPointCut()")
public void afterPrintLog() {
System.out.println("@After在切点方法后,return前执行");
}

@Before("logPointCut()")
public void beforePrintLog() {
System.out.println("@Before切点方法执行之前,输出日志");
}

// @Around(value = "logPointCut()")
// public void aroundPrintLog() {
// System.out.println("@Around方法执行,输出日志");
// }

输出结果

1
2
3
4
5
6
@Before方法执行之前,输出日志
// 进入Log注解标识方法后打印的
123
// 记录的日志信息
2020-12-02 10:20:27.320 INFO 64288 --- [nio-8085-exec-1] c.coderblue.studyaop.aspect.MyLogAspect : 异步处理日志信息中...SysOperationLog{logId=531456743704899584, title='自定义log注解', businessType=1, method='cn.coderblue.studyaop.controller.TestController.myAopAnnotation()', requestMethod='GET', operatorType=1, operName='null', deptName='null', operUrl='/log', operIp='null', operParam='{"id":"中文","deviceId":"3"}', jsonResult='"success"', status=200, errorMsg='null', operTime=Wed Dec 02 10:20:27 CST 2020}
@After在切点后,return前执行

详情请戳我的GitHub仓库

Spring4和Spring5的aop执行顺序区别

案例

1.定义自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author Lauy
* @date 2021/2/2
*/
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {

/**
* 操作模块
* @return
*/
String title() default "my annotation for printing log";

}

2.定义切面

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
/**
* @author Lauy
* @date 2021/2/2
*/
@Aspect
@Component
public class MyLogAspect {

@Pointcut("@annotation(cn.lauy.annotation.Log)")
public void logPointCut() {
}

@Before("logPointCut()")
public void beforePrintLog() {
System.out.println("@Before切点方法执行之前,输出日志");
}

@After(value = "logPointCut()")
public void afterPrintLog() {
System.out.println("@After在切点方法后,return前执行");
}

/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
System.out.println("@AfterReturning方法执行" + " / 访问的类方法:" + joinPoint + " / 调用业务层方法的返回:" + jsonResult);
}

/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
System.out.println("@AfterThrowing方法执行");
}

@Around("execution(public String cn.lauy.controller.UserController.*(..))")
public Object aroundPrintLog(ProceedingJoinPoint point) throws Throwable {
Object obj = null;
System.out.println("我是环绕通知前");
obj = point.proceed();
System.out.println("我是环绕通知后");
return obj;
}
}

3.控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author Lauy
* @date 2021/2/2
*/
@RestController
@RequestMapping("/user")
public class UserController {

@Resource
private UserService userService;

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@GetMapping("/query")
@Log
public String queryUser(String id) {
System.out.println("我是SpringBootVersion:" + SpringBootVersion.getVersion() + " / SpringVersion" + SpringVersion.getVersion());

System.out.println("进入Controller类的queryUser方法");
int i = 1 / 0; //模拟异常
String result = userService.queryUser(id);
return "我是SpringBootVersion:" + SpringVersion.getVersion() + " / " + result;
}
}

4.业务层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author Lauy
* @date 2021/2/2
*/
public interface UserService {
String queryUser(String id);
}

============

/**
* @author Lauy
* @date 2021/2/2
*/
@Service
public class UserServiceImpl implements UserService {
@Override
public String queryUser(String id) {
System.err.println("我进入了业务层的queryUser方法");
return UUID.randomUUID().toString();
}
}

代码测试

1.Spring4

正常

1
2
3
4
5
6
7
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
我是环绕通知后
@After在切点方法后,return前执行
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:4.3.6.RELEASE / fabcfd3c-7a9a-4290-a7a9-190f60bbf1ca
我进入了业务层的queryUser方法

异常

调用业务层方法queryUser前:对比可见没有执行service的queryUser方法

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:31:56.791 ERROR 59308 --- [nio-8089-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

调用业务层方法queryUser后:

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:35:40.903 ERROR 34152 --- [nio-8089-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

我进入了业务层的queryUser方法

2.Spring5

我测试了两个Springboot版本下的Spring5,但是有着不同的结果,目前还没有深入研究原因,先记录下来。

SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE

正常

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
我是环绕通知后
@After在切点方法后,return前执行
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:5.1.5.RELEASE / e973d3ff-8764-46e3-9f1a-d9862fb750b5
我进入了业务层的queryUser方法

异常

调用业务层方法queryUser前:对比可见没有执行service的queryUser方法

1
2
3
4
5
6
7
8
9
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:17:13.164 ERROR 31916 --- [nio-8089-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

调用业务层方法queryUser后:

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
我进入了业务层的queryUser方法
2021-02-02 14:16:30.185 ERROR 31916 --- [nio-8089-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

此版本下与Spring4的执行顺序竟然相同

SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE

正常

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:5.2.8.RELEASE / d07228cc-7d9d-41a7-b345-97208a848829
@After在切点方法后,return前执行
我是环绕通知后
我进入了业务层的queryUser方法

异常

方法前

1
2
3
4
5
6
7
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterThrowing方法执行
@After在切点方法后,return前执行
2021-02-02 22:54:24.878 ERROR 66232 --- [nio-8089-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

方法后

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterThrowing方法执行
@After在切点方法后,return前执行
我进入了业务层的queryUser方法
2021-02-02 22:53:26.961 ERROR 66232 --- [nio-8089-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

与上述Spring4对比,发现此时的 AfterThrowing / AfterReturning 是在 After 前先执行

打赏
  • 版权声明: 本网站所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  1. © 2020-2021 Lauy    湘ICP备20003709号-1

请我喝杯咖啡吧~

支付宝
微信