How to Build a Clean Business Operation Log System with Spring AOP
This article explains the design and implementation of a business operation logging feature in a Spring Boot application, contrasting flawed hard‑coded approaches with a clean AOP‑based solution, and provides full code snippets, configuration, database schema, and testing tips.
Introduction
Long ago I wanted to write this article but never had time; the requirement was simple: record and query business operation logs. The initial implementation was terrible, and the mistakes are detailed in the anti‑pattern example.
Requirement Description and Analysis
The client needs to log key business functions: who performed which operation, at what time, the request and response payloads, and optionally allow a one‑click rollback.
System Log
System logs capture program execution steps (debug, info, warn, error) for developers or operations staff to troubleshoot issues.
Operation Log
Operation logs record actual user business actions (e.g., menu clicks, configuration changes) and are stored in a database for administrators or users to view.
Key functional requirements:
Record fields: operator, time, function, log type, description, request payload, pre‑operation payload.
Provide a visual page for querying and tracing important operations.
Offer management features such as rollback of erroneous operations.
Anti‑Pattern Implementation
The naive approach adds logging code and exception handling to every controller method, tightly coupling logging with business logic and causing code duplication.
@RestController
@Slf4j
@BusLog(name = "Personnel Management")
@RequestMapping("/person")
public class PersonController2 {
@Autowired
private IPersonService personService;
@Autowired
private IBusLogService busLogService;
// Add person
@PostMapping
public Person add(@RequestBody Person person) {
try {
Person result = this.personService.registe(person);
this.saveLog(person);
log.info("// add person completed");
} catch (Exception e) {
this.saveExceptionLog(e);
}
return result;
}
}This hard‑coded method makes logging logic repeat across all endpoints and requires modifying existing controllers to weave in logging.
Design Idea
Using AOP is the optimal solution if you are familiar with it:
Define a custom annotation for business logs, allowing attributes such as function name and description.
Annotate methods (or classes) that need logging.
Define a pointcut and an aspect that intercepts the annotated methods and saves the log information.
Spring AOP
Aspect‑Oriented Programming (AOP) adds cross‑cutting concerns without modifying source code. Spring AOP is a concrete implementation that can intercept methods at any layer (controller, service, etc.) and provides before, after, around, and exception advice.
Filter and HandlerInterceptor
Filters and HandlerInterceptors were not chosen because of their limitations:
Filter
Filters work at the servlet level, can only read and modify request/response streams, and cannot fine‑tune to specific classes or methods. They depend on a servlet container.
Interceptor
HandlerInterceptors are specific to Spring MVC, cannot be used in non‑MVC projects, and share the same granularity limitations as filters.
Comparison of Spring AOP, Filter, and Interceptor
Execution priority when matching the same target: Filter > Interceptor > Spring AOP. Differences include scope, granularity, and ability to modify return values.
Scope: Filters require a servlet container; Interceptors need Spring MVC; Spring AOP works anywhere a pointcut is defined.
Granularity: Filters are coarse; Interceptors offer preHandle, postHandle, afterCompletion; Spring AOP provides before, after, after‑returning, after‑throwing, and around advice.
Implementation Plan
Environment Configuration
JDK 1.8, IntelliJ IDEA 2020.1
Spring Boot 2.3.9.RELEASE
mybatis-spring-boot-starter 2.1.4
Dependency Configuration
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>Table Structure Design
create table if not exists bus_log (
id bigint auto_increment comment 'auto‑increment id' primary key,
bus_name varchar(100) null comment 'business name',
bus_descrip varchar(255) null comment 'business description',
oper_person varchar(100) null comment 'operator',
oper_time datetime null comment 'operation time',
ip_from varchar(50) null comment 'source IP',
param_file varchar(255) null comment 'parameter file path'
) comment 'business operation log' default charset='utf8';Code Implementation
1. Define the @BusLog annotation.
/**
* Business log annotation
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BusLog {
String name() default ""; // function name
String descrip() default ""; // function description
}2. Annotate controller methods with @BusLog.
@RestController
@Slf4j
@BusLog(name = "Personnel Management")
@RequestMapping("/person")
public class PersonController {
@Autowired
private IPersonService personService;
@PostMapping
@BusLog(descrip = "Add a single person")
public Person add(@RequestBody Person person) {
Person result = this.personService.registe(person);
log.info("// add person completed");
return result;
}
// other CRUD methods similarly annotated ...
}3. Implement the aspect that records logs.
@Component
@Aspect
@Slf4j
public class BusLogAop implements Ordered {
@Autowired
private BusLogDao busLogDao;
@Pointcut("@annotation(com.fanfu.anno.BusLog)")
public void pointcut() {}
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("----BusAop around start");
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable t) {
t.printStackTrace();
throw t;
}
// collect annotation data
Object target = pjp.getTarget();
MethodSignature sig = (MethodSignature) pjp.getSignature();
BusLog classAnno = target.getClass().getAnnotation(BusLog.class);
BusLog methodAnno = sig.getMethod().getAnnotation(BusLog.class);
BusLogBean bean = new BusLogBean();
bean.setBusName(classAnno != null ? classAnno.name() : "");
bean.setBusDescrip(methodAnno != null ? methodAnno.descrip() : "");
bean.setOperPerson("fanfu");
bean.setOperTime(new Date());
// serialize parameters to file
String json = new ObjectMapper().writeValueAsString(pjp.getArgs());
String filePath = System.getProperty("user.dir") + File.separator +
new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + ".log";
try (FileOutputStream fos = new FileOutputStream(filePath)) {
fos.write(json.getBytes(StandardCharsets.UTF_8));
bean.setParamFile(filePath);
}
busLogDao.insert(bean);
log.info("----BusAop around end");
return result;
}
@Override
public int getOrder() { return 1; }
}Testing
Debugging Method
Instead of Postman, you can use IntelliJ IDEA's built‑in Test RESTful Web Service (Tools → HTTP Client → Test RESTful Web) to send requests directly from the IDE.
Another convenient method is to write a few lines in the HTTP client file to batch‑execute multiple requests.
Verification Result
Conclusion
The business operation log records the function name, description, operator, time, and parameter payload. The payload is stored in a file because it is only needed for rollback scenarios, where the previous parameters are used to reverse the operation.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
