前言

大家好,这里是经典鸡翅,今天给大家带来一篇基于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; }

总结

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

一文带你学会基于SpringAop实现操作日志的记录的更多相关文章

  1. 一文带你学会java的jvm精华知识点

    前言 本文分为20多个问题,通过问题的方式,来逐渐理解jvm,由浅及深.希望帮助到大家. 1. Java类实例化时,JVM执行顺序? 正确的顺序如下: 1父类静态代码块 2父类静态变量 3子类静态代码 ...

  2. 一文带你学会使用YOLO及Opencv完成图像及视频流目标检测(上)|附源码

    计算机视觉领域中,目标检测一直是工业应用上比较热门且成熟的应用领域,比如人脸识别.行人检测等,国内的旷视科技.商汤科技等公司在该领域占据行业领先地位.相对于图像分类任务而言,目标检测会更加复杂一些,不 ...

  3. 一文带你学会国产加密算法SM4的java实现方案

    前言 今天给大家带来一个国产SM4加密解密算法的java后端解决方案,代码完整,可以直接使用,希望给大家带来帮助,尤其是做政府系统的开发人员,可以直接应用到项目中进行加密解密. 画重点!是SM4哦,不 ...

  4. 一文带你学会国产加密算法SM4的vue实现方案

    前言 上篇文章我们介绍了国产SM4加密算法的后端java实现方案.没有看过的小伙伴可以看一下这篇文章. https://www.cnblogs.com/jichi/p/12907453.html 本篇 ...

  5. 一文带你学会AQS和并发工具类的关系

    1. 存在的意义   AQS(AbstractQueuedSynchronizer)是JAVA中众多锁以及并发工具的基础,其底层采用乐观锁,大量使用了CAS操作, 并且在冲突时,采用自旋方式重试,以实 ...

  6. 一文带你学会AQS和并发工具类的关系2

    1.创建公平锁 1.使用方式 Lock reentrantLock = new ReentrantLock(true); reentrantLock.lock(); //加锁 try{ // todo ...

  7. springAOP实现操作日志记录,并记录请求参数与编辑前后字段的具体改变

    本文为博主原创,未经允许不得转载: 在项目开发已经完成多半的情况下,需要开发进行操作日志功能的开发,由于操作的重要性,需要记录下操作前的参数和请求时的参数, 在网上找了很多,没找到可行的方法.由于操作 ...

  8. [原创]基于SpringAOP开发的方法调用链分析框架

    新人熟悉项目必备工具!基于SpringAOP开发的一款方法调用链分析插件,简单到只需要一个注解,异步非阻塞,完美嵌入Spring Cloud.Dubbo项目!再也不用担心搞不懂项目! 很多新人进入一家 ...

  9. Istio是啥?一文带你彻底了解!

    原标题:Istio是啥?一文带你彻底了解! " 如果你比较关注新兴技术的话,那么很可能在不同的地方听说过 Istio,并且知道它和 Service Mesh 有着牵扯. 这篇文章可以作为了解 ...

随机推荐

  1. 个人博客开发系列:Vue.js + Koa.js项目中使用JWT认证

    前言 JWT(JSON Web Token),是为了在网络环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519). 更多的介绍和说明,以及各种原理,我在此就不多赘诉了.JWT不是一个新鲜 ...

  2. WebStorm 2019 3.3 安装及破解教程附汉化教程

    WebStorm2019 3.3 安装及破解教程附加汉化教程 安装包及破解补丁 链接: https://pan.baidu.com/s/19ATTAW3Tsm0huIJSqYChTw 提取码:1ei7 ...

  3. 关于SPFA Bellman-Ford Dijkstra Floyd BFS最短路的共同点与区别

    关于模板什么的还有算法的具体介绍 戳我 这里我们只做所有最短路的具体分析. 那么同是求解最短路,这些算法到底有什么区别和联系: 对于BFS来说,他没有松弛操作,他的理论思想是从每一点做树形便利,那么时 ...

  4. python(递归函数)

    1.描述: 递归函数:在函数内部,可以调用其他函数.如果一个函数在内部调用自身本身,这个函数就是递归函数. 2.递归函数特性: 必须有一个明确的结束条件: 每次进入更深一层递归时,问题规模相比上次递归 ...

  5. pycharm 新建文件后选错文件格式怎么改

    经常在新建文件的时候,忘记填写文件后缀,导致文件无默认格式,而且同名字的文件怎么改都改不成想要的格式,所以随手记录一下怎么修正: 原因:肯定是pycharm已经默认指定了一个格式,所以再重复新建同样名 ...

  6. 开源 一套 Blazor Server 端精致套件

    Blazor 作为一种 Web 开发的新技术已经发展有一段时间了,有些人标称 无 JS 无 TS,我觉得有点误导新人的意味,也有人文章大肆宣传 Blazor 是 JavaScript 的终结者,是为了 ...

  7. file download hash mismatch

    在linux中使用cmake时,遇到了"file download hash mismatch",同时status显示"unsupported protocol" ...

  8. Spring官网阅读(十七)Spring中的数据校验

    文章目录 Java中的数据校验 Bean Validation(JSR 380) 使用示例 Spring对Bean Validation的支持 Spring中的Validator 接口定义 UML类图 ...

  9. 【Scala】利用akka实现Spark启动通信

    文章目录 思路分析 步骤 一.创建maven工程,导包 二.master进程代码开发 三.worker进程代码开发 思路分析 1.首先启动master,然后依次启动worker 2.启动worker时 ...

  10. 自定义IDOC

    目录 1需求场景    4 2配置发送端IDOC    4 2.1定义段(WE31)    4 2.2定义基本类型(WE30)    4 2.3定义消息类型(WE81)    5 2.4定义传输结构  ...