aop实现日志持久化记录
前言:项目中会要求对系统的操作日志进行记录,例如用户登录,添加、修改、删除等操作,记录进数据库,方便管理员查看数据或者明确事故责任链。
日志内容一般包含操作者id,请求ip,uri,方法类型,参数以及操作结果,返回值等等。获取请求ip,参数的话通过过滤器或者拦截器会比较简单,但是PostBody中参数是以流方式记录只能被读取一次,如果在过滤器或者拦截器中进行读取,则controller将捕获不到,影响正常的接口逻辑。
有大佬针对以上问题做了解决方案,详见原文链接:
使用拦截器获取请求信息和返回信息[https://blog.csdn.net/liar_____/article/details/118087961]
这里不对以上方案进行解释或补充,而选择使用AOP实现日志记录需求。
1.引入AOP依赖包
Springboot默认不带AOP拓展包,需要额外引入
Maven示例
<!-- 引入aop支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.创建注解annotation
注解名称随便起
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogPersistence {
}
3.实现AOP切面方法
这里赘述一下几个切面方法
- 前置通知(Before Advice):在目标方法执行之前执行的通知。可以在前置通知中进行一些预处理操作,例如参数校验、权限检查等。
- 后置通知(After Advice):在目标方法执行之后执行的通知。无论目标方法是否抛出异常,后置通知都会执行。可以用于进行一些清理操作,例如资源释放等。
- 返回通知(After Returning Advice):在目标方法正常返回后执行的通知。可以获取到目标方法的返回值,并进行相应的处理。
- 异常通知(After Throwing Advice):在目标方法抛出异常后执行的通知。可以捕获目标方法抛出的异常,并进行相应的处理。
- 环绕通知(Around Advice):在目标方法执行前后都执行的通知。环绕通知可以完全控制目标方法的执行过程,包括是否执行目标方法、在何时执行目标方法以及如何处理目标方法的返回值和异常。
这里主要做操作日志记录,且需要保存返回体,所以选择返回通知
简单创建切口类,和通知方法,先不做实现,因为下面还需要和拦截器结合获取请求IP
@Aspect
@Component
public class LogPersistenceAspect {
// com.jankin.inoteblog.aspect.annotation.LogPersistence 是我定义接口的文件路径
@Pointcut("@annotation(com.jankin.inoteblog.aspect.annotation.LogPersistence)")
public void logPersistenceAspect() {}
/**
* 返回通知切面方法
* @param joinPoint 切点,就是被注解的目标方法
* @param result 切点方法返回体
*/
@AfterReturning(pointcut = "logPersistenceAspect()", returning = "result")
public void logPostMapping(JoinPoint joinPoint, Object result) {
// 获取方法名
String method = joinPoint.getSignature().toShortString();
// 获取方法参数数组[],这里有个注意点,就是controller中post方法的请求体参数必须@RequestBody标注
Object[] args = joinPoint.getArgs();
// 其他to do ……
}
}
4.使用注解
在需要使用切面的方法上添加上面自定义的注解
/**
* 测试Post方法
* @param testDto 请求体,包含name和text两个属性
* @return
*/
@LogPersistence // 自定义切面注解
@PostMapping
public Result post(@RequestBody TestDto testDto){
System.out.println("执行业务方法");
return new Result(200,"请求成功","这是data");
}
至此,AOP定义完成,下面针对业务,即是实现日志持久化讲解
5.定义ThreadLocal存储请求IP
这一步骤主要是为存储请求者IP,假如你的日志记录不需要记录请求者IP,这一步以及下面实现Interceptor的步骤都可以省略
使用ThreadLocal是为了在整个请求过程都能随时获取该数据
public class IPContextHolder {
private static final ThreadLocal<String> ipThreadLocal = new ThreadLocal<>();
public static void setIP(String ip) {
ipThreadLocal.set(ip);
}
public static String getIP() {
return ipThreadLocal.get();
}
public static void clear() {
ipThreadLocal.remove();
}
}
6.创建Interceptor记录IP
通过继承重写HandlerInterceptor的preHandle方法,结合ThreadLocal保存IP
另外,可以在这个拦截器上做其他事,例如拦截IP等等,之前就有一个需求是对同一个IP频繁访问一个请求做限制,这里不做赘述
@Slf4j
public class IpHandlerInterceptor implements HandlerInterceptor {
/**
* handler执行前执行
**/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String remoteAddr = request.getRemoteAddr(); // 获取请求的IP
IPContextHolder.setIP(remoteAddr); // 写进ThreadLocal
// 控制台打印请求日志
log.debug("ip:{},访问url:{}",remoteAddr,request.getRequestURI());
return true; // 放行
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
创建的Interceptor需要配置才能生效
新建或者在已有的 WebMvcConfigurer 类中配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
public IpHandlerInterceptor getIpHandlerInterceptor(){
return new IpHandlerInterceptor();
}
/**
* 添加自定义拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 将自定义拦截器加入容器,且配置拦截所有请求
registry.addInterceptor(getIpHandlerInterceptor()).addPathPatterns("/**");
}
}
7.使用线程池
使用线程池的目的是想将日志持久化进mysql时,采用异步执行,即是另起线程执行,这样能将日志记录和用户请求逻辑解耦,其一是加快请求返回速度,其二是如果日志输入出错不会影响到正常请求逻辑返回。而如果每次都创建新线程,会增大服务器压力,是否需要线程池,还是看自己吧。
另外可以使用Springboot给我们提供的线程池,也可以自己创建新的线程池,这里用Springboot提供的线程池为例
将springboot提供的线程池加入容器,并在该配置类上标注@EnableAsync允许异步处理,例如
@Configuration
@EnableAsync
public class DefaultThreadPool {
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3); // 核心线程数
taskExecutor.setMaxPoolSize(15); // 最大线程数
taskExecutor.setQueueCapacity(100); // 缓冲队列数
taskExecutor.setKeepAliveSeconds(60); // 线程空闲时间
taskExecutor.setThreadNamePrefix("SysLogPersistence_Thread_"); // 线程名称前缀
// 销毁机制:超过核心线程数时,而且(超过最大值或者timeout过),就会销毁。默认false
taskExecutor.setAllowCoreThreadTimeOut(true);
// 拒绝策略
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//线程池初始化
taskExecutor.initialize();
return taskExecutor;
// 或者直接使用默认配置,直接retrun new ThreadPoolTaskExecutor(); 也可以
}
}
在需要异步处理的方法上标注@Async注解,注意被标注的方法所属的类需要被Spring管理,即标注@Service或@Component等注解
@Service
public class SysLogServiceImpl implements SysLogService {
@Async
@Override
public void addLog(SysLog sysLog) {
System.out.println("日志输入mysql操作……");
// 写进mysql数据库操作……
}
}
8.完善AOP方法
最后回到AOP方法,完善获取IP方式以及写进数据库操作
@Aspect
@Component
public class LogPersistenceAspect {
@Autowired
SysLogService sysLogService;
// com.jankin.inoteblog.aspect.annotation.LogPersistence 是我定义接口的文件路径
@Pointcut("@annotation(com.jankin.inoteblog.aspect.annotation.LogPersistence)")
public void logPersistenceAspect() {}
/**
* 返回通知切面方法
* @param joinPoint 切点,就是被注解的目标方法
* @param result 切点方法返回体
*/
@AfterReturning(pointcut = "logPersistenceAspect()", returning = "result")
public void logPostMapping(JoinPoint joinPoint, Object result) {
String userId = UserBaseInfoContextHolder.getUserInfo().getUserId(); // 获取操作用户Id
String ip = IPContextHolder.getIP(); // 获取请求的IP
String method = joinPoint.getSignature().toShortString(); // 获取方法名
// 获取方法参数数组[],这里有个注意点,就是controller中post方法的请求体参数必须@RequestBody标注
Object[] args = joinPoint.getArgs();
String argsStr = ""; // 请求参数字符串
for (Object object:args){
argsStr = argsStr + object;
}
String status = "ERROR";
String resultStr = "";
if (result!=null) {
resultStr = result.toString(); // 返回结果字符串
status = "SUCCESS";
}
// 将以上数据封装进SysLog类
SysLog sysLog = new SysLog(null,userId,ip,method,argsStr,status,resultStr,null);
// 写进数据库
sysLogService.addLog(sysLog);
}
}
至此全篇结束。
测试
将addLog方法暂停5秒,请求是否在addLog方法执行完成前返回
经验证,postman测试成功返回,addLog输出语句延迟输出成功。完结,撒花