优雅!Spring Boot 这样记录操作日志非常灵活强大

文摘   2024-12-10 08:01   新疆  

Spring Boot 3实战案例合集》现已囊括超过50篇精选实战文章,并且此合集承诺将永久持续更新,为您带来最前沿的技术资讯与实践经验。欢迎积极订阅,享受不断升级的知识盛宴!订阅用户将特别获赠合集内所有文章的最终版MD文档(详尽学习笔记),以及完整的项目源码,助您在学习道路上畅通无阻。

环境:SpringBoot3.2.5


1. 简介

项目中,优雅地记录操作日志不仅是运维监控的基石,更是保障业务透明度和安全性的关键。通过灵活配置日志框架(如Logback、Log4j2),结合AOP(面向切面编程)技术,可以实现对关键业务操作的无侵入式日志记录。这种方式不仅提高了代码的可维护性,还确保了日志信息的全面性和准确性。操作日志记录了用户行为、系统状态变化等重要信息,为问题排查、性能优化及安全审计提供了宝贵的数据支持,是构建稳定、可靠、可追踪系统的不可或缺的一环。通常在项目中通过AOP方式记录是非常常见的一种方式,如下示例:

@Log(module = "用户管理", desc = "导出用户信息")@GetMapping("/export")public R export(QueryParam param) {  // TODO}

这里通过@Log注解配置了当前的模块名称及给出了当前业务操作的简短描述。而在切面当中则以@Log注解为切入点进行读取相应信息然后保存操作的日志。

@Aspectpublic class LogAspect {    @Pointcut("@annotation(log)")  private void pclog(Log log) {}    @Around("pclog(log)")  public Object around(ProceedingJoinPoint pjp, Log log) throws Throwable {    String module = log.module() ;    String desc = log.desc() ;    // TODO, 保存操作日志    return pjp.proceed() ;  }}

通过上面的切面进行日志的记录,大多数情况下可能这样做就够了,毕竟足够的简单。

当前面临一个需求,即希望日志系统能够灵活记录各个模块的关键信息。具体而言,用户管理模块在保存用户时,需要记录所保存的用户名;商品模块在操作时,则需记录涉及的商品名称及价格信息;同样,订单模块在处理订单时,应记录订单的总价及数量等关键数据。鉴于这些信息都是动态生成的,我们需要设计一个能够灵活应对此类需求的日志记录方案。那么,该如何实现这一功能,以确保日志系统既能满足当前需求,又能适应未来的变化呢?

接下来我通过AOP+SpEL的方式实现上面这种需求。

2. 实战案例

2.1 准备环境

定义注解

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Log {
/**模块*/ String module() ; /**描述*/ String desc() default "" ;}

任意方法添加了该注解的都将被拦截。

定义基础业务操作

// 用户模块@Servicepublic class PersonService {  private final List<Person> PERSONS = List.of(        new Person(1L, "张三"),        new Person(2L, "李四"),        new Person(3L, "王五"),        new Person(4L, "赵六")      ) ;  public Person save(Person person) {    return person ;  }  public Person query(Long id) {    return PERSONS.stream().filter(user -> user.getId() == id).findFirst().orElse(null) ;  }}// 商品模块@Servicepublic class ProductService {  private final List<Product> PRODUCTS = List.of(        new Product(1L, "MySQL从删库到跑路", new BigDecimal("99.6")),        new Product(2L, "JAVA从入门到放弃", new BigDecimal("77.8")),        new Product(3L, "精通Spring全家桶", new BigDecimal("100"))      ) ;  public void save(Product product) {    System.out.println("保存商品") ;  }  public Product query(Long id) {    return PRODUCTS.stream().filter(user -> user.getId() == id).findFirst().orElse(null) ;  }}

接下我将基于上面2个模块进行日志操作的记录。

2.2 切面定义

既然要使用SpEL表达式的方式,那我们先来定义一个根对象(作用下面会说)

根对象定义

public class ContextRoot {
/**登录用户*/  // 包含了username,password,role信息 private LoginUser user ; public ContextRoot(LoginUser user) { this.user = user ;  } public final LoginUser getUser() { return this.user ; }}

该对象只有一个LoginUser对象,你有需要还可以定义其它对象信息。

根据上面的需求,我们需要记录每个模块中的关键信息,那么每个执行方法中的参数就非常的关键,如何在切面中当前拦截到的是哪个模块,参数又是什么呢?切面只有一个,你不可能针对每一个模块来一个切面吧!所以,切面中的定义非常的关键,切面应该无需关心具体是那个模块什么参数,它应该只负责执行即可。

核心切面定义结合SpEL

@Aspectpublic class LogAspect {    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class) ;    @Pointcut("@annotation(log)")  private void pclog(Log log) {}    @Around("pclog(log)")  public Object around(ProceedingJoinPoint pjp, Log log) throws Throwable {    // 取得当前的模块名称    String module = log.module() ;    // 获取操作的描述信息;该属性也将是我们定义SpEL表达式的地方    String desc = log.desc() ;    // 取得当前操作的Method对象    MethodSignature ms = (MethodSignature) pjp.getSignature() ;    Method method = ms.getMethod() ;    // 取得当前执行方法的执行参数    Object[] args = pjp.getArgs() ;    DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer() ;     // 创建根对象,根对象访问不需要#前缀,可直接访问。    // 这里直接给出了当前的登录用户,    ContextRoot rootObject = new ContextRoot(new LoginUser("admin", "123123", "ADMIN")) ;    // 定义上下文对象基于方法的执行上下文会将参数自动添加到当前执行上下文的变量中    // 但是访问就必须通过#前缀,这是与根对象的区别    MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(rootObject, method, args, parameterNameDiscoverer) ;    ExpressionParser parser = new SpelExpressionParser() ;    desc = parser.parseExpression(desc).getValue(context, String.class) ;    // TODO,你可以通过异步的方式将日志信息保存到数据库中    logger.info("{}, {}", module, desc) ;    return pjp.proceed() ;  }}

上面切面关键代码都添加了详细的注释,应该很容易理解。

切面有了,那接下来就是在上面的业务模块Service中的方法上添加注解定义及定义SpEL表达式了。

定义切点表达式

// 用户模块@Log(module = "用户模块", desc = "'保存【' + #person.name + '】'")public Person save(Person person) ;
@Log(module = "用户模块", desc = "'操作人:' + user.username + ', 根据ID查询【' + #id + '】'")public Person query(Long id) ;

注意:这里表达式的写法,比如:user.username就是从根对象中取值,#xxx.xx表示从SpEL执行上下文中的变量中取值。

@Log(module = "商品模块", desc = "'保存【' + #product.name + '】, 商品价格【' + #product.price + '】'")public void save(Product product);

接下来编写接口进行测试上面的日志记录情况

@PostMapping("")public Person save(@RequestBody Person user) {  return this.userService.save(user) ;}@GetMapping("/{id}")public Person queryById(@PathVariable("id") Long id) {  return this.userService.query(id) ;}

后台控制台输出

商品模块测试

控制台输出

成功灵活的输出日志信息,我们可以在注解中任意定义SpEL表达式来描述操作日志信息。

以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

推荐文章

强大!Spring Cloud网关Gateway新特性及高级开发技巧

优雅!基于Spring Boot字段加密后的模糊查询,支持MyBatis, JPA

基于Spring Boot 使用 JPA 优化性能的7大关键策略

Spring Boot 一个注解防止重复请求

强大!SpringBoot结合STOMP简化数据实时通信

10个SpringBoot开发技能,每个都很实用(二)

总结7种JVM出现OOM时的原因及解决方案

提升开发效率,Spring的强大数据绑定功能!

SpringBoot Redis的这几个高级功能一定要牢记

SpringBoot3虚拟线程 & 反应式(WebFlux) & 传统Tomcat线程池 性能对比

强烈推荐强大的异步编排处理工具AsyncTool

详解基于SpringBoot实现多数据源动态切换及原理

Redis结合Caffeine实现二级缓存:提高应用程序性能

解锁Spring隐藏工具类:让你的开发效率飞升

【必读】@Configuration注解天天用,你真的了解它吗?

JdbcTemplate已过时?Spring6.1新特性JdbcClient流畅链式API

Spring全家桶实战案例源码
spring, springboot, springcloud 案例开发详解
 最新文章