spring mvc 拓展 -- 同一url根據不一樣後綴返回不一樣的視圖

本文代碼已整理上傳 githubhtml

前言
如今新的web項目基本都是先後分離的了, 可是我以前一個項目組頁面所有是用的freemarker模版, 
沒有先後分離, 而後項目也想前端分離, 
因此就有了這個需求, 儘可能不應或者少改後端的代碼,來同時適應前端代碼, 同時儘可能兼容以前的freemarker模版。

而後就開始找解決方案, 找到了spring mvc的ContentNegotiatingViewResolver,
結合項目的使用, 能夠作到同一個url不加後綴就返回以前的html頁面, 加了.json 後綴就返回json數據

下面演示下具體的使用效果, 結合我上一篇文章搭建的web工程做爲示例springweb前端

配置ContentNegotiatingViewResolver

在使用spring mvc時, 在註冊requestMapping的時候, 除了會註冊寫在controller註解上的url,還會註冊url.*到requestMapping, 因此url後面加什麼拓展名都能映射到 原controller 上java

ContentNegotiatingViewResolver 能夠作到根據url後面不一樣的拓展名來返回不一樣的視圖, 固然還能夠根據 mediaType, formmat 參數 進行判斷, 這裏只演示根據拓展名的。git

接着上篇文章中的 SpringMvcConfig 加入下面的配置github

/**
     * 配置多視圖解析器
     *
     * @param manager       manager 會自動構建,configureContentNegotiation能夠進行配置
     * @param viewResolvers 當前項目的 viewResolver, (此時會包含上面配置的 freemarkerViewResolver)
     * @return ContentNegotiatingViewResolver
     * @see WebMvcConfigurerAdapter#configureContentNegotiation(org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer)
     */
    @Bean
    public ContentNegotiatingViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager, List<ViewResolver> viewResolvers) {

        ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver();
        viewResolver.setContentNegotiationManager(manager);

        // 設置默認view, default view 每次都會添加到 真正可用的視圖列表中, json視圖沒有對應的ViewResolver
        View jackson2JsonView = new MappingJackson2JsonView();
        viewResolver.setDefaultViews(Collections.singletonList(jackson2JsonView));

        viewResolver.setViewResolvers(viewResolvers);
        return viewResolver;
    }

就是這麼簡單, 如今你的項目就能夠根據拓展名返回不一樣的視圖了web

PS: json 視圖是將model中的數據經過 jackson(默認)序列化返回spring

運行效果

新建一個實體類, 叫Goods
public class Goods implements Serializable {

    private static final long serialVersionUID = -5018788390786034623L;

    public Goods(String code, String name, Double price) {
        this.code = code;
        this.name = name;
        this.price = price;
    }

    private Long id; // 商品編碼
    private String code; // 編碼
    private String name; // 品名
    private Double price; // 售價

}
新建一個controller, 叫GoodsController
@Controller
    @RequestMapping("/goods")
    public class GoodsController {
    
        private static final List<Goods> GOODS_LIST = new ArrayList<>();
    
        static {
            GOODS_LIST.add(new Goods("998765", "哇哈哈礦泉水", 2.0));
            GOODS_LIST.add(new Goods("568925", "蒙牛真果粒", 4.7));
        }
    
        @RequestMapping("/list")
        public String list(GoodsCondition condition, Model model) {
            model.addAttribute("data", GOODS_LIST);
            return "goods";
        }
    }

這個controller的寫法是咱們項目的通常寫法, 返回String的視圖名稱, 將頁面須要的數據放到model中, 通常都是 data.json

視圖模版文件, goods.ftl, 放到 /resources/templates/ 下
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>商品頁</title>
</head>
<body>
<h3>商品列表</h3>
<hr>
<table>
    <thead>
    <tr>
        <td style="width: 100px">code</td>
        <td style="width: 150px">name</td>
        <td style="width: 100px">price</td>
    </tr>
    </thead>
    <tbody>
    <#list data as item>
    <tr>
        <td>${item.code?html}</td>
        <td>${item.name!?html}</td>
        <td>${item.price!}</td>
    </tr>
    </#list>
    </tbody>
</table>

</body>
</html>

運行項目,
首先瀏覽器 訪問 http://localhost:8080/cat/goods/list, 不加任何後綴效果以下:segmentfault

clipboard.png

返回的是 freemarker的 html 視圖後端

而後訪問 http://localhost:8080/cat/goods/list.jon, 加上.json 後綴

clipboard.png

成功返回json數據!

源碼解析

咱們在配置ContentNegotiatingViewResolver的bean時候自動注入了 兩個參數: ContentNegotiationManager 和 List<ViewResolver>

List<ViewResolver> 咱們本身就配置了一個 freemarkerViewResolver 因此這個參數能注入, 沒有問題, 那麼 ContentNegotiationManager 是怎麼來的?

