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.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Implementing Operation Logging in SpringBoot with AOP

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.

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.

BackendJavaAOPLoggingSpringBootAspectJOperation Logging
Java Tech Workshop
Written by

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.

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.