aop实现日志持久化记录到mysql

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切面方法

这里赘述一下几个切面方法

  1. 前置通知(Before Advice):在目标方法执行之前执行的通知。可以在前置通知中进行一些预处理操作,例如参数校验、权限检查等。
  2. 后置通知(After Advice):在目标方法执行之后执行的通知。无论目标方法是否抛出异常,后置通知都会执行。可以用于进行一些清理操作,例如资源释放等。
  3. 返回通知(After Returning Advice):在目标方法正常返回后执行的通知。可以获取到目标方法的返回值,并进行相应的处理。
  4. 异常通知(After Throwing Advice):在目标方法抛出异常后执行的通知。可以捕获目标方法抛出的异常,并进行相应的处理。
  5. 环绕通知(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输出语句延迟输出成功。完结,撒花

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值