热门关键字:
jquery > jquery教程 > java > Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】

Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】

439
作者:管理员
发布时间:2020/4/26 15:39:12
评论数:0
转载请自觉注明原文:http://www.jq-school.com/Show.aspx?id=1455

每篇一句

在绝对力量面前,一切技巧都是浮云

前言

上文 介绍了Http内容协商的一些概念,以及Spring MVC内置的4种协商方式使用介绍。本文主要针对Spring MVC内容协商方式:从步骤、原理层面理解,最后达到通过自己来扩展协商方式效果。

首先肯定需要介绍的,那必然就是Spring MVC的默认支持的四大协商策略的原理分析喽:

ContentNegotiationStrategy

该接口就是Spring MVC实现内容协商的策略接口:

// A strategy for resolving the requested media types for a request.
// @since 3.2
@FunctionalInterface
public interface ContentNegotiationStrategy {
    // @since 5.0.5
    List<MediaType> MEDIA_TYPE_ALL_LIST = Collections.singletonList(MediaType.ALL);

    // 将给定的请求解析为媒体类型列表
    // 返回的 List 首先按照 specificity 参数排序,其次按照 quality 参数排序
    // 如果请求的媒体类型不能被解析则抛出 HttpMediaTypeNotAcceptableException 异常
    List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException;
}

说白了,这个策略接口就是想知道客户端的请求需要什么类型(MediaType)的数据List。从 上文 我们知道Spring MVC它支持了4种不同的协商机制,它都和此策略接口相关的。
它的继承树:
在这里插入图片描述
从实现类的名字上就能看出它和上文提到的4种方式恰好是一一对应着的(ContentNegotiationManager除外)。

Spring MVC默认加载两个该策略接口的实现类:
ServletPathExtensionContentNegotiationStrategy-->根据文件扩展名(支持RESTful)。
HeaderContentNegotiationStrategy-->根据HTTP Header里的Accept字段(支持Http)。

HeaderContentNegotiationStrategy

Accept Header解析:它根据请求头Accept来协商�时候效果是一样的,但更符合使用规范 @Override @Nullable protected String getMediaTypeKey(NativeWebRequest request) { return request.getParameter(getParameterName()); } }

根据一个查询参数(query parameter)判断请求的MediaType,该查询参数缺省使用format。

需要注意的是:基于param的此策略Spring MVC虽然支持,但默认是木有开启的,若想使用需要手动显示开启

PathExtensionContentNegotiationStrategy

它的扩展名需要从Path里面分析出来。

public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {

    private UrlPathHelper urlPathHelper = new UrlPathHelper();

    // 它额外提供了一个空构造
    public PathExtensionContentNegotiationStrategy() {
        this(null);
    }
    // 有参构造
    public PathExtensionContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) {
        super(mediaTypes);
        setUseRegisteredExtensionsOnly(false);
        setIgnoreUnknownExtensions(true); // 注意:这个值设置为了true
        this.urlPathHelper.setUrlDecode(false); // 不需要解码(url请勿有中文)
    }

    // @since 4.2.8  可见Spring MVC允许你自己定义解析的逻辑
    public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
        this.urlPathHelper = urlPathHelper;
    }


    @Override
    @Nullable
    protected String getMediaTypeKey(NativeWebRequest webRequest) {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        if (request == null) {
            return null;
        }

        // 借助urlPathHelper、UriUtils从URL中把扩展名解析出来
        String path = this.urlPathHelper.getLookupPathForRequest(request);
        String extension = UriUtils.extractFileExtension(path);
        return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null);
    }

    // 子类ServletPathExtensionContentNegotiationStrategy有使用和复写
    // 它的作用是面向Resource找到这个资源对应的MediaType ~
    @Nullable
    public MediaType getMediaTypeForResource(Resource resource) { ... }
}

根据请求URL路径中所请求的文件资源的扩展名部分判断请求的MediaType(借助UrlPathHelper和UriUtils解析URL)。

ServletPathExtensionContentNegotiationStrategy

它是对PathExtensionContentNegotiationStrategy的扩展,和Servlet容器有关了。因为Servlet额外提供了这个方法:ServletContext#getMimeType(String)来处理文件的扩展名问题。

public class ServletPathExtensionContentNegotiationStrategy extends PathExtensionContentNegotiationStrategy {
    private final ServletContext servletContext;
    ... // 省略构造函数

    // 一句话:在去工厂找之前,先去this.servletContext.getMimeType("file." + extension)这里找一下,找到就直接返回。否则再进工厂
    @Override
    @Nullable
    protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension) throws HttpMediaTypeNotAcceptableException { ... }

    //  一样的:先this.servletContext.getMimeType(resource.getFilename()) 再交给父类处理
    @Override
    public MediaType getMediaTypeForResource(Resource resource) { ... }

    // 两者调用父类的条件都是:mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)
}

说明:ServletPathExtensionContentNegotiationStrategy是Spring MVC默认就开启支持的策略,无需手动开启。

FixedContentNegotiationStrategy

