热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

SpringAop实现操作日志记录

这篇文章主要介绍了SpringAop实现操作日志记录的方法,帮助大家更好的理解和使用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 > 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 > 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;

}

总结

文章写到这,也就结束了,文中难免有不足,欢迎大家批评指正

以上就是SpringAop实现操作日志记录的详细内容,更多关于SpringAop 操作日志记录的资料请关注其它相关文章!


推荐阅读
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文介绍了使用postman进行接口测试的方法,以测试用户管理模块为例。首先需要下载并安装postman,然后创建基本的请求并填写用户名密码进行登录测试。接下来可以进行用户查询和新增的测试。在新增时,可以进行异常测试,包括用户名超长和输入特殊字符的情况。通过测试发现后台没有对参数长度和特殊字符进行检查和过滤。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • 本文介绍了如何使用Power Design(PD)和SQL Server进行数据库反向工程的方法。通过创建数据源、选择要反向工程的数据表,PD可以生成物理模型,进而生成所需的概念模型。该方法适用于SQL Server数据库,对于其他数据库是否适用尚不确定。详细步骤和操作说明可参考本文内容。 ... [详细]
  • 在数据分析工作中,我们通常会遇到这样的问题,一个业务部门由若干业务组构成,需要筛选出每个业务组里业绩前N名的业务员。这其实是一个分组排序的 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • Oracle Database 10g许可授予信息及高级功能详解
    本文介绍了Oracle Database 10g许可授予信息及其中的高级功能,包括数据库优化数据包、SQL访问指导、SQL优化指导、SQL优化集和重组对象。同时提供了详细说明,指导用户在Oracle Database 10g中如何使用这些功能。 ... [详细]
  • 本文介绍了adg架构设置在企业数据治理中的应用。随着信息技术的发展,企业IT系统的快速发展使得数据成为企业业务增长的新动力,但同时也带来了数据冗余、数据难发现、效率低下、资源消耗等问题。本文讨论了企业面临的几类尖锐问题,并提出了解决方案,包括确保库表结构与系统测试版本一致、避免数据冗余、快速定位问题等。此外,本文还探讨了adg架构在大版本升级、上云服务和微服务治理方面的应用。通过本文的介绍,读者可以了解到adg架构设置的重要性及其在企业数据治理中的应用。 ... [详细]
  • 本文详细介绍了PHP中与URL处理相关的三个函数:http_build_query、parse_str和查询字符串的解析。通过示例和语法说明,讲解了这些函数的使用方法和作用,帮助读者更好地理解和应用。 ... [详细]
  • 本文介绍了Svn和Maven的使用说明,包括版本控制和构建工具的功能和优势。同时提供了一个相关链接,链接中详细介绍了SvnMaven的使用方法和注意事项。通过学习和使用SvnMaven,开发人员可以更好地进行代码管理、软件开发和协作开发,提高项目管理的效率和质量。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 生成对抗式网络GAN及其衍生CGAN、DCGAN、WGAN、LSGAN、BEGAN介绍
    一、GAN原理介绍学习GAN的第一篇论文当然由是IanGoodfellow于2014年发表的GenerativeAdversarialNetworks(论文下载链接arxiv:[h ... [详细]
author-avatar
有风吹过best
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有