Skip to content

在项目中,记录用户操作日志是监控用户行为、分析系统安全性和排查问题的重要手段,操作日志通常包括以下关键信息:

1. 行为追踪:记录用户操作日志,便于追踪和审计用户行为,确保系统安全性和合规性。
2. 问题排查:通过操作日志快速定位和排查系统问题,提升故障排查效率。
3. 数据恢复:在数据异常或丢失时,操作日志可以作为恢复数据的依据。
4. 性能监控:记录操作日志有助于分析系统性能瓶颈,优化系统性能。
5. 责任追溯:通过操作日志明确责任归属,便于管理和追责。
6. 用户行为分析:分析用户操作日志,了解用户行为模式,优化产品功能。

AOP优势

在项目中使用 AOP(面向切面编程)注解实现操作日志具有以下优势:

1. 代码解耦:将日志记录逻辑与业务逻辑分离,避免代码重复,提升代码可读性和可维护性。
2. 灵活性与扩展性:通过注解可以灵活地控制哪些方法需要记录日志,便于扩展和修改日志记录逻辑。
3. 非侵入性:无需修改现有业务代码,只需在方法上添加注解即可实现日志记录。
4. 集中管理:日志记录逻辑集中在切面中,便于统一管理和维护。
5. 提高开发效率:通过注解方式快速实现日志功能,减少重复代码编写。

定义注解

xiaomayi-common/xiaomayi-logger 模块中定义的操作日志的 AOP 切面 RequestLog 文件,内容如下:

js
package com.xiaomayi.logger.annotation;

import com.xiaomayi.logger.enums.RequestSource;
import com.xiaomayi.logger.enums.RequestType;

import java.lang.annotation.*;

/**
 * <p>
 * 请求日志注解
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-05-25
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLog {

    /**
     * 日志标题
     *
     * @return 返回结果
     */
    String title() default "未知";

    /**
     * 请求类型
     *
     * @return 返回结果
     */
    RequestType type() default RequestType.OTHER;

    /**
     * 请求来源
     *
     * @return 返回结果
     */
    RequestSource source() default RequestSource.SYSTEM;

    /**
     * 需要排序的敏感字段
     *
     * @return 返回结果
     */
    String[] exclude() default {};

}

注解实现

js
package com.xiaomayi.logger.aspect;

import com.alibaba.fastjson2.JSON;
import com.xiaomayi.core.enums.HttpMethod;
import com.xiaomayi.core.utils.IpAddressUtils;
import com.xiaomayi.core.utils.IpUtils;
import com.xiaomayi.core.utils.ServletUtils;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.logger.annotation.RequestLog;
import com.xiaomayi.logger.enums.RequestSource;
import com.xiaomayi.logger.enums.RequestStatus;
import com.xiaomayi.logger.filter.ExcludePropertyPreFilter;
import com.xiaomayi.logger.vo.RequestLogVO;
import eu.bitwalker.useragentutils.UserAgent;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.NamedThreadLocal;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 请求日志切面
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-05-21
 */
@Slf4j
@Aspect
@Component
@Order(1) // 第一层切面
public abstract class RequestAspect {

