Implementing Operation Logging in SpringBoot with AOP
This article walks through a complete, step‑by‑step implementation of operation logging in a SpringBoot application using Spring AOP, covering log field design, Maven dependencies, entity and custom annotation creation, aspect definition with around advice, testing endpoints, and practical optimization tips such as real user extraction, database persistence, and sensitive data masking.
Operation log fields
Operator – current logged‑in user name or ID (typically obtained from Spring Security or a token).
Operation time – timestamp of the request (millisecond precision).
Operation module – e.g., "User Management", "Order Management".
Operation description – e.g., "Add User", "Delete Order".
Request URL – the invoked endpoint path.
Request method – GET/POST/PUT/DELETE.
Request parameters – JSON representation of method arguments.
Response result – JSON representation of the returned data.
Status – 1 for success, 0 for failure.
Error message – exception details when the call fails.
Operation IP – client IP address.
Implementation steps
Step 1 – Maven dependencies
<!-- Spring AOP dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- FastJSON2 for JSON conversion (optional) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<!-- UserAgentUtils for IP / Browser info (optional) -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>Step 2 – OperationLog entity
import lombok.Data;
import java.time.LocalDateTime;
/**
* Operation log entity
*/
@Data
public class OperationLog {
private Long id; // primary key (auto‑increment in real projects)
private String operator; // user name / ID
private LocalDateTime operationTime;
private String module; // e.g., "User Management"
private String description; // e.g., "Add User"
private String requestUrl;
private String requestMethod;
private String requestParams; // JSON string
private String responseResult; // JSON string
private Integer status; // 1 = success, 0 = failure
private String errorMsg; // exception message when failed
private String operationIp;
}Step 3 – Custom annotation
import java.lang.annotation.*;
/**
* Custom annotation for operation logging
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLogAnnotation {
/** Module name, e.g., "User Management" */
String module() default "";
/** Description, e.g., "Add User" */
String description() default "";
}Step 4 – AOP aspect
import com.alibaba.fastjson2.JSON;
import eu.bitwalker.useragentutils.UserAgent;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* Aspect that records operation logs
*/
@Aspect
@Component
public class OperationLogAspect {
// 1. Pointcut – match methods annotated with @OperationLogAnnotation
@Pointcut("@annotation(com.example.demo.annotation.OperationLogAnnotation)")
public void operationLogPointcut() {}
// 2. Around advice – collect data before and after method execution
@Around("operationLogPointcut()")
public Object recordOperationLog(ProceedingJoinPoint joinPoint) throws Throwable {
// Initialise log object
OperationLog operationLog = new OperationLog();
// 2.1 Obtain current HttpServletRequest
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 2.2 Fill basic fields (operator is mocked here as "admin")
operationLog.setOperator("admin"); // replace with real user in production
operationLog.setOperationTime(LocalDateTime.now());
operationLog.setRequestUrl(request.getRequestURI());
operationLog.setRequestMethod(request.getMethod());
operationLog.setOperationIp(getClientIp(request));
Object[] args = joinPoint.getArgs();
operationLog.setRequestParams(JSON.toJSONString(args));
// 2.3 Retrieve annotation attributes
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
OperationLogAnnotation annotation = method.getAnnotation(OperationLogAnnotation.class);
operationLog.setModule(annotation.module());
operationLog.setDescription(annotation.description());
// 2.4 Execute the target method and capture result / exception
Object result = null;
try {
result = joinPoint.proceed();
operationLog.setStatus(1);
operationLog.setResponseResult(JSON.toJSONString(result));
} catch (Throwable throwable) {
operationLog.setStatus(0);
operationLog.setErrorMsg(throwable.getMessage());
throw throwable; // re‑throw to keep original business logic
} finally {
// 2.5 Persist log – here we simply print JSON
System.out.println("Operation log: " + JSON.toJSONString(operationLog, true));
// TODO: replace with database insert, e.g., operationLogService.save(operationLog)
}
return result;
}
/**
* Utility method to obtain the real client IP, handling proxy headers.
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// If multiple IPs are present, take the first one
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}Step 5 – Test controller
import com.example.demo.annotation.OperationLogAnnotation;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* Test controller – User Management module
*/
@RestController
@RequestMapping("/api/user")
public class UserController {
@OperationLogAnnotation(module = "User Management", description = "Add User")
@PostMapping("/add")
public Map<String, Object> addUser(@RequestBody Map<String, String> params) {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "Add user succeeded");
result.put("data", params);
return result;
}
@OperationLogAnnotation(module = "User Management", description = "Delete User")
@DeleteMapping("/delete/{id}")
public Map<String, Object> deleteUser(@PathVariable Long id) {
if (id <= 0) {
throw new RuntimeException("Invalid user ID, cannot delete");
}
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "Delete user succeeded");
return result;
}
}Test 1 – Add User
Request:
URL: http://localhost:8080/api/user/add Method: POST
Body: {"username":"test","password":"123456"}
Console log (formatted):
{
"description":"Add User",
"module":"User Management",
"operationIp":"127.0.0.1",
"operationTime":"2026-04-14T15:30:00",
"operator":"admin",
"requestMethod":"POST",
"requestParams":"[{\"password\":\"123456\",\"username\":\"test\"}]",
"requestUrl":"/api/user/add",
"responseResult":"{\"code\":200,\"data\":{\"password\":\"123456\",\"username\":\"test\"},\"msg\":\"Add user succeeded\"}",
"status":1
}Test 2 – Delete User (error case)
Request:
URL: http://localhost:8080/api/user/delete/-1 Method: DELETE
Console log (formatted):
{
"description":"Delete User",
"errorMsg":"Invalid user ID, cannot delete",
"module":"User Management",
"operationIp":"127.0.0.1",
"operationTime":"2026-04-14T15:35:00",
"operator":"admin",
"requestMethod":"DELETE",
"requestParams":"[-1]",
"requestUrl":"/api/user/delete/-1",
"responseResult":"null",
"status":0
}Optimization tips
Retrieve the real operator
// Obtain authentication object
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !(authentication.getPrincipal() instanceof String)) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
operationLog.setOperator(userDetails.getUsername()); // real username
}Persist logs to a database
@Autowired
private OperationLogService operationLogService;
finally {
operationLogService.save(operationLog);
}Mask sensitive parameters
Define an annotation @IgnoreSensitive on fields such as passwords, then in the aspect replace those values with "****" via reflection before converting to JSON.
Common pitfalls
Missing @Component on the aspect : Only adding @Aspect prevents Spring from registering the bean, so the advice never runs.
Forgetting joinPoint.proceed() : Collecting logs without invoking the target method blocks business logic and breaks the API.
JSON serialization of MultipartFile parameters : Directly calling JSON.toJSONString(args) throws an error; detect MultipartFile types and log a placeholder like "File upload" instead.
Core interpretation
Pointcut matches methods annotated with @OperationLogAnnotation, providing precise control over which interfaces are logged.
Around advice collects request information and annotation attributes, executes the target method, then records success or failure and prints (or persists) the log.
IP retrieval handles Nginx or other proxy headers to obtain the real client IP.
Exception handling captures the exception message, sets status = 0, and rethrows the exception to preserve original error handling.
Log persistence is demonstrated with console output; replace with database insertion, ElasticSearch, etc., as needed.
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 Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
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.
