SpringBoot项目怎么设计业务操作日志功能?

日志在业务系统中是必不可少的一个功能,常见的有系统日志、操作日志等:

系统日志

这里的系统日志是指的是程序执行过程中的关键步骤,根据实际场景输出的debug、info、warn、error等不同级别的程序执行记录信息,这些一般是给程序员或运维看的,一般在出现异常问题的时候,可以通过系统日志中记录的关键参数信息和异常提示,快速排除故障。

操作日志

操作日志,是用户实际业务操作行为的记录,这些信息一般存储在数据库里,如什么时间哪个用户点了某个菜单、修改了哪个配置等这类业务操作行为,这些日志信息是给普通用户或系统管理员看到。

 

通过对需求的分析,客户想要是一个业务操作日志管理的功能:

1、记录用户的业务操作行为,记录的字段有:操作人、操作时间、操作功能、日志类型、操作内容描述、操作内容报文、操作前内容报文

2、提供一个可视化的页面,可以查询用户的业务操作行为,对重要操作回溯;

3、提供一定的管理功能,必要的时候可以对用户的误操作回滚;

反面实现

明确需求后,就是怎么实现的问题了,这里先上一个反面的实现案例,也是因为这一个反面案例,才让我对这个简单的需求印象深刻。

这里我以一个人员管理的功能为例还原一下,当时的具体实现:

1、每个接口里都加一段记录业务操作日志的记录;

2、每个接口里都要捕获一下异常,记录异常业务操作日志;

下面是伪代码:

@RestController
@Slf4j
@BusLog(name = "人员管理")
@RequestMapping("/person")
public class PersonController2 {@Autowiredprivate IPersonService personService;@Autowiredprivate IBusLogService busLogService;//添加人员信息@PostMappingpublic Person add(@RequestBody Person person) {try{//添加信息信息Person result = this.personService.registe(person);//保存业务日志this.saveLog(person);log.info("//增加person执行完成");        }catch(Exception e){//保存异常操作日志this.saveExceptionLog(e);       }return result;}
}

这种通过硬编码实现的业务操作日志管理功能,最大的问题就是业务操作日志收集与业务逻辑耦合严重,和代码重复,新开发的接口在完成业务逻辑后要织入一段业务操作日志保存的逻辑,已开发上线的接口,还要重新再修改织入业务操作日志保存的逻辑并测试,且每个接口需要织入的业务操作日志保存的逻辑是一样的。

设计思路

如果对AOP有一些印象的话,最好的方法就是使用aop实现:

1、定义业务操作日志注解,注解内可以定义一些属性,如操作功能名称、功能的描述等;

2、把业务操作日志注解标记在需要进行业务操作记录的方法上(在实际业务中,一些简单的业务查询行为通常没有必要记录);

3、定义切入点,编写切面:切入点就是标记了业务操作日志注解的目标方法;切面的主要逻辑就是保存业务操作日志信息;

 

Spring AOP

AOP (Aspect Orient Programming),直译过来就是 面向切面编程,AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向切面编程,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术,AOP可以拦截指定的方法并且对方法增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离;

而SpringAOP,则是AOP的一种具体实现,Spring内部对SpringAOP的应用最经典的场景就是Spring的事务,通过事务注解的配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略;与过滤器、拦截器相比,更加重要的是其适用范围不再局限于SpringMVC项目,可以在任意一层定义一个切点,织入相应的操作,并且还可以改变返回值;

 

Filter和HandlerInterceptor

之所以没有选择Filter和HandlerInterceptor,而是AOP来实现业务操作日志功能,是因为Filter和HandlerInterceptor自身的一些局限性:

过滤器

过滤器(Filter)是与servlet相关联的一个接口,主要适用于java web项目中,依赖于Servlet容器,是利用java的回调机制来实现过滤拦截来自浏览器端的http请求,可以拦截到访问URL对应的方法的请求和响应(ServletRequest request, ServletResponse response),但是不能对请求和响应信息中的值进行修改;一般用于设置字符编码、鉴权操作等;

 

如果想要做到更细一点的类和方法或者是在非servlet环境中使用,则是做不到的;所以凡是依赖Servlet容器的环境,过滤器都可以使用,如Struts2、SpringMVC;

 

 

