SpringBoot-SpringMVC中的ContentNegotiationStrategy内容协商策略的作用和原理

/**
 * 为什么要有内容协商策略?
 * 因为在客户端和服务端之前请求响应的时候,客户端期望得到服务端返回指定类型的内容
 * 而这个过程,是客户端和服务端之间进行协商的过程,从而可以有多种协商方式,也称为协商策略
 */
class ContentNegotiationManager {
    // 进行内容协商的各种策略集合
    private final List<ContentNegotiationStrategy> strategies = new ArrayList<>();
    // 解析对于文件类型的后缀,因为对于文件,文件后缀名一般就是响应的类型
    private final Set<MediaTypeFileExtensionResolver> resolvers = new LinkedHashSet<>();

    // 给定内容协商策略
    public ContentNegotiationManager(ContentNegotiationStrategy... strategies) {
        this(Arrays.asList(strategies));
    }

    // 初始化
    public ContentNegotiationManager(Collection<ContentNegotiationStrategy> strategies) {
        this.strategies.addAll(strategies);
        for (ContentNegotiationStrategy strategy : this.strategies) {
            // 保存媒体类型文件的解析器,因为对于文件,文件后缀名一般就是响应的类型
            if (strategy instanceof MediaTypeFileExtensionResolver) {
                this.resolvers.add((MediaTypeFileExtensionResolver) strategy);
            }
        }
    }

    // 支持默认的协商策略为获取请求头中的支持的类型
    public ContentNegotiationManager() {
        this(new HeaderContentNegotiationStrategy());
    }

    // 使用内容协商策略进行解析客户端需要的响应类型
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
        // 遍历所有的内容协商策略
        for (ContentNegotiationStrategy strategy : this.strategies) {
            // 使用内容协商策略解析协商之后的结果
            List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
            // 如果支持所有的媒体类型,不处理
            if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
                continue;
            }
            // 否则返回该协商策略解析后的结果
            return mediaTypes;
        }
        // 如果所有的协商策略都没有得到客户端需要的结果,则默认支持所有媒体类型
        return MEDIA_TYPE_ALL_LIST;
    }

    // 解析文件扩展名
    @Override
    public List<String> resolveFileExtensions(MediaType mediaType) {
        // 使用文件解析器对该媒体类型进行解析
        return doResolveExtensions(resolver -> resolver.resolveFileExtensions(mediaType));
    }

    // 使用文件解析器对该媒体类型进行解析
    private List<String> doResolveExtensions(Function<MediaTypeFileExtensionResolver, List<String>> extractor) {
        List<String> result = null;
        // 遍历所有的文件类型解析器
        for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
            // 执行解析,获取文件的扩展名
            List<String> extensions = extractor.apply(resolver);
            // 如果该解析器没有解析到扩展名,不处理
            if (CollectionUtils.isEmpty(extensions)) {
                continue;
            }
            // 解析到扩展名
            result = (result != null ? result : new ArrayList<>(4));
            // 遍历所有的扩展名
            for (String extension : extensions) {
                // 保存解析到的所有扩展名
                if (!result.contains(extension)) {
                    result.add(extension);
                }
            }
        }
        return (result != null ? result : Collections.emptyList());
    }
}

