Spring MVC內容協商實現原理及自定義配置【享學Spring MVC】

每篇一句

在絕對力量面前,一切技巧都是浮雲

前言

上文 介紹了Http內容協商的一些概念,以及Spring MVC內置的4種協商方式使用介紹。本文主要針對Spring MVC內容協商方式:從步驟、原理層面理解,最後達到經過本身來擴展協商方式效果。html

首先確定須要介紹的,那必然就是Spring MVC的默認支持的四大協商策略的原理分析嘍:java

ContentNegotiationStrategy

該接口就是Spring MVC實現內容協商的策略接口:web

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

Spring MVC默認加載兩個該策略接口的實現類:
ServletPathExtensionContentNegotiationStrategy-->根據文件擴展名(支持RESTful)。
HeaderContentNegotiationStrategy-->根據 HTTP Header裏的 Accept字段(支持Http)。

HeaderContentNegotiationStrategy

Accept Header解析:它根據請求頭Accept來協商。json

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

能夠看到,若是沒有傳遞Accept,則默認使用MediaType.ALL 也就是*/*segmentfault

AbstractMappingContentNegotiationStrategy

經過file extension/query param來協商的抽象實現類。在瞭解它以前,有必要先插隊先了解MediaTypeFileExtensionResolver它的做用:瀏覽器



MediaTypeFileExtensionResolverMediaType和路徑擴展名解析策略的接口,例如將 .json 解析成 application/json 或者反向解析安全

// @since 3.2
public interface MediaTypeFileExtensionResolver {

    // 根據指定的mediaType返回一組文件擴展名
    List<String> resolveFileExtensions(MediaType mediaType);
    // 返回該接口註冊進來的全部的擴展名
    List<String> getAllFileExtensions();
}

繼承樹以下:
在這裏插入圖片描述
顯然,本處只須要講解它的直接實現子類MappingMediaTypeFileExtensionResolver便可:架構

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

此抽象類維護一些Map以及提供操做的方法,它維護了一個文件擴展名和MediaType的雙向查找表。擴展名和MediaType的對應關係:併發

  1. 一個MediaType對應N個擴展名
  2. 一個擴展名最多隻會屬於一個MediaType~


繼續回到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());
    }
}

該抽象類實現了模版處理流程。
由子類去決定:你的擴展名是來自於URL的參數仍是來自於path...

ParameterContentNegotiationStrategy

上面抽象類的子類具體實現,從名字中能看出擴展名來自於param參數。

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

根據一個查詢參數(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(藉助UrlPathHelperUriUtils解析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)
}

說明:ServletPathExtensionContentNegotiationStrategySpring 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

顧名思義,它是專門用於來建立一個ContentNegotiationManagerFactoryBean

// @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:若是以爲本文對你有幫助,不妨點個讚唄。固然分享到你的朋友圈讓更多小夥伴看到也是被做者本人許可的~==

**若對技術內容感興趣能夠加入wx羣交流:Java高工、架構師3羣
若羣二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。而且備註:"java入羣" 字樣,會手動邀請入羣**==若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入羣一塊兒飛==

相關文章
相關標籤/搜索