Spring aop + 自定义注解 实现操作日志监控

一、创建操作日志服务

1.根据实际业务设计日志表(参考如下)

DROP TABLE IF EXISTS `sys_oper_log`;
CREATE TABLE `sys_oper_log`  (
  `oper_id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志主键',
  `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '模块标题',
  `business_type` int NULL DEFAULT 0 COMMENT '(模块类型)',
  `method` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '方法名称',
  `request_method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求方式',
  `operator_type` int NULL DEFAULT 0 COMMENT '操作类别(0其它 1后台用户 2手机端用户)',
  `oper_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作人员',
  `oper_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求URL',
  `oper_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '主机地址',
  `oper_location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作地点',
  `oper_param` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求参数',
  `json_result` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '返回参数',
  `status` int NULL DEFAULT 0 COMMENT '操作状态(0正常 1异常)',
  `error_msg` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '错误消息',
  `oper_time` datetime(0) NULL DEFAULT NULL COMMENT '操作时间',
  `user_agent` int NULL DEFAULT 0 COMMENT '用户类别(0:PC端用户 1:移动端用户 2:其它)',
  `org_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部门编码',
  PRIMARY KEY (`oper_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录' ROW_FORMAT = Dynamic;

2.根据表创建对应的CRUD服务,可以用mybatis-plus来实现,具体方式不再赘述,这里只贴上控制层的代码

@RestController
@Api(tags = "操作日志api")
public class SysOperlogController {
    @Autowired
    private SysOperLogService operLogService;

    @ApiOperation(value = "按条件获取操作日志列表")
    @PostMapping("/v1/operlog/list")
    public PageResult list(@RequestBody SysOperLogDto operLogDto) {
        Page page = operLogService.selectOperLogList(operLogDto);
        return PageResult.result(page.getTotal(), page.getRecords());
    }

    @ApiOperation(value = "批量删除操作日志")
    @Log(title="操作日志", businessType=BusinessTypeEnum.SYSTEM_LOG, operatorType=OperatorType.DELETE)
    @DeleteMapping("/v1/operlog")
    public R remove(@RequestBody List operIds) {
        int result = operLogService.deleteOperLogByIds(operIds);
        return R.ok(result);
    }

    @ApiOperation(value = "根据Id删除操作日志")
    @Log(title="操作日志", businessType=BusinessTypeEnum.SYSTEM_LOG, operatorType=OperatorType.DELETE)
    @DeleteMapping("/v1/operlog/{operId}")
    public R remove(@PathVariable("operId") Long operId) {
        int result = operLogService.deleteOperLogByIds(Arrays.asList(operId));
        return R.ok(result);
    }

    @ApiOperation(value = "清除日志")
    @Log(title="操作日志", businessType=BusinessTypeEnum.SYSTEM_LOG, operatorType=OperatorType.CLEAN)
    @DeleteMapping("/v1/operlog/clean")
    public R clean() {
        operLogService.cleanOperLog();
        return R.ok();
    }

    @ApiOperation(value = "保存日志")
    @PostMapping("/v1/operlog")
    public R add(@RequestBody SysOperLog operLog) {
        int result = operLogService.insertOperlog(operLog);
        return R.ok(result);
    }
}

二、日志注解和切面

1.如下所示,属性可根据实际项目来设置

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

    /**
     * 业务类型
     */
    BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;

    /**
     * 操作类型
     */
    OperatorType operatorType() default OperatorType.OTHER;

    /**
     * 操作人类别
     */
    UserAgentType userAgentType() default UserAgentType.PC;

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

    /**
     * 是否保存响应的参数
     */
    boolean isSaveResponseData() default true;
}

2.切面类

1. 属性 enable :在配置文件中设置的字段,用来判断是否开启日志

2.在后置切面中,将注解中的参数以及请求头中的信息,封装到实体类,保存在数据库中

@Aspect
@Component
@Slf4j
public class LogAspect {

    @Autowired
    private AsyncLogService asyncLogService;

    @Value("${log.enable:false}")
    private boolean enable;

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        if (!enable) {
            return;
        }
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        if (!enable) {
            return;
        }
        handleLog(joinPoint, controllerLog, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {
            // *========数据库日志=========*//
            SysOperLogDto operLog = new SysOperLogDto();
            operLog.setStatus(OperatorStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
            operLog.setOperIp(ip);
            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
            String username = SecurityUtils.getUserName();
            if (ObjectUtils.isNotEmpty(username)) {
                operLog.setOperName(username);
            }
            operLog.setOrgCode(SecurityUtils.getOrgCode());

            if (e != null) {
                operLog.setStatus(OperatorStatus.FAIL.ordinal());
                operLog.setErrorMsg(substring(e.getMessage(), 0, 2000));
            }
            // 设置请求方式
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 保存数据库
            asyncLogService.saveSysLog(operLog);
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("前置通知异常", exp);
            log.error("异常信息:{}", exp.getMessage());
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param log     日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLogDto operLog, Object jsonResult) throws Exception {
        // 设置业务类型
        operLog.setBusinessType(log.businessType().getCode());
        // 设置操作类型
        operLog.setOperatorType(log.operatorType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setUserAgent(log.userAgentType().ordinal());
        operLog.setOperTime(LocalDateTime.now());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息,传入到数据库中。
            setRequestValue(joinPoint, operLog);
        }
        // 是否需要保存response,参数和值
        if (log.isSaveResponseData() && ObjectUtils.isNotEmpty(jsonResult)) {
            operLog.setJsonResult(substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数,放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, SysOperLogDto operLog) throws Exception {
        String requestMethod = operLog.getRequestMethod();
        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
            String params = argsArrayToString(joinPoint.getArgs());
            operLog.setOperParam(substring(params, 0, 2000));
        }
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray) {
        String params = "";
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (ObjectUtils.isNotEmpty(o) && !isFilterObject(o)) {
                    try {
                        Object jsonObj = JSON.toJSON(o);
                        params += jsonObj.toString() + " ";
                    } catch (Exception e) {
                    }
                }
            }
        }
        return params.trim();
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象,则返回true;否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }

    private String substring(final String str, int start, int end) {
        if (str == null) {
            return "";
        }
        if (end < 0) {
            end = str.length() + end;
        }
        if (start < 0) {
            start = str.length() + start;
        }
        if (end > str.length()) {
            end = str.length();
        }
        if (start > end) {
            return "";
        }
        if (start < 0) {
            start = 0;
        }
        if (end < 0) {
            end = 0;
        }
        return str.substring(start, end);
    }
}

三、使用方式

一般会在增删改操作加上@Log注解,如下所示

@ApiOperation(value = "根据Id删除操作日志")
    @Log(title="操作日志", businessType=BusinessTypeEnum.SYSTEM_LOG, operatorType=OperatorType.DELETE)
    @DeleteMapping("/v1/operlog/{operId}")
    public R remove(@PathVariable("operId") Long operId) {
        int result = operLogService.deleteOperLogByIds(Arrays.asList(operId));
        return R.ok(result);
    }

总结

以上就是项目中实现操作日志的实现,实际上就是自定义注解和aop的方式结合,同样原理也可以用实现分布式锁,防止重复提交,等等;

你可能感兴趣的:(spring,java,spring,boot)