• 一文带你学会基于SpringAop实现操作日志的记录


    前言

    大家好,这里是经典鸡翅,今天给大家带来一篇基于SpringAop实现的操作日志记录的解决的方案。大家可能会说,切,操作日志记录这么简单的东西,老生常谈了。不!

    网上的操作日志一般就是记录操作人,操作的描述,ip等。好一点的增加了修改的数据和执行时间。那么!我这篇有什么不同呢!今天这种不仅可以记录上方所说的一切,还增加记录了操作前的数据,错误的信息,堆栈信息等。正文开始~~~~~

    思路介绍

    记录操作日志的操作前数据是需要思考的重点。我们以修改场景来作为探讨。当我们要完全记录数据的流向的时候,我们必然要记录修改前的数据,而前台进行提交的时候,只有修改的数据,那么如何找到修改前的数据呢。有三个大的要素,我们需要知道修改前数据的表名,表的字段主键,表主键的值。这样通过这三个属性,我们可以很容易的拼出 select * from 表名 where 主键字段 = 主键值。我们就获得了修改前的数据,转换为json之后就可以存入到数据库中了。如何获取三个属性就是重中之重了。我们采取的方案是通过提交的映射实体,在实体上打上注解,根据 Java 的反射取到值。再进一步拼装获得对象数据。那么AOP是在哪里用的呢,我们需要在记录操作日志的方法上,打上注解,再通过切面获取到切点,一切的数据都通过反射来进行获得。

    定义操作日志注解

    既然是基于spinrg的aop实现切面。那么必然是需要一个自定义注解的。用来作为切点。我们定义的注解,可以带一些必要的属性,例如操作的描述,操作的类型。操作的类型需要说一下,我们分为新增、修改、删除、查询。那么只有修改和删除的时候,我们需要查询一下修改前的数据。其他两种是不需要的,这个也可以用来作为判断。

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface OperateLog {
    
          String operation() default "";
    
          String operateType() default "";
    
    }
    

    定义用于找到表和表主键的注解

    表和表主键的注解打在实体上,内部有两个属性 tableName 和 idName。这两个属性的值获得后,可以进行拼接 select * from 表名 where 主键字段。

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SelectTable {
    
    	String tableName() default "";
    
    	String idName() default  "";
    }
    

    定义获取主键值的注解

    根据上面所说的三个元素,我们还缺最后一个元素主键值的获取,用于告诉我们,我们应该从提交的请求的那个字段,拿到其中的值。

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SelectPrimaryKey {
    
    }
    

    注解的总结

    有了上面的三个注解,注解的准备工作已经进行完毕。我们通过反射取到数据,可以获得一切。接下来开始实现切面,对于注解的值进行拼接处理,最终存入到我们的数据库操作日志表中。

    切面的实现

    对于切面来说,我们需要实现切点、数据库的插入、反射的数据获取。我们先分开进行解释,最后给出全面的实现代码。方便大家的理解和学习。

    切面的定义

    基于spring的aspect进行声明这是一个切面。

    @Aspect
    @Component
    public class OperateLogAspect {
    }
    

    切点的定义

    切点就是对所有的打上OperateLog的注解的请求进行拦截和加强。我们使用annotation进行拦截。

    	@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
    	private void operateLogPointCut(){
    	}
    

    获取请求ip的共用方法

    	private String getIp(HttpServletRequest request){
    		String ip = request.getHeader("X-forwarded-for");
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getHeader("Proxy-Client-IP");
    		}
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getHeader("WL-Proxy-Client-IP");
    		}
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getHeader("HTTP_CLIENT_IP");
    		}
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getHeader("HTTP_X_FORWARDED_FOR");
    		}
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getRemoteAddr();
    		}
    		return ip;
    	}
    

    数据库的日志插入操作

    我们将插入数据库的日志操作进行单独的抽取。

    private void insertIntoLogTable(OperateLogInfo operateLogInfo){
    	operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
    	String sql="insert into log values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
    	jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
    		operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
    		operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
    		operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
    		operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
    		operateLogInfo.getModule(),operateLogInfo.getOperateType());
    }
    

    环绕通知的实现

    日志的实体类实现

    @TableName("operate_log")
    @Data
    public class OperateLogInfo {
    
    	//主键id
    	@TableId
    	private String id;
    	//操作人id
    	private String userId;
    	//操作人名称
    	private String userName;
    	//操作内容
    	private String operation;
    	//操作方法名称
    	private String method;
    	//操作后的数据
    	private String modifiedData;
    	//操作前数据
    	private String preModifiedData;
    	//操作是否成功
    	private String result;
    	//报错信息
    	private String errorMessage;
    	//报错堆栈信息
    	private String errorStackTrace;
    	//开始执行时间
    	private Date executeTime;
    	//执行持续时间
    	private Long duration;
    	//ip
    	private String ip;
    	//操作类型
    	private String operateType;
    
    }
    

    准备工作全部完成。接下来的重点是对环绕通知的实现。思路分为数据处理、异常捕获、finally执行数据库插入操作。环绕通知的重点类就是ProceedingJoinPoint ,我们通过它的getSignature方法可以获取到打在方法上注解的值。例如下方。

    MethodSignature signature = (MethodSignature) pjp.getSignature();
    OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
    operateLogInfo.setOperation(declaredAnnotation.operation());
    operateLogInfo.setModule(declaredAnnotation.module());
    operateLogInfo.setOperateType(declaredAnnotation.operateType());
    //获取执行的方法
    String method = signature.getDeclaringType().getName() + "."  + signature.getName();
    operateLogInfo.setMethod(method);
    String operateType = declaredAnnotation.operateType();
    

    获取请求的数据,也是通过这个类来实现,这里有一点是需要注意的,就是我们要约定参数的传递必须是第一个参数。这样才能保证我们取到的数据是提交的数据。

    if(pjp.getArgs().length>0){
    	Object args = pjp.getArgs()[0];
    	operateLogInfo.setModifiedData(new Gson().toJson(args));
    }
    

    接下来的一步就是对修改前的数据进行拼接。之前我们提到过如果是修改和删除,我们才会进行数据的拼接获取,主要是通过类来判断书否存在注解,如果存在注解,那么就要判断注解上的值是否是控制或者,非空才能正确的进行拼接。取field的值的时候,要注意私有的变量需要通过setAccessible(true)才可以进行访问。

    if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
    	GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
    	String tableName = "";
    	String idName = "";
    	String selectPrimaryKey = "";
    	if(pjp.getArgs().length>0){
    		Object args = pjp.getArgs()[0];
    		//获取操作前的数据
    		boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
    		if(selectTableFlag){
    			tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
    			idName = args.getClass().getAnnotation(SelectTable.class).idName();
    		}else {
    			throw new RuntimeException("操作日志类型为修改或删除,实体类必须指定表面和主键注解!");
    		}
    		Field[] fields = args.getClass().getDeclaredFields();
    		Field[] fieldsCopy = fields;
    		boolean isFindField = false;
    		int fieldLength = fields.length;
    		for(int i = 0; i < fieldLength; ++i) {
    			Field field = fieldsCopy[i];
    			boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
    			if (hasPrimaryField) {
    				isFindField = true;
    				field.setAccessible(true);
    				selectPrimaryKey = (String)field.get(args);
    			}
    		}
    		if(!isFindField){
    			throw new RuntimeException("实体类必须指定主键属性!");
    		}
    	}
    	if(StringUtils.isNotEmpty(tableName) &&
    		StringUtils.isNotEmpty(idName)&&
    		StringUtils.isNotEmpty(selectPrimaryKey)){
    		StringBuffer sb = new StringBuffer();
    		sb.append(" select * from  ");
    		sb.append(tableName);
    		sb.append(" where ");
    		sb.append(idName);
    		sb.append(" = ? ");
    		String sql = sb.toString();
    		try{
    			List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey);
    			if(maps!=null){
    				operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
    			}
    		}catch (Exception e){
    			e.printStackTrace();
    			throw new RuntimeException("查询操作前数据出错!");
    		}
    	}else {
    		throw new RuntimeException("表名、主键名或主键值 存在空值情况,请核实!");
    	}
    }else{
    	operateLogInfo.setPreModifiedData("");
    }
    

    切面的完整实现代码

    @Aspect
    @Component
    public class OperateLogAspect {
    
    	@Autowired
    	private JdbcTemplate jdbcTemplate;
    
    	@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
    	private void operateLogPointCut(){
    	}
    
    	@Around("operateLogPointCut()")
    	public Object around(ProceedingJoinPoint pjp) throws Throwable {
    		Object responseObj = null;
    		OperateLogInfo operateLogInfo = new OperateLogInfo();
    		String flag = "success";
    		try{
    			HttpServletRequest request = SpringContextUtil.getHttpServletRequest();
    			DomainUserDetails currentUser = SecurityUtils.getCurrentUser();
    			if(currentUser!=null){
    				operateLogInfo.setUserId(currentUser.getId());
    				operateLogInfo.setUserName(currentUser.getUsername());
    			}
    			MethodSignature signature = (MethodSignature) pjp.getSignature();
    			OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
    			operateLogInfo.setOperation(declaredAnnotation.operation());
    			operateLogInfo.setModule(declaredAnnotation.module());
    			operateLogInfo.setOperateType(declaredAnnotation.operateType());
    			//获取执行的方法
    			String method = signature.getDeclaringType().getName() + "."  + signature.getName();
    			operateLogInfo.setMethod(method);
    			String operateType = declaredAnnotation.operateType();
    			if(pjp.getArgs().length>0){
    				Object args = pjp.getArgs()[0];
    				operateLogInfo.setModifiedData(new Gson().toJson(args));
    			}
    			if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
    				GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
    				String tableName = "";
    				String idName = "";
    				String selectPrimaryKey = "";
    				if(pjp.getArgs().length>0){
    					Object args = pjp.getArgs()[0];
    					//获取操作前的数据
    					boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
    					if(selectTableFlag){
    						tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
    						idName = args.getClass().getAnnotation(SelectTable.class).idName();
    					}else {
    						throw new RuntimeException("操作日志类型为修改或删除,实体类必须指定表面和主键注解!");
    					}
    					Field[] fields = args.getClass().getDeclaredFields();
    					Field[] fieldsCopy = fields;
    					boolean isFindField = false;
    					int fieldLength = fields.length;
    					for(int i = 0; i < fieldLength; ++i) {
    						Field field = fieldsCopy[i];
    						boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
    						if (hasPrimaryField) {
    							isFindField = true;
    							field.setAccessible(true);
    							selectPrimaryKey = (String)field.get(args);
    						}
    					}
    					if(!isFindField){
    						throw new RuntimeException("实体类必须指定主键属性!");
    					}
    				}
    				if(StringUtils.isNotEmpty(tableName) &&
    					StringUtils.isNotEmpty(idName)&&
    					StringUtils.isNotEmpty(selectPrimaryKey)){
    					StringBuffer sb = new StringBuffer();
    					sb.append(" select * from  ");
    					sb.append(tableName);
    					sb.append(" where ");
    					sb.append(idName);
    					sb.append(" = ? ");
    					String sql = sb.toString();
    					try{
    						List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey);
    						if(maps!=null){
    							operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
    						}
    					}catch (Exception e){
    						e.printStackTrace();
    						throw new RuntimeException("查询操作前数据出错!");
    					}
    				}else {
    					throw new RuntimeException("表名、主键名或主键值 存在空值情况,请核实!");
    				}
    			}else{
    				operateLogInfo.setPreModifiedData("");
    			}
    			//操作时间
    			Date beforeDate = new Date();
    			Long startTime = beforeDate.getTime();
    			operateLogInfo.setExecuteTime(beforeDate);
    			responseObj = pjp.proceed();
    			Date afterDate = new Date();
    			Long endTime = afterDate.getTime();
    			Long duration = endTime - startTime;
    			operateLogInfo.setDuration(duration);
    			operateLogInfo.setIp(getIp(request));
    			operateLogInfo.setResult(flag);
    		}catch (RuntimeException e){
    			throw new RuntimeException(e);
    		}catch (Exception e){
    			flag = "fail";
    			operateLogInfo.setResult(flag);
    			operateLogInfo.setErrorMessage(e.getMessage());
    			operateLogInfo.setErrorStackTrace(e.getStackTrace().toString());
    			e.printStackTrace();
    		}finally {
    			insertIntoLogTable(operateLogInfo);
    		}
    		return responseObj;
    	}
    
    	private void insertIntoLogTable(OperateLogInfo operateLogInfo){
    		operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
    		String sql="insert into energy_log values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
    		jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
    			operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
    			operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
    			operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
    			operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
    			operateLogInfo.getModule(),operateLogInfo.getOperateType());
    	}
    
    	private String getIp(HttpServletRequest request){
    		String ip = request.getHeader("X-forwarded-for");
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getHeader("Proxy-Client-IP");
    		}
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getHeader("WL-Proxy-Client-IP");
    		}
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getHeader("HTTP_CLIENT_IP");
    		}
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getHeader("HTTP_X_FORWARDED_FOR");
    		}
    		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    			ip = request.getRemoteAddr();
    		}
    		return ip;
    	}
    }
    

    示例的使用方式

    针对于示例来说我们要在controller上面打上操作日志的注解。

        @PostMapping("/updateInfo")
        @OperateLog(operation = "修改信息",operateType = GlobalStaticParas.OPERATE_MOD)
        public void updateInfo(@RequestBody Info info) {
            service.updateInfo(info);
        }
    

    针对于Info的实体类,我们则要对其中的字段和表名进行标识。

    @Data
    @SelectTable(tableName = "info",idName = "id")
    public class Info  {
    
        @SelectPrimaryKey
        private String id;
        
        private String name;
    
    }
    

    总结

    文章写到这,也就结束了,文中难免有不足,欢迎大家批评指正,另外可以关注我的公众号,进群交流哦。

  • 相关阅读:
    e-icon-picker 基于element-ui图标和fontawesome图标选择器组件
    js 前端将平级数据转为树形数据的方法
    发送邮件报User does not have send-as privilege for错误的解决办法
    Dynamics 365利用email实体的DeliverIncomingEmail来做抓取邮件的进一步处理
    Dynamics 365中邮件模板的使用
    导入解决方案报错:Unable to retrieve customActivityInfo using RetrieveCustomActivityInfoWithSandboxPlugin
    Dynamics 365组织服务使用Query Expression查询数据时候请谨慎使用ConditionOperator.Contains
    【代码审计】ESPCMSP8(易思企业建站管理系统)漏洞报告
    MS16-072域内中间人攻击
    域控权限提升PTH攻击
  • 原文地址:https://www.cnblogs.com/jichi/p/12969732.html
Copyright © 2020-2023  润新知