Spring Boot切面Aspect实现日志记录
刚接触到公司项目的时候还是能学到学校里学不到的东西,比如项目里将每个前端请求都记录在日志中,持久化到数据库,后来细看代码才发现,是使用切面实现的。
阅读这篇文章,你可能需要了解Spring Boot的一些知识,例如切面编程AOP
1、Maven依赖
首先肯定是要先引入依赖,依赖如下:
<!--spring切面aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、自定义注解,注入点方法注解
为什么要自定义注解,这里要提到面向切面编程思想:
这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
AOP即面向切面编程,可以说是OOP面向对象编程的补充和完善。
AOP的思想为在代码执行过程中,动态嵌入其他代码,叫做面向切面编程。常见的使用场景有事务,日志等
知道这点后,就不难察觉AOP中有个服务的中心,就是目标方法,因为是对一个方法进行横向的扩展,而在Spring Boot中标注一个方法,使用注解十分方便。
总而言之,自定义的这个注解,标注的就是需要横向拓展的方法。代码如下:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//注解放置的目标位置,METHOD是可注解在方法级别上的
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE})
//注解在哪个阶段执行
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionLog {
String value() default "";
}
3、实现切面日志前的一些准备
这一步主要是将日志持久化进数据库的一些实现。
3.1、定义一个日志类,用于封装日志信息
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
@Data
public class LogDto {
private String className;
private String methodName;
private String params;
private Long executionTime;
private String result;
private Short status;
private String operation;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private String createDate;
}
3.2、实现日志LogDto写进数据库操作,这里省略,简化为Service接口。
public interface LogDtoService {
/**
* 将logDto写入到数据库
*/
public Result<String> addLog(LogDto logDto);
}
4、使用切面完成日志的收集
这里才是重点戏,使用找到AOP切面,并对切面进行扩展,大致来说就是,找到标注了@ActionLog注解的Controller方法,然后在方法执行完成后,将执行过程的一些参数封装进LogDto中,最后写进数据库。
为了方便理解,这个类拆分几部分记录
4.1、先定义一个类
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect //声明这是一个切面
@Component
public class ActionLogAspect1 {
@Autowired
LogDtoService logDtoService;
/**
* 下面开始拆分方法进行记录
*/
//method1(){}
//method2(){}
}
4.2、定义切点 ,即是标注了@ActionLog注解的Controller方法
/**
* 定义切点 (controller切点,注解拦截)
*/
@Pointcut("@annotation(com.jankin.annotation.ActionLog)")
//com.jankin.annotation.ActionLog是自定义注解即ActionLog的全类名
public void logPointCut() {
}
表示凡是标注了@ActionLog这个注解的方法都会进行切面写日志
4.3、定义切面,即在切点(方法)执行的哪个位置进行扩展
这里使用的是环绕通知,忘记了的话就自己去查
/**
* 定义切面
*
* @param joinPoint 环绕通知
*/
@Around(value = "logPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result;
long beginTime = System.currentTimeMillis();
try {
//执行目标方法,将结果记录并返回
result = joinPoint.proceed();
} catch (Throwable e) {
//执行出错,记录出错日志
long time = System.currentTimeMillis() - beginTime;
//将错误日志写进数据库
//saveLog是本类中封装的方法,在下面有说
saveLog(joinPoint, time, 0, e.getMessage().trim());
throw e;
}
long time = System.currentTimeMillis() - beginTime;
//将成功日志写进数据库
saveLog(joinPoint, time, 1, result);
return result;
}、
4.4、上面引用的saveLog方法
saveLog方法中传过来joinPoint参数,可以通过其获得更多的信息
/**
* 将操作记录存入数据库
*
* @param joinPoint jo
* @author xuhongchun
* @date 2020/6/30 1:35 下午
*/
private void saveLog(ProceedingJoinPoint joinPoint, Long time, Integer status, String message) {
LogDto logDto=new LogDto();
//获取类名className,目标方法的类名
String className = joinPoint.getTarget().getClass().getName();
//获取方法名methodName
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getName();
//获取参数params
Object[] argValues = joinPoint.getArgs();
Map<String, Object> param = new HashMap<>(8);
if (argValues != null) {
for (int i = 0; i < argValues.length; i++) {
Object o = argValues[i];
if (o instanceof HttpServletRequest || o instanceof HttpServletResponse) {
continue;
}
param.put(argNames[i], argValues[i]);
}
}
//获取操作所需时长executionTime,方法参数已经传进来time
//获取操作需要的记录result,成功的话返回操作结果,失败的话返回异常信息,方法参数已经传进来message
//获取操作状态status,1代表成功,0代表失败,方法参数已经传进来status
//获取操作操作解释operation,即是标注在目标方法上的@ActionLog默认带的参数
Method method = signature.getMethod();
String operation = "";
if (null != method) {
ActionLog log = method.getAnnotation(ActionLog.class);
operation = log.value();
}
//将以上各参数封装进日志类LogDto中
logDto.setMethodName(methodName);
logDto.setParams(params);
logDto.setExecutionTime(executionTime);
logDto.setResult(result);
logDto.setStatus(status);
logDto.setOperation(operation);
//LoginDto持久化写入数据库
logDtoService.addLog(logDto);
//至此,日志结束
}
5、测试
编写一个Controller,然后通过接口访问handler方法,通过浏览器或者postman访问接口即可
Controller代码如下:
@ActionLog("登陆")
@PostMapping("login")
public Result<String> login(@RequestBody LoginDTO loginDTO) {
return loginService.login(loginDTO);
}
6、总结
这个实现还是比较直观的,花点小心机就能很好掌握,主要是通过这个小例子,还能很好地了解面向切面编程的思想,日后还可以考虑通过自定义注解+Aspect实现更多更有趣的功能。