系統操作日誌的實現——自定義註解、切面、線程池...

前述:

項目需要系統操作日誌,非查詢操作,都進行統計,如:小明編輯用戶信息。
我的實現思路:
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章