    /**
     * 敏感属性过滤
     */
    public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};

    /**
     * 计算耗时线程
     */
    ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("ConsumeTime");

    /**
     * 扫描切入点注解
     */
    @Pointcut("@annotation(com.xiaomayi.logger.annotation.RequestLog)")
    public void doPointCut() {

    }

    /**
     * 处理请求前执行
     *
     * @param requestLog 请求日志
     */
    @Before("doPointCut() && @annotation(requestLog)")
    public void doBefore(RequestLog requestLog) {
        log.info("请求AOP日志处理开始");
        // 设置请求开始时间
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    /**
     * 处理完请求后执行
     */
    @After("doPointCut()")
    public void doAfter() {
        log.info("请求AOP日志处理结束");
    }

    /**
     * 处理完请求后执行
     *
     * @param point      切点
     * @param requestLog 请求日志
     * @param jsonResult 响应结果
     */
    @AfterReturning(pointcut = "@annotation(requestLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint point, RequestLog requestLog, Object jsonResult) {
        log.info("请求AOP日志结果已响应");
        saveLog(point, requestLog, jsonResult, null);
    }

    /**
     * 拦截异常处理
     *
     * @param point      切点
     * @param requestLog 请求日志
     * @param exception  异常处理
     */
    @AfterThrowing(value = "@annotation(requestLog)", throwing = "exception")
    public void doAfterThrowing(JoinPoint point, RequestLog requestLog, Exception exception) {
        log.info("请求AOP日志处理异常");
        saveLog(point, requestLog, null, exception);
    }

    /**
     * 处理请求日志
     *
     * @param point      切点
     * @param annotation 日志注解
     * @param jsonResult 响应结果
     * @param exception  异常处理
     */
    private void saveLog(JoinPoint point, RequestLog annotation, Object jsonResult, Exception exception) {
        try {
            // 获取日志标题
            String title = annotation.title();

            // 实例化请求日志VO
            RequestLogVO requestLogVO = new RequestLogVO();
            // 日志标题
            requestLogVO.setTitle(title);
            // 请求类型
            requestLogVO.setType(RequestStatus.SUCCESS.ordinal());
            // 请求来源
            requestLogVO.setSource(RequestSource.SYSTEM.ordinal());

            // 客户端IP
            String ip = IpUtils.getIpAddr();
            requestLogVO.setIp(ip);
            // 根据IP获取真实地址
            String address = IpAddressUtils.getRealAddress(ip);
            requestLogVO.setLocation(address);

            // 请求地址
            String url = StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255);
            requestLogVO.setUrl(url);

            // 浏览器解析
            UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
            // 获取客户端操作系统
            String os = userAgent.getOperatingSystem().getName();
            requestLogVO.setOs(os);
            // 获取客户端浏览器
            String browser = userAgent.getBrowser().getName();
            requestLogVO.setBrowser(browser);

            // 设置方法名称
            String className = point.getTarget().getClass().getName();
            String methodName = point.getSignature().getName();
            requestLogVO.setMethod(className + "." + methodName + "()");
            // 设置方法名称
            requestLogVO.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            requestLogVO.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            setRequestParam(point, requestLogVO, annotation.exclude(), jsonResult);
            // 设置请求耗时
            requestLogVO.setConsumeTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());

            // 请求日志状态
            if (exception != null) {
                // 异常处理
                requestLogVO.setStatus(RequestStatus.FAIL.ordinal());
                requestLogVO.setError(StringUtils.substring(exception.getMessage(), 0, 2000));
            } else {
                requestLogVO.setStatus(0);
                requestLogVO.setError(null);
            }

            // 调用抽象类保存请求地址
            if (requestLog(requestLogVO)) {
                log.info("请求AOP日志已存储");
            }
        } catch (Exception e) {
            log.error("异常信息:{}", e.getMessage());
        } finally {
            TIME_THREADLOCAL.remove();
        }
    }

    /**
     * 设置请求参数
     *
     * @param point             切点
     * @param requestLogVO      请求日志VO
     * @param excludeParamNames 敏感属性字段
     * @param jsonResult        返回结果
     */
    private void setRequestParam(JoinPoint point, RequestLogVO requestLogVO, String[] excludeParamNames, Object jsonResult) {
        // 获取所有请求参数
        Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        // 获取请求方式
        String requestMethod = requestLogVO.getRequestMethod();
        // 请求参数判空
        if (StringUtils.isEmpty(paramsMap) && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) {
            // 格式化请求参数
            String params = getJSONParam(point.getArgs(), excludeParamNames);
            // 设置请求日志参数
            requestLogVO.setParam(StringUtils.substring(params, 0, 2000));
        } else {
            // 设置请求日志参数
            requestLogVO.setParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
        }
        // 设置网络响应
        if (StringUtils.isNotNull(jsonResult)) {
            requestLogVO.setResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 请求参数转JSON字符串
     *
     * @param objects           参数对象
     * @param excludeParamNames 敏感属性字段名称集合
     * @return 返回结果
     */
    private String getJSONParam(Object[] objects, String[] excludeParamNames) {
        // 参数对象判空
        if (StringUtils.isEmpty(objects)) {
            return "";
        }
        // 实例化参数列表
        List<String> paramList = new ArrayList<>();
        // 遍历属性
        for (Object obj : objects) {
            // 对象判空
            if (StringUtils.isNull(obj) || isExcludeObject(obj)) {
                continue;
            }
            // 对象转JSON,排除敏感属性字段
            String jsonObj = JSON.toJSONString(obj, excludePropertyPreFilter(excludeParamNames));
            if (StringUtils.isEmpty(jsonObj)) {
                continue;
            }
            // 加入列表
            paramList.add(jsonObj);
        }
        // 参数列表转拼接字符串
        String params = StringUtils.join(paramList.toArray(), " ");
        // 返回结果
        return params;
    }

    /**
     * 敏感属性过滤
     *
     * @param excludeParamNames 参数名称集合
     * @return 返回结果
     */
    public ExcludePropertyPreFilter excludePropertyPreFilter(String[] excludeParamNames) {
        return new ExcludePropertyPreFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
    }

    /**
     * 判断是否需要排除的对象
     *
     * @param obj 对象
     * @return 返回结果
     */
    @SuppressWarnings("rawtypes")
    public boolean isExcludeObject(final Object obj) {
        // 获取类
        Class<?> clazz = obj.getClass();
        // 判断是否数组
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        }
        // 判断是否集合
        else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) obj;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        }
        // 判断是否Map
        else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) obj;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        // 返回结果
        return obj instanceof MultipartFile
                || obj instanceof HttpServletRequest
                || obj instanceof HttpServletResponse
                || obj instanceof BindingResult;
    }

    /**
     * 请求日志抽象类
     *
     * @param requestLogVO 请求日志VO
     * @return 返回结果
     */
    public abstract boolean requestLog(RequestLogVO requestLogVO);

}

