本文代碼已整理上傳
githubhtml
前言
如今新的web項目基本都是先後分離的了, 可是我以前一個項目組頁面所有是用的freemarker模版, 沒有先後分離, 而後項目也想前端分離, 因此就有了這個需求, 儘可能不應或者少改後端的代碼,來同時適應前端代碼, 同時儘可能兼容以前的freemarker模版。 而後就開始找解決方案, 找到了spring mvc的ContentNegotiatingViewResolver, 結合項目的使用, 能夠作到同一個url不加後綴就返回以前的html頁面, 加了.json 後綴就返回json數據
下面演示下具體的使用效果, 結合我上一篇文章搭建的web工程做爲示例springweb前端
在使用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
返回的是 freemarker的 html 視圖後端
而後訪問 http://localhost:8080/cat/goods/list.jon
, 加上.json 後綴
成功返回json數據!
咱們在配置ContentNegotiatingViewResolver的bean時候自動注入了 兩個參數: ContentNegotiationManager 和 List<ViewResolver>
List<ViewResolver> 咱們本身就配置了一個 freemarkerViewResolver 因此這個參數能注入, 沒有問題, 那麼 ContentNegotiationManager 是怎麼來的?
接觸過 spring mvc 註解配置的同窗知道, spring 提供了WebMvcConfigurer接口供使用者自定義spring mvc 配置,
其實spring mvc有本身的一個配置類DelegatingWebMvcConfiguration
來收集用戶自定義配置,並提供一些默認配置
看到沒, 那個 setConfigurers 方法就是用來收集自定義配置的, 而這個自己也是個配置類, 繼承了WebMvcConfigurationSupport類, spring mvc的各類初始化 就是從這裏開始的
咱們須要的參數ContentNegotiationManager就是定義在WebMvcConfigurationSupport裏的
並且 當在 classpath 下有 jackson 存在就會添加 json 拓展名映射,
jaxb存在就添加 xml拓展名映射, 很智能
這樣,項目啓動階段就結束了,接下來分析運行階段,
ContentNegotiatingViewResolver自己也ViewResolver, 咱們還定義FreeMarkerViewResolver,二者同時存在, 爲何必定先執行的是ContentNegotiatingViewResolver?
若是以前配置過多視圖共存(volecity, jsp, freemarker)的同窗會知道, ViewResolver是有 order屬性的, 執行的前後是按照order的順序來的, order越小越先執行,
看下兩個類中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 數據提交, 從而使用不一樣的參數處理器 來接收參數