前言
现在的项目大部分都是前后端分离。
后端数据返回给前端的时候就涉及到统一格式的问题,并且还需要对异常进行统一处理。
项目地址
参考文献
SpringBoot 统一接口返回和全局异常处理,大佬们怎么玩
SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!
Not annotated parameter overrides @NonNullApi parameter
Java 包注解和 package-info.java 文件的作用和用法
1. 统一结果返回格式
1.1 springBoot 项目接口的返回
默认情况下,SpringBoot 项目会有如下 3 种返回情况。
1.1.1 直接返回字符串
@GetMapping("/testString")
public String testString() {
return "Hello World";
}
调用接口返回结果:
Hello World
1.1.2 返回实体类
@GetMapping("/testEntity")
public User testEntity() {
return new User("wangguangwu", 22, "Java 开发");
}
调用接口返回结果:
{
"name": "wangguangwu",
"age": 24,
"description": "Java 开发"
}
1.1.3 异常情况下返回
@GetMapping("/testException")
public String testException() {
List<String> list = new ArrayList<>();
return list.get(0);
}
调用接口返回结果:
{
"timestamp": "2022-04-20T06:06:49.826+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "java.lang.IndexOutOfBoundsException: Index: 0,...
"message": "Index: 0, Size: 0",
"path": "/hello/testException"
}
如果整个项目没有定义统一的返回格式,不仅代码臃肿,而且会降低前后端对接效率。
1.2 基础解决方式
项目中最常见的是封装一个工具类。
在工具类中定义需要返回的字段信息,把需要返回前端的接口信息,通过该类进行封装,这样就可以解决返回格式不统一的现象了。
1.2.1 参数说明
- code:状态码,后台可以维护一套统一的状态码;
- message:描述信息,接口调用成功/失败的提示信息;
- data:返回数据。
1.2.2 代码
数据返回格式:
import com.wangguangwu.springbootstandard.enums.ResponseEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
- 通用响应类型,用于封装API响应数据
*
- @param <T> 响应数据的类型
- @author wangguangwu
*/
@Data
@NoArgsConstructor(force = true)
@AllArgsConstructor
public class Response<T> {
/**
- 响应状态码
*/
int code;
/**
- 响应信息
*/
String message;
/**
- 响应数据
*/
T data;
/**
- 构造成功的响应
*
- @param data 响应数据
- @param <T> 响应数据的类型
- @return 包含成功状态码和消息的响应对象
*/
public static <T> Response<T> success(T data) {
return new Response<>(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getMessage(), data);
}
/**
- 构造错误的响应
*
- @param code 自定义错误码
- @param message 错误信息
- @param <T> 响应数据的类型
- @return 包含错误状态码和消息的响应对象
*/
public static <T> Response<T> error(int code, String message) {
return new Response<>(code, message, null);
}
/**
- 构造错误的响应,附带响应数据
*
- @param resultCodeEnum 错误码枚举
- @param data 附带的响应数据
- @param <T> 响应数据的类型
- @return 包含错误状态码、消息和数据的响应对象
*/
public static <T> Response<T> error(ResponseEnum resultCodeEnum, T data) {
return new Response<>(resultCodeEnum.getCode(), resultCodeEnum.getMessage(), data);
}
}
状态返回码:
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
- 响应结果枚举,用于定义通用的响应状态和信息
- <p>
- {@link com.wangguangwu.springbootstandard.response.Response}
*
- @author wangguangwu
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum ResponseEnum {
/**
- 成功
*/
SUCCESS(0, "成功"),
/**
- 失败
*/
FAIL(-1, "失败"),
/**
- 业务层异常
*/
SERVICE_UNKNOWN(1000, "业务层异常"),
/**
- 系统未知异常
*/
SYSTEM_UNKNOWN(1001, "系统未知异常");
/**
- 状态码
*/
private final int code;
/**
- 状态信息
*/
private final String message;
}
1.2.3 使用示例
@GetMapping("/testString")
public Response<String> testString() {
return Response.success("Hello World");
}
调用接口返回结果:
{
"code": 0,
"message": "成功",
"data": "Hello World"
}
优点:统一接口返回结果,方便前后端数据传输。
缺点:如果有大量的接口,并且在每个接口中都使用 Result 来包装返回信息,会新增很多重复代码。
1.3 优化做法
基本用法学会后,接下来我们试着进行优化。
1.3.1 相关知识
我们需要用到两个知识点:
- ResponseBodyAdvice 接口:该接口是 SpringMVC 4.1 提供的,它允许在执行 @ResponseBody 注解后自定义返回数据,用来封装统一数据格式返回;
- @RestControllerAdvice 注解:该注解是对 Controller 进行增强的,并且可以全局捕获抛出的异常。
1.3.2 代码
- 新建 ResponeAdvice 类,用于统一封装 controller 中接口的返回结果;
- 该类需要实现 ResponseBodyAdvice 接口,并且实现其中的 supports、beforeBodyWrite 方法。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wangguangwu.springbootstandard.response.Response;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.json.JsonParseException;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
- 定义一个响应建议,用于拦截响应体并添加成功状态码。
- 如果原始响应体为null,则返回带有成功状态码的响应;
- 如果响应体尚未封装为Result对象,则将其封装为Result对象。
*
- @author wangguangwu
*/
@RequiredArgsConstructor
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
// 用于处理 JSON 序列化的 ObjectMapper 实例
private final ObjectMapper objectMapper;
/**
- 确定是否要对给定的响应执行beforeBodyWrite操作
*
- @param returnType 方法的返回类型
- @param converterType 使用的HttpMessageConverter类型
- @return 是否支持拦截响应
*/
@Override
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
// 这里返回true,表示所有响应都将执行beforeBodyWrite操作
return true;
}
/**
- 在响应体写入前执行操作
*
- @param body 原始地响应体
- @param returnType 方法的返回类型
- @param selectedContentType 响应的内容类型
- @param selectedConverterType 使用的HttpMessageConverter类型
- @param request 当前的HTTP请求
- @param response 当前的HTTP响应
- @return 修改后的响应体
*/
@Override
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
@NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response) {
// 如果响应体为null,则返回带有成功状态码的默认响应
if (body == null) {
return Response.success(null);
}
// 如果响应体已经是Response对象,则直接返回,不做修改
if (body instanceof Response) {
return body;
}
// 如果响应体是String类型,则需要手动处理字符串的包装
if (body instanceof String) {
try {
// 使用ObjectMapper将Response对象转换为JSON字符串
return objectMapper.writeValueAsString(Response.success(body));
} catch (JsonProcessingException e) {
// 捕获JSON处理异常并重新抛出为JsonParseException
throw new JsonParseException(e);
}
}
// 对于其他类型的响应体,封装为Response对象返回
return Response.success(body);
}
}
创建该类后,我们对接口也进行一下调整:
@GetMapping("/testString")
public String testString() {
return "Hello World";
}
接口返回结果:
{
"code":0,
"message":"成功",
"data":"Hello World"
}
优点:切面管理,统一返回格式的同时,代码量更少更优雅。
缺点:只对成功响应的请求进行了处理,对于异常之类的结果并没有处理。
2. 全局异常处理
一般都是使用 try catch 对代码进行捕获处理,虽然满足要求,不过这种方式会导致大量代码重复,维护困难,逻辑臃肿等问题,不够优雅。
在此基础上,我们可以采用全局异常处理的方式,从而减少代码量。
2.1 使用讲解
新增一个类,标注 @RestControllerAdvice
注解,从而捕获全局异常。
@RestControllerAdvice
public class GlobalExceptionHandler {
}
对于想要拦截的异常类型,只需要新增一个方法,使用 @ExceptionHandler
注解修饰,注解参数为目标异常类型。
@ExceptionHandler
注解:统一处理某一类异常,从而减少代码重复率和复杂度。
2.2 自定义异常
import com.wangguangwu.springbootstandard.enums.ResponseEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
- 自定义业务异常。
*
- @author wangguangwu
*/
@SuppressWarnings("unused")
@EqualsAndHashCode(callSuper = true)
@Data
public class ServiceException extends RuntimeException {
protected final Integer data;
protected final String message;
public ServiceException() {
this.data = ResponseEnum.SERVICE_UNKNOWN.getCode();
this.message = ResponseEnum.SERVICE_UNKNOWN.getMessage();
}
public ServiceException(String message) {
this.data = ResponseEnum.SERVICE_UNKNOWN.getCode();
this.message = message;
}
}
2.3 完整代码
import com.wangguangwu.springbootstandard.enums.ResponseEnum;
import com.wangguangwu.springbootstandard.exception.ServiceException;
import com.wangguangwu.springbootstandard.response.Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
- 定义全局异常处理
- 该类用于捕获和处理应用程序中的异常,并返回统一的响应格式。
*
- @author wangguangwu
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
- 处理自定义的业务异常 ServiceException
- 当业务逻辑抛出 ServiceException 时,捕获该异常并返回错误响应。
*
- @param exception 捕获的 ServiceException
- @return 包含错误信息的响应对象
*/
@ExceptionHandler(ServiceException.class)
private Response<String> handleServiceException(ServiceException exception) {
log.error("ServiceException occurred: {}", exception.getMessage(), exception);
return Response.error(ResponseEnum.SERVICE_UNKNOWN.getCode(), exception.getMessage());
}
/**
- 处理所有未捕获的通用异常
- 捕获应用程序中未处理的其他异常,并返回通用错误响应。
*
- @param exception 捕获的通用异常
- @return 包含错误信息的响应对象
*/
@ExceptionHandler(Exception.class)
public Response<String> handleGeneralException(Exception exception) {
log.error("Unhandled exception occurred: {}", exception.getMessage(), exception);
return Response.error(ResponseEnum.SYSTEM_UNKNOWN, exception.getMessage());
}
}
3. 打印入参出参
上面我们说过,可以通过实现 ResponseBodyAdvice 接口去做统一的返回数据处理。
beforeBodyWrite 方法的执行时间:responseBody 被写入之前。
如果 controller 本身就已经报错了,即 responseBody 没有被写入,这个方法是不会被执行的,加在其中的日志也就不会被打印了。
那有没有方式可以修复这个问题呢?
有的,spring 除了提供 ResponseBodyAdvice 之外,还提供了相对应的 requestBodyAdvice 接口。
方案:
- 在
beforeBodyRead
中打印请求日志; - 在
beforeBodyWrite
中打印正常返回日志; - 在
@ExceptionHandler
中打印异常返回日志。
但这样的方式还是过于繁琐,所以我们采用另外一种方式:
单独创建一个切面来做统一的日志打印。
3.1 日志切面
/**
- Define an aspect to print logging.
*
- @author wangguangwu
*/
@RequiredArgsConstructor
@Slf4j
@Aspect
@Component
public class RequestLoggingAspect {
final HttpServletRequest httpServletRequest;
@Around(value = "execution(* com.wangguangwu.springbootstandard.controller..*.*(..))")
public Object around(final ProceedingJoinPoint joinPoint) throws Throwable {
log.info("request url: {}", httpServletRequest.getRequestURL().toString());
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
log.info("request params: {}", gson.toJson(joinPoint.getArgs()));
Object result = joinPoint.proceed();
log.info("response:{}", gson.toJson(result));
return result;
}
}
介绍一下代码中的 AOP 表达式:
"execution(* com.wangguangwu.springbootstandard.controller..*.*(..))"
execution()
:是最常用的切点函数,表示切面作用于方法执行时。- 第一个
*
:表示不限返回类型; - 第二个
*
:表示不限类名; - 第三个
*
:表示不限方法名; (..)
表示不限参数;@Around()
表示该切面的类型是包围类型。
所以这个 AOP 表达式的整体含义是:
在 com.wangguangwu.springbootstandard.controller
包下的所有类的所有方法的执行前后进行拦截。
通过定义这样一个切面,我们就可以在 controller 的方法被调用前打印请求日志,被调用后打印响应日志。
当然,在抛出异常的情况,日志还是打印在 @ExceptionHandler 里的。
如果只是想要打印出全部的日志,以上的代码已经完成需求了。
真正在生产中,我们往往会遇到一个问题,就是有些接口的日志我们并不想打印出来。
特别是一些批量查询接口的响应结果,一打就一堆,如果调用频繁,就可能会造成大量空间的浪费,也不方便日志的排查。
那我们就需要针对不同的类,甚至方法进行区别对待。
对于不同的类,可以通过自定义切面的方式来解决。
但是如果同一个类中不同的方法有不同的日志需求,会引入大量的切点。
3.2 自定义注解实现更复杂的日志需求
3.2.1 定义日志级别
日志级别:
/**
- 日志类型枚举。
- 用于指定在请求处理过程中,应该记录哪些类型的日志。
*
- @author wangguangwu
*/
public enum LogType {
/**
- 记录请求的URL。
*/
URL,
/**
- 记录请求的参数。
*/
REQUEST,
/**
- 记录响应结果。
*/
RESPONSE,
/**
- 记录所有类型的日志(URL、请求参数和响应结果)。
*/
ALL,
/**
- 不记录任何日志。
*/
NONE
}
日志选项类:
import lombok.*;
/**
- 日志选项类,用于配置在请求处理过程中应该记录哪些类型的日志。
- <p>
- 通过静态方法 {@code all()} 和 {@code none()} 可以方便地创建记录所有日志或不记录日志的选项。
*
- @author wangguangwu
*/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class LogOptions {
/**
- 是否记录请求的URL
*/ private boolean url;
/**
- 是否记录请求参数
*/
private boolean request;
/**
- 是否记录响应结果
*/
private boolean response;
/**
- 创建一个记录所有日志选项的实例。
*
- @return 包含所有日志记录选项的 LogOptions 实例
*/
public static LogOptions all() {
return LogOptions.builder()
.url(true)
.request(true)
.response(true)
.build();
}
/**
- 创建一个不记录任何日志选项的实例。
*
- @return 不包含任何日志记录选项的 LogOptions 实例
*/
public static LogOptions none() {
return LogOptions.builder()
.url(false)
.request(false)
.response(false)
.build();
}
}
3.2.2 自定义注解
import com.wangguangwu.springbootstandard.log.LogType;
import java.lang.annotation.*;
/**
- 自定义注解,用于控制方法级别的日志打印行为。
- <p>
- 通过设置 {@code type()} 属性,可以指定需要打印哪些类型的日志。
- 如果未设置,默认不打印任何日志({@code LogType.NONE})。
*
- @author wangguangwu
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LessLog {
/**
- 指定要打印的日志类型。
- 默认值为 {@code LogType.NONE},表示不打印任何日志。
- <p>
- 如果想打印所有日志,可以使用 {@code LogType.ALL}。
*
- @return 需要打印的日志类型数组
*/
LogType[] type() default LogType.NONE;
}
3.2.3 完整的日志切面代码
import com.alibaba.fastjson2.JSON;
import com.wangguangwu.springbootstandard.anno.LessLog;
import com.wangguangwu.springbootstandard.log.LogOptions;
import com.wangguangwu.springbootstandard.log.LogType;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EnumSet;
/**
- 定义一个切面用于打印日志。
- 该切面拦截指定包路径下的控制器方法,在方法执行前后打印请求和响应的日志信息。
- 日志打印行为可以通过注解 @LessLog 配置。
*
- @author wangguangwu
*/
@RequiredArgsConstructor
@Slf4j
@Aspect
@Component
public class RequestLoggingAspect {
/**
- 注入 HttpServletRequest 用于获取请求的详细信息
*/
private final HttpServletRequest httpServletRequest;
/**
- 环绕通知,拦截指定包路径下的控制器方法,打印请求和响应日志
*
- @param joinPoint 连接点,表示被拦截的方法
- @return 方法的返回结果
- @throws Throwable 如果方法执行过程中发生异常,则抛出
*/
@Around(value = "execution(* com.wangguangwu.springbootstandard.controller..*.*(..))")
public Object aroundBack(final ProceedingJoinPoint joinPoint) throws Throwable {
// 获取日志选项,判断是否打印特定日志
LogOptions logOptions = getLogOptions(joinPoint);
// 打印请求的 URL if (logOptions.isUrl()) {
log.info("request url: {}", httpServletRequest.getRequestURL().toString());
}
// 打印请求参数
if (logOptions.isRequest()) {
log.info("request params: {}", JSON.toJSON(joinPoint.getArgs()));
}
// 执行目标方法
Object result = joinPoint.proceed();
// 打印响应结果
if (logOptions.isResponse()) {
log.info("response:{}", JSON.toJSON(result));
}
return result;
}
/**
- 获取方法的日志选项,根据 @LessLog 注解确定哪些日志需要打印
*
- @param joinPoint 连接点,表示被拦截的方法
- @return 日志选项,确定是否打印 URL、请求参数和响应结果
*/
private LogOptions getLogOptions(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LessLog lessLog = method.getAnnotation(LessLog.class);
// 如果方法未标注 @LessLog 注解,则打印所有日志
if (lessLog == null) {
return LogOptions.all();
}
// 获取注解中的日志类型
LogType[] typeArray = lessLog.type();
EnumSet<LogType> logTypes = EnumSet.copyOf(Arrays.asList(typeArray != null && typeArray.length > 0 ? typeArray : new LogType[]{LogType.NONE}));
// 如果包含 NONE,则打印所有日志
if (logTypes.contains(LogType.NONE)) {
return LogOptions.all();
}
// 如果包含 ALL,则不打印任何日志
if (logTypes.contains(LogType.ALL)) {
return LogOptions.none();
}
// 根据注解中的日志类型决定是否打印对应的日志
return new LogOptions(!logTypes.contains(LogType.URL), !logTypes.contains(LogType.REQUEST), !logTypes.contains(LogType.RESPONSE));
}
}
4. 接入 logback
application.yml
:
# logger configuration
logging:
file:
level: info
# logs location
path: ./logs
在 resources
目录下创建 logback-spring.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 属性文件:在 properties 文件中找到对应的配置项 -->
<springProperty scope="context" name="log.path" source="logging.file.path"/>
<springProperty scope="context" name="log.level" source="logging.file.level"/>
<!-- 定义日志保留天数 -->
<property name="MAX_HISTORY" value="10"/>
<!-- 定义单个日志文件大小 -->
<property name="MAX_FILE_SIZE" value="100MB"/>
<!-- 定义日志文件总大小 -->
<property name="TOTAL_SIZE_CAP" value="1GB"/>
<!-- 彩色日志 -->
<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %green([%thread]) %highlight(%-5level) %cyan(%logger{20}) - [%method,%line] - %msg%n"/>
<!-- 默认的控制台日志输出,一般生产环境都是后台启动,这个没太大作用 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
</encoder>
</appender>
<!-- 定义了一个切面用来监听入参出参 -->
<!-- 打印 controller 层日志 -->
<appender name="WANG-ASPECT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 追加写入 -->
<append>true</append>
<file>
${log.path}/aspect/aspect.log
</file>
<!-- 日志过滤 -->
<!-- 会打印出当前层级及以上层级的日志,如果想要只打印当前层级,可以更换过滤器为 LevelFilter -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${log.level}</level>
</filter>
<!-- 不会打印 error 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<!-- 如果命中就禁止这条日志 -->
<onMatch>DENY</onMatch>
<!-- 如果没有命中就使用这条规则 -->
<onMismatch>ACCEPT</onMismatch>
</filter>
<!-- 基于文件大小和时间的滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${log.path}/aspect/aspect-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!-- 日志文件保留天数 -->
<MaxHistory>${MAX_HISTORY}</MaxHistory>
<!-- 单个日志文件大小 -->
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<!-- 日志归档文件总大小 -->
<totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
</rollingPolicy>
<!-- 日志输出格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}: %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 定义了一个切面用来监听 exception -->
<!-- 打印 controller 层日志 -->
<appender name="WANG-EXCEPTION" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 追加写入 -->
<append>true</append>
<file>
${log.path}/exception/exception.log
</file>
<!-- 过滤器,只记录 error 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<!-- 基于文件大小和时间的滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${log.path}/exception/exception-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!-- 日志文件保留天数 -->
<MaxHistory>${MAX_HISTORY}</MaxHistory>
<!-- 单个日志文件大小 -->
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<!-- 日志归档文件总大小 -->
<totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
</rollingPolicy>
<!-- 日志输出格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}: %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 打印 controller 层日志 -->
<appender name="WANG-CONTROLLER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 追加写入 -->
<append>true</append>
<file>
${log.path}/controller/controller.log
</file>
<!-- 日志过滤 -->
<!-- 会打印出当前层级及以上层级的日志,如果想要只打印当前层级,可以更换过滤器为 LevelFilter -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${log.level}</level>
</filter>
<!-- 不会打印 error 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<!-- 如果命中就禁止这条日志 -->
<onMatch>DENY</onMatch>
<!-- 如果没有命中就使用这条规则 -->
<onMismatch>ACCEPT</onMismatch>
</filter>
<!-- 基于文件大小和时间的滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${log.path}/controller/controller-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!-- 日志文件保留天数 -->
<MaxHistory>${MAX_HISTORY}</MaxHistory>
<!-- 单个日志文件大小 -->
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<!-- 日志归档文件总大小 -->
<totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
</rollingPolicy>
<!-- 日志输出格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}: %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 打印 service 层日志 -->
<appender name="WANG-SERVICE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 追加写入 -->
<append>true</append>
<file>
${log.path}/service/service.log
</file>
<!-- 日志过滤 -->
<!-- 会打印出当前层级及以上层级的日志,如果想要只打印当前层级,可以更换过滤器为 LevelFilter -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${log.level}</level>
</filter>
<!-- 不会打印 error 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<!-- 如果命中就禁止这条日志 -->
<onMatch>DENY</onMatch>
<!-- 如果没有命中就使用这条规则 -->
<onMismatch>ACCEPT</onMismatch>
</filter>
<!-- 基于文件大小和时间的滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${log.path}/service/service-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!-- 日志文件保留天数 -->
<MaxHistory>${MAX_HISTORY}</MaxHistory>
<!-- 单个日志文件大小 -->
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<!-- 日志归档文件总大小 -->
<totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
</rollingPolicy>
<!-- 日志输出格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}: %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 错误日志 appender:按照每天生成日志文件 -->
<appender name="ERROR-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<append>true</append>
<!-- 过滤器,只记录 error 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<!-- 日志名称 -->
<file>${log.path}/error/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/error-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!-- 日志文件保留天数 -->
<maxHistory>${MAX_HISTORY}</maxHistory>
<!-- 日志归档文件总大小 -->
<totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
<!-- 单个日志文件大小 -->
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}: %msg%n</pattern>
<!-- 编码 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 定义了一个切面用来监听入参出参 -->
<!-- additivity 设置为 true,表示只在当前 logger 中的 appender-ref 中生效 -->
<logger name="com.wangguangwu.standard.aspect.RequestLoggingAspect" level="${log.level}" additivity="false">
<appender-ref ref="WANG-ASPECT"/>
</logger>
<!-- 定义了一个切面用来监听入参出参 -->
<!-- additivity 设置为 true,表示只在当前 logger 中的 appender-ref 中生效 -->
<logger name="com.wangguangwu.standard.aspect.ExceptionLoggingAspect" level="${log.level}" additivity="false">
<appender-ref ref="WANG-EXCEPTION"/>
</logger>
<!-- logger 负责打印 com.wangguangwu.standard.controller 下的日志 -->
<!-- additivity 设置为 true,表示只在当前 logger 中的 appender-ref 中生效 -->
<logger name="com.wangguangwu.standard.controller" level="${log.level}" additivity="false">
<appender-ref ref="WANG-CONTROLLER"/>
<appender-ref ref="ERROR-APPENDER"/>
</logger>
<!-- logger 负责打印 com.wangguangwu.standard.service 下的日志 -->
<!-- additivity 设置为 true,表示只在当前 logger 中的 appender-ref 中生效 -->
<logger name="com.wangguangwu.standard.service" level="${log.level}" additivity="false">
<appender-ref ref="WANG-SERVICE"/>
<appender-ref ref="ERROR-APPENDER"/>
</logger>
<!-- root 指向控制台输出 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>