1. Background
Logs exist in almost all systems. We have log4j, logback, etc. to record the development and debug logs. However, for the logs to be displayed to users, I have not found a simple and general implementation scheme. So it was decided to provide a general operation log component for subsequent development projects.
2. System log and operation log
All systems will have logs, but we distinguish between system logs and operation logs
- System log: mainly used for developers to debug and troubleshoot system problems, does not require fixed format and readability
- Operation log: mainly user-oriented, the requirements are simple and easy to understand, reflecting the actions made by the user.
Action logs can be traced back to someone doing something at a certain time, such as:
tenant | Operator | time | operate | content |
---|---|---|---|---|
A tenant | Xiao Ming | 2022/2/27 20:15:00 | new | Added a new user: Mr.Wang |
Tenant B | rice | 2022/2/28 10:35:00 | renew | Modify the order [xxxxxx] The price is xx yuan |
tenant C | Lao Wang | 2022/2/28 22:55:00 | Inquire | Searched all transactions named: [xx] |
3. What features are required
3.1 Claims:
- Fast access based on SpringBoot
- Low invasiveness to business code
3.2 Solutions:
Based on the above two points, we think about how to achieve it.
Spring boot fast access, we need to customize the spring boot starter;
The business is less intrusive. First of all, AOP comes to mind. Generally, operation logs are in the methods of adding, deleting, modifying and checking, so we can use annotations on these methods to intercept these methods through AOP.
3.3 To be implemented:
Therefore, we need to implement the following functions:
- Custom spring boot starter
- Define log annotations
- AOP interception log annotation method
- Define a log dynamic content template
The template also needs to implement:
- Dynamic Template Expression Parsing: Parsing Expressions with the Power of SpEL
- Custom functions: custom functions that support the pre/post of the target method
3.4 Presentation
So what we end up expecting is something like this:
@EasyLog(module = "用户模块", type = "新增",
content = "测试 {functionName{#userDto.name}}",
condition = "#userDto.name == 'easylog'")
public String test(UserDto userDto) {
return "test";
}
4. Implementation steps
4.1 Defining log annotations
@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 "";
}
field | significance | Support for SpEl expressions | Required |
---|---|---|---|
tenant | Tenants, distinguish different tenants in SAAS system | Yes | no |
operator | operator | Yes | no |
module | Module, distinguish different business modules | no | no |
type | Operation type, such as: CRUD | no | no |
bizNo | Business number for easy query | Yes | no |
content | log template content | Yes | Yes |
fail | Template content when the operation fails | Yes | no |
detail | Additional logging information | Yes | no |
condition | Whether to record the condition (default: true record) | Yes | no |
4.2 Custom Functions
The custom function here does not refer to the custom function in SpEL, because the custom function in SpEL must be a static method before it can be registered in it, because the use of static methods does not have the convenience of defining our own methods, so here The custom function just refers to a normal method we define.
public interface ICustomFunction {
/**
* 目标方法执行前 执行自定义函数
* @return 是否是前置函数
*/
boolean executeBefore();
/**
* 自定义函数名
* @return 自定义函数名
*/
String functionName();
/**
* 自定义函数
* @param param 参数
* @return 执行结果
*/
String apply(String param);
}
We define the custom function interface and hand it over to the user. The user will hand over the implementation class to the Spring container for management, and we can get it from the Spring container when parsing.
4.3 SpEL expression parsing
It mainly involves the following core classes:
- Parser ExpressionParser, used to convert string expressions to Expression expression objects.
- The expression Expression, and finally evaluates the expression through its getValue method.
- The context EvaluationContext calculates the final result by combining the expression with the context object.
ExpressionParser parser =new SpelExpressionParser(); // 创建一个表达式解析器
StandardEvaluationContext ex = new StandardEvaluationContext(); // 创建上下文
ex.setVariables("name", "easylog"); // 将自定义参数添加到上下文
Expression exp = parser.parseExpression("'欢迎你! '+ #name"); //模板解析
String val = exp.getValue(ex,String.class); //获取值
We only need to get the dynamic template in the log annotation to parse it through SpEL.
4.4 Analysis of custom functions
We use the form of { functionName { param }}
to display the custom function in the template. Before parsing the entire template, let's parse the custom function and replace the parsed value with the string in the template.
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 Get operator information
Generally, we store the login information in the application context, so we don't have to point it out in the log annotation every time. We can set it uniformly and define an access operator interface, which is implemented by the user.
public interface IOperatorService {
// 获取当前操作者
String getOperator();
// 当前租户
String getTenant();
}
4.6 Define log content reception
We want to send the log content entity information after parsing to our users, so we need to define an interface for log reception, and the specific implementation is handed over to the user to implement, no matter if the logs he receives are stored in the database, MQ or wherever , let the user decide.
public interface ILogRecordService {
/**
* 保存 log
* @param easyLogInfo 日志实体
*/
void record(EasyLogInfo easyLogInfo);
}
4.7 Defining AOP Interception
@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 Customize spring boot starter
Create an auto-configuration class and pass some of the definitions to the Spring container for management:
@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();
}
}
In the last article, I have completely introduced how to customize the spring boot starter, you can refer to:
How to customize spring boot starter ?
5. What can we learn?
You can pull the easy-log source code for learning. Through easy-log, you can learn:
- Definition and use of annotations
- Application of AOP
- Parsing of SpEL expressions
- Custom Spring boot starter
- Design Patterns
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。