// 内容协商策略,还可以自定义,就是获取某个地方设置的需要的响应类型,比如请求头,请求参数,请求路径中
public class ContentNegotiationStrategy {
    /**
     * 基于参数的内容协商策略实现
     * {@link org.springframework.web.accept.ParameterContentNegotiationStrategy}
     */
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
        // private String parameterName = "format";这就是默认的参数名称,/url?format=json,表示客户端需要json类型的数据
        String key = request.getParameter(getParameterName());
        // 如果存在该参数的值
        if (StringUtils.hasText(key)) {
            // 查询服务器端支持的所有媒体类型,是否存在客户端指定的媒体类型
            MediaType mediaType = this.lookupMediaType(key);
            // 如果存在
            if (mediaType != null) {
                // 处理匹配上的逻辑
                this.handleMatch(key, mediaType);
                // 返回协商的结果类型
                return Collections.singletonList(mediaType);
            }
            // 没有找到客户端匹配的媒体类型,处理没有匹配上的逻辑,用其他方式看一下是否能返回该媒体类型
            mediaType = this.handleNoMatch(webRequest, key);
            // 如果其他方式匹配上了
            if (mediaType != null) {
                // 保存映射关系,防止下次再去找
                this.addMapping(key, mediaType);
                // 返回兜底的媒体类型
                return Collections.singletonList(mediaType);
            }
        }
        // 如果客户端没有指定参数format的key媒体类型,默认支持所有
        return MEDIA_TYPE_ALL_LIST;
    }

    /**
     * 基于请求头的内容协商策略实现
     * {@link org.springframework.web.accept.HeaderContentNegotiationStrategy}
     */
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
        // 获取请求头ACCEPT
        String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
        // 如果没有请求头
        if (headerValueArray == null) {
            // 支持所有
            return MEDIA_TYPE_ALL_LIST;
        }
        //
        List<String> headerValues = Arrays.asList(headerValueArray);
        // 将请求头的字符串解析成MediaType
        List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
        // 找到最合适的媒体类型,根据权重等等方式
        MediaType.sortBySpecificityAndQuality(mediaTypes);
        // 返回找到的媒体类型
        return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
    }

}

// 这是一个处理返回值的处理器,内部调用所有的消息转换器,将内容进行响应
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {
    // 内容协商管理器,管理了多个内容协商策略
    private final ContentNegotiationManager contentNegotiationManager;

    // 使用消息处理器对消息进行响应,我们只保留内容协商的核心源码
    protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) {
        // 最终需要响应给客户端的数据类型
        MediaType selectedMediaType = null;
        // 响应流中是否已经设置了响应类型
        MediaType contentType = outputMessage.getHeaders().getContentType();
        boolean isContentTypePreset = contentType != null && contentType.isConcrete();
        // 如果设置了,直接使用指定的响应类型,不需要进行协商
        if (isContentTypePreset) {
            selectedMediaType = contentType;
        }
        // 开始进行内容协商
        else {
            HttpServletRequest request = inputMessage.getServletRequest();
            // 获取客户端需要的响应类型
            List<MediaType> acceptableTypes = this.getAcceptableMediaTypes(request);
            // 获取服务端需要的响应类型
            List<MediaType> producibleTypes = this.getProducibleMediaTypes(request, valueType, targetType);

            // 客户端和服务端进行类型协商,得到最终响应的结果
            List<MediaType> mediaTypesToUse = new ArrayList<>();
            for (MediaType requestedType : acceptableTypes) {
                for (MediaType producibleType : producibleTypes) {
                    if (requestedType.isCompatibleWith(producibleType)) {
                        mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
                    }
                }
            }
            // 如果没有协商一致,抛出异常
            if (mediaTypesToUse.isEmpty()) {
                if (body != null) {
                    throw new HttpMediaTypeNotAcceptableException(producibleTypes);
                }
                return;
            }
            // 因为协商之后,可能存在多种符合条件的组合,我们需要选出最佳的一组响应类型
            for (MediaType mediaType : mediaTypesToUse) {
                if (mediaType.isConcrete()) {
                    selectedMediaType = mediaType;
                    break;
                }
                if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
                    selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                    break;
                }
            }
        }
        // 协商完成,得到最终的响应类型
        if (selectedMediaType != null) {
            // 开始遍历所有的消息转换器,将最终的结果按照指定的媒体类型响应
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
                // 是否可以按照指定的媒体类型响应最终的结果
                if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) {
                    // 如果可以,执行ResponseBodyAdvice接口的回调,ResponseBodyAdvice接口是响应之前对象返回值做的最后操作
                    // 可以改变最终的返回结果,达到最终的统一结果返回
                    body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);
                    // 如果存在返回结果
                    if (body != null) {
                        // 写如响应头
                        Object theBody = body;
                        addContentDispositionHeader(inputMessage, outputMessage);
                        // 调用消息转换器的写如方法,将数据写到响应流中
                        if (genericConverter != null) {
                            genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                        } else {
                            ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                        }
                    }
                    return;
                }
            }
        }
    }

    // 获取客户端能接受的内容类型
    private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) {
        // 使用内容协商管理器,获取客户端需要的响应类型
        return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
    }

    // 获取服务端能响应的内容类型
    protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
        Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        // 如果在请求域中设置当前请求服务器响应的媒体类型,因为可以在@RequestMapping中设置
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            return new ArrayList<>(mediaTypes);
        }
        // 如果没有设置
        List<MediaType> result = new ArrayList<>();
        // 遍历所有的消息转换器,获取所有可以处理指定类型的消息转换器支持的媒体类型
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            // 如果可以响应该返回值
            if (converter.canWrite(valueClass, null)) {
                // 则获取该转换器支持的媒体类型
                result.addAll(converter.getSupportedMediaTypes(valueClass));
            }
        }
        // 返回消息转换器中支持写出的媒体类型
        return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
    }

}

