SpringBoot ResponseBodyAdvice使用以及常见问题

简介

PS:

  1. advice, 在这里意思是顾问, 其余很多场景也是顾问的意思
  2. 由于篇幅问题, 注释已删, 如想看注释, 请在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);
    }
}

常见问题

  1. 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");
    }
    
  2. 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);
                }
            };
        }
    }
    
     
    
  3. 直接写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());
    }
    
  4. 你可能感兴趣的:(java,springboot)