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仓库

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

请我喝杯咖啡吧~

支付宝
微信