关于java:Spring-Boot-项目设计业务操作日志功能写得太好了

1次阅读

共计 8294 个字符,预计需要花费 21 分钟才能阅读完成。

前言

很久以前都想写这篇文章,始终没有空,但直到现在我对过后的情景还有印象,之所以有印象是因为需要很简略,业务操作日志的记录与查问的性能,然而具体实现真的很烂,具体的烂法会在背面示例里细说,领导以及客户层面很认可,一系列迷之操作,让我印象粗浅。

需要形容与剖析

客户侧提出需要很简略:要对几个要害的业务性能进行操作日志记录,即什么人在什么工夫操作了哪个性能,操作前的数据报文是什么、操作后的数据报文是什么,必要的时候能够一键回退。

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

系统日志

这里的系统日志是指的是程序执行过程中的关键步骤,依据理论场景输入的 debug、info、warn、error 等不同级别的程序执行记录信息,这些个别是给程序员或运维看的,个别在出现异常问题的时候,能够通过系统日志中记录的要害参数信息和异样提醒,疾速排除故障。

操作日志

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

通过对需要的剖析,客户想要是一个业务操作日志治理的性能:

1、记录用户的业务操作行为,记录的字段有:操作人、操作工夫、操作性能、日志类型、操作内容形容、操作内容报文、操作前内容报文

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

3、提供肯定的治理性能,必要的时候能够对用户的误操作回滚;

背面实现

明确需要后,就是怎么实现的问题了,这里先上一个背面的实现案例,也是因为这一个背面案例,才让我对这个简略的需要印象粗浅。

这里我以一个人员治理的性能为例还原一下,过后的具体实现:

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

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

上面是伪代码:

@RestController
@Slf4j
@BusLog(name = "人员治理")
@RequestMapping("/person")
public class PersonController2 {
    @Autowired
    private IPersonService personService;
    @Autowired
    private IBusLogService busLogService;
    // 增加人员信息
    @PostMapping
    public 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;
    }
}

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

举荐一个开源收费的 Spring Boot 实战我的项目:

https://github.com/javastacks/spring-boot-best-practice

设计思路

如果对 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 {
    @Autowired
    private 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 {
    @Autowired
    private 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;
    }
 
    @Override
    public int getOrder() {return 1;}
}

测试

调试办法

平时后端调试接口,个别都是应用 postman,这里给大家安利一款工具,即 Intellij IDEA 的 Test RESTful web service,性能和应用和 postman 差不多,惟一的益处就是不必在电脑上再额定装个 postman,性能入口:工具栏的 Tools–>http client–>Test RESTful web

另外还有一种用法,我比拟喜爱用这种,简略几句就能够发动一个 http 申请,还能够一次批量执行;

验证后果

总结

业务操作日志记录中蕴含了用户操作的性能名称、性能形容、操作人、操作工夫和操作的参数报文,参数报文之所以抉择存储在文件中,是因为失常状况下,是不须要晓得具体的参数报文,只有在回滚操作的时候才会用到,能够依据上一次的参数报文逆向操作。

版权申明:本文为 CSDN 博主「凡夫贩夫」的原创文章,遵循 CC 4.0 BY-SA 版权协定,转载请附上原文出处链接及本申明。原文链接:https://blog.csdn.net/fox9916/article/details/130175379

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0