前言
不久前,因为需求的原因,需要实现一个操作日志。几乎每一个接口被调用后,都要记录一条跟这个参数挂钩的特定的日志到数据库。举个例子,就比如禁言操作,日志中需要记录因为什么禁言,被禁言的人的id和各种信息。方便后期查询。
这样的接口有很多个,而且大部分接口的参数都不一样。可能大家很容易想到的一个思路就是,实现一个日志记录的工具类,然后在需要记录日志的接口中,添加一行代码。由这个日志工具类去判断此时应该处理哪些参数。
但是这样有很大的问题。如果需要记日志的接口数量非常多,先不讨论这个工具类中需要做多少的类型判断,仅仅是给所有接口添加这样一行代码在我个人看来都是不能接受的行为。首先,这样对代码的侵入性太大。其次,后期万一有改动,维护的人将会十分难受。想象一下,全局搜索相同的代码,再一一进行修改。
所以我放弃了这个略显原始的方法。我最终采用了Aop的方式,采取拦截的请求的方式,来记录日志。但是即使采用这个方法,仍然面临一个问题,那就是如何处理大量的参数。以及如何对应到每一个接口上。
我最终没有拦截所有的controller,而是自定义了一个日志注解。所有打上了这个注解的方法,将会记录日志。同时,注解中会带有类型,来为当前的接口指定特定的日志内容以及参数。
那么如何从众多可能的参数中,为当前的日志指定对应的参数呢。我的解决方案是维护一个参数类,里面列举了所有需要记录在日志中的参数名。然后在拦截请求时,通过反射,获取到该请求的request和response中的所有参数和值,如果该参数存在于我维护的param类中,则将对应的值赋值进去。
然后在请求结束后,将模板中的所有预留的参数全部用赋了值的参数替换掉。这样一来,在不大量的侵入业务的前提下,满足了需求,同时也保证了代码的可维护性。
下面我将会把详细的实现过程列举出来。
开始操作前
文章结尾我会给出这个demo项目的所有源码。所以不想看过程的兄台可移步到末尾,直接看源码。(听说和源码搭配,看文章更美味...)
开始操作
新建项目
大家可以参考我之前写的另一篇文章,手把手教你从零开始搭建SpringBoot后端项目框架。只要能请求简单的接口就可以了。本项目的依赖如下。
org.springframework.boot
spring-boot-starter-web
2.1.1.RELEASE
org.aspectj
aspectjrt
1.9.2
org.aspectj
aspectjweaver
1.9.2
org.projectlombok
lombok
1.18.2
cn.hutool
hutool-all
4.1.14
新建Aop类
新建LogAspect类。代码如下。
package spring.aop.log.demo.api.util;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/*** LogAspect** @author Lunhao Hu* @date 2019-01-30 16:21**/
@Aspect
@Component
public class LogAspect {
/*** 定义切入点*/
@Pointcut("@annotation(spring.aop.log.demo.api.util.Log)")
public void operationLog() {
}
/*** 新增结果返回后触发** @param point* @param returnValue*/
@AfterReturning(returning = "returnValue", pointcut = "operationLog() && @annotation(log)")
public void doAfterReturning(JoinPoint point, Object returnValue, Log log) {
System.out.println("test");
}
}
Pointcut中传入了一个注解,表示凡是打上了这个注解的方法,都会触发由Pointcut修饰的operationLog函数。而AfterReturning则是在请求返回之后触发。
自定义注解
上一步提到了自定义注解,这个自定义注解将打在controller的每个方法上。新建一个annotation的类。代码如下。
package spring.aop.log.demo.api.util;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*** Log** @author Lunhao Hu* @date 2019-01-30 16:19**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String type() default "";
}
Target和Retention都属于元注解。共有4种,分别是@Retention、@Target、@Document、@Inherited。
Target注解说明了该Annotation所修饰的范围。可以传入很多类型,参数为ElementType。例如TYPE,用于描述类、接口或者枚举类;FIELD用于描述属性;METHOD用于描述方法;PARAMETER用于描述参数;CONSTRUCTOR用于描述构造函数;LOCAL_VARIABLE用于描述局部变量;ANNOTATION_TYPE用于描述注解;PACKAGE用于描述包等。
Retention注解定义了该Annotation被保留的时间长短。参数为RetentionPolicy。例如SOURCE表示只在源码中存在,不会在编译后的class文件存在;CLASS是该注解的默认选项。 即存在于源码,也存在于编译后的class文件,但不会被加载到虚拟机中去;RUNTIME存在于源码、class文件以及虚拟机中,通俗一点讲就是可以在运行的时候通过反射获取到。
加上普通注解
给需要记录日志的接口加上Log注解。
package spring.aop.log.demo.api.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import spring.aop.log.demo.api.util.Log;
/*** HelloController** @author Lunhao Hu* @date 2019-01-30 15:52**/
@RestController
public class HelloController {
@Log
@GetMapping("test/{id}")
public String test(@PathVariable(name = "id