使用SpringBoot AOP记录操作日志和异常日志
平时我们在做项目时经常需要对一些重要功能操作记录日志,方便以后跟踪是谁在操作此功能;我们在操作某些功
能时也有可能会发生异常,但是每次发生异常要定位原因我们都要到服务器去查询日志才能找到,而且也不能对发
生的异常进行统计,从而改进我们的项目,要是能做个功能专门来记录操作日志和异常日志那就好了。
当然我们肯定有方法来做这件事情,而且也不会很难,我们可以在需要的方法中增加记录日志的代码,和在每个方
法中增加记录异常的代码,最终把记录的日志存到数据库中。听起来好像很容易,但是我们做起来会发现,做这项
工作很繁琐,而且都是在做一些重复性工作,还增加大量冗余代码,这种方式记录日志肯定是不可行的。
我们以前学过Spring 三大特性,IOC(控制反转),DI(依赖注入),AOP(面向切面),那其中AOP的主要功能
就是将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来。今天我们就来用
springBoot Aop 来做日志记录,好了,废话说了一大堆还是上货吧。
1、创建日志记录表、异常日志表,表结构如下
操作日志表OperationLog
CREATE TABLE `OperationLog` (`oper_id` varchar(64) DEFAULT NULL COMMENT '主键id',`oper_modul` varchar(64) DEFAULT NULL COMMENT '功能模块',`oper_type` varchar(64) DEFAULT NULL COMMENT '操作类型',`oper_desc` varchar(500) DEFAULT NULL COMMENT '操作描述',`oper_requ_param` text COMMENT '请求参数',`oper_resp_param` text COMMENT '返回参数',`oper_user_id` varchar(64) DEFAULT NULL COMMENT '操作员ID',`oper_user_name` varchar(64) DEFAULT NULL COMMENT '操作员名称',`oper_method` varchar(255) DEFAULT NULL COMMENT '操作方法',`oper_uri` varchar(255) DEFAULT NULL COMMENT '请求URI',`oper_ip` varchar(64) DEFAULT NULL COMMENT '请求ID',`oper_create_time` datetime DEFAULT NULL COMMENT '操作时间',`oper_ver` varchar(64) DEFAULT NULL COMMENT '操作版本号',PRIMARY KEY (`oper_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
异常日志表ExceptionLog
CREATE TABLE `ExceptionLog` (`exp_id` varchar(64) NOT NULL DEFAULT "" COMMENT '主键id',`exp_requ_param` text COMMENT '请求参数',`exp_name` varchar(255) COMMENT '异常名称',`exp_message` text COMMENT '异常信息',`oper_user_id` varchar(64) DEFAULT NULL COMMENT '操作员ID',`oper_user_name` varchar(64) DEFAULT NULL COMMENT '操作员名称',`oper_method` varchar(255) DEFAULT NULL COMMENT '操作方法',`oper_uri` varchar(255) DEFAULT NULL COMMENT '请求URI',`oper_ip` varchar(64) DEFAULT NULL COMMENT '请求ID',`oper_create_time` datetime DEFAULT NULL COMMENT '操作时间',`oper_ver` varchar(64) DEFAULT NULL COMMENT '操作版本号',PRIMARY KEY (`exp_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2、添加Maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.6</version><relativePath/></parent><groupId>com.aop.log</groupId><artifactId>spring-boot-aop-log</artifactId><version>0.0.1-SNAPSHOT</version><name>spring-boot-aop-log</name><description>spring-boot-aop-log</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- mybatis-plus依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.1.tmp</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.58</version></dependency><!-- mysql依赖 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
3、创建实体类
ExceptionLog
实体
package com.aop.log.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;/*** @author zhangshixing* @date 2021年11月05日 10:04*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ExceptionLog")
public class ExceptionLog {// 主键id@TableId(value = "exp_id", type = IdType.ASSIGN_UUID)private String expId;// 请求参数private String expRequParam;// 异常名称private String expName;// 异常信息private String expMessage;// 操作员IDprivate String operUserId;// 操作员名称private String operUserName;// 操作方法private String operMethod;// 请求URIprivate String operUri;// 请求IDprivate String operIp;// 操作时间private Date operCreateTime;// 操作版本号private String operVer;
}
OperationLog
实体
package com.aop.log.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;
import java.util.Date;/*** @author zhangshixing* @date 2021年11月05日 9:52*/@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("OperationLog")
public class OperationLog implements Serializable {// 主键id@TableId(value = "oper_id", type = IdType.ASSIGN_UUID)private String operId;// 功能模块private String operModul;// 操作类型private String operType;// 操作描述private String operDesc;// 请求参数private String operRequParam;// 返回参数private String operRespParam;// 操作员IDprivate String operUserId;// 操作员名称private String operUserName;// 操作方法private String operMethod;// 请求URIprivate String operUri;// 请求IDprivate String operIp;// 操作时间private Date operCreateTime;// 操作版本号private String operVer;
}
OrderInfo
订单实体类
package com.aop.log.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @author zhangshixing* @date 2021年11月05日 10:33*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderInfo {/*** 这里只是为了测试所以只添加了两个参数*/// 订单编号private String orderid;// 订单金额private int price;}
RespBean
公共返回对象
package com.aop.log.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @author zhangshixing* @date 2021年11月05日 10:31*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {private long code;private String message;private Object obj;public static RespBean success(String message) {return new RespBean(200, message, null);}public static RespBean success(String message, Object obj) {return new RespBean(200, message, obj);}public static RespBean error(String message) {return new RespBean(500, message, null);}public static RespBean error(String message, Object obj) {return new RespBean(500, message, obj);}
}
4、创建Mapper
OperationLogMapper
package com.aop.log.mapper;import com.aop.log.entity.OperationLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface OperationLogMapper extends BaseMapper<OperationLog> {}
ExceptionLogMapper
package com.aop.log.mapper;import com.aop.log.entity.ExceptionLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface ExceptionLogMapper extends BaseMapper<ExceptionLog> {}
5、创建service
OperationLogService
package com.aop.log.service;import com.aop.log.entity.OperationLog;
import com.baomidou.mybatisplus.extension.service.IService;public interface OperationLogService extends IService<OperationLog> {
}
ExceptionLogService
package com.aop.log.service;import com.aop.log.entity.ExceptionLog;
import com.baomidou.mybatisplus.extension.service.IService;public interface ExceptionLogService extends IService<ExceptionLog> {}
OperationLogServiceImpl
package com.aop.log.service.impl;import com.aop.log.entity.OperationLog;
import com.aop.log.mapper.OperationLogMapper;
import com.aop.log.service.OperationLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;/*** @author zhangshixing* @date 2021年11月05日 10:10*/
@Service
public class OperationLogServiceImpl extends ServiceImpl<OperationLogMapper, OperationLog>implements OperationLogService {
}
ExceptionLogServiceImpl
package com.aop.log.service.impl;import com.aop.log.entity.ExceptionLog;
import com.aop.log.mapper.ExceptionLogMapper;
import com.aop.log.service.ExceptionLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;/*** @author zhangshixing* @date 2021年11月05日 10:10*/
@Service
public class ExceptionLogServiceImpl extends ServiceImpl<ExceptionLogMapper, ExceptionLog>implements ExceptionLogService {
}
6、创建操作日志注解类OperLog
package com.aop.log.annotation;import java.lang.annotation.*;/*** @author zhangshixing* @date 2021年11月05日 9:43* 自定义操作日志注解*/
//注解放置的目标位置,METHOD是可注解在方法级别上
@Target(ElementType.METHOD)
//注解在哪个阶段执行
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog {// 操作模块String operModul() default "";// 操作类型String operType() default "";// 操作说明String operDesc() default "";
}
package com.aop.log.annotation;public class OprLogConst {public static final String ADD = "add";public static final String INSERET = "insert";public static final String UPDATE = "update";public static final String DELETE = "delete";
}
7、创建切面类记录操作日志
package com.aop.log.aop;import com.alibaba.fastjson.JSON;
import com.aop.log.annotation.OperLog;
import com.aop.log.entity.ExceptionLog;
import com.aop.log.entity.OperationLog;
import com.aop.log.service.ExceptionLogService;
import com.aop.log.service.OperationLogService;
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.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;/*** @author zhangshixing* @date 2021年11月05日 9:46* 切面处理类,操作日志异常日志记录处理*/
@Aspect
@Component
public class OperLogAspect {/*** 操作版本号* 项目启动时从命令行传入,例如:java -jar xxx.war --version=201902*/@Value("${version}")private String operVer;@Autowiredprivate OperationLogService operationLogService;@Autowiredprivate ExceptionLogService exceptionLogService;/*** 设置操作日志切入点 记录操作日志 在注解的位置切入代码*/@Pointcut("@annotation(com.aop.log.annotation.OperLog)")public void operLogPoinCut() {}/*** 设置操作异常切入点记录异常日志 扫描所有controller包下操作*/@Pointcut("execution(* com.aop.log.controller..*.*(..))")public void operExceptionLogPoinCut() {}/*** 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行** @param joinPoint 切入点* @param keys 返回结果*/@AfterReturning(value = "operLogPoinCut()", returning = "keys")public void saveOperLog(JoinPoint joinPoint, Object keys) {// 获取RequestAttributesRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();// 从获取RequestAttributes中获取HttpServletRequest的信息HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);// 创建OperationLog对象OperationLog operlog = new OperationLog();try {// 主键IDString uuid = UUID.randomUUID().toString();System.out.println("saveOperLog:" + uuid);operlog.setOperId(uuid);// 从切面织入点处通过反射机制获取织入点处的方法MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 获取切入点所在的方法Method method = signature.getMethod();// 获取操作OperLog opLog = method.getAnnotation(OperLog.class);if (opLog != null) {String operModul = opLog.operModul();String operType = opLog.operType();String operDesc = opLog.operDesc();// 操作模块operlog.setOperModul(operModul);// 操作类型operlog.setOperType(operType);// 操作描述operlog.setOperDesc(operDesc);}// 获取请求的类名String className = joinPoint.getTarget().getClass().getName();// 获取请求的方法名String methodName = method.getName();methodName = className + "." + methodName;// 请求方法operlog.setOperMethod(methodName);// 请求的参数Map<String, String> rtnMap = converMap(request.getParameterMap());// 将参数所在的数组转换成jsonString params = JSON.toJSONString(rtnMap);// 请求参数operlog.setOperRequParam(params);// 返回结果operlog.setOperRespParam(JSON.toJSONString(keys));// 请求用户ID// operlog.setOperUserId(UserShiroUtil.getCurrentUserLoginName());// 这里写一个固定的用户IDoperlog.setOperUserId("100293784");// 请求用户名称// operlog.setOperUserName(UserShiroUtil.getCurrentUserName());// // 这里写一个固定的用户名operlog.setOperUserName("yiyiyi");// 请求IP// operlog.setOperIp(IPUtil.getRemortIP(request));// 这里写一个固定的IPoperlog.setOperIp("127.0.0.1");// 请求URIoperlog.setOperUri(request.getRequestURI());// 创建时间operlog.setOperCreateTime(new Date());// 操作版本operlog.setOperVer(operVer);// 将数据插入到数据库operationLogService.save(operlog);} catch (Exception e) {e.printStackTrace();}}/*** 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行** @param joinPoint 切入点* @param e 异常信息*/@AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e")public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {// 获取RequestAttributesRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();// 从获取RequestAttributes中获取HttpServletRequest的信息HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);ExceptionLog excepLog = new ExceptionLog();try {// 从切面织入点处通过反射机制获取织入点处的方法MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 获取切入点所在的方法Method method = signature.getMethod();String uuid = UUID.randomUUID().toString();System.out.println("saveExceptionLog:" + uuid);excepLog.setExpId(uuid);// 获取请求的类名String className = joinPoint.getTarget().getClass().getName();// 获取请求的方法名String methodName = method.getName();methodName = className + "." + methodName;// 请求的参数Map<String, String> rtnMap = converMap(request.getParameterMap());// 将参数所在的数组转换成jsonString params = JSON.toJSONString(rtnMap);// 请求参数excepLog.setExpRequParam(params);// 请求方法名excepLog.setOperMethod(methodName);// 异常名称excepLog.setExpName(e.getClass().getName());// 异常信息excepLog.setExpMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));// 操作员ID// excepLog.setOperUserName(UserShiroUtil.getCurrentUserName());excepLog.setOperUserId("100293784");// 操作员名称excepLog.setOperUserName("yiyiyi");// 操作URIexcepLog.setOperUri(request.getRequestURI());// 操作员IP// excepLog.setOperIp(IPUtil.getRemortIP(request));excepLog.setOperIp("127.0.0.1");// 操作版本号excepLog.setOperVer(operVer);// 发生异常时间excepLog.setOperCreateTime(new Date());exceptionLogService.save(excepLog);} 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 + "\n");}String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();return message;}
}
8、在Controller层方法添加@OperLog注解
package com.aop.log.controller;import com.aop.log.annotation.OperLog;
import com.aop.log.annotation.OprLogConst;
import com.aop.log.entity.OrderInfo;
import com.aop.log.entity.RespBean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;/*** @author zhangshixing* @date 2021年11月05日 10:25*/
@Controller
public class OrderController {/*** 新增订单信息*/@RequestMapping(value = "addOrderInfo")@ResponseBody@OperLog(operModul = "销售管理-订单新增", operType = OprLogConst.ADD, operDesc = "订单新增功能")public RespBean addOrderInfo(OrderInfo orderInfo) {if (orderInfo.getPrice() < 0) {System.out.println(1 / 0);return RespBean.error("提交失败!!!");} else if (orderInfo.getPrice() < 30) {return RespBean.success("提交成功!!!");} else {return RespBean.error("提交失败!!!");}}
}
9、启动类和配置文件
version = 2.1.1
server.port = 9000
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/log?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username = root
spring.datasource.password = root
package com.aop.log;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class SpringBootAopLogApplication {public static void main(String[] args) {SpringApplication.run(SpringBootAopLogApplication.class, args);}}
10、操作日志、异常日志查询功能
10.1 记录操作日志
10.2 记录异常日志
11、execution 表达式
学习Spring中的aop组装切面时遇到的execution表达式,下面是execution表达式的详解。
切入点表达式:
整个表达式可以分为五个部分:
1、execution(): 表达式主体。
2、第一个
*
号:方法返回类型,*
号表示所有的类型。3、包名:表示需要拦截的包名。
4、第二个
*
号:表示类名,*
号表示所有的类。5、
*(..)
:最后这个星号表示方法名,*
号表示所有的方法,后面( )
里面表示方法的参数,两个句点表示任何参数。
表达式语法:
execution(* 包名.*.*(..))
规范写法:execution(* com.baizhi.service.UserServiceImpl.*(..))
这个表达式是重点 ,是最通用的,表示执行接口下的所有接口方法。
execution表达式举例:
书写接口实现方法:UserServiceImpl
方法类型:
add();
query();
add(String name);
add(User user);
add(String name,Integer age);
execution(* service.UserServiceImpl.add(..)) //执行add()方法
execution(* service.UserServiceImpl.add(String)) //执行add(String name)方法
execution(* service.UserServiceImpl.add(com.baizhi.entity.User)) //执行add(User user)方法
execution(* service.UserServiceImpl.add(String , Integer)) //执行add(String name,Interger age)方法
execution表达式的一般用法:
execution(* service.UserServiceImpl.*(java.util.List))
返回值:任意
包:com.baizhi.service
类:UserServiceImpl
方法:任意
参数:必须是List集合
execution(* service.UserServiceImpl.add*(..)) 重点
返回值:任意
包:com.baizhi.service
类:UserServiceImpl
方法:以add关键字开头的方法
参数:任意
execution(* service.UserServiceImpl.*.*(..)) 重点
返回值:任意
包:com.baizhi.service
类:当前包下的所有类
方法:所有类中的所有方法
参数:任意
execution(* service..*.*(..)) 重点
返回值:任意
包:service包以及它下面所有子包
类:所有包中的所有类
方法:所有类中的所有方法
参数:任意
execution(* *(..)) 重点,不建议这样写,栈溢出
注意:要尽可能精准的切入。