How to Implement Fine‑Grained Operation Logging in Spring Boot with BizLog SDK
This article explains how the BizLog component records who performed which action at what time in Spring Boot applications, covering Maven setup, annotation usage, custom operators, detail fields, category segregation, SpEL expressions, custom parse functions, and extension points for persistence and operator retrieval.
Usage
Basic Usage
Maven Dependency
<dependency>
<groupId>io.github.mouzt</groupId>
<artifactId>bizlog-sdk</artifactId>
<version>1.0.4</version>
</dependency>SpringBoot Entry, Add @EnableLogRecord
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableTransactionManagement
@EnableLogRecord(tenant = "com.mzt.test")
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}Log Recording
1. Normal Log
prefix: a marker placed before the bizNo to avoid ID collisions.
bizNo: the business identifier, e.g., order ID.
success: content recorded when the method returns successfully.
SpEL expression: values inside double braces (e.g., {{#order.purchaseName}}) are evaluated by Spring.
@LogRecordAnnotation(
success = "{{#order.purchaseName}} placed an order, purchased product \"{{#order.productName}}\", result:{{#_ret}}",
prefix = LogRecordType.ORDER,
bizNo = "{{#order.orderNo}}"
)
public boolean createOrder(Order order) {
log.info("【创建订单】orderNo={}", order.getOrderNo());
// db insert order
return true;
}2. Fail Log
@LogRecordAnnotation(
fail = "Create order failed, reason: \"{{#_errorMsg}}\"",
success = "{{#order.purchaseName}} placed an order, purchased product \"{{#order.productName}}\", result:{{#_ret}}",
prefix = LogRecordType.ORDER,
bizNo = "{{#order.orderNo}}"
)
public boolean createOrder(Order order) {
log.info("【创建订单】orderNo={}", order.getOrderNo());
// db insert order
return true;
}3. Log Categories
Use the category attribute to separate logs, for example, user logs vs. manager logs.
@LogRecordAnnotation(
fail = "Create order failed, reason: \"{{#_errorMsg}}\"",
category = "MANAGER",
success = "{{#order.purchaseName}} placed an order, purchased product \"{{#order.productName}}\", result:{{#_ret}}",
prefix = LogRecordType.ORDER,
bizNo = "{{#order.orderNo}}"
)
public boolean createOrder(Order order) { /* ... */ }4. Detail Field
If the success template is too short to contain all changed fields, store the full details in the detail field (as a JSON string or toString() output).
@LogRecordAnnotation(
fail = "Create order failed, reason: \"{{#_errorMsg}}\"",
category = "MANAGER_VIEW",
detail = "{{#order.toString()}}",
success = "{{#order.purchaseName}} placed an order, purchased product \"{{#order.productName}}\", result:{{#_ret}}",
prefix = LogRecordType.ORDER,
bizNo = "{{#order.orderNo}}"
)
public boolean createOrder(Order order) { /* ... */ }5. Operator
Manually specify the operator in the annotation (requires an operator method parameter).
@LogRecordAnnotation(
fail = "Create order failed, reason: \"{{#_errorMsg}}\"",
category = "MANAGER_VIEW",
detail = "{{#order.toString()}}",
operator = "{{#currentUser}}",
success = "{{#order.purchaseName}} placed an order, purchased product \"{{#order.productName}}\", result:{{#_ret}}",
prefix = LogRecordType.ORDER,
bizNo = "{{#order.orderNo}}"
)
public boolean createOrder(Order order, String currentUser) { /* ... */ }Alternatively, implement IOperatorGetService to obtain the operator automatically from the thread context.
@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"));
}
}6. Custom Parse Functions
Introduce functions like ORDER{#orderId}} to convert IDs into readable strings. Implement IParseFunction.
@Component
public class OrderParseFunction implements IParseFunction {
@Resource @Lazy private OrderQueryService orderQueryService;
@Override
public String functionName() { return "ORDER"; }
@Override
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(")");
}
}7. Ternary Expression in Log
@LogRecordAnnotation(
prefix = LogRecordTypeConstant.CUSTOM_ATTRIBUTE,
bizNo = "{{#businessLineId}}",
success = "{{#disable ? 'Disable' : 'Enable'}} custom attribute {ATTRIBUTE{#attributeId}}"
)
public CustomAttributeVO disableAttribute(Long businessLineId, Long attributeId, boolean disable) {
return xxx;
}8. Using Variables Outside Method Parameters
Put a variable into LogRecordContext and reference it in the SpEL expression.
@LogRecordAnnotation(
success = "{{#order.purchaseName}} placed an order, product \"{{#order.productName}}\", test variable \"{{#innerOrder.productName}}\", result:{{#_ret}}",
prefix = LogRecordType.ORDER,
bizNo = "{{#order.orderNo}}"
)
public boolean createOrder(Order order) {
log.info("【创建订单】orderNo={}", order.getOrderNo());
Order order1 = new Order();
order1.setProductName("inner variable test");
LogRecordContext.putVariable("innerOrder", order1);
return true;
}9. Diff List Parse Function
Record added and removed items by comparing old and new lists stored in LogRecordContext.
@LogRecord(
success = "{DIFF_LIST{'Document link'}}",
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);
// update logic ...
} @Component
public class DiffListParseFunction implements IParseFunction {
@Override
public String functionName() { return "DIFF_LIST"; }
@Override
public String apply(String 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> deleted = Sets.difference(Sets.newHashSet(oldList), Sets.newHashSet(newList));
Set<String> added = Sets.difference(Sets.newHashSet(newList), Sets.newHashSet(oldList));
StringBuilder sb = new StringBuilder();
if (!added.isEmpty()) {
sb.append("Added <b>").append(value).append("</b>:");
for (String item : added) sb.append(item).append(",");
}
if (!deleted.isEmpty()) {
sb.append("Deleted <b>").append(value).append("</b>:");
for (String item : deleted) sb.append(item).append(",");
}
return StringUtils.isBlank(sb) ? null : sb.substring(0, sb.length() - 1);
}
}Extension Points
Override OperatorGetServiceImpl to customize operator retrieval.
Implement ILogRecordService to persist logs (e.g., database, Elasticsearch).
Implement IParseFunction for custom placeholder conversion.
@Service
public class DbLogRecordServiceImpl implements ILogRecordService {
@Resource private LogRecordMapper logRecordMapper;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void record(LogRecord logRecord) {
LogRecordPO po = LogRecordPO.toPo(logRecord);
logRecordMapper.insert(po);
}
@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);
}
} @Component
public class UserParseFunction implements IParseFunction {
@Resource @Lazy private UserQueryService userQueryService;
@Override
public String functionName() { return "USER"; }
@Override
public String apply(String value) {
if (StringUtils.isEmpty(value)) return value;
List<String> userIds = Lists.newArrayList(Splitter.on(",").trimResults().split(value));
List<User> users = userQueryService.getUserList(userIds);
Map<String, User> userMap = StreamUtil.extractMap(users, User::getId);
StringBuilder sb = new StringBuilder();
for (String id : userIds) {
sb.append(id);
if (userMap.get(id) != null) {
sb.append("(").append(userMap.get(id).getUsername()).append(")");
}
sb.append(",");
}
return sb.toString().replaceAll(",$", "");
}
}Note: the logging interceptor runs after method execution, so SpEL expressions see the final (possibly modified) parameter values.
Source Code
https://github.com/mouzt/mzt-biz-log
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
