zoukankan      html  css  js  c++  java
  • 一文带你学会基于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;
    
    }
    

    总结

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

  • 相关阅读:
    centos8 将SSSD配置为使用LDAP并要求TLS身份验证
    Centos8 搭建 kafka2.8 .net5 简单使用kafka
    .net core 3.1 ActionFilter 拦截器 偶然 OnActionExecuting 中HttpContext.Session.Id 为空字符串 的问题
    Springboot根据不同环境加载对应的配置
    VMware Workstation12 安装 Centos8.3
    .net core json配置文件小结
    springboot mybatisplus createtime和updatetime自动填充
    .net core autofac依赖注入简洁版
    .Net Core 使用 redis 存储 session
    .Net Core 接入 RocketMQ
  • 原文地址:https://www.cnblogs.com/jichi/p/12969732.html
Copyright © 2011-2022 走看看