Spring MVC content negotiation the principle and learn to enjoy the custom configuration [Spring MVC]

Each one

In the face of absolute power, all skills are the clouds

Foreword

Above describes some of the concepts Http content negotiation, as well as Spring MVCfour kinds of negotiation built on the use. This paper mainly for Spring MVCcontent negotiation: from step to understand the level of principle, and finally reached to extend the negotiation results by themselves.

First need to introduce affirmative, that is bound to Spring MVCthe principle of tacit support of the four major negotiation strategy analysis myself:

ContentNegotiationStrategy

This interface is Spring MVCfor content negotiation strategy interfaces:

// 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;
}

To put it plainly, this strategy interfaces just want to know what the needs of the requesting client type ( MediaType) data List. From the above, we know that Spring MVCit supports four different consultation mechanisms, which are associated with the interface, and this policy.
Its inheritance tree:
Here Insert Picture Description
from the implementation class name can be seen in four ways mentioned above, and it happens to be one to one correspondence with the ( ContentNegotiationManagerexception).

Spring MVCThe default implementation class is loaded two of the strategy interface:
ServletPathExtensionContentNegotiationStrategy-> according to the file extension (support RESTful).
HeaderContentNegotiationStrategy-> According to HTTP Headeryears of Acceptfield (support Http).

HeaderContentNegotiationStrategy

Accept HeaderAnalysis: It is based on the request header Acceptto negotiate.

public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
    
        // 我的Chrome浏览器值是:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3]
        // postman的值是:[*/*]
        String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
        if (headerValueArray == null) {
            return MEDIA_TYPE_ALL_LIST;
        }

        List<String> headerValues = Arrays.asList(headerValueArray);
        try {
            List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
            // 排序
            MediaType.sortBySpecificityAndQuality(mediaTypes);
            // 最后Chrome浏览器的List如下:
            // 0 = {MediaType@6205} "text/html"
            // 1 = {MediaType@6206} "application/xhtml+xml"
            // 2 = {MediaType@6207} "image/webp"
            // 3 = {MediaType@6208} "image/apng"
            // 4 = {MediaType@6209} "application/signed-exchange;v=b3"
            // 5 = {MediaType@6210} "application/xml;q=0.9"
            // 6 = {MediaType@6211} "*/*;q=0.8"
            return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
        } catch (InvalidMediaTypeException ex) {
            throw new HttpMediaTypeNotAcceptableException("Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
        }
    }
}