/**
 * 使用案例
 */
@Configuration
class Demo {
    /**
     * http://localhost:8080/luck?format=luck,这个时候,就会执行LuckHttpMessageConverter的write方法,返回luck.toString()
     * {@link LuckConfig#configureMessageConverters}
     */
    @GetMapping("/luck")
    public Luck luck() {
        return new Luck(100, "luck");
    }

    @Configuration
    public class LuckConfig implements WebMvcConfigurer {
        /**
         * 启请求参数内容协商,可以通过?format=json,xxx来与服务器协商需要得到什么样的数据内容
         * {@link WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureContentNegotiation(org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer)}
         * 如果是SpringBoot,会有自动配置,使用spring.mvc.contentnegotiation.favor-parameter=true开启之后,就会自动添加ParameterContentNegotiationStrategy
         * 否则只有默认的HeaderContentNegotiationStrategy
         */
        @Override
        public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
            // 基于请求参数的内容协商策略,基于参数的内容协商支持一下这三种响应类型
            Map<String, MediaType> mediaTypes = Map.of(
                    "json", MediaType.APPLICATION_JSON,
                    "xml", MediaType.APPLICATION_XML,
                    // 自定义的响应类型
                    "luck", MediaType.parseMediaType("application/luck")
            );
            // 基于参数的协商策略,可以自定义参数名称,默认为format
            ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
            // 基于请求头的内容协商策略
            HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
            // 配置mvc应用的所有内容协商策略,这些协商策略在请求响应的时候发生作用,客户端和服务端进行内容协商,服务器响应与客户端期望的媒体类型
            configurer.strategies(Arrays.asList(paramStrategy, headerStrategy));
        }

        // 添加自定义的消息内容转换器,与内容协商策略可以搭配使用,完成自定义响应媒体类型的响应
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            // 这个一个自定义类型的案例
            class LuckHttpMessageConverter implements HttpMessageConverter<Luck> {

                @Override
                public boolean canRead(Class<?> clazz, MediaType mediaType) {
                    // 该消息转换器只支持写入,不支持读取,读取的操作交给其他的消息转换器处理
                    return false;
                }

                @Override
                public boolean canWrite(Class<?> clazz, MediaType mediaType) {
                    return clazz.isAssignableFrom(Luck.class);
                }

                @Override
                public List<MediaType> getSupportedMediaTypes() {
                    // 只支持自定义的媒体类型
                    return Collections.singletonList(MediaType.parseMediaType("application/luck"));
                }


                @Override
                public Luck read(Class<? extends Luck> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
                    // 因为当前消息转换器不可读,所以不用处理
                    return null;
                }

                @Override
                public void write(Luck luck, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                    // 当前消息转换器可写,所以在这里处理响应
                    OutputStream stream = outputMessage.getBody();
                    stream.write(luck.toString().getBytes(StandardCharsets.UTF_8));
                }
            }
            converters.add(new LuckHttpMessageConverter());
        }

    }
}

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值