springboot AOP记录用户操作日志

2,925 阅读6分钟

需求

记录用户的增删改操作到数据库:

  1. 是谁修改的
  2. 修改了哪张表
  3. 修改了哪条数据
  4. 操作的时间

开发环境

  1. Springboot
  2. Mybatis
  3. Lombok

依赖

<!-- Mybatis依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.1</version>
</dependency>

<!-- mysql依赖 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- AOP依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- Lombok依赖 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!-- 连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

数据库

用户表

CREATE TABLE `user`  (
  `oid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户主键',
  `userName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  PRIMARY KEY (`oid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

日志表

CREATE TABLE `log`  (
  `oid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `tableName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '表名',
  `tableId` int(11) NOT NULL COMMENT '操作那个表中的主键',
  `type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '操作方式',
  `userId` bigint(20) NOT NULL COMMENT '操作用户id',
  `time` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '操作时间',
  PRIMARY KEY (`oid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

实体类

用户


/**
 * @author dclup
 * @Description 用户实体类
 **/
@Data
public class UserDO {

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 用户姓名
     */
    private String userName;
}

日志


/**
 * @author dclup
 * @Description 日志实体类
 **/
@Data
public class LogDO {

    /**
     * 主键
     */
    private Long oid;

    /**
     * 表名
     */
    private String tableName;

    /**
     * 操作那个表中的主键
     */
    private Long tableId;

    /**
     * 操作方式
     */
    private String type;

    /**
     * 操作时间
     */
    private String time;

    /**
     * 操作人id
     */
    private Long userId;
}


枚举类

使用枚举类限定类型,防止瞎传。
主要用来存放==数据库表名==、==操作类型==和==操作那张表的主键id==。
此项目只用到了user的相关信息,department只是用来举例。

数据库表名


/**
 * @author dclup
 * @Description 数据库表名
 **/
public enum TableName {

    UNKNOWN("unknown"),
    USER("user"),
    DEPARTMENT("department");

    private final String name;

    TableName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

操作类型


/**
 * @author dclup
 * @Description 操作类型名称
 **/
public enum TypeName {

    UNKNOWN("unknown"),
    INSERT("insert"),
    UPDATE("update"),
    DELETE("delete");

    private final String name;

    TypeName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

id名称


/**
 * @author dclup
 * @Description 某个表主键名称
 **/
public enum IdName {

    UNKNOWN("unknown"),
    USERID("userId"),
    DEPARTMENTID("departmentId");

    private final String name;

    IdName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}


以下是删改查的基本实现(可以不用看):基本实现开始

Controller


/**
 * @author dclup
 * @Description 用户信息
 **/

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 获取一位用户信息
     *
     * @param userId 用户id
     * @return
     */
    @GetMapping("/user/{userId}")
    public Map<String, Object> getByPrimaryKey(
            @PathVariable("userId") Long userId) {
        return userService.getByPrimaryKey(userId);
    }

    /**
     * 修改一位用户信息
     * @param userDO
     * @param userId userId 用户id
     * @return
     */
    @PutMapping("/user/{userId}")
    @Log(table = TableNameEnum.USER,type = TypeNameEnum.UPDATE)
    public Map<String, Object> updateByPrimaryKey(
            @RequestBody UserDO userDO,
            @PathVariable("userId") Long userId) {
        userDO.setUserId(userId);
        return userService.updateByPrimaryKey(userDO);
    }

    /**
     * 删除一位用户信息
     * @param userId userId 用户id
     * @return
     */
    @DeleteMapping("/user/{userId}")
    @Log(table = TableNameEnum.USER,type = TypeNameEnum.DELETE)
    public Map<String, Object> deleteByPrimaryKey(
            @PathVariable("userId") Long userId
    ) {
        return userService.deleteByPrimaryKey(userId);
    }

}

Service

用户


/**
 * @author dclup
 * @create 2020-04-27 09:58
 **/
public interface UserService {

    /**
     * 获取用户信息
     * @param userId
     * @return
     */
    Map<String, Object> getByPrimaryKey(Long userId);

    /**
     * 修改用户信息
     * @param userDO
     * @return
     */
    Map<String, Object> updateByPrimaryKey(UserDO userDO);

    /**
     * 删除用户信息
     * @param userId
     * @return
     */
    Map<String, Object> deleteByPrimaryKey(Long userId);
}


日志


/**
 * @author dclup
 * @Description 日志服务层
 **/
public interface LogService {

    /**
     * 插入日志
     * @param logDO
     * @return
     */
    void insertLog(LogDO logDO);
}

ServiceImpl

用户


/**
 * @author dclup
 * @Description 用户信息管理 服务层实现
 **/

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 获取用户信息
     *
     * @param userId
     * @return
     */
    @Override
    public Map<String, Object> getByPrimaryKey(Long userId) {
        return userMapper.getByPrimaryKey(userId);
    }

    /**
     * 修改用户信息
     *
     * @param userDO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> updateByPrimaryKey(UserDO userDO) {

        if (userMapper.updateByPrimaryKey(userDO) > 0){
            return userMapper.getByPrimaryKey(userDO.getUserId());
        }

        HashMap<String, Object> result = new HashMap<>();
        result.put("result","修改失败");
        return result;
    }

    /**
     * 删除用户信息
     *
     * @param userId
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> deleteByPrimaryKey(Long userId) {
        HashMap<String, Object> result = new HashMap<>();

        if (userMapper.deleteByPrimaryKey(userId) > 0){
            result.put("result","删除成功");
        }else {
            result.put("result","删除失败");
        }
        return result;
    }
}


日志


/**
 * @author dclup
 * @Description 日志信息管理 服务层实现
 **/
@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogMapper logMapper;

    /**
     * 插入日志
     *
     * @param logDO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insertLog(LogDO logDO) {
        if (logMapper.insertLog(logDO) > 0){
            System.out.println("日志插入成功");
            return;
        }
        System.out.println("日志插入失败");
    }
}

Dao 接口

用户


/**
 * @author dclup
 * @Description 用户信息管理数据层
 **/

@Mapper
@Repository
public interface UserMapper {

    /**
     * 获取用户信息
     *
     * @param userId
     * @return
     */
    Map<String,Object> getByPrimaryKey(@Param("userId") Long userId);

    /**
     * 修改用户信息
     *
     * @param userDO
     * @return
     */
    Integer updateByPrimaryKey(UserDO userDO);

    /**
     * 删除用户信息
     *
     * @param userId
     * @return
     */
    Integer deleteByPrimaryKey(@Param("userId") Long userId);

}


日志


/**
 * @author dclup
 * @Description 日志管理数据层
 **/

@Mapper
@Repository
public interface LogMapper {

    /**
     * 插入操作日志
     * @param logDO
     * @return 受影响行数
     */
    Integer insertLog(LogDO logDO);
}

Mapper XML

用户


<mapper namespace="com.dclup.dao.UserMapper">

    <select id="getByPrimaryKey" resultType="java.util.Map">
        SELECT userName
        FROM user
        WHERE oid = #{userId,jdbcType=BIGINT}
    </select>

    <update id="updateByPrimaryKey">
        UPDATE user
        SET userName = #{userName,jdbcType=VARCHAR}
        WHERE oid = #{userId,jdbcType=BIGINT}
    </update>

    <delete id="deleteByPrimaryKey">
        DELETE
        FROM user
        WHERE oid = #{userId,jdbcType=BIGINT}
    </delete>
</mapper>

日志


<mapper namespace="com.dclup.dao.LogMapper">

    <insert id="insertLog">
        INSERT INTO log
            (tableName, tableId, type, `time`, userId)
        VALUES (#{tableName,jdbcType=VARCHAR},
                #{tableId,jdbcType=BIGINT},
                #{type,jdbcType=VARCHAR},
                #{time,jdbcType=VARCHAR},
                #{userId,jdbcType=BIGINT})
    </insert>

</mapper>

以上是删改查的基本实现(可以不用看):基本实现结束


自定义Log注解

  1. 使用==枚举类==限定类型
  2. 此注解限定为==方法级别==
/**
 * @author dclup
 * @Description 日志注解
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    /**
     * 操作方式 默认unknown
     */
    TypeNameEnum type() default TypeNameEnum.UNKNOWN;

    /**
     * 操作的表名 默认unknown
     */
    TableNameEnum table() default TableNameEnum.UNKNOWN;

    /**
     * 实体类的主键名 默认unknown
     */
    IdNameEnum id() default IdNameEnum.UNKNOWN;
}

定义切面

  1. 使用@Aspect注解定义切面
  2. 切点:使用@Log注解的方法上
  3. 通知:@AfterReturning在原方法执行成功之后,才调用此通知(才执行插入日志操作)

/**
 * @author dclup
 * @Description 日志切面
 **/
@Aspect
@Component
public class LogAspect {

    @Autowired
    private LogService logService;

    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.dclup.annotation.Log)")
    // @Pointcut("execution(* com.dclup.service.impl.*.*(..))")
    public void pointcutLog() {
    }

    /**
     * 在原方法成功执行之后,再调用此通知。
     * @param joinPoint
     */
    @AfterReturning("pointcutLog()")
    public void afterReturning(JoinPoint joinPoint){
        insertLog(joinPoint);
    }


    /**
     * 进行插入日志
     *
     * @param joinPoint
     */
    private void insertLog(JoinPoint joinPoint) {
        // 获取切点方法上的注解
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        Log annotation = method.getAnnotation(Log.class);

        // 获取表名和操作类型
        LogDO logDO = new LogDO();
        logDO.setTableName(annotation.table().getName());
        logDO.setType(annotation.type().getName());

        // 获取当前用户id
        /*
        ServletRequestAttributes  requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        Object userIdObject = requestAttributes.getRequest().getAttribute("currentUserId");
        Long currentUserId = Long.valueOf(String.valueOf(userIdObject));
        logDO.setUserId(currentUserId);
        */

        // 模拟获取当前用户
        logDO.setUserId(10086L);

        // 获取当前时间
        LocalDateTime nowTime = LocalDateTime.now();
        String formatNowTime = nowTime.format(DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss"));
        logDO.setTime(formatNowTime);

        //获取方法参数值(按方法定义参数的顺序:google查询)
        Object[] args = joinPoint.getArgs();

        //获取Controller中传入参数(按方法定义参数的顺序:手动测试)
        LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] parameterNames = nameDiscoverer.getParameterNames(method);

        //获取idName的值
        if (parameterNames!=null && args !=null){

            for (int i = 0; i <parameterNames.length ; i++) {
                if (IdNameEnum.USER_ID.getName().equals(parameterNames[i])){
                    logDO.setTableId(Long.valueOf(String.valueOf(args[i])));
                }
            }
        }

        //保存日志
        logService.insertLog(logDO);
    }
}

测试

User中的数据

postMan测试

http://localhost:9999/user/1

{
	"userName": "dclup"
}

结果

源码

github.com/yam-congee/…

参考

mrbird.cc/Spring-Boot…