1.前言
SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧
本文将通过源码(基于Spring4.3.7)分析,弄清楚SpringMVC如何完成视图解析的
2.源码分析
在SpringMVC源码阅读:拦截器分析过doDispatch的运行过程,这里再分析一遍
回到DispatcherServlet类的doDispatch方法,看看doDispatch如何获取ModelAndView
HandlerMapping根据request获得HandlerExecutionChain
根据HandlerExecutionChain获取HandlerAdapter
HandlerAdapter根据request,response和HandlerExecutionChain调用handle方法返回ModelAndView
然后交由processDispatchResult处理
1023行检测从doDispatch方法运行到此是否有异常
1037行根据ModelAndView渲染视图
1259根据视图名称解析成View对象
1282行调用AbstractView的render方法进行渲染
点进去,这里以解析ftl做测试
301行创建包含动态值和静态属性的Map
302为待渲染的响应做准备
303行调用子类实现方法渲染这个包含动态值和静态属性的Map
接下来会进入AbstractTemplateView的renderMergedOutputModel方法,AbstractTemplateView是AbstractView的子类
AbstractTemplateView的renderMergedOutputModel方法会调用内部renderMergedTemplateModel方法,该方法被子类FreemarkerView实现
打开FreemarkerView的renderMergedTemplateModel方法
进入doRender方法,该方法根据response渲染FreeMarker视图
275行exposeModelAsRequestAttributes方法在父类AbstractView被实现,该方法把Model设置到request中
277行创建Freemarker模板模型
284行处理、加工Freemarker模板给response
再看看FreeMarkerViewResolver做了什么,打开类接口继承图
构造函数设置了前缀和后缀,再看看AbstractTemplateViewResolver
104~108行给视图设置属性
我们发现103行super引用了父类,点开进入UrlBasedViewResolver的buildView方法
532行拼接前缀+视图名称+后缀,例如"employee/form.ftl",并设置url
534~553行判断属性是否为空,非空则设置到View中
再看createView方法
460行如果以"redirect:"开头,交给RedirectView处理
467行如果以"forward:"开头,交给InternalResourceView处理
Freemarker解析逻辑至此分析完毕
现在看JSP的解析流程
回到AbstractView类的render方法,刚刚我们分析过
304行此时renderMergedOutputModel方法被子类InternalResourceView实现
139行将Model设置到Request中
142将辅助性参数设置到Request中
145行获取视图文件路径
148行获取请求分发器
155到170行如果response已经提交,则把资源文件纳入到response中,否则调用forward方法转发
再看下InternalResourceViewResolver,该类辅助InternalResourceView,为其设置属性,其余的ViewResolver的实现类分析同Freemarker
3.自定义视图解析
3.1 自定义支持Freemarker和Jsp的视图解析器
自定义视图解析器,"jsp:"开头构造InternalResourceView解析jsp,以"freemarker:"开头则构造FreemarkerView解析ftl
这样做的好处是可以区分同名的jsp和ftl
public class CustomViewResolver extends UrlBasedViewResolver { public static final String JSP_URL_PREFIX = "jsp:"; public static final String FTL_URL_PREFIX = "freemarker:"; private static final boolean jstlPresent = ClassUtils.isPresent( "javax.servlet.jsp.jstl.core.Config", CustomViewResolver.class.getClassLoader()); private Boolean exposePathVariables = false; private boolean exposeRequestAttributes = false; private boolean allowRequestOverride = false; private boolean exposeSessionAttributes = false; private boolean allowSessionOverride = false; private boolean exposeSpringMacroHelpers = true; public CustomViewResolver() { this.setViewClass(FreeMarkerView.class); } @Override protected AbstractUrlBasedView buildView(String viewName) throws Exception { if(viewName.startsWith(FTL_URL_PREFIX)) { return buildFreemarkerView(viewName.substring(FTL_URL_PREFIX.length())); } else if(viewName.startsWith(JSP_URL_PREFIX)) { Class viewCls = jstlPresent ? JstlView.class : InternalResourceView.class; return build(viewCls, viewName.substring(JSP_URL_PREFIX.length()), getPrefix(), ".jsp"); } else { //默认以freemarker处理 return buildFreemarkerView(viewName); } } /** * @Author 谷天乐 * @Description 使用UrlBasedViewResolver的buildView方法 * 因为CustomViewResolver重写了buildView,不再执行UrlBasedViewResolver的buildView方法 * @Date 2019/1/17 11:26 * @Param [viewClass, viewName, prefix, suffix] * @return org.springframework.web.servlet.view.AbstractUrlBasedView **/ private AbstractUrlBasedView build(Class viewClass, String viewName, String prefix, String suffix) { AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass); view.setUrl(prefix + viewName + suffix); String contentType = getContentType(); if (contentType != null) { view.setContentType(contentType); } view.setRequestContextAttribute(getRequestContextAttribute()); view.setAttributesMap(getAttributesMap()); if (this.exposePathVariables != null) { view.setExposePathVariables(exposePathVariables); } return view; } /** * @Author 谷天乐 * @Description 使用AbstractTemplateViewResolver的buildView,为view设置属性 * Freemarker解析所需 * * @Date 2019/2/7 10:22 * @Param [viewName] * @return org.springframework.web.servlet.view.AbstractUrlBasedView **/ private AbstractUrlBasedView buildFreemarkerView(String viewName) throws Exception { AbstractTemplateView view = (AbstractTemplateView) build(FreeMarkerView.class, viewName, "", getSuffix()); view.setExposeRequestAttributes(this.exposeRequestAttributes); view.setAllowRequestOverride(this.allowRequestOverride); view.setExposeSessionAttributes(this.exposeSessionAttributes); view.setAllowSessionOverride(this.allowSessionOverride); view.setExposeSpringMacroHelpers(this.exposeSpringMacroHelpers); return view; } public boolean isExposeRequestAttributes() { return exposeRequestAttributes; } public void setExposeRequestAttributes(boolean exposeRequestAttributes) { this.exposeRequestAttributes = exposeRequestAttributes; } public boolean isExposeSessionAttributes() { return exposeSessionAttributes; } public void setExposeSessionAttributes(boolean exposeSessionAttributes) { this.exposeSessionAttributes = exposeSessionAttributes; } public boolean isExposeSpringMacroHelpers() { return exposeSpringMacroHelpers; } public void setExposeSpringMacroHelpers(boolean exposeSpringMacroHelpers) { this.exposeSpringMacroHelpers = exposeSpringMacroHelpers; } }
在dispatcher-servlet.xml加入自定义视图解析器
<bean class="org.format.demo.custom.CustomViewResolver"> <property name="prefix" value="/WEB-INF/view/"/> <property name="suffix" value=".ftl"/> <property name="contentType" value="text/html;charset=utf-8"/> <property name="exposeRequestAttributes" value="true"/> <property name="exposeSessionAttributes" value="true"/> <property name="exposeSpringMacroHelpers" value="true"/> <property name="requestContextAttribute" value="request"/> </bean>
引入freemarker和jsp的配置,配置在前遇到同名情况具备更高的优先级,比如index.ftl和index.jsp会优先解析index.ftl
<!--遇到同名的ftl和jsp文件,配置在前的解析器优先级更高--> <import resource="classpath:springConfig/viewConfig/freemarker.xml"/> <import resource="classpath:springConfig/viewConfig/jsp.xml"/>
测试Controller
@Controller @RequestMapping(value = "/tvrc") public class TestViewResolverController { @RequestMapping("jsp") public ModelAndView jsp(ModelAndView view) { view.setViewName("jsp:tvrc/test"); return view; } @RequestMapping("/ftl") public ModelAndView freemarker(ModelAndView view) { view.setViewName("freemarker:tvrc/test"); return view; } }
浏览器输入http://localhost:8080/springmvcdemo/tvrc/jsp
浏览器输入http://localhost:8080/springmvcdemo/tvrc/ftl
3.2 自定义Pdf视图
测试Controller
@Controller @RequestMapping("otherview") public class OtherViewController { @RequestMapping("/pdf") public ModelAndView mypdf() { ModelAndView mav = new ModelAndView(); //添加自定义视图 mav.setView(new MyPdfView()); List<String> list = new ArrayList<String>(); for (int i = 0; i < 10; i++) { list.add(i+""); } mav.addObject("list", list); return mav; } @RequestMapping("/excel") public ModelAndView myexcle() { ModelAndView mav = new ModelAndView(); //添加视图 mav.setView(new MyExcelView()); List list = new ArrayList<String>(); for (int i = 0; i < 10; i++) { list.add(i+""); } mav.addObject("list", list); return mav; } }
创建测试类继承AbstractPdfView,重写buildPdfDocument方法
public class MyPdfView extends AbstractPdfView { @Override protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response) throws Exception { List list = (List) model.get("list"); for (int i = 0; i < list.size(); i++) { //将数据加载到视图上 document.add(new Paragraph((String)(list.get(i)))); } } }
浏览器输入http://localhost:8080/springmvcdemo/otherview/pdf
3.3 自定义Excel视图
创建测试类继承AbstractExcelView,重写buildExcelDocument方法
public class MyExcelView extends AbstractExcelView { @Override protected void buildExcelDocument(Map<String, Object> model, HSSFWorkbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception { HSSFSheet sheet;//工作簿的名字 HSSFCell cell;//单元格 sheet = workbook.createSheet("spring"); sheet.setDefaultColumnWidth(12); List list = (List) model.get("list"); for (int i = 0; i < list.size(); i++) { cell = getCell(sheet, 0, i); setText(cell, (String) list.get(i)); } } }
浏览器输入http://localhost:8080/springmvcdemo/otherview/excel
4.总结
View作为视图接口,AbstractView的render方法创建待渲染的model,调用子类,如果待解析视图类型是jsp,则调用InternalResourceView,如果是ftl,则调用FreemarkerView,执行渲染
ViewResolver作为视图解析接口,主要为View提供支持,createView方法创建View,buildView根据路径构造View,并为View设置一系列属性
DispatcherServlet根据ModelAndView调用View实现类的render方法进行视图渲染,ViewResolver实现类起到辅助作用,为View设置属性(前缀、后缀等)
5.参考
https://docs.spring.io/spring/docs/current/javadoc-api/
https://github.com/spring-projects/spring-framework
文中难免有不足,欢迎指出