前述:
项目需要系统操作日志,非查询操作,都进行统计,如:小明编辑用户信息。
我的实现思路:
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/473395193.子线程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