添加依赖

pom.xml 配置文件中引入以下依赖:

js
<!-- 依赖声明 -->
<dependencies>
    <!-- 安全认证依赖模块 -->
    <dependency>
        <groupId>com.xiaomayi</groupId>
        <artifactId>xiaomayi-security</artifactId>
    </dependency>
    <!-- 日志依赖模块 -->
    <dependency>
        <groupId>com.xiaomayi</groupId>
        <artifactId>xiaomayi-logger</artifactId>
    </dependency>
</dependencies>

注解使用

在需要记录操作日志的方法上添加以下注解:

js
@RequestLog(title = "获取验证码", type = RequestType.OTHER)

使用案例:

js
package com.xiaomayi.admin.controller;


import com.xiaomayi.core.utils.R;
import com.xiaomayi.logger.annotation.LoginLog;
import com.xiaomayi.logger.annotation.RequestLog;
import com.xiaomayi.logger.enums.LoginType;
import com.xiaomayi.logger.enums.RequestType;
import com.xiaomayi.system.dto.LoginDTO;
import com.xiaomayi.admin.service.LoginService;
import com.xiaomayi.system.dto.RegisterDTO;
import com.xiaomayi.tenant.annotation.TenantIgnore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;


/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-05-21
 */
@RestController
@Tag(name = "登录", description = "登录")
@AllArgsConstructor
public class LoginController {

    @Autowired
    private LoginService loginService;

    /**
     * 获取验证码
     *
     * @return 返回结果
     */
    @Operation(summary = "获取验证码", description = "获取验证码")
    @RequestLog(title = "获取验证码", type = RequestType.OTHER)
    @GetMapping("/captcha")
    public R captcha() {
        return loginService.captcha();
    }

}

总结

通过 AOP 注解实现操作日志,能够显著提升代码质量和开发效率。

小蚂蚁云团队 · 提供技术支持

小蚂蚁云 新品首发
新品首发,限时特惠,抢购从速! 全场95折
赋能开发者,助理企业发展,提供全方位数据中台解决方案。
获取官方授权