AOP 是一种编程思想,Spring AOP 可以让我们从业务流程中抽取出一些通用的功能,然后针对这些通用的功能编写业务代码。而系统操作日志的记录功能就是这种通用的功能。

1. 添加依赖

Spring Boot 提供了 spring-boot-starter-aop 组件,我们直接拿来使用就可以。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 自定义注解

出于便捷性,通常情况下我们使用注解方式来标明通用功能的生效范围。

定义自定义日志记录注解 @RecordLog

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordLog {
    String value() default "";
}

这里我们定义的是一个方法级别的注解,包含一个属性 value。

3. 准备工作

创建系统日志类:

public class SysLog implements Serializable {

    private static final long serialVersionUID = -751464683105237224L;

    private Integer logNo;

    private String username;

    private String ipAddress;

    private String operation;

    private String method;

    private String params;

    private Long time;

    private String createTime;

    // 省略 setter、getter
}

建表 SQL 语句:

CREATE TABLE SYS_LOG
(
    LOG_NO      INTEGER PRIMARY KEY AUTO_INCREMENT COMMENT '日志编号',
    USERNAME    VARCHAR(20)  NOT NULL COMMENT '操作人',
    IP          VARCHAR(64)  NOT NULL COMMENT 'IP地址',
    OPERATION   VARCHAR(50)  NOT NULL COMMENT '操作内容',
    METHOD      VARCHAR(100) NOT NULL COMMENT '请求方法',
    PARAMS      VARCHAR(300) NOT NULL COMMENT '请求参数',
    TIME        LONG         NOT NULL COMMENT '操作耗时',
    CREATE_TIME DATETIME  DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT '日志表'
    ENGINE = InnoDB
    DEFAULT CHARSET = utf8
    AUTO_INCREMENT 10000;

Dao 层使用 Mybatis 注解方式记录日志:

@Mapper
public interface SysLogMapper {

    @Insert("insert into sys_log (USERNAME, IP, OPERATION, METHOD, PARAMS, TIME, CREATE_TIME) values (#{username}, " +
            "#{ipAddress}, #{operation}, #{method}, #{params}, #{time}, #{createTime} )")
    int save(SysLog sysLog);

    // ......
}

4. AOP部分

4.1. 定义切面、指定切点

@Aspect
@Component
public class LogAspect {

    @Autowired
    private ISysLogService sysLogService;

    @Pointcut("@annotation(com.demo.log.annotation.RecordLog)")
    public void pointCut() {

    }

    // ......
}

4.2. 通知内容

Spring AOP 中分为以下几种通知方式:

  • @Before:前置通知
  • @After:后置通知
  • @Around:环绕通知
  • @AfterReturning:成功返回后通知
  • @AfterThrowing:异常时通知

这里我们使用 环绕通知 的方式进行处理:

@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint) {
    long beginMills = System.currentTimeMillis();
    try {
        // 执行方法
        Object obj = joinPoint.proceed();
        long endMills = System.currentTimeMillis();
        Long time = endMills - beginMills;
        saveSysLog(joinPoint, time);
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
}

private void saveSysLog(ProceedingJoinPoint joinPoint, Long time) {
    SysLog sysLog = new SysLog();
    // 获取用户名
    String username = getLoginUser(joinPoint).getUsername();
    // 获取IP地址
    HttpServletRequest request =
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(
    String ipAddress = IPUtil.getIpAddress(request);
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method methodInfo = methodSignature.getMethod();
    // 获取注解中的描述
    RecordLog recordLog = methodInfo.getAnnotation(RecordLog.class);
    String operation = recordLog.value();
    // 获取类名
    String className = joinPoint.getTarget().getClass().getName();
    // 获取方法名
    String methodName = methodInfo.getName();
    // 获取参数
    String params = getParams(joinPoint);
    sysLog.setUsername(username);
    sysLog.setIpAddress(ipAddress);
    sysLog.setOperation(operation);
    sysLog.setMethod(className + "." + methodName);
    sysLog.setParams(params);
    sysLog.setTime(time);
    sysLogService.record(sysLog);
}

/**
 * 获取当前登录的用户信息
 *
 * @param joinPoint 切点
 * @return 用户信息
 */
private SysUser getLoginUser(ProceedingJoinPoint joinPoint) {
    // 这里模仿一个用户,实际场景中,一般从session或者token中获取用户信息
    SysUser user = new SysUser();
    user.setUserId("001");
    user.setUsername("NekoChips");
    return user;
}

/**
 * 获取请求参数
 *
 * @param joinPoint 切点
 * @return 请求参数
 */
private String getParams(ProceedingJoinPoint joinPoint) {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    String[] parameterNames = methodSignature.getParameterNames();
    Object[] args = joinPoint.getArgs();
    String params = null;
    if (parameterNames.length != 0 && args.length != 0) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < parameterNames.length; i++) {
            stringBuilder.append(parameterNames[i]).append(" : ").append(args[i]);
            if (i < parameterNames.length - 1) {
                stringBuilder.append(", ");
            }
        }
        params = stringBuilder.toString();
    }
    return params;
}

5. 使用@RecordLog

@GetMapping("info")
@RecordLog("查询员工信息")
public HttpEntity<?> queryById(String emNo) {
    Employee employee = employeeService.queryById(emNo);
    return ResponseEntity.ok(employee);
}

启动项目后,调用该方法,查看数据库内容:

image.png

源码地址:https://github.com/NekoChips/SpringDemo/tree/master/06.springboot-aop-log


关于作者:NekoChips
本文地址:https://chenyangjie.com.cn/articles/2020/04/26/1587887374923.html
版权声明:本篇所有文章仅用于学习和技术交流,本作品采用 BY-NC-SA 4.0 许可协议,如需转载请注明出处!
许可协议:知识共享许可协议