简介
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 修改后的响应内容*/@NullableT beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,Class<? extends HttpMessageConverter<?>> selectedConverterType,ServerHttpRequest request, ServerHttpResponse response);}
示例
@RestControllerAdvice
public class ResAdvice implements ResponseBodyAdvice<Object> {@Overridepublic boolean supports(@NotNull MethodParameter returnType, @NotNull Class<? extends HttpMessageConverter<?>> converterType) {return returnType.getContainingClass().getPackageName().startsWith("kim.nzxy.ly");}@Overridepublic 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 uncheckedreturn 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 {@Overridepublic 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 {@Beanpublic ReturnTypeParser returnTypeParser() {return new ReturnTypeParser() {@Overridepublic 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<Object>if (parameterType.isAssignableFrom(void.class)) {return TypeUtils.parameterize(Res.class, Object.class);}// 包装Resreturn 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()); }