固定类型解析:返回固定的MediaType。

public class FixedContentNegotiationStrategy implements ContentNegotiationStrategy {
    private final List<MediaType> contentTypes;

    // 构造函数:必须指定MediaType
    // 一般通过@RequestMapping.produces这个注解属性指定(可指定多个)
    public FixedContentNegotiationStrategy(MediaType contentType) {
        this(Collections.singletonList(contentType));
    }
    // @since 5.0
    public FixedContentNegotiationStrategy(List<MediaType> contentTypes) {
        this.contentTypes = Collections.unmodifiableList(contentTypes);
    }
}

固定参数类型非常简单,构造函数传进来啥返回啥(不能为null)。


==ContentNegotiationManager==

介绍完了上面4中协商策略,开始介绍这个协商"容器"。
这个管理器它的作用特别像之前讲述的xxxComposite这种“容器”管理类,总体思想是管理、委托,有了之前的基础了解起他还是非常简单的了。

//  它不仅管理一堆strategies(List),还管理一堆resolvers(Set)
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
    private final List<ContentNegotiationStrategy> strategies = new ArrayList<>();
    private final Set<MediaTypeFileExtensionResolver> resolvers = new LinkedHashSet<>();
    
    ...
    // 若没特殊指定,至少是包含了这一种的策略的:HeaderContentNegotiationStrategy
    public ContentNegotiationManager() {
        this(new HeaderContentNegotiationStrategy());
    }
    ... // 因为比较简单,所以省略其它代码
}

它是一个ContentNegotiationStrategy容器,同时也是一个MediaTypeFileExtensionResolver容器。自身同时实现了这两个接口。

ContentNegotiationManagerFactoryBean

顾名思义,它是专门用于来创建一个ContentNegotiationManager的FactoryBean。

// @since 3.2  还实现了ServletContextAware,可以得到当前servlet容器上下文
public class ContentNegotiationManagerFactoryBean implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
    
    // 默认就是开启了对后缀的支持的
    private boolean favorPathExtension = true;
    // 默认没有开启对param的支持
    private boolean favorParameter = false;
    // 默认也是开启了对Accept的支持的
    private boolean ignoreAcceptHeader = false;

    private Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
    private boolean ignoreUnknownPathExtensions = true;
    // Jaf是一个数据处理框架,可忽略
    private Boolean useJaf;
    private String parameterName = "format";
    private ContentNegotiationStrategy defaultNegotiationStrategy;
    private ContentNegotiationManager contentNegotiationManager;
    private ServletContext servletContext;
    ... // 省略普通的get/set

    // 注意这里传入的是:Properties  表示后缀和MediaType的对应关系
    public void setMediaTypes(Properties mediaTypes) {
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            for (Entry<Object, Object> entry : mediaTypes.entrySet()) {
                String extension = ((String)entry.getKey()).toLowerCase(Locale.ENGLISH);
                MediaType mediaType = MediaType.valueOf((String) entry.getValue());
                this.mediaTypes.put(extension, mediaType);
            }
        }
    }
    public void addMediaType(String fileExtension, MediaType mediaType) {
        this.mediaTypes.put(fileExtension, mediaType);
    }
    ...
    
    // 这里面处理了很多默认逻辑
    @Override
    public void afterPropertiesSet() {
        List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();

        // 默认favorPathExtension=true,所以是支持path后缀模式的
        // servlet环境使用的是ServletPathExtensionContentNegotiationStrategy,否则使用的是PathExtensionContentNegotiationStrategy
        // 
        if (this.favorPathExtension) {
            PathExtensionContentNegotiationStrategy strategy;
            if (this.servletContext != null && !isUseJafTurnedOff()) {
                strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
            } else {
                strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
            }
            strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
            if (this.useJaf != null) {
                strategy.setUseJaf(this.useJaf);
            }
            strategies.add(strategy);
        }

        // 默认favorParameter=false 木有开启滴
        if (this.favorParameter) {
            ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
            strategy.setParameterName(this.parameterName);
            strategies.add(strategy);
        }

        // 注意这前面有个!,所以默认Accept也是支持的
        if (!this.ignoreAcceptHeader) {
            strategies.add(new HeaderContentNegotiationStrategy());
        }

        // 若你喜欢,你可以设置一个defaultNegotiationStrategy  最终也会被add进去
        if (this.defaultNegotiationStrategy != null) {
            strategies.add(this.defaultNegotiationStrategy);
        }

        // 这部分我需要提醒注意的是:这里使用的是ArrayList,所以你add的顺序就是u最后的执行顺序
        // 所以若你指定了defaultNegotiationStrategy,它也是放到最后的
        this.contentNegotiationManager = new ContentNegotiationManager(strategies);
    }

    // 三个接口方法
    @Override
    public ContentNegotiationManager getObject() {
        return this.contentNegotiationManager;
    }
    @Override
    public Class<?> getObjectType() {
        return ContentNegotiationManager.class;
    }
    @Override
    public boolean isSingleton() {
        return true;
    }
}

