打印效果
- 正常响应时的日志输出
[2025-08-02 23:58:36.717][http-nio-8181-exec-1][org.learn.goodnight.aop.HttpLogAspect][INFO] - 收到请求[UserController.login], 来源IP:127.0.0.1, 请求URL:/user/login, HTTP方法:POST, 请求参数:{"username":"Job","password":"123"}
[2025-08-02 23:58:36.721][http-nio-8181-exec-1][org.learn.goodnight.aop.HttpLogAspect][INFO] - 请求成功[UserController.login], 响应结果:{"code":"00000000","message":"登录成功!","username":"Job"}
[2025-08-03 00:00:27.326][http-nio-8181-exec-3][org.learn.goodnight.aop.HttpLogAspect][INFO] - 收到请求[UserController.queryNickname], 来源IP:127.0.0.1, 请求URL:/user/queryNickname, HTTP方法:GET, 请求参数:["xx",18]
[2025-08-03 00:00:27.336][http-nio-8181-exec-3][org.learn.goodnight.aop.HttpLogAspect][INFO] - 请求成功[UserController.queryNickname], 响应结果:{"code":"00000000","message":"查询昵称成功!","username":"xx","age":18,"nickname":"周博"}
[2025-08-03 00:00:29.825][http-nio-8181-exec-4][org.learn.goodnight.aop.HttpLogAspect][INFO] - 收到请求[UserController.queryAge], 来源IP:127.0.0.1, 请求URL:/user/queryAge/job, HTTP方法:GET, 请求参数:job
[2025-08-03 00:00:29.827][http-nio-8181-exec-4][org.learn.goodnight.aop.HttpLogAspect][INFO] - 请求成功[UserController.queryAge], 响应结果:{"code":"00000000","message":"查询年龄成功!","username":"job","age":18}
- 出现异常时的日志输出
[2025-08-03 00:17:30.949][http-nio-8181-exec-1][org.learn.goodnight.aop.HttpLogAspect][INFO] - 收到请求[UserController.login], 来源IP:127.0.0.1, 请求URL:/user/login, HTTP方法:POST, 请求参数:{"username":"Job","password":"123"}
[2025-08-03 00:17:30.952][http-nio-8181-exec-1][org.learn.goodnight.aop.HttpLogAspect][INFO] - 请求失败[UserController.login], 出现异常:
java.lang.ArithmeticException: / by zero
...
[2025-08-03 00:17:30.957][http-nio-8181-exec-1][org.learn.goodnight.exception.GlobalExceptionHandler][ERROR] - 全局异常拦截器生效, 请求[UserController.login, URL:/user/login, HTTP方法:POST]时出现异常, 响应结果:{"code":"99999999","message":"系统异常"}, 异常是
java.lang.ArithmeticException: / by zero
...
0.依赖
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.13</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--日志打印-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.7.13</version>
</dependency>
<!-- aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.13</version>
</dependency>
<!-- hutools依赖,用来格式化json -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
1.Controller层
import org.learn.goodnight.aop.HttpLog;
import org.learn.goodnight.pojo.UserDTO;
import org.learn.goodnight.pojo.UserVO;
import org.learn.goodnight.service.SleepService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
SleepService sleepService;
@PostMapping("login")
@HttpLog
public UserVO login(@RequestBody UserDTO userDTO){
UserVO userVO = new UserVO();
int i = 1/0;
userVO.setCode("00000000").setMessage("登录成功!").setUsername(userDTO.getUsername());
return userVO;
}
@GetMapping("queryNickname")
@HttpLog
public UserVO queryNickname(@RequestParam String username, Integer age){
UserVO userVO = new UserVO();
userVO.setCode("00000000").setMessage("查询昵称成功!").setUsername(username).setAge(age).setNickname("周博");
return userVO;
}
@GetMapping("queryAge/{username}")
@HttpLog
public UserVO queryAge(@PathVariable String username){
UserVO userVO = new UserVO();
userVO.setCode("00000000").setMessage("查询年龄成功!").setUsername(username).setAge(18);
return userVO;
}
}
2.自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpLog {
}
3.切面类
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Configuration
@EnableAspectJAutoProxy
@Aspect
@Slf4j
public class HttpLogAspect {
private static final String POINT_CUT = "@annotation(org.learn.goodnight.aop.HttpLog)";
@Pointcut(POINT_CUT)
public void pointCut() {}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Signature signature = joinPoint.getSignature();
//joinPoint.getArgs().length==1?joinPoint.getArgs()[0] 可以避免入参为@RequestBody时,打印出的参数最外层是[]。
String jsonParam = JSONUtil.toJsonStr(joinPoint.getArgs().length == 1 ? joinPoint.getArgs()[0] : joinPoint.getArgs());
log.info("收到请求[{}.{}], 来源IP:{}, 请求URL:{}, HTTP方法:{}, 请求参数:{}",
signature.getDeclaringType().getSimpleName(),
signature.getName(),
request.getRemoteAddr(),
request.getRequestURI(),
request.getMethod(),
jsonParam);
}
@AfterReturning(pointcut = "pointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
log.info("请求成功[{}.{}], 响应结果:{}",signature.getDeclaringType().getSimpleName(), signature.getName(), JSONUtil.toJsonStr(result));
}
@AfterThrowing(pointcut = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
Signature signature = joinPoint.getSignature();
log.info("请求失败[{}.{}], 出现异常:",signature.getDeclaringType().getSimpleName(), signature.getName(),e);
}
}
4.添加全局异常处理器
添加全局异常处理器后,出现的异常会被异常处理器拦截,然后标准化响应输出给客户端。由于@AfterThrowing只能打印出异常,无法打印返回给客户端的响应信息。所以我们需要在异常处理类中打印是哪个controller出现的异常,以及响应给客户端的内容是什么。
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.learn.goodnight.pojo.Response;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.HandlerMethod;
import javax.servlet.http.HttpServletRequest;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Response handleException(HttpServletRequest request, HandlerMethod handlerMethod, Exception ex){
Response response = Response.builder().code("99999999").message("系统异常").build();
String className = handlerMethod.getBeanType().getSimpleName(); // Controller 类名
String methodName = handlerMethod.getMethod().getName(); // Controller 方法名
log.error("全局异常拦截器生效, 请求[{}.{}, URL:{}, HTTP方法:{}]时出现异常, 响应结果:{}, 异常是",
className, methodName, request.getRequestURI(), request.getMethod(), JSONUtil.toJsonStr(response), ex);
// ex.printStackTrace();
return response;
}
}
附log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--status="OFF"-->
<!--monitorInterval="30" 每30秒自动检测配置文件变化-->
<Configuration status="OFF" monitorInterval="30">
<Appenders>
<!-- 控制台输出 -->
<Console name="Console">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%c][%p] - %m%n"/>
</Console>
<!-- 滚动文件输出(按大小和日期分割) -->
<!--fileName用作当前正在写入的活跃日志文件路径, filePattern仅用于生成归档日志文件路径-->
<RollingFile name="File" fileName="logs/app.log"
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout>
<Pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%c][%p] - %m%n</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="100 MB"/>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="org.springframework" level="info" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Logger>
<!--控制mybatis sql打印, debug级别及以下会打印sql-->
<!--additivity="false",日志会打印两遍,一遍由root打印, 一遍由此Logger打印-->
<Logger name="org.learn.goodnight.dao" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Logger>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>