拦截器

拦截器的(HandlerInterceptor)使用范围以及功能和过滤器很类似,但是也是有区别的。首先,拦截器(HandlerInterceptor)适用于SpringMVC中,因为HandlerInterceptor接口是SpringMVC相关的一个接口,而实现java Web项目,SpringMVC是目前的首选选项,但不是唯一选项,还有struts2等;因此,如果是非SpingMVC的项目,HandlerInterceptor无法使用的;

其次,和过滤器一样,拦截器可以拦截到访问URL对应的方法的请求和响应(ServletRequest request, ServletResponse response),但是不能对请求和响应信息中的值进行修改;一般用于设置字符编码、鉴权操作等;如果想要做到更细一点的类和方法或者是在非servlet环境中使用,则也是是做不到的;

总之,过滤器和拦截器的功能很类似,但是拦截器的适用范围比过滤器更小;

 

 

SpringAOP、过滤器、拦截器对比

在匹配中同一目标时,过滤器、拦截器、SpringAOP的执行优先级是:过滤器>拦截器>SpringAOP,执行顺序是先进后出,具体的不同则体现在以下几个方面:

1、作用域不同

  • 过滤器依赖于servlet容器,只能在 servlet容器,web环境下使用,对请求-响应入口处进行过滤拦截;

  • 拦截器依赖于springMVC,可以在SpringMVC项目中使用,而SpringMVC的核心是DispatcherServlet,而DispatcherServlet又属于Servlet的子类,因此作用域和过滤器类似;

  • SpringAOP对作用域没有限制,只要定义好切点,可以在请求-响应的入口层(controller层)拦截处理,也可以在请求的业务处理层(service层)拦截处理;

 

2、颗粒度的不同

  • 过滤器的控制颗粒度比较粗,只能在doFilter()中对请求和响应进行过虑和拦截处理;

  • 拦截器提供更精细颗粒度的控制,有preHandle()、postHandle()、afterCompletion(),可以在controller对请求处理之前、请求处理后、请求响应完毕织入一些业务操作;

  • SpringAOP,提供了前置通知、后置通知、返回后通知、异常通知、环绕通知,比拦截器更加精细化的颗粒度控制,甚至可以修改返回值;

实现方案

 

环境配置

  • jdk版本:1.8开发工具:Intellij iDEA 2020.1

  • springboot:2.3.9.RELEASE

  • mybatis-spring-boot-starter:2.1.4

依赖配置

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 

表结构设计

create table if not exists bus_log
(id bigint auto_increment comment '自增id'primary key,bus_name varchar(100) null comment '业务名称',bus_descrip varchar(255) null comment '业务操作描述',oper_person varchar(100) null comment '操作人',oper_time datetime null comment '操作时间',ip_from varchar(50) null comment '操作来源ip',param_file varchar(255) null comment '操作参数报文文件'
)
comment '业务操作日志' default charset ='utf8';

代码实现

1、定义业务日志注解@BusLog,可以作用在控制器或其他业务类上,用于描述当前类的功能;也可以用于方法上,用于描述当前方法的作用;

/*** 业务日志注解* 可以作用在控制器或其他业务类上,用于描述当前类的功能;* 也可以用于方法上,用于描述当前方法的作用;*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BusLog {/*** 功能名称* @return*/String name() default "";/*** 功能描述* @return*/String descrip() default "";}

 

2、把业务操作日志注解BusLog标记在PersonController类和方法上;

