Elegant Operation Log Recording in Backend Systems Using AOP and Annotations

The article demonstrates how to implement elegant, business‑logic‑independent operation logging in Java back‑ends by using AOP‑driven annotations, SpEL dynamic templates, custom parse functions, thread‑local context, and a Spring Boot starter, offering flexible persistence and automatic operator retrieval.

Meituan Technology Team
Meituan Technology Team
Meituan Technology Team
Elegant Operation Log Recording in Backend Systems Using AOP and Annotations

Operation logs exist in almost every system and must be simple, readable, and independent of business logic. This article explains how to record operation logs elegantly in Java backend applications.

1. Usage Scenarios

Operation logs differ from system logs: they are user‑facing records of actions such as order creation, status changes, or customer service handling. Typical formats include plain text, dynamic text with variables, and change‑type records that show before/after values.

2. Implementation Methods

2.1 Using Canal to Listen to Database Binlog

Canal parses MySQL binlog to capture data changes, allowing operation logs to be generated without coupling to business code. The drawback is that only database changes can be captured; RPC‑based actions must be logged manually.

2.2 Logging via Log Files

log.info("订单创建");
log.info("订单已经创建,订单编号:{}", orderNo);
log.info("修改了订单的配送地址:从“{}”修改到“{}”,", oldAddress, newAddress);

Key issues include recording the operator (using SLF4J MDC), separating operation logs from system logs (different appender), and generating readable log messages.

@Component
public class UserInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String userNo = getUserNo(request);
        MDC.put("userId", userNo);
        return super.preHandle(request, response, handler);
    }
    private String getUserNo(HttpServletRequest request) { return null; }
}
<pattern>"%d{yyyy-MM-dd HH:mm:ss.SSS} %t %-5level %X{userId} %logger{30}.%method:%L - %msg%n"</pattern>

2.3 Using LogUtil

LogUtil.log(orderNo, "订单创建", "小明");
LogUtil.log(orderNo, "订单创建,订单号" + "NO.11089999", "小明");
String template = "用户%s修改了订单的配送地址:从“%s”修改到“%s";
LogUtil.log(orderNo, String.format(template, "小明", "金灿灿小区", "银盏盏小区"), "小明");

2.4 Method Annotation with AOP

@LogRecord(content = "修改了配送地址")
public void modifyAddress(UpdateDeliveryRequest request) {
    doUpdate(request);
}

Static annotation text is insufficient for dynamic data; therefore, SpEL and custom functions are introduced to build dynamic templates.

3. AOP‑Driven Dynamic Templates

SpEL expressions (e.g., #request.address) and custom functions (e.g., {deliveryUser{#oldDeliveryUserId}}) allow placeholders to be resolved at runtime. The operator can be automatically fetched from a user context when not supplied in the annotation.

@LogRecord(content = "修改了订单的配送地址:从“#oldAddress”, 修改到“#request.address"", bizNo = "#request.deliveryOrderNo")
public void modifyAddress(UpdateDeliveryRequest request) {
    LogRecordContext.putVariable("oldAddress", DeliveryService.queryOldAddress(request.getDeliveryOrderNo()));
    doUpdate(request);
}

4. Core Implementation Details

4.1 Code Structure

The component consists of four modules: AOP interceptor, log parsing, log persistence, and a Spring Boot starter. Extension points include custom functions, default operator retrieval, business persistence, and query logic.

4.2 Parsing Logic

SpEL is used via LogRecordExpressionEvaluator, which caches expressions for performance. An EvaluationContext combines method parameters, variables from LogRecordContext, the method return value, and error messages.

EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(method, args, targetClass, ret, errorMsg, beanFactory);

4.3 LogRecordContext

A thread‑local stack (

InheritableThreadLocal<Stack<Map<String, Object>>>

) stores variables per method invocation, preventing variable leakage when nested annotated methods are executed.

public class LogRecordContext {
    private static final InheritableThreadLocal<Stack<Map<String, Object>>> variableMapStack = new InheritableThreadLocal<>();
    // putVariable, getVariables, clear, etc.
}

4.4 Default Operator Retrieval

If the operator attribute is empty, the framework calls IOperatorGetService.getUser() (e.g., from UserContext.getCurrentUser()) to obtain the current user.

public class DefaultOperatorGetServiceImpl implements IOperatorGetService {
    @Override
    public Operator getUser() {
        return Optional.ofNullable(UserUtils.getUser())
            .map(u -> new Operator(u.getName(), u.getLogin()))
            .orElseThrow(() -> new IllegalArgumentException("user is null"));
    }
}

4.5 Custom Functions

Implement IParseFunction to provide additional processing (e.g., converting an ID to a name). Functions can be marked as executeBefore() to run prior to business logic.

public interface IParseFunction {
    default boolean executeBefore() { return false; }
    String functionName();
    String apply(String value);
}

4.6 Persistence

The ILogRecordService interface abstracts log storage. Implementations may write to files, databases, Elasticsearch, etc.

public class DefaultLogRecordServiceImpl implements ILogRecordService {
    @Override
    public void record(LogRecord logRecord) {
        log.info("【logRecord】log={}", logRecord);
    }
}

4.7 Spring Boot Starter

Adding @EnableLogRecord(tenant = "com.mzt.test") to the application bootstraps all components. The starter configures beans such as LogRecordOperationSource, LogRecordInterceptor, custom function factories, and default operator/log services.

@SpringBootApplication
@EnableLogRecord(tenant = "com.mzt.test")
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

5. Summary

The article presents a comprehensive solution for operation‑log recording that separates logging concerns from business logic, supports dynamic templates via SpEL and custom functions, and provides a pluggable Spring Boot starter for easy integration.

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.

BackendJavaaopspringloggingannotationOperation Log
Meituan Technology Team
Written by

Meituan Technology Team

Over 10,000 engineers powering China’s leading lifestyle services e‑commerce platform. Supporting hundreds of millions of consumers, millions of merchants across 2,000+ industries. This is the public channel for the tech teams behind Meituan, Dianping, Meituan Waimai, Meituan Select, and related services.

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.