Spring的事件发布模式实现异步解耦日志

14 阅读2分钟

第一步:Common 包里定义好注解和事件(只做一次) 1. 定义注解 @Log

package com.odb.core.log.annotation;

import java.lang.annotation.*;
import com.odb.core.log.enums.BusinessType;

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{
    /** 模块名称 */
    String title() default "";

    /** 业务类型(0其它 1新增 2修改 3删除) */
    BusinessType businessType() default BusinessType.OTHER;

    /** 是否保存请求参数 */
    boolean isSaveRequestData() default true;

    /** 是否保存响应参数 */
    boolean isSaveResponseData() default true;
}
  1. 定义切面 LogAspect (负责发布事件)
package com.odb.core.log.aspects;

import com.odb.core.log.SysOperLog;
import com.odb.core.log.annotation.Log;
import com.odb.core.log.event.SysOperLogEvent; // 事件类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
// ... 其他 import 略

@Aspect
@Component

package com.odb.log.aspects;

import java.net.InetAddress; import java.net.UnknownHostException;

// 注意这里换成了 jakarta import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;

import com.alibaba.fastjson2.JSON; import com.odb.log.SysOperLog; import com.odb.log.annotation.Log; import com.odb.log.service.AsyncLogService; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.multipart.MultipartFile;

@Aspect @Component public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<>();

@Autowired
private AsyncLogService asyncLogService;

@Before("@annotation(controllerLog)")
public void doBefore(JoinPoint joinPoint, Log controllerLog)
{
    TIME_THREADLOCAL.set(System.currentTimeMillis());
}

@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
{
    handleLog(joinPoint, controllerLog, null, jsonResult);
}

@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
{
    handleLog(joinPoint, controllerLog, e, null);
}

protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
{
    try
    {
        // 获取 Request (Spring 自动适配 Jakarta)
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (attributes != null) ? attributes.getRequest() : null;

        SysOperLog operLog = new SysOperLog();
        operLog.setStatus(1);

        if (request != null)
        {
            operLog.setOperIp(getIpAddr(request));
            operLog.setOperUrl(request.getRequestURI());
            operLog.setRequestMethod(request.getMethod());
        }

        if (e != null)
        {
            operLog.setStatus(0);
            operLog.setErrorMsg(e.getMessage());
        }

        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        operLog.setMethod(className + "." + methodName + "()");

        if (controllerLog != null)
        {
            operLog.setTitle(controllerLog.title());
            operLog.setBusinessType(controllerLog.businessType().ordinal());

            if (controllerLog.isSaveRequestData() && request != null)
            {
                Object[] args = joinPoint.getArgs();
                String params = argsArrayToString(args);
                operLog.setOperParam(params.length() > 2000 ? params.substring(0, 2000) : params);
            }
        }

        if (jsonResult != null)
        {
            String json = JSON.toJSONString(jsonResult);
            operLog.setJsonResult(json.length() > 2000 ? json.substring(0, 2000) : json);
        }

        Long startTime = TIME_THREADLOCAL.get();
        if (startTime != null)
        {
            operLog.setCostTime(System.currentTimeMillis() - startTime);
        }

// operLog.setOperName("admin");

        asyncLogService.saveSysLog(operLog);
    }
    catch (Exception exp)
    {
        log.error("==前置通知异常==", exp);
    }
    finally
    {
        TIME_THREADLOCAL.remove();
    }
}

private String argsArrayToString(Object[] paramsArray)
{
    if (paramsArray == null || paramsArray.length == 0)
    {
        return "";
    }
    StringBuilder params = new StringBuilder();
    for (Object o : paramsArray)
    {
        if (o != null && !isFilterObject(o))
        {
            try
            {
                params.append(JSON.toJSONString(o)).append(" ");
            }
            catch (Exception e)
            {
            }
        }
    }
    return params.toString().trim();
}

public boolean isFilterObject(final Object o)
{
    return o instanceof MultipartFile
            || o instanceof HttpServletRequest
            || o instanceof HttpServletResponse;
}

public static String getIpAddr(HttpServletRequest request)
{
    if (request == null)
    {
        return "unknown";
    }
    String ip = request.getHeader("x-forwarded-for");
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
    {
        ip = request.getHeader("Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
    {
        ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
    {
        ip = request.getRemoteAddr();
        if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip))
        {
            try
            {
                ip = InetAddress.getLocalHost().getHostAddress();
            }
            catch (UnknownHostException e)
            {
                e.printStackTrace();
            }
        }
    }
    return ip;
}

}

  1. 定义事件类 SysOperLogEvent
package com.odb.core.log.event;

import com.odb.core.log.SysOperLog;
import org.springframework.context.ApplicationEvent;

public class SysOperLogEvent extends ApplicationEvent
{
    public SysOperLogEvent(SysOperLog source)
    {
        super(source);
    }

    public SysOperLog getSysOperLog()
    {
        return (SysOperLog) getSource();
    }
}

第二步:在业务项目中使用(如 odb-admin) 1. 引入 Common 包依赖

<dependency>
    <groupId>com.odb</groupId>
    <artifactId>odb-common-core</artifactId>
    <version>1.0.0</version>
</dependency>
  1. 在 Controller 方法上加注解 这是开发者日常使用的方式。
@RestController
@RequestMapping("/system/user")
public class SysUserController
{
    @Autowired
    private ISysUserService userService;

    /**
     * 新增用户
     */
    @Log(title = "用户管理", businessType = BusinessType.INSERT) // 加在这里
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysUser user)
    {
        return toAjax(userService.insertUser(user));
    }

    /**
     * 修改用户
     */
    @Log(title = "用户管理", businessType = BusinessType.UPDATE) // 加在这里
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysUser user)
    {
        return toAjax(userService.updateUser(user));
    }
}
  1. 实现日志监听器(负责入库) 这是业务项目必须实现的,否则日志只会被发布,没人处理。
package com.odb.web.listener;

import com.odb.core.log.event.SysOperLogEvent;
import com.odb.core.log.SysOperLog;
import com.odb.system.service.ISysOperLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * 异步监听日志事件
 */
@Component
public class SysLogListener {
    
    @Autowired
    private SysOperLogMapper operLogMapper; // 业务项目的 Mapper

    @Async // 异步处理
    @EventListener
    public void saveLog(SysOperLogEvent event) {
        SysOperLog sysLog = event.getSysOperLog();

        // 1. 填充操作人(只有业务项目才知道当前是谁登录)
        try {
            String username = SecurityUtils.getUsername(); 
            sysLog.setOperName(username);
        } catch (Exception e) {
            sysLog.setOperName("unknown");
        }

        // 2. 入库
        operLogMapper.insert(sysLog);
    }
}
  1. 开启异步支持 在启动类上加 @EnableAsync
@EnableAsync
@SpringBootApplication
public class OdbApplication { ... }