系统操作日志的实现——自定义注解、切面、线程池...

前述:

项目需要系统操作日志,非查询操作,都进行统计,如:小明编辑用户信息。
我的实现思路:
1.利用注解+切面实现,自定义注解:@SysLog,切面使用@AfterReturning(value = "sysLog()",returning="rObj")注解,注:只有当执行方法正常返回时,进行切面拦截
2.由子线程执行。有这么几个看得见好处:(1.)这样降低切面中的操作与业务代码的耦合,(2.)即使切面中的日志记录出问题,也不会对业务的返回产生影响,(3.)子线程去执行日志记录,业务接口的返回时间不受影响
3.具体的记录由Base服务提供,所以需要在子线程中进行服务调用,在Base服务会获取request中Header内的token,进行校验与数据隔离等功能(所以需要在子线程获取request与token,这就埋下了伏笔与问题)
(common包中提供的切面与注解,该common是包,不是服务,所以本身不进行db,只能由其他服务提供具体的db操作,这与我们这个项目的服务设计有关)

注:本次记录均是在springboot+springcloud框架下进行

问题:

由第3点需求,从而引发第2点------子线程获取reuest的问题

1.线程池:

子线程的使用,我没有选择

new Runnable(){
    ...
}

而是选择了线程池,原因在于:
    线程数量与资源的考虑,如果每次操作接口后,都在切面中new Runnable(),那么这种new线程的数量是不可控的,假设1s内有十个用户进行了操作,那么除了操作本身的主线程外,还会产生10个子线程同时执行,而前述第3点中表明,子线程还会进行服务调用与db,那么可想,子线程每次要执行的操作,所需要的资源是不可忽视的。
    当访问量稍一提高,new线程的数量变是成倍增加,且不可控,如果服务器配置又不高的情况下,后果....
    所以选择了线程池来实现,线程池的配置将在另一篇文章记录(可以参考:https://www.cnblogs.com/panxuejun/p/9240504.html)

2. @async 无效

  • 在@SpringBootApplication启动类 添加注解@EnableAsync
  • 异步方法使用注解@Async ,返回值为void或者Future
  • 切记一点 ,异步方法和调用方法一定要**** 写在不同的类中 ****,如果写在一个类中,
    是没有效果的

第三点!!!一定要注意!我就是踩了第三点的坑,想要在方法中调用另一个异步方法,需要将该方法写入另一个类中,该类注入spring,通过@Autowired/@Resource方式调用该方法,具体原因可参考:
https://blog.csdn.net/clementad/article/details/47339519

3.子线程HttpServletRequest获取:

一般我们获取当前requst有两种方式:
1.@RequestMapping 来直接获取 HttpServletRequest
2.ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes();            HttpServletRequest request = attributes.getRequest();

但在子线程方法中,1方式肯定是不行了,那2方式呢?如果你尝试之后,会发现null,这是因为只有在负责request处理的线程才能调用到 RequestContextHolder 对象,那该怎么办?

方案一:将主线程的request作为参数传给子线程方法
     你如果尝试了,会发现应该可以,但如果你将子线程Sleep一段时间后,子线程方法再获取requst,为null了,这是为什么?因为主线程请求在返回response后,会销毁HttpServletRequest等对象,除非你的子线程优先于主线程结束,但是你可以保证么?(参考:https://blog.csdn.net/kid551/article/details/88703414)

方案二:不在子线程获取request
     
这是一种取巧方式:将主线程request中需要的信息取出,作为参数再传给request,这种方式在某些需求下是可行的
但如果你真的需要子线程的request怎么办?

方案三:RequestAttributes子线程共享!(再与方案二结合)
在主线程中,使用如下代码,(参考:https://blog.csdn.net/qq516071744/article/details/98643136)

ServletRequestAttributes att = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();//先获取当前主线程的ServletRequestAttributes
RequestContextHolder.setRequestAttributes(att, true);//再将其开启子线程共享

这样,b我们在子线程使用RequestContextHolder来获取子线程中的request了!

但是!你会发现,共享后,主线程中request数据并未与子线程共享,也就是说子线程中的request并不是主线程那个request了,

4.reuest中token 的赋值

无法从子线程的request中取出你存储在主线程中request的数据,该怎么办?

我是与上面主线程取值传参的方式相结合:在主线程中取出request的值,作为参数传给子线程,并且在主线程中开启RequestAttributes子线程共享,然后在request中获取request,再将参数赋值给子线程的request

如果需要对token赋值呢?可以参考我的这篇转发文章https://blog.csdn.net/With_Her/article/details/103112272

这样,我们便实现了:在子线程获取requst,并可将数据赋值给request

5.request.setAttribute();Null指针

在使用中,我曾想过,在子线程中将参数赋值到setAttribute中,但出现了NullException,后来采用了赋值token的方式,出现这种问题的原因未来得及深究,在此记录,如果你也遇到这个问题,希望可以留言交流

总结:

这此这个功能,我一开始想到的就是注解+切面的方式实现就足够了,但随着功能的继续,发现这些这是初步实现了可以拦截到想拦截方法的功能,如何记录?如何更好的记录,却又出现了一些新的问题

一开始没想到用线程池,而是队列的方式去做,即:主线程中将要记录的信息添加到队列中,通过定时任务pull队列中的信息,进行存储,但后来与同事的交流与自己的了解,发现线程池却是更好的选择

但在确定使用线程池方案后,又陆陆续续出现了以上等的一些问题,心心念念的想着周末在家花时间远程来解决这些问题,结果从中午两点一直到下午五六点才基本解决,

吃完饭又花了两个小时对代码整理提交,并写了这篇记录的博客,但感觉应该还有优化的地方,如果你有什么疑惑或者建议,希望可以在下面留言交流!

交流才可以更加进步,闭门造车,可能就困在了自己的小胡同内

 

参考文档:

https://blog.csdn.net/qq_34545192/article/details/80484780
https://blog.csdn.net/clementad/article/details/47339519
https://blog.csdn.net/qq516071744/article/details/98643136
https://blog.csdn.net/kid551/article/details/88703414
https://blog.csdn.net/u010698072/article/details/79973830
https://www.cnblogs.com/panxuejun/p/9240504.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,针对您的问题,我可以简单地为您介绍一下在 Spring 中如何定义注解切面。 首先,您需要使用 Spring AOP 实现切面编程。Spring AOP 是基于代理模式的 AOP 框架,它可以在不修改原有代码的情况下,通过动态代理的方式对方法进行增强。 其次,您需要定义一个自定义注解,例如: ``` @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value() default ""; } ``` 这个注解可以在方法上进行标注,用来表示需要被切面增强的方法。 接下来,您可以定义一个切面类,实现对 MyAnnotation 注解进行切面增强,例如: ``` @Aspect @Component public class MyAspect { @Pointcut("@annotation(com.example.demo.MyAnnotation)") public void myPointcut() {} @Around("myPointcut()") public Object around(ProceedingJoinPoint pjp) throws Throwable { // 在方法执行前进行增强 System.out.println("before method execute..."); // 执行原有方法 Object result = pjp.proceed(); // 在方法执行后进行增强 System.out.println("after method execute..."); return result; } } ``` 在这个切面类中,我们使用 @Pointcut 定义了一个切点,表示需要增强被 MyAnnotation 注解标注的方法。在 around 方法中,我们可以在方法执行前后进行增强操作。 最后,您需要在 Spring 配置文件中将切面类注册为 Bean,并开启 AOP 自动代理,例如: ``` @Configuration @EnableAspectJAutoProxy @ComponentScan(basePackages = "com.example.demo") public class AppConfig { @Bean public MyAspect myAspect() { return new MyAspect(); } } ``` 这样,当您使用 MyAnnotation 注解标注一个方法时,该方法就会被 MyAspect 切面类增强。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值