Implementing a Dynamic AOP Log Aspect for Unified API Parameter Logging in Spring Boot
The article explains why logging API request and response parameters is essential for debugging, shows how to configure environment‑ and interface‑level switches, defines a simple @ApiLog annotation, implements an AOP aspect that records request details and response results, and demonstrates separating logs via Logback configuration while noting performance considerations.
Background
Incorrect request parameters often cause API failures, especially for third‑party callbacks and open APIs. Developers need a way to capture request and response data for evidence.
Configurable Switch
A Boolean property ptc.apiLog.enable (default false) controls logging. It is bound to ApiLogProperties:
@Data
@ConfigurationProperties(prefix = "ptc.apiLog")
public class ApiLogProperties {
/** Whether to enable API log printing, default false */
private Boolean enable = Boolean.FALSE;
}The bean is injected into the aspect, allowing runtime toggling without restart.
@ApiLog Annotation
A marker annotation without attributes indicates that a controller class or method should have its parameters logged:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ApiLog {}AOP Aspect Implementation
The core aspect ApiLogPrintAspect intercepts controller methods, checks the switch, verifies the presence of @ApiLog, records request details, proceeds with execution, then logs the response and execution time.
@Aspect
@Slf4j
@Order(value = OrderConstant.AOP_API_LOG)
public class ApiLogPrintAspect {
@Resource
private ApiLogProperties apiLogProperties;
@Around("execution(* com.plasticene..controller..*(..))")
public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable {
if (!apiLogProperties.getEnable()) {
return joinPoint.proceed();
}
ApiLog apiLog = getApiLog(joinPoint);
if (Objects.isNull(apiLog)) {
return joinPoint.proceed();
}
long start = System.currentTimeMillis();
HttpServletRequest request = getRequest();
RequestInfo requestInfo = new RequestInfo();
requestInfo.setIp(request.getRemoteAddr());
requestInfo.setUrl(request.getRequestURL().toString());
requestInfo.setHttpMethod(request.getMethod());
requestInfo.setClassMethod(String.format("%s.%s",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName()));
requestInfo.setRequestParams(getRequestParams(joinPoint, request));
log.info("Request Info : {}", JsonUtils.toJsonString(requestInfo));
Object result = joinPoint.proceed();
log.info("Response result: {}", JsonUtils.toJsonString(result));
log.info("time cost: {}", System.currentTimeMillis() - start);
return result;
}
private Object getRequestParams(ProceedingJoinPoint joinPoint, HttpServletRequest request) throws UnsupportedEncodingException {
Object[] args = joinPoint.getArgs();
Object params = null;
String queryString = request.getQueryString();
String method = request.getMethod();
if (args.length > 0) {
if ("POST".equals(method) || "PUT".equals(method) || "DELETE".equals(method)) {
int length = args.length;
int index = 0;
Object object = null;
while (index < length) {
Object o = args[index];
index++;
if (o instanceof HttpServletRequest || o instanceof HttpServletResponse) {
continue;
} else {
object = o;
break;
}
}
if (object instanceof MultipartFile) {
MultipartFile multipartFile = (MultipartFile) object;
params = MessageFormat.format("文件名: {0}, 大小: {1}", multipartFile.getOriginalFilename(), multipartFile.getSize());
} else {
params = object;
}
} else if ("GET".equals(method) && StrUtil.isNotBlank(queryString)) {
params = URLDecoder.decode(queryString, "utf-8");
}
}
return params;
}
private HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return requestAttributes.getRequest();
}
private ApiLog getApiLog(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
ApiLog apiLog = method.getAnnotation(ApiLog.class);
if (Objects.isNull(apiLog)) {
apiLog = method.getDeclaringClass().getAnnotation(ApiLog.class);
}
return apiLog;
}
}The aspect logs a single‑line JSON representation of request information (IP, URL, HTTP method, class method, parsed parameters) and the response object, then records the execution time.
Enabling the Feature
Set the switch to true in application.yml (or application.properties).
ptc:
api:
log:
enable: trueTest Controller
Sample endpoint:
@PostMapping("/test/api/log")
@ApiLog
public User printApiLog(@RequestBody User user) throws InterruptedException {
log.info("api log打印啦....");
TimeUnit.SECONDS.sleep(1);
return user;
}Console logs include request info, response result, and time cost, e.g.:
[INFO] Request Info : {"ip":"127.0.0.1","url":"http://127.0.0.1:18888/fds/test/test/api/log","httpMethod":"POST","classMethod":"com.plasticene.fast.controller.TestController.printApiLog","requestParams":{...}}
[INFO] Response result: {"id":123,"name":"she哈哈",...}
[INFO] time cost: 1115Separating Interface Logs
Change the Lombok logger to a named topic to separate interface logs:
@Slf4j(topic = "ptc.api.log")Add an appender and logger definition to logback.xml:
<appender name="INTERFACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/app-interface.log</file>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/app-interface-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<logger name="ptc.api.log" level="info" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INTERFACE_FILE"/>
</logger>After configuration, interface parameter logs are written to app-interface.log, while regular business logs continue to go to app-info.log.
Conclusion
The solution provides a dynamically configurable way to capture API request and response data via an AOP aspect, aiding rapid issue location. Enabling extensive logging in production may affect performance and increase load on distributed log collection systems.
Source code of the aspect: https://github.com/plasticene/plasticene-boot-starter-parent/blob/main/plasticene-boot-starter-web/src/main/java/com/plasticene/boot/web/core/aop/ApiLogPrintAspect.java
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.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
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.
