How to Elegantly Implement Decoupled Operation Logging with AOP in Spring
This article explains the difference between system and operation logs, presents common log formats, and provides step‑by‑step implementations—including Canal binlog listening, log‑file recording, LogUtil helpers, and a full AOP‑based @LogRecord solution—while detailing template parsing, context handling, custom functions, and persistence in a Spring Boot environment.
What is an operation log and how it differs from a system log
System logs are mainly for developers to troubleshoot issues and often contain low‑level details such as class names and line numbers. Operation logs, on the other hand, record user‑visible events like order creation or status changes and must be highly readable.
Typical operation‑log formats
Simple text, e.g., "2021‑09‑16 10:00 Order created".
Dynamic text with variables, e.g., "2021‑09‑16 10:00 Order created, orderNo: NO.11089999".
Change‑type text showing before/after values, e.g., "User XiaoMing changed delivery address from \"Gold Community\" to \"Silver Community\"".
Form‑style updates that modify multiple fields at once.
Implementation methods
Canal binlog listener Canal parses MySQL binlog entries, allowing the system to capture every data change without touching business code. This cleanly separates logging from business logic but only works for database‑driven changes; RPC‑based actions still need manual logging.
Log‑file recording Directly write log statements using SLF4J/Logback. Three key problems must be solved:
Who performed the operation Put the user identifier into MDC in an interceptor and reference it in the log pattern:
@Component
public class UserInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String userNo = getUserNo(request);
MDC.put("userId", userNo);
return super.preHandle(request, response, handler);
}
private String getUserNo(HttpServletRequest request) { return null; }
}Logback pattern example:
%d{yyyy-MM-dd HH:mm:ss.SSS} %t %-5level %X{userId} %logger{30}.%method:%L - %msg%nSeparate operation logs from system logs Define a dedicated appender and logger for business logs:
<appender name="businessLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>logs/business.log</File>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/business.%d.%i.log</fileNamePattern>
<maxHistory>90</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %t %-5level %X{userId} %logger{30}.%method:%L - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name="businessLog" additivity="false" level="INFO">
<appender-ref ref="businessLogAppender"/>
</logger>Business code then uses the dedicated logger:
private final Logger businessLog = LoggerFactory.getLogger("businessLog");
businessLog.info("Modified delivery address");Generating readable log messages Wrap log creation in a utility (LogUtil) or use AOP to build templates automatically.
AOP‑based @LogRecord Define an annotation to describe the log content, operator, and business identifier. Example:
@LogRecord(content="User %s created order %s", operator="#request.userName", bizNo="#request.orderNo")
public void createOrder(CreateOrderRequest request) { /* business logic */ }The AOP interceptor parses the annotation, evaluates SpEL expressions, and records the log after method execution. Core classes include LogRecordAnnotation , LogRecordPointcut , LogRecordInterceptor , LogRecordExpressionEvaluator , and LogRecordContext . Key points:
SpEL is used for dynamic placeholders; custom functions can be added via IParseFunction implementations. LogRecordContext stores variables in an
InheritableThreadLocal<Stack<Map<String,Object>>>to avoid variable leakage across nested method calls.
The operator is resolved from the annotation or, if empty, from an IOperatorGetService implementation that reads the current user from thread‑local context.
Component structure
The library consists of four modules:
AOP module – intercepts methods annotated with @LogRecord.
Log parsing module – evaluates templates, SpEL, and custom functions.
Log persistence module – defines ILogRecordService for storing logs (file, DB, Elasticsearch, etc.).
Starter module – auto‑configures beans and provides @EnableLogRecord(tenant="com.example") for easy integration.
Summary
By combining Canal, log‑file recording, LogUtil helpers, and a full AOP‑based @LogRecord solution, developers can achieve a clean, decoupled, and easily readable operation‑logging system that works across complex business scenarios while keeping the logging code out of core business logic.
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.
dbaplus Community
Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.
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.
