开源:如何优雅的实现一个操作日志组件
1. 背景
日志幾乎存在于所有系統中,開發調試日志的記錄我們有log4j,logback等來實現,但對于要展示給用戶看的日志,我并沒有發現一個簡單通用的實現方案。所以決定為之后的開發項目提供一個通用的操作日志組件。
?
2. 系統日志和操作日志
所有系統都會有日志,但我們區分了 系統日志 和 操作日志
系統日志:主要用于開發者調試排查系統問題的,不要求固定格式和可讀性
操作日志:主要面向用戶的,要求簡單易懂,反映出用戶所做的動作。
通過操作日志可追溯到 某人在某時干了某事情,如:
?
3. 需要哪些功能
3.1 訴求:
基于SpringBoot能夠快速接入
對業務代碼具有低入侵性
3.2 解決思路:
基于以上兩點,我們想想如何實現。
spingboot快速接入,需要我們來自定義spring boot starter;
業務入侵性低,首先想到了AOP,一般操作日志都是在增刪改查的方法中,所以我們可以使用注解在這些方法上,通過AOP攔截這些方法。
3.3 待實現:
因此,我們需要實現以下功能:
自定義spring boot starter
定義日志注解
AOP攔截日志注解方法
定義日志動態內容模板
模板中又需要實現:
動態模板表達式解析:用強大的SpEL來解析表達式
自定義函數:支持目標方法前置/后置的自定義函數
3.4 展現
所以我們最終期望的大概是這樣:
@EasyLog(module?=?"用戶模塊",?type?=?"新增",content?=?"測試?{functionName{#userDto.name}}",condition?=?"#userDto.name?==?'easylog'") public?String?test(UserDto?userDto)?{return?"test"; }?
4. 實現步驟
4.1 定義日志注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public?@interface?EasyLog?{String?tenant()?default?"";String?operator()?default?"";String?module()?default?"";String?type()?default?"";String?bizNo()?default?"";String?content();String?fail()?default?"";String?detail()?default?"";String?condition()?default?""; }4.2 自定義函數
這里的自定義函數,并不是指SpEL中的自定義函數,因為SpEL中的自定義函數必須是靜態方法才可以注冊到其中,因為靜態方法使用中并沒有我們自己定義方法來的方便,所以這里的自定義函數僅僅指代我們定義的一個普通方法。
public?interface?ICustomFunction?{/***?目標方法執行前?執行自定義函數*?@return?是否是前置函數*/boolean?executeBefore();/***?自定義函數名*?@return?自定義函數名*/String?functionName();/***?自定義函數*?@param?param?參數*?@return?執行結果*/String?apply(String?param); }我們定義好自定義函數接口,實現交給使用者。使用者將實現類交給Spring容器管理,我們解析的時候從Spring容器中獲取即可。
4.3 SpEL表達式解析
主要牽涉下面幾個核心類:
解析器ExpressionParser,用于將字符串表達式轉換為Expression表達式對象。
表達式Expression,最后通過它的getValute方法對表達式進行計算取值。
上下文EvaluationContext,通過上下文對象結合表達式來計算最后的結果。
我們只需要拿到日志注解中的動態模板即可通過SpEL來解析。
4.4 自定義函數的解析
我們采用 { functionName { param }} 的形式在模板中展示自定義函數,解析整個模板前,我們先來解析下自定義函數,將解析后的值替換掉模板中的字符串即可。
if?(template.contains("{"))?{Matcher?matcher?=?PATTERN.matcher(template);while?(matcher.find())?{String?funcName?=?matcher.group(1);String?param?=?matcher.group(2);if?(customFunctionService.executeBefore(funcName))?{String?apply?=?customFunctionService.apply(funcName,?param);}} }4.5 獲取操作者信息
一般我們都是將登錄者信息存入應用上下文中,所以我們不必每次都在日志注解中指出,我們可統一設置,定義一個獲取操作者接口,由使用者實現。
public?interface?IOperatorService?{//?獲取當前操作者String?getOperator();//?當前租戶String?getTenant(); }4.6 定義日志內容接收
我們要將解析完成后的日志內容實體信息發送給我們的使用者,所以我們需要定義一個日志接收的接口,具體的實現交給使用者來實現,無論他接收到日志存儲在數據庫,MQ還是哪里,讓使用者來決定。
public?interface?ILogRecordService?{/***?保存?log*?@param?easyLogInfo?日志實體*/void?record(EasyLogInfo?easyLogInfo); }4.7 定義AOP攔截
@Aspect @Component @AllArgsConstructor public?class?EasyLogAspect?{@Pointcut("@annotation(**.EasyLog)")public?void?pointCut()?{}//?環繞通知@Around("pointCut()?&&?@annotation(easyLog)")public?Object?around(ProceedingJoinPoint?joinPoint,?EasyLog?easyLog)?throws?Throwable?{//前置自定義函數解析try?{result?=?joinPoint.proceed();}?catch?(Throwable?e)?{}//SpEL解析//后置自定義函數解析return?result;} }4.8 自定義 spring boot starter
創建自動配置類,將定義的一些來交給Spring容器管理:
@Configuration @ComponentScan("**") public?class?EasyLogAutoConfiguration?{@Bean@ConditionalOnMissingBean(ICustomFunction.class)@Role(BeanDefinition.ROLE_APPLICATION)public?ICustomFunction?customFunction(){return?new?DefaultCustomFunction();}@Bean@ConditionalOnMissingBean(IOperatorService.class)@Role(BeanDefinition.ROLE_APPLICATION)public?IOperatorService?operatorGetService()?{return?new?DefaultOperatorServiceImpl();}@Bean@ConditionalOnMissingBean(ILogRecordService.class)@Role(BeanDefinition.ROLE_APPLICATION)public?ILogRecordService?recordService()?{return?new?DefaultLogRecordServiceImpl();} }上一篇我已經完整的介紹了如何自定義 spring boot starter ,可去參考:
開源:如何自定義spring boot starter
?
5. 我們可以學到什么?
你可以拉取easy-log源碼,用于學習,通過easy-log你可以學到:
注解的定義及使用
AOP的應用
SpEL表達式的解析
自定義 Spring boot starter
設計模式
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的开源:如何优雅的实现一个操作日志组件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vrp 节约算法 c++_数据结构和算法
- 下一篇: pythonmail添加附件_Pytho