Blog.csdn.net/weixin_4395…
Springboot – Annotations – Operation log
This component addresses the question: Who did what to what when
This component currently does Autoconfig for Spring-Boot, or if you are SpringMVC, you can initialize beans in XML yourself
use
The basic use
Maven dependencies add SDK dependencies
< the dependency > < groupId > IO. Making. Mouzt < / groupId > < artifactId > bizlog - SDK < / artifactId > < version > 1.0.4 < / version > </dependency>Copy the code
Open the SpringBoot entry switch and add the @enablelogRecord annotation
Tenant is an identification that represents a tenant. Generally, you can write a Tenant for a service or multiple services in a business
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableTransactionManagement @EnableLogRecord(tenant = "com.mzt.test") public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); }}Copy the code
Log buried point
1. Common logs
- Pefix: is an identifier concatenated to bizNo as a log. Ensure that bizNo ids are not the same as those in other services. Such as order IDS, user ids, and so on
- BizNo: refers to the business ID, such as the order ID. When we query, we can query the operation logs related to bizNo
- Success: The success of the method call is recorded in the contents of the log
- SpEL expression: Enclosed in double braces (for example: {{# order.purchasename}}) # Order.purchasename is an SpEL expression. It supports everything that is supported in Spring. Such as calling static methods, ternary expressions. SpEL can use any parameter in a method
@logRecordannotation (success = "{{# order.productName}} ", order result :{{#_ret}}", prefix = LogRecordType.ORDER, BizNo = "{{#order.orderNo}}") public Boolean createOrder(order order) {log.info(" orderNo={}", order.getOrderNo()); // db insert order return true; }Copy the code
At this point, the operation log will be printed: “Zhang SAN placed an order to buy the product” super value discount braised pork set “, order result: true”
2. Logs about failure are expected. If exceptions are thrown, logs about FAIL are recorded
@logRecordannotation (fail = "Failed to create order because: "{{# _errorMsg}}", "success =" {{# order. PurchaseName}} down an order, buy goods "{{# order. ProductName}}", order results: {{# _ret}} ", prefix = LogRecordType.ORDER, BizNo = "{{#order.orderNo}}") public Boolean createOrder(order order) {log.info(" orderNo={}", order.getOrderNo()); // db insert order return true; }Copy the code
Where #_errorMsg is the errorMessage of the exception thrown by the method.
3. Log types
For example, some operation logs of an order are operated by users themselves, and some operations are modified by system operation personnel. Our system does not want to expose operation logs to users, but operation personnel expect to see user logs and operation logs of their own operations. BizNo of these operation logs are order numbers, so type fields are added for expansion, mainly to classify logs, facilitate query, and support more services.
@logRecordannotation (fail = "Failed to create order because: {{#_errorMsg}} "", category = "MANAGER", Success = "{{#order. productName}} ", order result :{{#_ret}}", prefix = logRecordType. order, BizNo = "{{#order.orderNo}}") public Boolean createOrder(order order) {log.info(" orderNo={}", order.getOrderNo()); // db insert order return true; }Copy the code
4. Record operation details or additional information
If an operation changes many fields, but the success log template is too long to display all the details of the changes, you need to save the details to the detail field. The detail field is a String and needs to be serialized by itself. Here # order.tostring () is the toString() method that calls order. To save JSON, rewrite the Order toString() method yourself.
@logRecordannotation (fail = "Failed to create order because: "{{# _errorMsg}}", "the category =" MANAGER_VIEW ", the detail = "{{# order. The toString ()}}", Success = "{{#order. productName}} ", order result :{{#_ret}}", prefix = logRecordType. order, BizNo = "{{#order.orderNo}}") public Boolean createOrder(order order) {log.info(" orderNo={}", order.getOrderNo()); // db insert order return true; }Copy the code
5. How to specify the operator of operation logs? The framework provides two approaches
- The first: manually specify it on a LogRecord annotation. This requires an operator on the method argument
@logRecordannotation (fail = "Failed to create order because: "{{# _errorMsg}}", "the category =" MANAGER_VIEW ", the detail = "{{# order. The toString ()}}," operator = "{{# currentUser}}", Success = "{{#order. productName}} ", order result :{{#_ret}}", prefix = logRecordType. order, bizNo = "{{#order.orderNo}}") public boolean createOrder(Order order, String currentUser) {log.info(" orderNo={}", order.getorderno ()); // db insert order return true; }Copy the code
This method is specified manually, either with an operator parameter on the method argument or by calling a static method through SpEL to get the current user.
- The second: Automatically by the default implementation class to the retrieval of the people, because the current user in most web applications are stored in a thread context, so each note is added an operator retrieval people seem to be some duplication of effort, so provides an extension to the retrieval framework provides an extended interface, Businesses that use frameworks can implement the interface’s own implementation of retrieving the current user’s logic, while those that use Springboot just implement the IOperatorGetService interface. This Service is then placed in the context of Spring as a singleton. Those using Spring Mvc need to assemble these beans by hand.
@Configuration public class LogRecordConfiguration { @Bean public IOperatorGetService operatorGetService() { return () -> Optional.of(OrgUserUtils.getCurrentUser()) .map(a -> new OperatorDO(a.getMisId())) .orElseThrow(() -> new IllegalArgumentException("user is null")); }} // @Service public class DefaultOperatorGetServiceImpl implements IOperatorGetService { @Override public OperatorDO getUser() { OperatorDO operatorDO = new OperatorDO(); operatorDO.setOperatorId("SYSTEM"); return operatorDO; }}Copy the code
6. Adjust log copy
The order {{#orderId}} is updated, and the update content is… , which is difficult to understand for operations or products, so the ability to customize functions is introduced. This is done by adding a function name between the braces of the original variable such as “{ORDER{#orderId}}” where ORDER is a function name. A function name is not enough; you need to add the definition and implementation of the function. A custom function needs to implement the IParseFunction interface in the framework. It needs to implement two methods:
-
The functionName() method returns the functionName above the annotation;
-
The apply() function takes the value of the #orderId parsed by SpEL in “{ORDER{#orderId}}”, which is a number 1223110, and then converts the ID into a readable string in the implementation class. Generally, the name and ID are displayed to facilitate troubleshooting, for example, in the form of “order name (ID)”.
Here’s the problem: how can the framework call a custom function when it’s added? A: For Spring Boot applications, it’s easy. Just expose it in the context of Spring. You can add @Component or @service to Spring for convenience 😄. Spring MVC applications need to assemble beans themselves.
@logRecordannotation (success = "updated order {{#orderId}}, updated to...." , prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}", detail = "{{#order.toString()}}") public boolean update(Long orderId, Order order) { return false; {{#orderId}}} functionName@logRecordannotation (success = "Update ORDER{#orderId}} with the update content..." , prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}", detail = "{{#order.toString()}}") public boolean update(Long orderId, Order order) { return false; @component public class OrderParseFunction implements IParseFunction {@resource @lazy // To avoid class loading order OrderQueryService OrderQueryService OrderQueryService @override public String functionName() {return "ORDER"; } @override // the value of the Order object can be passed, Public String apply(String value) {if(stringutils.isempty (value)){return value; } Order order = orderQueryService.queryOrder(Long.parseLong(value)); Return order.getproductName ().concat("(").concat(value).concat(")"); return order.getproductName ().concat(value). }}Copy the code
7. Log text adjustment using SpEL ternary expression
@LogRecordAnnotation(prefix = LogRecordTypeConstant.CUSTOM_ATTRIBUTE, bizNo = "{{#businessLineId}}", success = "{{#disable ? 'deactivate' : ATTRIBUTE{#attributeId}}") public CustomAttributeVO disableAttribute(Long businessLineId, Long attributeId, boolean disable) { return xxx; }Copy the code
8. Log copy adjusts the use of variables other than method parameters in the template
Can be passed in method LogRecordContext. PutVariable (variableName, Object) method to add variables, the first Object as a variable name, behind for the variable Object, and then we can use SpEL use this variable, such as: The {{# innerorder.productName}} in this example is a variable set in the method
@Override @LogRecordAnnotation( success = "{{# order.productName}} ", test variable "{{# innerOrder.productName}} ", order result :{{# _RET}}", prefix = LogRecordType.ORDER, BizNo = "{{#order.orderNo}}") public Boolean createOrder(order order) {log.info(" orderNo={}", order.getOrderNo()); // db insert order Order order1 = new Order(); Order1. setProductName(" Internal variable test "); LogRecordContext.putVariable("innerOrder", order1); return true; }Copy the code
9. Use the LogRecordContext variable in the function
Using LogRecordContext. PutVariable (variableName, Object) to add variables in addition to being able to use on the annotation SpEL expression, also can use in a custom function, this way is more complex, the following example indicated the change of the list, For example, change from [A,B,C] to [B,D], then log: “A was deleted,D was added”
@ LogRecord (success = "{DIFF_LIST {' document address '}}", bizNo = "{{# id}}", prefix = REQUIREMENT) public void updateRequirementDocLink(String currentMisId, Long id, List<String> docLinks) { RequirementDO requirementDO = getRequirementDOById(id); LogRecordContext.putVariable("oldList", requirementDO.getDocLinks()); LogRecordContext.putVariable("newList", docLinks); requirementModule.updateById("docLinks", RequirementUpdateDO.builder() .id(id) .docLinks(docLinks) .updater(currentMisId) .updateTime(new Date()) .build()); } @Component public class DiffListParseFunction implements IParseFunction { @Override public String functionName() { return "DIFF_LIST"; } @SuppressWarnings("unchecked") @Override public String apply(String value) { if (StringUtils.isBlank(value)) { return value; } List<String> oldList = (List<String>) LogRecordContext.getVariable("oldList"); List<String> newList = (List<String>) LogRecordContext.getVariable("newList"); oldList = oldList == null ? Lists.newArrayList() : oldList; newList = newList == null ? Lists.newArrayList() : newList; Set<String> deletedSets = Sets.difference(Sets.newHashSet(oldList), Sets.newHashSet(newList)); Set<String> addSets = Sets.difference(Sets.newHashSet(newList), Sets.newHashSet(oldList)); StringBuilder stringBuilder = new StringBuilder(); If (CollectionUtils isNotEmpty (addSets)) {stringBuilder. Append (" new < b > "). Append (value). Append (" < / b > : "); For (String item: addSets) {stringBuilder.appEnd (item).append(", "); }} the if (CollectionUtils isNotEmpty (deletedSets)) {stringBuilder. Append (" delete < b > "). Append (value). Append (" < / b > : "); For (String item: deletedSets) {stringBuilder.appEnd (item).append(", "); } } return StringUtils.isBlank(stringBuilder) ? null : stringBuilder.substring(0, stringBuilder.length() - 1); }}Copy the code
Extension points for the framework
- Override OperatorGetServiceImpl to get the user’s extension from the context, as shown in the following example
@Service
public class DefaultOperatorGetServiceImpl implements IOperatorGetService {
@Override
public Operator getUser() {
return Optional.ofNullable(UserUtils.getUser())
.map(a -> new Operator(a.getName(), a.getLogin()))
.orElseThrow(()->new IllegalArgumentException("user is null"));
}
}
Copy the code
- ILogRecordService is an example of saving/querying logs to a suitable storage medium, such as a database or ES, depending on the amount of data. Save and delete by yourself
You can also implement only the query interface. After all, it has been stored in the service storage. The query service can be implemented by itself without using the ILogRecordService interface, because the product manager will ask for some strange query requirements.
@Service public class DbLogRecordServiceImpl implements ILogRecordService { @Resource private LogRecordMapper logRecordMapper; @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void record(LogRecord logRecord) { Log.info (" [logRecord] log={}", logRecord); LogRecordPO logRecordPO = LogRecordPO.toPo(logRecord); logRecordMapper.insert(logRecordPO); } @Override public List<LogRecord> queryLog(String bizKey, Collection<String> types) { return Lists.newArrayList(); } @Override public PageDO<LogRecord> queryLogByBizNo(String bizNo, Collection<String> types, PageRequestDO pageRequestDO) { return logRecordMapper.selectByBizNoAndCategory(bizNo, types, pageRequestDO); }}Copy the code
- IParseFunction custom conversion function interface, can implement IParseFunction implementation used in the LogRecord annotation function extension example:
@Component public class UserParseFunction implements IParseFunction { private final Splitter splitter = Splitter.on(",").trimResults(); @Resource @Lazy private UserQueryService userQueryService; @Override public String functionName() { return "USER"; } @override // 11,12 returns 11, 12(三 三) public String apply(String value) {if (stringutils.isEmpty (value)) {return value; } List<String> userIds = Lists.newArrayList(splitter.split(value)); List<User> misDOList = userQueryService.getUserList(userIds); Map<String, User> userMap = StreamUtil.extractMap(misDOList, User::getId); StringBuilder stringBuilder = new StringBuilder(); for (String userId : userIds) { stringBuilder.append(userId); if (userMap.get(userId) ! = null) { stringBuilder.append("(").append(userMap.get(userId).getUsername()).append(")"); } stringBuilder.append(","); } return stringBuilder.toString().replaceAll(",$", ""); }}Copy the code
Variables related to
The LogRecordAnnotation notation can use a variable with an argument or use the return value #_ret variable with an error message #_errorMsg. It can also call static methods using SpEL T
Change Log & TODO
Note:
⚠️ Global logging interception is recorded after the method is executed, so the SpEL in the LogRecordAnnotation annotation takes the value of the variable after modifying the method arguments inside the method
The source code
Github.com/mouzt/mzt-b…
Recommend 3 original Springboot +Vue projects, with complete video explanation and documentation and source code:
Build a complete project from Springboot+ ElasticSearch + Canal
- Video tutorial: www.bilibili.com/video/BV1Jq…
- A complete development documents: www.zhuawaba.com/post/124
- Online demos: www.zhuawaba.com/dailyhub
【VueAdmin】 hand to hand teach you to develop SpringBoot+Jwt+Vue back-end separation management system
- Full 800 – minute video tutorial: www.bilibili.com/video/BV1af…
- Complete development document front end: www.zhuawaba.com/post/18
- Full development documentation backend: www.zhuawaba.com/post/19
【VueBlog】 Based on SpringBoot+Vue development of the front and back end separation blog project complete teaching
- Full 200 – minute video tutorial: www.bilibili.com/video/BV1af…
- Full development documentation: www.zhuawaba.com/post/17
If you have any questions, please come to my official account [Java Q&A Society] and ask me