这里解释了 该文 的顺序(后缀 > 请求参数 > HTTP首部Accept)现象。Spring MVC是通过它来创建ContentNegotiationManager进而管理协商策略的。

内容协商的配置:ContentNegotiationConfigurer

虽然说默认情况下Spring开启的协商支持能覆盖我们绝大部分应用场景了,但不乏有的时候我们也还是需要对它进行个性化的,那么这部分就讲解下对它的个性化配置~

ContentNegotiationConfigurer

它用于"收集"配置项,根据你提供的配置项来创建出一个ContentNegotiationManager。

public class ContentNegotiationConfigurer {

    private final ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
    private final Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();

    public ContentNegotiationConfigurer(@Nullable ServletContext servletContext) {
        if (servletContext != null) {
            this.factory.setServletContext(servletContext);
        }
    }
    // @since 5.0
    public void strategies(@Nullable List<ContentNegotiationStrategy> strategies) {
        this.factory.setStrategies(strategies);
    }
    ...
    public ContentNegotiationConfigurer defaultContentTypeStrategy(ContentNegotiationStrategy defaultStrategy) {
        this.factory.setDefaultContentTypeStrategy(defaultStrategy);
        return this;
    }

    // 手动创建出一个ContentNegotiationManager 此方法是protected 
    // 唯一调用处是:WebMvcConfigurationSupport
    protected ContentNegotiationManager buildContentNegotiationManager() {
        this.factory.addMediaTypes(this.mediaTypes);
        return this.factory.build();
    }
}

ContentNegotiationConfigurer可以认为是提供一个设置ContentNegotiationManagerFactoryBean的入口(自己内容new了一个它的实例),最终交给WebMvcConfigurationSupport向容器内注册这个Bean:

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
    ...
    // 请注意是BeanName为:mvcContentNegotiationManager
    // 若实在有需要,你是可以覆盖的~~~~
    @Bean
    public ContentNegotiationManager mvcContentNegotiationManager() {
        if (this.contentNegotiationManager == null) {
            ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
            configurer.mediaTypes(getDefaultMediaTypes()); // 服务端默认支持的后缀名-->MediaType们~~~

            // 这个方法就是回调我们自定义配置的protected方法~~~~
            configureContentNegotiation(configurer);
        
            // 调用方法生成一个管理器
            this.contentNegotiationManager = configurer.buildContentNegotiationManager();
        }
        return this.contentNegotiationManager;
    }


    // 默认支持的协商MediaType们~~~~
    protected Map<String, MediaType> getDefaultMediaTypes() {
        Map<String, MediaType> map = new HashMap<>(4);
        // 几乎不用
        if (romePresent) {
            map.put("atom", MediaType.APPLICATION_ATOM_XML);
            map.put("rss", MediaType.APPLICATION_RSS_XML);
        }
        // 若导了jackson对xml支持的包,它就会被支持
        if (jaxb2Present || jackson2XmlPresent) {
            map.put("xml", MediaType.APPLICATION_XML);
        }
        // jackson.databind就支持json了,所以此处一般都是满足的
        // 额外还支持到了gson和jsonb。希望不久将来内置支持fastjson
        if (jackson2Present || gsonPresent || jsonbPresent) {
            map.put("json", MediaType.APPLICATION_JSON);
        }
        if (jackson2SmilePresent) {
            map.put("smile", MediaType.valueOf("application/x-jackson-smile"));
        }
        if (jackson2CborPresent) {
            map.put("cbor", MediaType.valueOf("application/cbor"));
        }
        return map;
    }
    ...
}

Tips:WebMvcConfigurationSupport是@EnableWebMvc导进去的。

配置实践

有了上面理论的支撑,那么使用Spring MVC协商的最佳实践配置可参考如下(大多数情况下都无需配置):

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorParameter(true)
        //.parameterName("mediaType")
        //.defaultContentTypeStrategy(new ...) // 自定义一个默认的内容协商策略
        //.ignoreAcceptHeader(true) // 禁用Accept协商方式
        //.defaultContentType(MediaType.APPLICATION_JSON) // 它的效果是new FixedContentNegotiationStrategy(contentTypes)  增加了对固定策略的支
        //.strategies(list);
        //.useRegisteredExtensionsOnly() //PathExtensionContentNegotiationStrategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
        ;
    }
}

总结

本文从原理上分析了Spring MVC对内容协商策略的管理、使用以及开放的配置,旨在做到心中有数,从而更好、更安全、更方便的进行扩展,对下文内容协商视图的理解有非常大的帮助作用,有兴趣的可持续关注~

相关阅读

ContentNegotiation内容协商机制(一)---Spring MVC内置支持的4种内容协商方式【享学Spring MVC】
ContentNegotiation内容协商机制(二)---Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】
ContentNegotiation内容协商机制(三)---在视图View上的应用:ContentNegotiatingViewResolver深度解析【享学Spring MVC】

知识交流

==The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~==





如果您觉得本文的内容对您的学习有所帮助:支付鼓励



关键字:Spring MVC
友荐云推荐