You can see, if not passed Accept, the default is to use MediaType.ALL*/*

AbstractMappingContentNegotiationStrategy

By file extension/ query paramto negotiate the abstract implementation class. Before looking at it, it is necessary to jump the queue to understand MediaTypeFileExtensionResolverits role:

---

MediaTypeFileExtensionResolver: MediaTypeAnd path extension resolution policy interface, for example, .jsonresolves application/jsonor reverse analysis

// @since 3.2
public interface MediaTypeFileExtensionResolver {

    // 根据指定的mediaType返回一组文件扩展名
    List<String> resolveFileExtensions(MediaType mediaType);
    // 返回该接口注册进来的所有的扩展名
    List<String> getAllFileExtensions();
}

Inheritance tree as follows:
Here Insert Picture Description
Obviously, the department only needs to explain its immediate implementation subclasses MappingMediaTypeFileExtensionResolvercan:

MappingMediaTypeFileExtensionResolver
public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExtensionResolver {

    // key是lowerCaseExtension,value是对应的mediaType
    private final ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<>(64);
    // 和上面相反,key是mediaType,value是lowerCaseExtension(显然用的是多值map)
    private final MultiValueMap<MediaType, String> fileExtensions = new LinkedMultiValueMap<>();
    // 所有的扩展名(List非set哦~)
    private final List<String> allFileExtensions = new ArrayList<>();

    ...
    public Map<String, MediaType> getMediaTypes() {
        return this.mediaTypes;
    }
    // protected 方法
    protected List<MediaType> getAllMediaTypes() {
        return new ArrayList<>(this.mediaTypes.values());
    }
    // 给extension添加一个对应的mediaType
    // 采用ConcurrentMap是为了避免出现并发情况下导致的一致性问题
    protected void addMapping(String extension, MediaType mediaType) {
        MediaType previous = this.mediaTypes.putIfAbsent(extension, mediaType);
        if (previous == null) {
            this.fileExtensions.add(mediaType, extension);
            this.allFileExtensions.add(extension);
        }
    }

    // 接口方法:拿到指定的mediaType对应的扩展名们~
    @Override
    public List<String> resolveFileExtensions(MediaType mediaType) {
        List<String> fileExtensions = this.fileExtensions.get(mediaType);
        return (fileExtensions != null ? fileExtensions : Collections.emptyList());
    }
    @Override
    public List<String> getAllFileExtensions() {
        return Collections.unmodifiableList(this.allFileExtensions);
    }

    // protected 方法:根据扩展名找到一个MediaType~(当然可能是找不到的)
    @Nullable
    protected MediaType lookupMediaType(String extension) {
        return this.mediaTypes.get(extension.toLowerCase(Locale.ENGLISH));
    }
}

This abstract class maintains some of the Map and a method for operation, it maintains a file extension and MediaTypetwo-way lookup table. Extension and MediaTypecorrespondence between:

  1. A MediaTypecorresponds to the extension of the N
  2. An extension it will only belong to a MediaType ~


    Continue to return AbstractMappingContentNegotiationStrategy.
// @since 3.2 它是个协商策略抽象实现,同时也有了扩展名+MediaType对应关系的能力
public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver implements ContentNegotiationStrategy {

    // Whether to only use the registered mappings to look up file extensions,
    // or also to use dynamic resolution (e.g. via {@link MediaTypeFactory}.
    // org.springframework.http.MediaTypeFactory是Spring5.0提供的一个工厂类
    // 它会读取/org/springframework/http/mime.types这个文件,里面有记录着对应关系
    private boolean useRegisteredExtensionsOnly = false;
    // Whether to ignore requests with unknown file extension. Setting this to
    // 默认false:若认识不认识的扩展名,抛出异常:HttpMediaTypeNotAcceptableException
    private boolean ignoreUnknownExtensions = false;

    // 唯一构造函数
    public AbstractMappingContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) {
        super(mediaTypes);
    }

    // 实现策略接口方法
    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
        // getMediaTypeKey:抽象方法(让子类把扩展名这个key提供出来)
        return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
    }

    public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException {
        if (StringUtils.hasText(key)) {
            // 调用父类方法:根据key去查找出一个MediaType出来
            MediaType mediaType = lookupMediaType(key); 
            // 找到了就return就成(handleMatch是protected的空方法~~~  子类目前没有实现的)
            if (mediaType != null) {
                handleMatch(key, mediaType); // 回调
                return Collections.singletonList(mediaType);
            }

            // 若没有对应的MediaType,交给handleNoMatch处理(默认是抛出异常,见下面)
            // 注意:handleNoMatch如果通过工厂找到了,那就addMapping()保存起来(相当于注册上去)
            mediaType = handleNoMatch(webRequest, key);
            if (mediaType != null) {
                addMapping(key, mediaType);
                return Collections.singletonList(mediaType);
            }
        }
        return MEDIA_TYPE_ALL_LIST; // 默认值:所有
    }

    // 此方法子类ServletPathExtensionContentNegotiationStrategy有复写
    @Nullable
    protected MediaType handleNoMatch(NativeWebRequest request, String key) throws HttpMediaTypeNotAcceptableException {

        // 若不是仅仅从注册里的拿,那就再去MediaTypeFactory里看看~~~  找到了就返回
        if (!isUseRegisteredExtensionsOnly()) {
            Optional<MediaType> mediaType = MediaTypeFactory.getMediaType("file." + key);
            if (mediaType.isPresent()) {
                return mediaType.get();
            }
        }

        // 忽略找不到,返回null吧  否则抛出异常:HttpMediaTypeNotAcceptableException
        if (isIgnoreUnknownExtensions()) {
            return null;
        }
        throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
    }
}

The abstract class implements a process flow template.
Go to the subclass: Your extension is a parameter from the URL, or from the path ...

ParameterContentNegotiationStrategy

Subclasses implement particular abstract class above, it can be seen from the name extension from param parameters.

public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
    // 请求参数默认的key是format,你是可以设置和更改的。(set方法)
    private String parameterName = "format";

    // 唯一构造
    public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
        super(mediaTypes);
    }
    ... // 生路get/set

    // 小Tips:这里调用的是getParameterName()而不是直接用属性名,以后建议大家设计框架也都这么使用 虽然很多时候效果是一样的,但更符合使用规范
    @Override
    @Nullable
    protected String getMediaTypeKey(NativeWebRequest request) {
        return request.getParameter(getParameterName());
    }
}

According to a query parameter (query parameter) determination request MediaType, the query parameters used by default format.

Note that: param based on this strategy Spring MVC, while supporting, but the default is turned wood, if you want to use the need to manually open the display

PathExtensionContentNegotiationStrategy

Its extension need to be analyzed from the Path inside.

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) { ... }
}

The request URLpath requested file extension resource portion determination request MediaType(via UrlPathHelperand UriUtilsparsing URL).

ServletPathExtensionContentNegotiationStrategy

It is PathExtensionContentNegotiationStrategyan extension and Servletcontainer related to the. Because Servletadditional offers this method: ServletContext#getMimeType(String)to deal with the issue of file extensions.

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)
}

Description: ServletPathExtensionContentNegotiationStrategyis the Spring MVCdefault policy on open support, without the need to manually open.

FixedContentNegotiationStrategy

Type of analysis: the return 固定of 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);
    }
}

Fixed parameter type is very simple, the constructor returns the passed in Han Han (never null).


==ContentNegotiationManager==

Negotiation strategies introduced over 4 above, began to introduce this negotiation "container."
This is about the manager before its role especially like xxxCompositethis "container" management class, the whole idea is to manage, delegate, the foundation had before he was starting to understand the very simple.

//  它不仅管理一堆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());
    }
    ... // 因为比较简单,所以省略其它代码
}

It is a ContentNegotiationStrategycontainer, but also a MediaTypeFileExtensionResolvercontainer. Itself while achieving both of these interfaces.

ContentNegotiationManagerFactoryBean

As the name suggests, it is dedicated to creating a ContentNegotiationManager's 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;
    }
}

Here to explain the text of the order ( 后缀 > 请求参数 > HTTP首部Accept) phenomenon. Spring MVCIt was created by ContentNegotiationManagerthen management negotiation strategy.

Content negotiation configuration:ContentNegotiationConfigurer

Although the default Springopen consultation support we can cover most of the scenarios, but there is no lack sometimes we still need to personalize it, then it would explain at this part of the customized configuration -

ContentNegotiationConfigurer

It is used to "collect" items, to create a configuration item based on your offer 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();
    }
}

ContentNegotiationConfigurerIt may be considered to provide a set ContentNegotiationManagerFactoryBeanentry (content themselves with a new instance of it), and ultimately to WebMvcConfigurationSupportregister the Bean into the container:

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: WebMvcConfigurationSupporta @EnableWebMvcguide go.

Configuring practice

With the support of the above theory, the use of Spring MVCnegotiation best practices configuration, refer to the following (in most cases no configuration):

@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);
        ;
    }
}

to sum up

This paper analyzes from the principle of the Spring MVCmanagement of content negotiation strategies, use and open configurations designed to be aware of in order to better, safer and more convenient to extend consultations to understand the view of the following contents have a very large helpful, interested sustainable attention ~

Related Reading

ContentNegotiation content negotiation mechanisms (a) --- Spring MVC 4 Zhong built-in support content negotiation [enjoy learning Spring MVC]
ContentNegotiation content negotiation (two) --- Spring MVC content negotiation the principle and learn to enjoy the custom configuration [Spring MVC]
ContentNegotiation content negotiation mechanisms (c) application --- in view view of: ContentNegotiatingViewResolver depth analysis [enjoy learning Spring MVC]

Knowledge Exchange

== The last: If you think this article helpful to you, may wish to point a praise chant. Of course, your circle of friends to share so that more small partners also are seeing 作者本人许可的~==

If interested in technology content can join the group wx exchange: Java高工、架构师3群.
If the group fails two-dimensional code, please add wx number: fsx641385712(or two-dimensional code is scanned beneath wx). And Note: "java入群"the word, will be invited into the group manually

== If for Spring, SpringBoot, MyBatis source code analysis and other interested can add me wx: fsx641385712, manually invite you into the group took off ==

Guess you like

Origin www.cnblogs.com/fangshixiang/p/11420805.html