PS:
- advice, 在这里意思是
顾问
, 其余很多场景也是顾问
的意思- 由于篇幅问题, 注释已删, 如想看注释, 请在github中查看
作用: 用于在Controller返回后, HttpMessageConverter执行转换之前执行一些转换
常见场景: 统一响应结构, 如json统一包装
由于版本不同, 多少有些差异, 所以不贴源码了, 基本上springboot2.x和3.x是通用的
简单做个翻译(springboot3.1.5为例):
public interface ResponseBodyAdvice<T> {
/**
* 此Advice是否使用于该返回类型和Converter类型(意思是可以配置多个哦)
* @param returnType 返回类型(这里可以获取很多东西, 别被名字误导了)
* @param converterType 自动选择的转换器类型
* @return 返回true表示将会走接下来的方法(beforeBodyWrite), 否则不会
*/
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
/**
* HttpMessageConverter转换之前进行的操作
* @param body 要转换的body
* @param returnType 返回类型
* @param selectedContentType 根据请求头协商的ContentType
* @param selectedConverterType 自动选择的转换器类型
* @param request 当前请求
* @param response 当前响应
* @return 修改后的响应内容
*/
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}
@RestControllerAdvice
public class ResAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(@NotNull MethodParameter returnType, @NotNull Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.getContainingClass().getPackageName().startsWith("kim.nzxy.ly");
}
@Override
public Object beforeBodyWrite(Object body,
@NotNull MethodParameter returnType,
@NotNull MediaType selectedContentType,
@NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
@NotNull ServerHttpRequest request,
@NotNull ServerHttpResponse response) {
if (body instanceof Res<?> || !selectedContentType.equals(MediaType.APPLICATION_JSON)) {
return body;
}
if (body instanceof Page<?>) {
// 我的分页有特殊处理
return Res.page((Page<?>)body);
}
return Res.ok(body);
}
}
解释一下代码:
supports判断, 如果类为自己的包下的类, 则允许处理
beforeBodyWrite作用:
如果响应内容不是JSON(可能是文件之类的), 或者已经被公共响应(
Res
)类包装过了, 就直接返回;否则则在外面包装一层Res类
附Res.java
package kim.nzxy.ly.common.res;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import kim.nzxy.ly.common.exception.LyException;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
public class Res<T> {
private static final String SUCCESS_MESSAGE = "操作成功";
private String message;
private int code;
private T data;
private long timestamp = System.currentTimeMillis();
public static <T> Res<T> ok(T data, String message) {
if (data instanceof Page) {
throw new LyException.Panic("使用Res#page方法来返回分页数据");
}
Res<T> msg = new Res<T>();
msg.setCode(2000);
msg.setMessage(message);
msg.setData(data);
return msg;
}
public static <T> Res<T> ok(T data) {
return Res.ok(data, SUCCESS_MESSAGE);
}
public static <T> Res<T> ok(String message) {
// noinspection unchecked
return Res.ok((T) message, message);
}
public static <T> Res<T> ok() {
return Res.ok(null, SUCCESS_MESSAGE);
}
public static <T> Res<T> fail(String message, int code) {
Res<T> msg = new Res<>();
msg.setCode(code);
msg.setMessage(message);
return msg;
}
public static <T> Res<T> fail(String message) {
return Res.fail(message, 5000);
}
public static <T> Res<PagingVO<T>> page(Page<T> page) {
PagingVO<T> data = new PagingVO<>();
data.setPages(Math.toIntExact(page.getPages()));
data.setPageSize(Math.toIntExact(page.getSize()));
data.setList(page.getRecords());
data.setTotal(Math.toIntExact(page.getTotal()));
data.setPageNum(Math.toIntExact(page.getCurrent()));
return ok(data);
}
}
Controller中返回String
类型, 会报类转换异常错误
解决方案: 如果项目中String类型都是要统一包装的, 那就直接干掉所有
StringHttpMessageConverter
@Configuration public class StringHttpMessageConvertRemover implements WebMvcConfigurer { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.removeIf(it -> it instanceof StringHttpMessageConverter); } }
或者不管String类型了
@Override public boolean supports(@NotNull MethodParameter returnType, @NotNull Class<? extends HttpMessageConverter<?>> converterType) { if ("java.lang.String".equals(returnType.getParameterType().getName())) { return false; } return returnType.getContainingClass().getPackageName().startsWith("kim.nzxy.ly"); }
OpenAPI Knife4J等, 额外包装一层
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import kim.nzxy.ly.common.res.PagingVO; import kim.nzxy.ly.common.res.Res; import org.apache.commons.lang3.reflect.TypeUtils; import org.springdoc.core.parsers.ReturnTypeParser; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.MethodParameter; import org.springframework.core.io.Resource; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Optional; /** * @author ly-chn * @since 2024/1/24 10:58 */ @Configuration public class ApiDocOperationCustomizer { @Bean public ReturnTypeParser returnTypeParser() { return new ReturnTypeParser() { @Override public Type getReturnType(MethodParameter methodParameter) { Type returnType = ReturnTypeParser.super.getReturnType(methodParameter); Class<?> parameterType = methodParameter.getParameterType(); // 资源文件或者已经被包装了, 直接返回 if (parameterType.isAssignableFrom(Resource.class) || parameterType.isAssignableFrom(Res.class)) { return returnType; } // 分页特殊处理, 转为PagingVO类 if (parameterType.isAssignableFrom(Page.class) && returnType instanceof ParameterizedType) { Optional<Type> t = TypeUtils.getTypeArguments((ParameterizedType) returnType) .values().stream().findFirst(); Type type = t.orElse(Object.class); return TypeUtils.parameterize(Res.class, TypeUtils.parameterize(PagingVO.class, type)); } // void转为Res if (parameterType.isAssignableFrom(void.class)) { return TypeUtils.parameterize(Res.class, Object.class); } // 包装Res return TypeUtils.parameterize(Res.class, returnType); } }; } }
直接写Response 直接写 OutputStream 怎么办
本来我也担心, 但是
ResponseBodyAdvice
类是Controller返回后, HttpMessageConverter执行转换之前执行, 所以无需担心直接写, 然后返回void的问题我做了这么一个测试, 不会走
ResponseBodyAdvice
, 但是此时Swagger/Knife4f, openapi就无能为力了, 因为没法从代码中获取是否有文件下载@GetMapping("void-with-byte") public void testVoidWithByte(HttpServletResponse response) throws IOException { response.setContentType("application/octet-stream;charset=utf-8"); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=application.yml"); ClassPathResource resource = new ClassPathResource("application.yml"); response.getOutputStream().write(resource.getContentAsByteArray()); }