首先,本博客充分参考了《使用SpringBoot AOP 记录操作日志、异常日志》该篇博文,并且亲自在项目中进行了成功地尝试,在项目使用过程中进行了一些修改,记录了集成过程中出现的问题和解决办法。
平时我们在做项目时经常需要对一些重要功能操作记录日志,方便以后跟踪是谁在操作此功能;我们在操作某些功能时也有可能会发生异常,但是每次发生异常要定位原因我们都要到服务器去查询日志才能找到,而且也不能对发生的异常进行统计,从而改进我们的项目,要是能做个功能专门来记录操作日志和异常日志那就好了, 当然我们肯定有方法来做这件事情,而且也不会很难,我们可以在需要的方法中增加记录日志的代码,和在每个方法中增加记录异常的代码,最终把记录的日志存到数据库中。听起来好像很容易,但是我们做起来会发现,做这项工作很繁琐,而且都是在做一些重复性工作,还增加大量冗余代码,这种方式记录日志肯定是不可行的。
我们以前学过Spring 三大特性,IOC(控制反转),DI(依赖注入),AOP(面向切面),那其中AOP的主要功能就是将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来。今天我们就来用springBoot Aop 来做日志记录。
一、操作日志数据存储的方式:
第一种:将操作日志数据存储在Mysql数据库中
该存储方式可以将用户的各种操作记录储存在数据库中,但如果用户较多,操作较频繁,数据库存储数据量大,也会给数据库带来一定的压力。
日志记录表名:operation_logger
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for operation_logger -- ---------------------------- DROP TABLE IF EXISTS `operation_logger`; CREATE TABLE `operation_logger` ( `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户id', `modul` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模块', `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '方法', `type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '类型', `describe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', `req_param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求参数', `res_param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '返回参数', `uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求URI', `ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip', `version` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作版本号', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE, INDEX `user_id`(`user_id`) USING BTREE, CONSTRAINT `operation_logger_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `admin_user` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
该表增加了外键user_id作为admin_user表的关联,如果不需要可以去掉。
异常日志表名:exception_logger
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for exception_logger -- ---------------------------- DROP TABLE IF EXISTS `exception_logger`; CREATE TABLE `exception_logger` ( `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id', `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', `req_param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求参数', `res_param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '返回参数', `message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '异常信息', `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '方法', `uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求URI', `ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip', `version` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作版本号', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE, INDEX `user_id`(`user_id`) USING BTREE, CONSTRAINT `exception_logger_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `admin_user` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
该表增加了外键user_id作为admin_user表的关联,如果不需要可以去掉。
第二种:将操作日志数据存储在Redis缓存中
该存储方式可以将用户的各种操作记录储存在Redis中,好处是可以减少Mysql的存储压力,还可以设置存储时长。
本博客就是将操作日记存储在Redis缓存中,如果需要存储在Mysql中,只需要获取到相关数据作为对象储存就好。
二、整合步骤
2.1 添加Maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2.2 根据数据库创建正常日志记录类、异常日志记录类
OperationLogger.class
import com.fasterxml.jackson.annotation.JsonFormat; import com.microplay.util.dateTime.MPDateTimeUtils; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; import java.time.LocalDateTime; @Data @Entity @Table(name = "operation_logger") public class OperationLogger implements Serializable { private static final long serialVersionUID = 2899031301824275986L; /** * null */ @Id private String id; /** * 用户id */ private String userId; /** * 模块 */ private String modul; /** * 方法 */ private String method; /** * 类型 */ private String type; /** * 描述 */ private String describe; /** * 请求参数 */ private String reqParam; /** * 返回参数 */ private String resParam; /** * 请求URI */ private String uri; /** * ip */ private String ip; /** * 操作版本号 */ private String version; /** * 创建时间 */ @JsonFormat(pattern = MPDateTimeUtils.DEFAULT_TIME_PATTERN, timezone = MPDateTimeUtils.SHANG_HAI) @DateTimeFormat(pattern = MPDateTimeUtils.DEFAULT_TIME_PATTERN) private LocalDateTime createTime; }
ExceptionLogger.class
import lombok.Data; import com.fasterxml.jackson.annotation.JsonFormat; import com.microplay.util.dateTime.MPDateTimeUtils; import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; import java.time.LocalDateTime; @Data @Entity @Table(name = "exception_logger") public class ExceptionLogger implements Serializable { private static final long serialVersionUID = 7571752296816657892L; /** * null */ @Id private String id; /** * 用户id */ private String userId; /** * 名称 */ private String name; /** * 请求参数 */ private String reqParam; /** * 返回参数 */ private String resParam; /** * 异常信息 */ private String message; /** * 方法 */ private String method; /** * 请求URI */ private String uri; /** * ip */ private String ip; /** * 操作版本号 */ private String version; /** * 创建时间 */ @JsonFormat(pattern = MPDateTimeUtils.DEFAULT_TIME_PATTERN, timezone = MPDateTimeUtils.SHANG_HAI) @DateTimeFormat(pattern = MPDateTimeUtils.DEFAULT_TIME_PATTERN) private LocalDateTime createTime; }
2.3 创建操作日志注解类OperLog.java
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 自定义操作日志注解 */ @Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上 @Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行 @Documented public @interface OperationLog { String operModul() default ""; // 操作模块 String operType() default ""; // 操作类型 String operDesc() default ""; // 操作说明 }
2.4 创建切面类记录操作日志
import com.alibaba.fastjson.JSON; import com.microplay.config.attribute.MPAttribute; import com.microplay.config.redis.RedisUtils; import com.microplay.util.dateTime.MPLocalDateTimeUtils; 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.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * 切面处理类,操作日志异常日志记录处理 */ @Aspect @Component public class OperLogAspect { @Resource private MPAttribute mpAttribute; @Resource private RedisUtils redisUtils; /** * 设置操作日志切入点 记录操作日志 在注解的位置切入代码 */ @Pointcut("@annotation(OperationLog)") public void operLogPoinCut() { } /** * 设置操作异常切入点记录异常日志 扫描所有controller包下操作 * 重点讲解下execution表达式部分, * execution是执行的意思。public * com.cx.timer...看起来非常复杂,晦涩难懂。其实这里就是一个方法名的定义:作用域 返回类型 方法名(参数..)。 * 其中上图的作用域是: * public返回类型: * * (* 表示Object类型)方法名(参数..) : * com.microplay是指具体的包名; * ** 表示多级路径 * .* 表示 该包名下的所有的类; * *(..) 表示类下所有的方法,不限定参数。 */ @Pointcut("execution(public * com.microplay.**.controller..*.*(..))") public void operExceptionLogPoinCut() { } /** * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行 * * @param joinPoint 切入点 * @param keys 返回结果 */ @AfterReturning(value = "operLogPoinCut()", returning = "keys") public void saveOperLog(JoinPoint joinPoint, Object keys) { // 获取RequestAttributes RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); // 从获取RequestAttributes中获取HttpServletRequest的信息 HttpServletRequest request = (HttpServletRequest) requestAttributes .resolveReference(RequestAttributes.REFERENCE_REQUEST); OperationLogger operationLog = new OperationLogger(); try { // 从切面织入点处通过反射机制获取织入点处的方法 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 获取切入点所在的方法 Method method = signature.getMethod(); // 获取操作 OperationLog opLog = method.getAnnotation(OperationLog.class); if (opLog != null) { operationLog.setModul(opLog.operModul()); // 操作模块 operationLog.setType(opLog.operType()); // 操作类型 operationLog.setDescribe(opLog.operDesc()); // 操作描述 } // 获取请求的类名 String className = joinPoint.getTarget().getClass().getName(); // 获取请求的方法名 String methodName = method.getName(); methodName = className + "." + methodName; operationLog.setMethod(methodName); // 请求方法 // 请求的参数 Map<String, String> rtnMap = converMap(request.getParameterMap()); // 将参数所在的数组转换成json String params = JSON.toJSONString(rtnMap); operationLog.setReqParam(params); // 请求参数 operationLog.setResParam(JSON.toJSONString(keys)); // 返回结果 // operationLog.setUserId(UserShiroUtil.getCurrentUserLoginName()); // 请求用户ID // operationLog.setIp(IPUtil.getRemortIP(request)); // 请求IP operationLog.setUri(request.getRequestURI()); // 请求URI operationLog.setVersion(mpAttribute.operVer); // 操作版本 operationLog.setCreateTime(MPLocalDateTimeUtils.getCurrentLocalDateTime()); // 创建时间 ArrayList<OperationLogger> operationLoggerList = (ArrayList<OperationLogger>) redisUtils.get("OperationLogList"); if (Objects.isNull(operationLoggerList)) { operationLoggerList = new ArrayList<OperationLogger>(); } operationLoggerList.add(0, operationLog); redisUtils.set("OperationLogList", operationLoggerList); } catch (Exception e) { e.printStackTrace(); } } /** * 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行 * * @param joinPoint 切入点 * @param e 异常信息 */ @AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e") public void saveExceptionLog(JoinPoint joinPoint, Throwable e) { // 获取RequestAttributes RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); // 从获取RequestAttributes中获取HttpServletRequest的信息 HttpServletRequest request = (HttpServletRequest) requestAttributes .resolveReference(RequestAttributes.REFERENCE_REQUEST); ExceptionLogger excepLog = new ExceptionLogger(); try { // 从切面织入点处通过反射机制获取织入点处的方法 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 获取切入点所在的方法 Method method = signature.getMethod(); // excepLog.setExcId(UuidUtil.get32UUID()); // 获取请求的类名 String className = joinPoint.getTarget().getClass().getName(); // 获取请求的方法名 String methodName = method.getName(); methodName = className + "." + methodName; // 请求的参数 Map<String, String> rtnMap = converMap(request.getParameterMap()); // 将参数所在的数组转换成json String params = JSON.toJSONString(rtnMap); excepLog.setReqParam(params); // 请求参数 excepLog.setMethod(methodName); // 请求方法名 excepLog.setName(e.getClass().getName()); // 异常名称 excepLog.setMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace())); // 异常信息 // excepLog.setUserId(UserShiroUtil.getCurrentUserLoginName()); // 操作员ID // excepLog.setOperIp(IPUtil.getRemortIP(request)); // 操作员IP excepLog.setUri(request.getRequestURI()); // 操作URI excepLog.setVersion(mpAttribute.operVer); // 操作版本号 excepLog.setCreateTime(MPLocalDateTimeUtils.getCurrentLocalDateTime()); // 发生异常时间 ArrayList<ExceptionLogger> exceptionLoggerList = (ArrayList<ExceptionLogger>) redisUtils.get("ExceptionLoggerList"); if (Objects.isNull(exceptionLoggerList)) { exceptionLoggerList = new ArrayList<ExceptionLogger>(); } exceptionLoggerList.add(0, excepLog); redisUtils.set("ExceptionLoggerList", exceptionLoggerList); } catch (Exception e2) { e2.printStackTrace(); } } /** * 转换request 请求参数 * * @param paramMap request获取的参数数组 */ public Map<String, String> converMap(Map<String, String[]> paramMap) { Map<String, String> rtnMap = new HashMap<String, String>(); for (String key : paramMap.keySet()) { rtnMap.put(key, paramMap.get(key)[0]); } return rtnMap; } /** * 转换异常信息为字符串 * * @param exceptionName 异常名称 * @param exceptionMessage 异常信息 * @param elements 堆栈信息 */ public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) { StringBuffer strbuff = new StringBuffer(); for (StackTraceElement stet : elements) { strbuff.append(stet + " "); } String message = exceptionName + ":" + exceptionMessage + " " + strbuff.toString(); return message; } }
设置操作异常切入点记录异常日志 扫描所有controller包下操作:
@Pointcut("execution(public * com.microplay.**.controller..*.*(..))")
注解这个包名一定要写正确,除此之外最好将切入点的包缩小范围到controller这个包下,
否则会将其它的工具类也设置成AOP造成启动失败。
我在项目启动时候提示:Could not generate CGLIB subclass of class com.microplay.config.redis.RedisUtils.class
2.5 在Controller层方法添加@OperationLog注解
@RequestMapping("/hello") @ResponseBody @OperationLog(operModul = "模块-测试",operType = "查询",operDesc = "查询信息") public String hello(){ // int i = 1 / 0; return "Hello World"; }
@OperationLog(operModul = "模块-测试",operType = "查询",operDesc = "查询信息")
通过注解:
正常操作会执行该saveOperLog方法获取操作日志的相关信息进行保存;
异常操作会执行saveExceptionLog方法获取异常日志的相关信息进行保存;
2.6 操作日志、异常日志查询功能
五、在Controller层方法添加@OperLog注解