Java中實(shí)現(xiàn)日志記錄的方案總結(jié)
在平時(shí)使用到一些軟件中,比如某寶或者某書,通過(guò)記錄用戶的行為來(lái)構(gòu)建和分析用戶的行為數(shù)據(jù),同時(shí)也能更好優(yōu)化產(chǎn)品設(shè)計(jì)和提升用戶體驗(yàn)。比如在一個(gè)訂單系統(tǒng)中,需要確定追蹤用戶的行為,比如:
- 登錄/登出
- 瀏覽商品
- 加購(gòu)商品
- 搜索商品關(guān)鍵字
- 下單
上述行為就需要使用到日志系統(tǒng)來(lái)存儲(chǔ)或者記錄數(shù)據(jù),Java 有幾種日志方案,簡(jiǎn)單介紹幾種實(shí)現(xiàn)方案,以及需要注意的點(diǎn)。
日志添加需要注意問(wèn)題
根據(jù)業(yè)務(wù)的不同,需要使用匹配到合適的日志方案。也需要注意幾個(gè)問(wèn)題:
- 不能影響原來(lái)的業(yè)務(wù)邏輯。
- 不能報(bào)錯(cuò),即使報(bào)錯(cuò),不能影響原有的業(yè)務(wù)代碼。
- 對(duì)于耗時(shí)的日志代碼,使用異步方法
- 侵入性低,盡量少改動(dòng)原始代碼
Spring AOP
Spring AOP 通過(guò)切面編程實(shí)現(xiàn)不修改原有代碼,而動(dòng)態(tài)添加功能的能力。這種方式有以下幾個(gè)好處:
- 侵入性低
- 可重用性強(qiáng)
本文使用基于注解的 AOP,首先定義一個(gè)切面注解 AopTest:
/** * 切面注解 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AopTest { }
再創(chuàng)建通知 @Around :
@Around("@annotation(com.test.annotation.AopTest)") public Object annotationTest(ProceedingJoinPoint joinPoint) throws Throwable { log.info("執(zhí)行前"); Object result = joinPoint.proceed(); // 執(zhí)行目標(biāo)方法 Object[] args = joinPoint.getArgs(); log.info("執(zhí)行后"); return result; }
一般都會(huì)在執(zhí)行后
添加日志即可,想要在那個(gè)方法或者接口加日志,只需要在方法上添加注解即可,比如在接口添加注解:
@GetMapping @AopTest public String first(String param) { log.info("執(zhí)行first方法"); return "result " + param; }
請(qǐng)求接口后,就有如下的輸出:
執(zhí)行前
執(zhí)行first方法
執(zhí)行后
但是切面有一個(gè)問(wèn)題,執(zhí)行切面報(bào)錯(cuò),方法也無(wú)法執(zhí)行,就需要捕獲異常,保證業(yè)務(wù)代碼正常執(zhí)行,改造一下上面的通知:
@Around("@annotation(com.test.annotation.AopTest)") public Object annotationTest(ProceedingJoinPoint joinPoint) throws Throwable {log.info("執(zhí)行前"); log.info("執(zhí)行前"); Object result = joinPoint.proceed(); // 執(zhí)行目標(biāo)方法 try { Object[] args = joinPoint.getArgs(); // 添加日志 log.info("執(zhí)行后"); int a = 1/0; } catch (Exception e) { log.error(e.getMessage()); } return result; }
改造后,即使切面報(bào)錯(cuò),也不會(huì)影響業(yè)務(wù)代碼的執(zhí)行了。
AOP 是同步執(zhí)行的,如果日志添加是一個(gè)比較耗時(shí)的操作,也會(huì)影響接口的響應(yīng)速度,此時(shí)可以使用異步的方式,比如消息隊(duì)列。
總結(jié)一下,Spring AOP 有以下幾個(gè)優(yōu)點(diǎn)和缺點(diǎn)。
優(yōu)點(diǎn):
- 侵入性低
- 可重用性強(qiáng)
缺點(diǎn)及解決方案:
1、切面報(bào)錯(cuò)可能會(huì)影響業(yè)務(wù)代碼
問(wèn)題:在切面的異常如果沒有正確處理,可能會(huì)影響業(yè)務(wù)代碼的正常執(zhí)行。
解決方案:
- 捕獲異常,確保業(yè)務(wù)代碼正常執(zhí)行
- 一般使用try catch 捕獲異常,防止向上傳播
2、同步執(zhí)行會(huì)影響接口響應(yīng)速度
問(wèn)題:如果切面中存在耗時(shí)的操作,同步操作會(huì)導(dǎo)致接口的響應(yīng)速度變慢。
解決方案:
- 通過(guò)消息隊(duì)列將耗時(shí)操作異步執(zhí)行
- 使用 @Async 將方法異步執(zhí)行,將任務(wù)從主線程脫離出來(lái),交給其他線程池執(zhí)行
事件監(jiān)聽 + 異步
Spring 事件監(jiān)聽機(jī)制是一種發(fā)布-訂閱
模式,將事件的發(fā)布和監(jiān)聽解耦開來(lái)。通過(guò)這種機(jī)制,事件的發(fā)布者無(wú)需關(guān)注監(jiān)聽的邏輯,監(jiān)聽者也無(wú)需直接依賴發(fā)布者。
Spring 事件監(jiān)聽有三個(gè)部分組成:
1.事件
自定義一個(gè)事件,繼承 ApplicationEvent:
@Getter @Setter public class DemoEvent extends ApplicationEvent { private String name; public DemoEvent(Object source) { super(source); } }
2.事件監(jiān)聽
基于 @EventListener 注解監(jiān)聽事件:
@Component @Slf4j public class DemoEventListener { @EventListener public void handleEvent(DemoEvent event){ log.info(event.getName()); log.info("事件監(jiān)聽"); } }
3.事件發(fā)布
使用 applicationEventPublisher.publishEvent 方法發(fā)布事件,
@Autowired private ApplicationEventPublisher applicationEventPublisher; @Override public void publish() { // 執(zhí)行業(yè)務(wù)代碼 log.info("執(zhí)行登錄,當(dāng)前時(shí)間 {}",LocalTime.now()); DemoEvent event = new DemoEvent(this); event.setName("hello"); applicationEventPublisher.publishEvent(event); log.info("完成登錄,當(dāng)前時(shí)間 {}",LocalTime.now()); log.info("執(zhí)行 service"); }
配置好事件的發(fā)布和監(jiān)聽之后,在業(yè)務(wù)代碼添加事件的發(fā)布,監(jiān)聽方法內(nèi)添加日志的記錄。
Spring 事件監(jiān)聽雖然解耦了發(fā)布和監(jiān)聽,只是解耦邏輯代碼,兩者還是同步執(zhí)行
,并且都是在同一個(gè)線程執(zhí)行的,所以事件監(jiān)聽也無(wú)法解決添加日志報(bào)錯(cuò),以及耗時(shí)的問(wèn)題。
4.驗(yàn)證監(jiān)聽方法是否同步
在事件監(jiān)聽添加延遲:
@EventListener public void handleEvent(DemoEvent event){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } log.info(event.getName()); log.info("事件監(jiān)聽"); }
控制臺(tái)輸出:
執(zhí)行登錄,當(dāng)前時(shí)間 16:52:30.799
hello
事件監(jiān)聽
完成登錄,當(dāng)前時(shí)間 16:52:33.799
執(zhí)行 service
接口執(zhí)行了 3 秒多,并且 publish 方法要等待監(jiān)聽方法執(zhí)行完畢之后才能執(zhí)行,說(shuō)明事件監(jiān)聽是同步的
。
5.驗(yàn)證監(jiān)聽方法報(bào)錯(cuò)是否會(huì)影響主流程
在監(jiān)聽方法添加錯(cuò)誤代碼:
@EventListener public void handleEvent(DemoEvent event){ int a = 1/0; log.info(event.getName()); log.info("事件監(jiān)聽"); }
控制臺(tái)輸出:
執(zhí)行登錄,當(dāng)前時(shí)間 17:10:08.396
java.lang.ArithmeticException: / by zero
監(jiān)聽方法報(bào)錯(cuò),接口也報(bào)錯(cuò),業(yè)務(wù)代碼無(wú)法執(zhí)行,說(shuō)明監(jiān)聽方法報(bào)錯(cuò)會(huì)影響事件發(fā)布方法
。
解決報(bào)錯(cuò)的問(wèn)題,使用異常捕獲即可。而延遲的問(wèn)題,就需要使用到異步
的操作,異步就是另啟一個(gè)線程執(zhí)行監(jiān)聽方法
,異步除了能解決延遲的問(wèn)題,也順便解決了報(bào)錯(cuò)的問(wèn)題。
實(shí)現(xiàn)異步在監(jiān)聽方法上添加 @Async 異步注解,監(jiān)聽方法添加延遲和錯(cuò)誤代碼:
執(zhí)行登錄,當(dāng)前時(shí)間 17:21:50.971
完成登錄,當(dāng)前時(shí)間 17:21:50.974
執(zhí)行 service
publish 方法既不會(huì)延遲,也不會(huì)因?yàn)楸O(jiān)聽報(bào)錯(cuò)影響執(zhí)行,異步完美解決耗時(shí)和報(bào)錯(cuò)的問(wèn)題
消息隊(duì)列
海量日志場(chǎng)景,消息隊(duì)列是一個(gè)很好的選擇,它也是解耦了發(fā)布者和訂閱者,如果訂閱者開啟了手動(dòng)確認(rèn),消費(fèi)者也需要使用 try catch 捕獲異常信息,確保消息能正常消費(fèi)。
總結(jié)
本文介紹幾種日志記錄的實(shí)現(xiàn)方案:
Spring AOP
:- 優(yōu)點(diǎn): 侵入性低,代碼可重復(fù)性強(qiáng)
- 問(wèn)題以及解決方案:
- 切面中可能會(huì)報(bào)錯(cuò),報(bào)錯(cuò)會(huì)影響業(yè)務(wù)代碼的正常執(zhí)行,解決方法是使用 try catch 捕獲異常。
- 日志記錄會(huì)影響業(yè)務(wù)代碼執(zhí)行效率,可以使用消息隊(duì)列異步執(zhí)行日志操作
事件監(jiān)聽 + 異步
:- 優(yōu)點(diǎn): 解耦業(yè)務(wù)邏輯和日志記錄,提升代碼的內(nèi)聚性。
- 缺點(diǎn)以及解決方案:
- AOP 存在的問(wèn)題,事件監(jiān)聽同樣存在,報(bào)錯(cuò)和耗時(shí)都會(huì)影響業(yè)務(wù)代碼。報(bào)錯(cuò)可以使用異常捕獲,延遲問(wèn)題可以使用異步方式解決,而異步另起線程也順便解決了報(bào)錯(cuò)影響業(yè)務(wù)代碼的問(wèn)題。
消息隊(duì)列
:- 優(yōu)點(diǎn): 使用于高并發(fā)日記記錄場(chǎng)景
- 問(wèn)題: 增加系統(tǒng)的復(fù)雜性和穩(wěn)定性,還需要考慮消息的丟失和重復(fù)消費(fèi)問(wèn)題。
到此這篇關(guān)于Java中實(shí)現(xiàn)日志記錄的方案總結(jié)的文章就介紹到這了,更多相關(guān)Java日志記錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring-boot-starter-parent的作用詳解
這篇文章主要介紹了spring-boot-starter-parent的作用詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08Java 超詳細(xì)講解數(shù)據(jù)結(jié)構(gòu)的應(yīng)用
數(shù)據(jù)結(jié)構(gòu)是計(jì)算機(jī)存儲(chǔ)、組織數(shù)據(jù)的方式。數(shù)據(jù)結(jié)構(gòu)是指相互之間存在一種或多種特定關(guān)系的數(shù)據(jù)元素的集合,讓我們一起來(lái)了解數(shù)據(jù)結(jié)構(gòu)是如何應(yīng)用的2022-04-04log4j2的高并發(fā)死鎖問(wèn)題配置優(yōu)化方式
這篇文章主要介紹了log4j2的高并發(fā)死鎖問(wèn)題配置優(yōu)化方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12springboot+vue實(shí)現(xiàn)阿里云oss上傳的示例代碼
文件上傳是常用的功能,本文主要介紹了springboot+vue實(shí)現(xiàn)阿里云oss上傳的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-06-06Java整合Redis實(shí)現(xiàn)坐標(biāo)附近查詢功能
這篇文章主要介紹了Java整合Redis實(shí)現(xiàn)坐標(biāo)附近查詢,我們可以在redis服務(wù)器使用命令 help xxx 查看指令的具體用法,本文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2023-11-11如何通過(guò)Java實(shí)現(xiàn)PDF轉(zhuǎn)高質(zhì)量圖片
在Java中,將PDF文件轉(zhuǎn)換為高質(zhì)量的圖片可以使用不同的庫(kù),其中最常用的庫(kù)之一是?Apache?PDFBox,下面我們就來(lái)看看這個(gè)庫(kù)的具體使用吧2024-10-10詳解Java的內(nèi)置異常以及創(chuàng)建自定義異常子類的方法
這篇文章主要介紹了詳解Java的內(nèi)置異常以及創(chuàng)建自定義異常子類的方法,是Java入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09Java Synchronize下的volatile關(guān)鍵字詳解
這篇文章主要介紹了Java Synchronize下的volatile關(guān)鍵字詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Java實(shí)現(xiàn)Android拼圖游戲設(shè)計(jì)過(guò)程解析
這篇文章主要介紹了Java實(shí)現(xiàn)Android拼圖游戲設(shè)計(jì)過(guò)程解析,下面文章要接受的這是一款基于 Java 開發(fā)的移動(dòng)端安卓小游戲,可以作為大家在學(xué)習(xí)期間的一個(gè)小練習(xí),接下來(lái)和小編一起進(jìn)入文章學(xué)習(xí)具體內(nèi)容吧2022-02-02win10操作系統(tǒng)下重啟電腦java環(huán)境變量失效
這篇文章主要介紹了win10操作系統(tǒng)下重啟電腦java環(huán)境變量失效,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09