AOP行为日志
最近新项目要记录行为日志,很久没有用AOP,研究了一下。
废话不多说,先上个流程图:
数据库日志表设计
字段名称 | 字段类型 | 注释 |
LOG_ID | VARCHAR2(255) | |
LOG_LEVEL | NUMBER | 日志级别 |
START_TIME | DATE | 开始时间 |
RUN_TIME | NUMBER | 运行时间(ms) |
OPERATION_MODULE | VARCHAR2(255) | 被操作的模块 |
OPERATION_UNIT | VARCHAR2(255) | 被操作的单元 |
OPERATION_TYPE | VARCHAR2(255) | 操作类型 |
OPERATION_DETAIL | VARCHAR2(500 CHAR) | 操作详情 |
USER_CODE | VARCHAR2(255) | 用户编号 |
USER_NAME | VARCHAR2(255) | 用户名称 |
注:数据库使用的Oracle
JAVA端
1、创建日志实体类
import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.util.Date; @Data public class OperationLog { private String logId; private String userCode; private String userName; @JsonFormat(locale="zh", timezone="GMT+8", pattern="yyyy-MM-dd HH:mm:ss") private Date startTime; private Long runTime; private String operationUnit; private String operationType; private String operationDetail; private String operationModule; private Integer logLevel; }
2、创建日志操作类型、单元、模块等枚举类
(1)操作模块枚举类
public enum OperationModule { /** * 被操作的模块 */ UNKNOWN("XX系统"), USER("用户模块"), PRODUCT("产品模块"), SALE("销售信息模块"); private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; } OperationModule(String s) { this.value = s; } }
(2)操作单元枚举类
public enum OperationUnit { /** * 被操作的单元 */ UNKNOWN(""), /** * 用户模块 */ USER_INFO("用户信息"), USER_ROLE("用户角色"), USER_PERMISSION("用户权限"), /** * 产品模块 */ PRODUCT_INFO("产品信息"), PRODUCT_INV("产品库存"), /** * 销售信息模块 */ SALE_INFO("销售信息"), SALE_PLAN("销售计划"); private String value; OperationUnit(String value) { this.value = value; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
(3)操作类型枚举类
public enum OperationType { /** * 基本操作类型 */ UNKNOWN(""), LOGIN("登录"), INPUT("导入"), QUERY("查询"), EXPORT("导出"), DELETE("删除"), INSERT("插入"), UPDATE("更新"), /** * 用户 */ USER_SET_ROLE("设置用户角色"), USER_SET_PERMISSION("设置用户权限"), /** * 商品 */ PRODUCT_EXPORT_INFO("导出商品信息"), PRODUCT_SET_RANK("设置商品级别"), /** * 销售 */ SALE_EXPORT_INFO("导出销售信息"), SALE_SET_SALE_PLAN("设置销售计划"); private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; } OperationType(String s) { this.value = s; } }
3、创建日志注解
import com.XXX.XXX.domin.OperationModule; import com.XXX.XXX.domin.OperationType; import com.XXX.XXX.domin.OperationUnit; import java.lang.annotation.*; @Documented //表明这个注解应该被 javadoc工具记录 @Target({ElementType.METHOD}) //声明该注解作用于方法之上 @Retention(RetentionPolicy.RUNTIME) //声明该注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在 public @interface OperationLogDetail { /** * 方法描述,可使用占位符获取参数:{{param}} */ String operationDetail() default ""; /** * 日志等级:1-9 */ int logLevel() default 1; /** * 操作类型(enum) */ OperationType operationType() default OperationType.UNKNOWN; /** * 被操作的对象(此处使用enum) */ OperationUnit operationUnit() default OperationUnit.UNKNOWN; /** * 被操作的系统模块(此处使用enum) */ OperationModule operationModule() default OperationModule.UNKNOWN; }
4、创建AOP方法,使用了环绕通知
@Aspect //表明该类是一个切面 @Component //实例化到spring容器中 public class OperationLogAop { @Autowired private OperationLogService operationLogService; //表明切点在加了OperationLogDetail注解的方法 @Pointcut("@annotation(com.topsports.adbuhuo.annotation.OperationLogDetail)") public void operationLog(){} //环绕通知 @Around("operationLog()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { Object res = null; //获取系统当前时间 long time = System.currentTimeMillis(); try { //获取切入点(要记录日志的方法)的参数 Object[] args = joinPoint.getArgs(); //调用要记录日志的方法 res = joinPoint.proceed(args); //获取方法执行时长 time = System.currentTimeMillis() - time; return res; } finally { try { //方法执行完成后增加日志 addOperationLog(joinPoint,res,time); }catch (Exception e){ System.out.println("LogAspect 操作失败:" + e.getMessage()); e.printStackTrace(); } } } private void addOperationLog(JoinPoint joinPoint, Object res, long time){ //获取当前登录的用户 UserInfo userInfo = SecurityUserUtil.getThisUserInfo(); //获取方法的签名,用来获取加在方法上的注解 MethodSignature signature = (MethodSignature)joinPoint.getSignature(); //创建日志对象 OperationLog operationLog = new OperationLog(); operationLog.setRunTime(time); operationLog.setLogId(UUID.randomUUID().toString()); operationLog.setStartTime(new Date()); operationLog.setUserName(userInfo.getUserName()); operationLog.setUserCode(userInfo.getUserCode()); //获取加在方法上的注解 OperationLogDetail annotation = signature.getMethod().getAnnotation(OperationLogDetail.class); if(annotation != null){ operationLog.setLogLevel(annotation.logLevel()); operationLog.setOperationDetail(getDetail(((MethodSignature)joinPoint.getSignature()).getParameterNames(),joinPoint.getArgs(),annotation)); operationLog.setOperationType(annotation.operationType().getValue()); operationLog.setOperationUnit(annotation.operationUnit().getValue()); operationLog.setOperationModule(annotation.operationModule().getValue()); } //保存日志 operationLogService.insertSystemLog(operationLog); } /** * 对占位符处理 * @param argNames 方法参数名称数组 * @param args 方法参数数组 * @param annotation 注解信息 * @return 返回处理后的描述 */ private String getDetail(String[] argNames, Object[] args, OperationLogDetail annotation){ Map<Object, Object> map = new HashMap<>(4); for(int i = 0;i < argNames.length;i++){ map.put(argNames[i],args[i]); } //获取详情信息 String detail = annotation.operationDetail(); try { //遍历传入方法的参数 for (Map.Entry<Object, Object> entry : map.entrySet()) { Object k = entry.getKey(); Object v = entry.getValue(); //request和response不可序列化,XSSFWorkbook也不可序列化 if(!(v instanceof HttpServletRequest) && !(v instanceof HttpServletResponse) && !(v instanceof XSSFWorkbook)){ if(v instanceof JSONObject){ //处理JSONObject格式的参数 JSONObject jsonObject = (JSONObject) v; for (String jk : jsonObject.keySet()) { detail = detail.replace("{{" + jk + "}}", jsonObject.get(jk)!=null?jsonObject.get(jk).toString():""); } }else{ detail = detail.replace("{{" + k + "}}", JSON.toJSONString(v)); } }else if(v instanceof HttpServletRequest){ //处理HttpServletRequest JSONObject jsonObject = CommonUtil.request2Json((HttpServletRequest) v); for (String jk : jsonObject.keySet()) { detail = detail.replace("{{" + jk + "}}", jsonObject.get(jk)!=null?jsonObject.get(jk).toString():""); } } } }catch (Exception e){ e.printStackTrace(); } return detail; } }
5、创建Service与Mapper
public interface OperationLogService { //插入 void insertSystemLog(OperationLog operationLog); } -------------------------------------------------- @Service public class OperationLogServiceImpl implements OperationLogService { @Autowired private OperationLogMapper operationLogMapper; @Override public void insertSystemLog(OperationLog operationLog) { operationLogMapper.insertSystemLog(operationLog); } } -------------------------------------------------- @Mapper @Repository public interface OperationLogMapper { void insertSystemLog(OperationLog operationLog); } --------------------------------------------------- <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.XXX.XXX.dao.OperationLogMapper"> <insert id="insertSystemLog" parameterType="com.XXX.XXX.domin.OperationLog"> insert into SYSTEM_OPERATION_LOG( LOG_ID, USER_CODE, USER_NAME, START_TIME, RUN_TIME, OPERATION_UNIT, OPERATION_TYPE, OPERATION_DETAIL, LOG_LEVEL, OPERATION_MODULE ) values( #{logId}, #{userCode}, #{userName}, #{startTime}, #{runTime}, #{operationUnit}, #{operationType}, #{operationDetail}, #{logLevel}, #{operationModule} ) </insert> </mapper>
使用
在需要记录日志的方法上添加创建的注解
@OperationLogDetail( operationDetail = "{{userCode}}", //该占位符将在创建日志对象时扫描参数列表获取 operationType = OperationType.QUERY, operationUnit = OperationUnit.USER_INFO, operationModule = OperationModule.USER) @PostMapping("/getUserInfo") public JSONObject getUserInfo(@RequestBody JSONObject jsonObject){ return userInfoService.getUserInfo(jsonObject); }