
在平时使用到一些软件中,比如某宝或者某书,通过记录用户的行为来构建和分析用户的行为数据,同时也能更好优化产品设计和提升用户体验。比如在一个订单系统中,需要确定追踪用户的行为,比如:
上述行为就需要使用到日志系统来存储或者记录数据,Java 有几种日志方案,简单介绍几种实现方案,以及需要注意的点。
根据业务的不同,需要使用匹配到合适的日志方案。也需要注意几个问题:
Spring AOP 通过切面编程实现不修改原有代码,而动态添加功能的能力。这种方式有以下几个好处:
本文使用基于注解的 AOP,首先定义一个切面注解 AopTest:
/** * 切面注解 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AopTest { } 再创建通知 @Around:
@Around("@annotation(com.test.annotation.AopTest)") public Object annotationTest(ProceedingJoinPoint joinPoint) throws Throwable { log.info("执行前"); Object result = joinPoint.proceed(); // 执行目标方法 Object[] args = joinPoint.getArgs(); log.info("执行后"); return result; } 一般都会在执行后添加日志即可,想要在那个方法或者接口加日志,只需要在方法上添加注解即可,比如在接口添加注解:
@GetMapping @AopTest public String first(String param) { log.info("执行 first 方法"); return "result " + param; } 请求接口后,就有如下的输出:
执行前 执行 first 方法 执行后 但是切面有一个问题,执行切面报错,方法也无法执行,就需要捕获异常,保证业务代码正常执行,改造一下上面的通知:
@Around("@annotation(com.test.annotation.AopTest)") public Object annotationTest(ProceedingJoinPoint joinPoint) throws Throwable {log.info("执行前"); log.info("执行前"); Object result = joinPoint.proceed(); // 执行目标方法 try { Object[] args = joinPoint.getArgs(); // 添加日志 log.info("执行后"); int a = 1/0; } catch (Exception e) { log.error(e.getMessage()); } return result; } 改造后,即使切面报错,也不会影响业务代码的执行了。
AOP 是同步执行的,如果日志添加是一个比较耗时的操作,也会影响接口的响应速度,此时可以使用异步的方式,比如消息队列。
总结一下,Spring AOP 有以下几个优点和缺点。
优点:
缺点及解决方案:
1 、切面报错可能会影响业务代码
2 、同步执行会影响接口响应速度
Spring 事件监听机制是一种发布-订阅模式,将事件的发布和监听解耦开来。通过这种机制,事件的发布者无需关注监听的逻辑,监听者也无需直接依赖发布者。
Spring 事件监听有三个部分组成:
自定义一个事件,继承 ApplicationEvent:
@Getter @Setter public class DemoEvent extends ApplicationEvent { private String name; public DemoEvent(Object source) { super(source); } } 基于 @EventListener 注解监听事件:
@Component @Slf4j public class DemoEventListener { @EventListener public void handleEvent(DemoEvent event){ log.info(event.getName()); log.info("事件监听"); } } 使用 applicationEventPublisher.publishEvent 方法发布事件,
@Autowired private ApplicationEventPublisher applicationEventPublisher; @Override public void publish() { // 执行业务代码 log.info("执行登录,当前时间 {}",LocalTime.now()); DemoEvent event = new DemoEvent(this); event.setName("hello"); applicationEventPublisher.publishEvent(event); log.info("完成登录,当前时间 {}",LocalTime.now()); log.info("执行 service"); } 配置好事件的发布和监听之后,在业务代码添加事件的发布,监听方法内添加日志的记录。
Spring 事件监听虽然解耦了发布和监听,只是解耦逻辑代码,两者还是同步执行,并且都是在同一个线程执行的,所以事件监听也无法解决添加日志报错,以及耗时的问题。
在事件监听添加延迟:
@EventListener public void handleEvent(DemoEvent event){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } log.info(event.getName()); log.info("事件监听"); } 控制台输出:
执行登录,当前时间 16:52:30.799 hello 事件监听 完成登录,当前时间 16:52:33.799 执行 service 接口执行了 3 秒多,并且 publish 方法要等待监听方法执行完毕之后才能执行,说明
事件监听是同步的。
在监听方法添加错误代码:
@EventListener public void handleEvent(DemoEvent event){ int a = 1/0; log.info(event.getName()); log.info("事件监听"); } 控制台输出:
执行登录,当前时间 17:10:08.396 java.lang.ArithmeticException: / by zero 监听方法报错,接口也报错,业务代码无法执行,说明
监听方法报错会影响事件发布方法。
解决报错的问题,使用异常捕获即可。而延迟的问题,就需要使用到异步的操作,异步就是另启一个线程执行监听方法,异步除了能解决延迟的问题,也顺便解决了报错的问题。
实现异步在监听方法上添加 @Async 异步注解,监听方法添加延迟和错误代码:
执行登录,当前时间 17:21:50.971 完成登录,当前时间 17:21:50.974 执行 service publish 方法既不会延迟,也不会因为监听报错影响执行,异步完美解决耗时和报错的问题
海量日志场景,消息队列是一个很好的选择,它也是解耦了发布者和订阅者,如果订阅者开启了手动确认,消费者也需要使用 try catch 捕获异常信息,确保消息能正常消费。
本文介绍几种日志记录的实现方案:
Spring AOP:事件监听 + 异步: 消息队列: 