@RestController
@Slf4j
@BusLog(name = "人员管理")
@RequestMapping("/person")
public class PersonController {@Autowiredprivate IPersonService personService;private Integer maxCount=100;@PostMapping@NeedEncrypt@BusLog(descrip = "添加单条人员信息")public Person add(@RequestBody Person person) {Person result = this.personService.registe(person);log.info("//增加person执行完成");return result;}@PostMapping("/batch")@BusLog(descrip = "批量添加人员信息")public String addBatch(@RequestBody List<Person> personList){this.personService.addBatch(personList);return String.valueOf(System.currentTimeMillis());}@GetMapping@NeedDecrypt@BusLog(descrip = "人员信息列表查询")public PageInfo<Person> list(Integer page, Integer limit, String searchValue) {PageInfo<Person> pageInfo = this.personService.getPersonList(page,limit,searchValue);log.info("//查询person列表执行完成");return pageInfo;}@GetMapping("/{loginNo}")@NeedDecrypt@BusLog(descrip = "人员信息详情查询")public Person info(@PathVariable String loginNo,String phoneVal) {Person person= this.personService.get(loginNo);log.info("//查询person详情执行完成");return person;}@PutMapping@NeedEncrypt@BusLog(descrip = "修改人员信息")public String edit(@RequestBody Person person) {this.personService.update(person);log.info("//查询person详情执行完成");return String.valueOf(System.currentTimeMillis());}@DeleteMapping@BusLog(descrip = "删除人员信息")public String edit(@PathVariable(name = "id") Integer id) {this.personService.delete(id);log.info("//查询person详情执行完成");return String.valueOf(System.currentTimeMillis());}
}

3、编写切面类BusLogAop,并使用@BusLog定义切入点,在环绕通知内执行过目标方法后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述, 把方法的参数报文写入到文件中,最后保存业务操作日志信息;

