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.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
How to Implement Fine‑Grained Operation Logging in Spring Boot with BizLog SDK

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

Change Log Diagram
Change Log Diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaaoploggingSpring BootannotationBizLog
Java High-Performance Architecture
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.