1. 背景
日誌幾乎存在於所有系統中,開發除錯日誌的記錄我們有log4j,logback等來實現,但對於要展示給使用者看的日誌,我並沒有發現一個簡單通用的實現方案。所以決定為之後的開發專案提供一個通用的操作日誌元件。
2. 系統日誌和操作日誌
所有系統都會有日誌,但我們區分了 系統日誌 和 操作日誌
- 系統日誌:主要用於開發者除錯排查系統問題的,不要求固定格式和可讀性
- 操作日誌:主要面向使用者的,要求簡單易懂,反映出使用者所做的動作。
通過操作日誌可追溯到 某人在某時幹了某事情,如:
租戶 | 操作人 | 時間 | 操作 | 內容 |
---|---|---|---|---|
A租戶 | 小明 | 2022/2/27 20:15:00 | 新增 | 新增了一個使用者:Mr.Wang |
B租戶 | 大米 | 2022/2/28 10:35:00 | 更新 | 修改訂單 [xxxxxx] 價格為 xx 元 |
C租戶 | 老王 | 2022/2/28 22:55:00 | 查詢 | 查詢了名為: [xx] 的所有交易 |
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 "";
}
欄位 | 意義 | 支援SpEl表示式 | 必填 |
---|---|---|---|
tenant | 租戶,SAAS系統中區分不同租戶 | 是 | 否 |
operator | 操作者 | 是 | 否 |
module | 模組,區分不同業務模組 | 否 | 否 |
type | 操作型別,形如:增刪改查 | 否 | 否 |
bizNo | 業務編號,便於查詢 | 是 | 否 |
content | 日誌模板內容 | 是 | 是 |
fail | 操作失敗時的模板內容 | 是 | 否 |
detail | 額外的記錄資訊 | 是 | 否 |
condition | 是否記錄的條件 (預設:true 記錄) | 是 | 否 |
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,通過上下文物件結合表示式來計算最後的結果。
ExpressionParser parser =new SpelExpressionParser(); // 建立一個表示式解析器
StandardEvaluationContext ex = new StandardEvaluationContext(); // 建立上下文
ex.setVariables("name", "easylog"); // 將自定義引數新增到上下文
Expression exp = parser.parseExpression("'歡迎你! '+ #name"); //模板解析
String val = exp.getValue(ex,String.class); //獲取值
我們只需要拿到日誌註解中的動態模板即可通過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
- 設計模式