@Component
@Aspect
@Slf4j
public class BusLogAop implements Ordered {@Autowiredprivate BusLogDao busLogDao;/*** 定义BusLogAop的切入点为标记@BusLog注解的方法*/@Pointcut(value = "@annotation(com.fanfu.anno.BusLog)")public void pointcut() {}/*** 业务操作环绕通知** @param proceedingJoinPoint* @retur*/@Around("pointcut()")public Object around(ProceedingJoinPoint proceedingJoinPoint) {log.info("----BusAop 环绕通知 start");//执行目标方法Object result = null;try {result = proceedingJoinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}//目标方法执行完成后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述Object target = proceedingJoinPoint.getTarget();Object[] args = proceedingJoinPoint.getArgs();MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();BusLog anno1 = target.getClass().getAnnotation(BusLog.class);BusLog anno2 = signature.getMethod().getAnnotation(BusLog.class);BusLogBean busLogBean = new BusLogBean();String logName = anno1.name();String logDescrip = anno2.descrip();busLogBean.setBusName(logName);busLogBean.setBusDescrip(logDescrip);busLogBean.setOperPerson("fanfu");busLogBean.setOperTime(new Date());JsonMapper jsonMapper = new JsonMapper();String json = null;try {json = jsonMapper.writeValueAsString(args);} catch (JsonProcessingException e) {e.printStackTrace();}//把参数报文写入到文件中OutputStream outputStream = null;try {String paramFilePath = System.getProperty("user.dir") + File.separator + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN) + ".log";outputStream = new FileOutputStream(paramFilePath);outputStream.write(json.getBytes(StandardCharsets.UTF_8));busLogBean.setParamFile(paramFilePath);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (outputStream != null) {try {outputStream.flush();outputStream.close();} catch (IOException e) {e.printStackTrace();}}}//保存业务操作日志信息this.busLogDao.insert(busLogBean);log.info("----BusAop 环绕通知 end");return result;}@Overridepublic int getOrder() {return 1;}
}

总结

业务操作日志记录中包含了用户操作的功能名称、功能描述、操作人、操作时间和操作的参数报文,参数报文之所以选择存储在文件中,是因为正常情况下,是不需要知道具体的参数报文,只有在回滚操作的时候才会用到,可以根据上一次的参数报文逆向操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/240.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入理解深度学习——注意力机制(Attention Mechanism):带掩码的多头注意力(Masked Multi-head Attention)

分类目录&#xff1a;《深入理解深度学习》总目录 相关文章&#xff1a; 注意力机制&#xff08;AttentionMechanism&#xff09;&#xff1a;基础知识 注意力机制&#xff08;AttentionMechanism&#xff09;&#xff1a;注意力汇聚与Nadaraya-Watson核回归 注意力机制&#…

设计模式之备忘录模式笔记

设计模式之备忘录模式笔记 说明Memento(备忘录)目录白箱备忘录模式备忘录模式示例类图游戏角色类备忘录角色类备忘录对象管理对象测试类 黑箱备忘录模式备忘录模式示例类图备忘录接口游戏角色类备忘录对象管理对象测试类 说明 记录下学习设计模式-备忘录模式的写法。JDK使用版…

自制聊天机器人实现与chatgpt或微信好友对话【附代码】

闲来无事&#xff0c;想实现一个可与chatgpt或者微信好友对话的聊天机器人。该聊天机器人还可应用于QQ好友或者其他地方的语音输入。功能还是比较简单的&#xff0c;后期会慢慢更新&#xff0c;让人机交互体验感不断提升。 项目描述&#xff1a; 语音输入"开启语音助手&…

【kubernetes】负载均衡器安装部署-Haproxy与keepalived

前言:二进制部署kubernetes集群在企业应用中扮演着非常重要的角色。无论是集群升级,还是证书设置有效期都非常方便,也是从事云原生相关工作从入门到精通不得不迈过的坎。通过本系列文章,你将从虚拟机准备开始,到使用二进制方式从零到一搭建起安全稳定的高可用kubernetes集…

vite构建工具初识

一、什么是vite vite官网地址&#xff1a;https://cn.vitejs.dev/ Vite 是一个由 Vue.js 作者尤雨溪开发的新一代前端构建工具&#xff0c;它相比于传统的 webpack&#xff0c;具有更快的启动速度、更高的开发效率和更简洁的配置方式。 Vite的主要特点包括&#xff1a; 快速…

Java(七):项目部署

项目部署 运行容器解决Centos8中yum命令遇到的问题打包项目拷贝.jar到容器中安装jdk后台运行.jar后台运行.jar并输入日志实时查看日志查看/杀死运行程序目录结构日志配置 运行容器 $ docker run -d -p 8001:8001 -p 8081:8081 -p 8082:8082 --namelocal_centos --privilegedtr…

EasyCode代码生成插件-模板分享(基于数据表生成MyBatisPlus格式的dao,service,controller和vue组件)

目录 概述 使用演示 模板代码 实体类pojo 表现层controller 业务层service接口 业务层serviceImpl实现类 持久层dao Vue组件 概述 本片博客用于分享EasyCode的自定义模板&#xff08;模板在篇末&#xff09;&#xff0c;用于简化开发&#xff0c;免去重复性的工作。 …

6.28作业

作业1 结构体不能被继承&#xff0c;类可以被继承结构体默认的都是公共&#xff0c;类默认是私有的 转载【结构体和类的区别】 结构体是值类型&#xff0c;类是引用类型 结构体存在栈中&#xff0c;类存在堆中 结构体成员不能使用protected访问修饰符&#xff0c;而类可以 结…

无法更新iPhone,提示“无法检查更新”怎么办?

当我们需要 iPhone更新系统时&#xff0c;可以前往iPhone设置-通用-软件更新中获取更新推送。不过一些用户可能会遇到无法更新的问题&#xff0c;例如会提示“无法检查更新&#xff0c;检查软件更新时出错”。 以上情况可能是网络问题&#xff0c;可以尝试重新打开设置&#xf…

vue中使用mock.js

安装mock npm install mockjs --save-dev或者在ui中选择依赖 查看安装是否成功 进入package.json文件 配置mock 在src目录下新建mock文件夹&#xff0c;在mock文件夹下建立index.js 配置模拟请求数据 index.js import Mock from mockjsconst loginData Mock.mock(http://l…

YOLOv5图像和视频对象生成边界框的目标检测实践(GPU版本PyTorch错误处理)

识别图像和视频里面的对象&#xff0c;在计算机视觉中是一个很重要的应用&#xff0c;比如无人驾驶&#xff0c;这个就需要实时的检测到周边环境的各种对象&#xff0c;并及时做出处理。目标检测在以往的文章中有重点讲解过几种&#xff0c;其中Faster R-CNN的源码解读&#xf…

Spring Bean的实例化过程

一、前言 对于写Java的程序员来说&#xff0c;Spring已经成为了目前最流行的第三方开源框架之一&#xff0c;在我们充分享受Spring IOC容器带来的红利的同时&#xff0c;我们也应该考虑一下Spring这个大工厂是如何将一个个的Bean生产出来的&#xff0c;本期我们就一起来讨论一…