接觸過 spring mvc 註解配置的同窗知道, spring 提供了WebMvcConfigurer接口供使用者自定義spring mvc 配置,
其實spring mvc有本身的一個配置類DelegatingWebMvcConfiguration來收集用戶自定義配置,並提供一些默認配置

clipboard.png

看到沒, 那個 setConfigurers 方法就是用來收集自定義配置的, 而這個自己也是個配置類, 繼承了WebMvcConfigurationSupport類, spring mvc的各類初始化 就是從這裏開始的

咱們須要的參數ContentNegotiationManager就是定義在WebMvcConfigurationSupport裏的

clipboard.png

並且 當在 classpath 下有 jackson 存在就會添加 json 拓展名映射,
jaxb存在就添加 xml拓展名映射, 很智能

這樣,項目啓動階段就結束了,接下來分析運行階段,

ContentNegotiatingViewResolver自己也ViewResolver, 咱們還定義FreeMarkerViewResolver,二者同時存在, 爲何必定先執行的是ContentNegotiatingViewResolver?

若是以前配置過多視圖共存(volecity, jsp, freemarker)的同窗會知道, ViewResolver是有 order屬性的, 執行的前後是按照order的順序來的, order越小越先執行,

clipboard.png

看下兩個類中order的定義, ContentNegotiatingViewResolver默認是最高優先級的,因此當一個請求走到返回視圖階段, 先執行的是ContentNegotiatingViewResolver

那ContentNegotiatingViewResolver作了什麼事情呢? 先上源碼

@Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
        List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
        if (requestedMediaTypes != null) {
            List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
            View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
            if (bestView != null) {
                return bestView;
            }
        }
        if (this.useNotAcceptableStatusCode) {
            if (logger.isDebugEnabled()) {
                logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
            }
            return NOT_ACCEPTABLE_VIEW;
        }
        else {
            logger.debug("No acceptable view found; returning null");
            return null;
        }
    }

    private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
            throws Exception {

        List<View> candidateViews = new ArrayList<View>();
        for (ViewResolver viewResolver : this.viewResolvers) {
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                candidateViews.add(view);
            }
            for (MediaType requestedMediaType : requestedMediaTypes) {
                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                for (String extension : extensions) {
                    String viewNameWithExtension = viewName + '.' + extension;
                    view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                    if (view != null) {
                        candidateViews.add(view);
                    }
                }
            }
        }
        if (!CollectionUtils.isEmpty(this.defaultViews)) {
            candidateViews.addAll(this.defaultViews);
        }
        return candidateViews;
    }

    private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
        for (View candidateView : candidateViews) {
            if (candidateView instanceof SmartView) {
                SmartView smartView = (SmartView) candidateView;
                if (smartView.isRedirectView()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Returning redirect view [" + candidateView + "]");
                    }
                    return candidateView;
                }
            }
        }
        for (MediaType mediaType : requestedMediaTypes) {
            for (View candidateView : candidateViews) {
                if (StringUtils.hasText(candidateView.getContentType())) {
                    MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
                    if (mediaType.isCompatibleWith(candidateContentType)) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Returning [" + candidateView + "] based on requested media type '" +
                                    mediaType + "'");
                        }
                        attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
                        return candidateView;
                    }
                }
            }
        }
        return null;
    }

上面這段就是ContentNegotiatingViewResolver如何處理視圖的邏輯
requestedMediaTypes 就是拿到根據請求拿到須要的 MediaType,
而後就是根據 requestedMediaTypes 找到全部可用的 View,
咱們在配置ContentNegotiatingViewResolver裏設置的 default view
每次都會出如今 candidateViews 中, 可是是在最後加上的。
而後就是在 candidateViews 中找到 bestView 返回

當咱們不加後綴時,沒法根據後綴映射 MediaType, 因此requestedMediaTypes就是
candidateViews 就是freemarker的 view 加上設置的 默認的 json view.
在getBestView的時候 requestedMediaTypes 和 freemarkerView的content-type: text/html匹配, 因此就返回了freemarkerView。

當加了.json後綴, 根據以前的拓展名和MediaType的映射, requestedMediaTypes是 application/json
candidateViews 依然是freemarker的 view 加上設置的 默認的 json view.
可是此次freemarkerView的content-type不匹配, 而是和json view的 application/json 匹配, 因此返回 json 視圖。

後續

這樣新的前端項目在訪問url加上.json後綴就能拿到json數據了,
可是還有一個問題, 就是在接收請求參數時, 老的項目全是 form表單提交, 可是新的前端項目須要以json格式提交, 若是在controller中的方法中 都加上 @RequestBody 註解, 工做量很多, 並且不兼容form表單提交, 因此這樣不可行。

下一篇講下controller接收參數時如何自動判斷是 form 提交仍是 json 數據提交, 從而使用不一樣的參數處理器 來接收參數

相關文章
相關標籤/搜索