Spring MVC各组件近距离接触--下下--05

2022-08-23 11:00:26 浏览数 (1)

Spring MVC各组件近距离接触--下下--05

  • 各司其职的View
    • View实现原理回顾
    • 可用的View实现类
    • AbstractUrlBasedView
      • 1.使用JSP技术的view实现
      • 2.使用通用模板技术的view实现
      • 3.面向二进制文档格式的view实现
      • 4.面向JsperReport的view实现
      • 5. RedirectView和逻辑视图名前缀
    • 自定义View
    • 前后端分离大背景
  • 小结

各司其职的View

org.springframework.web.serviet.view是SpringMVC中将原本可能存在于Dispatcherservlet中的视图渲染逻辑得以剥离出来的关键组件。

通过引入该策略抽象接口, 我们可以极具灵活servlet中的视图渲染逻辑得以剥离出来的关键组件。通过引入该策略抽象接口,我们可以极具灵活性地支持各种视图渲染技术。

代码语言:javascript复制
public interface View {
    //三个常量属性省略 
    ...
	
	String getContentType();

	void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

}

各种view实现类主要的职责就是在render(⋯)方法中实现最终的视图渲染工作,但这些对Dispatcherservlet来说是透明的, Dispatcherservlet只是直接接触ViewResolver所返回的view接口, 获得相应引用后把视图渲染工作转交给返回的view实例即可。

至于该view实例是什么类型,具体如何完成工作, Dispatcherservlet是无须关心的。

不过,对于我们来说,了解各个view实现类的实现原理, 有助于我们更好地理解整个框架是如何运作的, 并且, 如果现有的view不能够满足我们的需要, 我们也可以自定义一个需要的view实现类。


View实现原理回顾

总地来说,当前绝大多数的视图渲染技术都是构建在模板的原理之上。我们回想一下,这种基于模板视图生成方式在我们的生活中到处可见。

  • 厨师为了能够提供统一样式的蛋糕, 会使用模子来制作, 只要提供不同成分的面团,经过相同的模子压制,就能够获得统一样式却不同口味的蛋糕。厨师用的模子(可能木质也可能金属的模子压制)是不是与我们提供的JSP文件相似? 那不同成分的面团跟我们提供的不同的模型数据是否类似?
  • 篆刻后的方印,只要蘸上不同颜色的印泥就能印出同一式样但不同颜色的印章图案。方印就是模板,不同的印泥就是要表现的数据, 是否可以这么理解呢?

实际上,不管是生活中还是视图渲染过程中,只要使用模板这种模式,他们的工作原理就是一条路子下来的:

所以,只要能够理解当前视图渲染的实现与生活中这些使用模板的场景之间的共同之处, 那么,余下的工作将不再神秘。

一个view实现类所要做的,就是使用相应的技术API将模板和最终提供的模型数据合并到一起, 最终输出结果页面给客户端, 所以, 不难想象对应不同视图技术的view实现是一个什么样子。

如果我们要使用JSP文件作为模板输出视图页面,那么我们的View实现类可能如下面所示:

代码语言:javascript复制
public class JspView implements View {

    private String jspTemplateFileLocation;

    @Override
    public String getContentType() {
        return "text/html;charset=UTF-8";
    }

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setContentType(getContentType());
        exposeModelToRequest(model,request);
        request.getRequestDispatcher(jspTemplateFileLocation).forward(request,response);
    }

    private void exposeModelToRequest(Map<String,?> model, HttpServletRequest request) {
         if(!model.isEmpty()){
             Iterator<? extends Map.Entry<String, ?>> iter = model.entrySet().iterator();
             while(iter.hasNext()){
                 Map.Entry<String, ?> entry = iter.next();
                 String name = entry.getKey();
                 Object value = entry.getValue();
                 request.setAttribute(name,value);
             }
         }
    }

    public String getJspTemplateFileLocation() {
        return jspTemplateFileLocation;
    }

    public void setJspTemplateFileLocation(String jspTemplateFileLocation) {
        this.jspTemplateFileLocation = jspTemplateFileLocation;
    }
}

JSP模板文件与模型数据的合并(merge)操作将由Web容器(比如Tomcat)来完成,所以,这里我们只是通过Servlet API将合并的工作转发给Web容器即可。


如果我们使用Velocity模板输出视图页面,那么我们的View实现类可能如下所示:

代码语言:javascript复制
public class VelocityView implements View {
    private String vmTemplateLocation;
    private VelocityEngine engine;

    @Override
    public String getContentType() {
        return "text/html;charset=UTF-8";
    }


    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
          response.setContentType(getContentType());
          Context ctx=new VelocityContext();
          copyMapToContext(model,ctx);
          engine.mergeTemplate(vmTemplateLocation,ctx,response.getWriter());
    }

    private void copyMapToContext(Map<String,?> model, Context ctx) {
        if(!model.isEmpty()){
            Iterator<? extends Map.Entry<String, ?>> iter = model.entrySet().iterator();
            while(iter.hasNext()){
                Map.Entry<String, ?> entry = iter.next();
                String name = entry.getKey();
                Object value = entry.getValue();
                ctx.put(name,value);
            }
        }
    }
}

如果我们要使用Excel作为输出对象,那么我们的View实现类可能如下面所示:

代码语言:javascript复制
public class ExcelView implements View {
    private String xlsTemplateLocation;

    @Override
    public String getContentType() {
        return "application/vnd.ms-excel";
    }


    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setContentType(getContentType());
        //1.定位模板位置
        HSSFWorkBook workBook=readInExcelTemplate(xlsTemplateLocation);
       //2.合并数据和模板
        mergeModelWithTemplate(model,workBook);
       //3.输出到客户端
        ServletOutputStream out = response.getOutputStream();
        workBook.write(out);
        out.flush();
    }   
}

怎么样?虽然只是原型代码,但已经足够说明问题了,不是吗?

实际上,Spring MVC提供的针对各种视图技术的View实现也是按照同一条路子走下来的,只不过比我们的原型代码要严谨罢了。


可用的View实现类

Spring MVC提供的View实现类都直接或者间接继承自org.springframework.web.servlet. view.AbstractView

该类定义了大多数View实现类都需要的一些属性和简单的模板化的实现流程。

AbstractView为所有view子类定义的属性是如下几个。

代码语言:javascript复制
	private String contentType = DEFAULT_CONTENT_TYPE;

DEFAULT_CONTENT_TYPE的内容是 “text/html;charset=IS0-8859-1”。我们可以通过contentType的setter方法更改这一默认值。

代码语言:javascript复制
	public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1";
	
	public void setContentType(String contentType) {this.contentType = contentType;} 

代码语言:javascript复制
	private String requestContextAttribute;

requestContextAttribute属性是要公开给视图模板使用的org.springframework.web.servlet.support.RequestContext对应的属性名,比如,如果setRequestContextAttribute("rc")的话,那么,相应的RequestContext实例将以rc作为键放入模型中。

这样,我们就可以在视图模板中通过rc引用到该RequestContext。通常情况下,如果我们使用Spring提供的自定义标签,那么不需要公开相应的RequestContext

但如果不使用Spring提供的自定义标签,那么为了能够访问处理过程中所返回的错误信息等,就需要通过公开给视图模板的RequestContext来进行了。可以参考RequestContextJavadoc文档了解它能够赋予我们的能力。


代码语言:javascript复制
	private final Map<String, Object> staticAttributes = new LinkedHashMap<String, Object>();

如果视图有某些静态属性,比如 页眉、页脚的固定信息等,只要将它们加入staticAttributes,那么,AbstractView将保证这些静 态属性将一并放入模型数据中,最终一起公开给视图模板。

既然所有的View实现子类都继承自AbstractView,那么它们也就都拥有了指定静态属性的能力。

比如我们在“面向多视图类型支持的ViewResolver”中定义视图映射的时候,为某些具体视图定义指定了静态属性,如下所示:

代码语言:javascript复制
<bean name="viewTemplate" class="org.springframework.Web.servlet.view.InternalResourceView" abstract="true" p:attributesCSV="copyRight=spring21.cn,author= fujohnwang">
</bean>

那么,现在我们就可以像普通的模型数据那样,在视图模板中访问这些静态属性,如下所示:

代码语言:javascript复制
...
Author: ${author}
<br/>
Copyright: ${copyRight}
...

不过,除了通过attriutesCSV属性以CSV字符串形式传入多个静态属性,我们还可以通过attributes属性以Properties的形式传入静态属性,或者通过attributeMap属性以Map的形式传入静态参数。

代码语言:javascript复制
	public void setAttributesCSV(String propString) throws IllegalArgumentException {
       ...
       //核心是
       this.staticAttributes.put(name, value);
	}
    
    
	public void setAttributes(Properties attributes) {
		CollectionUtils.mergePropertiesIntoMap(attributes, this.staticAttributes);
	}
    
    public void setAttributesMap(Map<String, ?> attributes) {
		if (attributes != null) {
			for (Map.Entry<String, ?> entry : attributes.entrySet()) {
			//核心是: this.staticAttributes.put(name, value);
				addStaticAttribute(entry.getKey(), entry.getValue());
			}
		}
	}

AbstractView除了定义了以上公共属性以外,还定义了一个简单的模板化的方法流程。

(1)将添加的静态属性全部导入到现有的模型数据Map中,以便后继流程在合并视图模板的时候可以获取这些数据。

(2)如果requestContextAttribute被设置(默认为null),则将其一并导入现有的模型数据 Map中:

(3)根据是否要产生下载内容,设置相应的HTTP Header。

(4)公开renderMergedOutputModel(..)模板方法给子类实现。

代码语言:javascript复制
	public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
		...
		//将相关属性全部导入模型数据Map中
		Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
		//根据是否要产生下载内容,设置相应的HTTP Header
		prepareResponse(request, response);
		//公开renderMergedOutputModel(..)模板方法给子类实现
		renderMergedOutputModel(mergedModel, request, response);
	}

createMergedOutputModel会将哪些属性放入模型数据的map集合中呢?

代码语言:javascript复制
protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request,
			HttpServletResponse response) {
        //判断是否要讲路径变量设置到模型map集合
		@SuppressWarnings("unchecked")
		Map<String, Object> pathVars = (this.exposePathVariables ?
				(Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null);

		//将静态属性也都放入模型map
		int size = this.staticAttributes.size();
		size  = (model != null ? model.size() : 0);
		size  = (pathVars != null ? pathVars.size() : 0);

		Map<String, Object> mergedModel = new LinkedHashMap<String, Object>(size);
		mergedModel.putAll(this.staticAttributes);
		if (pathVars != null) {
			mergedModel.putAll(pathVars);
		}
		if (model != null) {
			mergedModel.putAll(model);
		}

		// 如果requestContextAttribute被设置(默认为null),则将其一并导入现有的模型数据 Map中:
		if (this.requestContextAttribute != null) {
			mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
		}

		return mergedModel;
	}

prepareResponse是否要产生下载内容,设置相应的HTTP Header:

代码语言:javascript复制
	protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
	    //例如如果需要渲染的是Excel或者pdf,那么返回的结果就会产生下载内容
		if (generatesDownloadContent()) {
			response.setHeader("Pragma", "private");
			response.setHeader("Cache-Control", "private, must-revalidate");
		}
	}

这样,AbstractView的直接或者间接子类,就可以在现有属性和流程的基础上进行开发了。


AbstractUrlBasedView

AbstractView中一个主要的扩展类是org.springframework.web.servlet.view.AbstractUrlBasedView,AbstractUrlBasedView为子类提供的公共设施很简单,只有一个String型的 ur1。那些需要根据模板路径读入模板文件的View实现,大都属于AbstractUrlBasedView门下。

代码语言:javascript复制
public abstract class AbstractUrlBasedView extends AbstractView implements InitializingBean {
    //内部维护一个模板文件路径的URL
	private String url;
	...
	public void afterPropertiesSet() throws Exception {
		//判断是否强制要求有默认文件路径---这个判断方法就需要子类来决定了,父类默认返回true
		if (isUrlRequired() && getUrl() == null) {
			throw new IllegalArgumentException("Property 'url' is required");
		}
	}
	
	protected boolean isUrlRequired() {
		return true;
	}
	
	//默认会去检查对应的模板文件资源是否存在--子类按需求覆盖
    public boolean checkResource(Locale locale) throws Exception {
		return true;
	}

AbstractView和AbstractUrlBasedView是所有View实现类的“总统领”,那些不需要指定url的View实现类大都归于AbstractView门下,余下的则由AbstractUrlBasedView管辖。在这样的前提 下,我们再来看各种实际可用的view实现类。


1.使用JSP技术的view实现

属于该类别的View实现主要包括:

  • org.springframework.web.servlet.view.InternalResourceView
  • org.springframework.web.servlet.view.JstlView
  • org.springframework.web. servlet.view.tiles.TilesView
  • org. springframework.web.servlet.view.tiles.TilesJstlView

其中,org.springframework.web.servlet.view.InternalResourceView是面向JSP技术的主 要view实现类,它们之间的关系如图所示。

InteralResourceViewJstlView都是面向单一JSP模板的view实现,二者的区别在于J2EE 1.4 之前的Web应用程序不支持JSTL

所以,这些Web应用程序只能使用InternalResourceView,而之后的Web应用程序因为支持JSTL,所以,使用JstlView是没有问题的。

TilesView和TilesJstlView之间的区别与InteralResourceView和JstlView是类似的。

不过,TilesView和TilesJstlView 使用了Struts的Tiles视图技术,它们支持的是复合JSP视图。

另外,Spring 2.5之后也引入了对Tiles 2(http://tiles.apache.org/)的支持,对应的TilesView实现位于org.springframework.web.servlet. view.tiles2包下面,与org.springframework.web.servlet.view.tiles包下面的Tiles 1.x版本 的TilesView和TilesJstlView相区别。

这些使用JSP技术的View实现,虽然可以在"面向多视图类型的ViewResolver"的映射关系中单独配置,不过,因为它们有特定与自定的ViewResolver,即InternalResourceViewResolver,所以,更多时候,只需要在使用之前变换一下如下配置中具体的viewClass类型即可。

代码语言:javascript复制
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
           <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
           <property name="prefix" value="/WEB-INF/jsp"/>
           <property name="suffix" value=".jsp"/>
    </bean>

不过,Tiles视图的使用与单纯的JSP视图在使用上存在一点儿差异,我们需要为TilesView和TilesJstlView的渲染提供必需的DefinitionsFactory。

这个工作可以通过TilesConfigurer类完 成,将TilesConfigurer添加到webApplicationContext之后,它将为容器内的TilesView和TilesJstlView的渲染提供绑定到ServletContext的DefinitionsFactory。

TilesConfigurer的配置如下所示:

代码语言:javascript复制
<bean id="tilesConfigurer" class="org.springframework.Web.servlet.view.tiles.TilesConfigurer">    
  <property name="definitions">
    <list>
        <value>/WEB-INF/defs/tiles-def1.xml</value> 
        <value>/WEB-INF/defs/tiles-def2.xml</value> 
        ...
    </list>
  </property> 
</bean> 

2.使用通用模板技术的view实现

通用模板技术现在比较主流的是Velocity和Freemarker。

如果我们的Web应用程序要启用这两种技术渲染视图,那么,Spring MVC提供了FreeMarkerView和velocityView两种View实现。

因为二者都是基于同样的理念构建视图,所以,FreeMarkerView和velocityView有着共同的父类AbstractTemplateView,它们之间的继承层次关系如图所示。

AbstractTemplateView定义了几个boolean属性,让我们可以决定是否公开暴露某些数据给最终的合并过程,如下所述。

  • private boolean exposeRequestAttributes=false。是否需要将request中的所有属性公开给合并过程,默认为false。
  • private boolean allowRequestoverride = false。是否允许request中的属性覆盖ModelAndView中同名的attribute,默认不允许这么做。
  • private boolean exposesessionAttributes =false。是否要将session中的属性公开给视图模板与模型数据的合并过程,默认不做。
  • private boolean allowSessionoverride = false。是否允许session中同名的属性覆盖掉返回的ModelAndview中的属性,默认也是不允许这么做。
  • private boolean exposespringMaсroHelpers=true。是否需要为Spring提供的宏(macro)公开一个需要的RequestContext对象,默认需要,将以“springMacroRequestContext”为键公开一个RequestContext给合并过程。

除了这些,FreeMarkerView和VelocityView自身也定义了几个属性可以进一步限定视图渲染过程,比如velocityView允许我们通过dateToolAttribute和numberToolAttribute公开Velocity Tools(http://velocity.apache.org/tools/devel/)的DateTool和NumberTool给模板使用。

FreeMarkerView和velocityView的使用都有相应的ViewResolver支持,即FreeMarkerViewResolver和VelocityViewResolver。

不过,我们也可以在“面向多视图类型的ViewResolver”中 使用它们。唯一需要注意的就是,使用这两种视图类型的时候,不要忘记通过FreeMarkerConfigurer和velocityConfigurer为它们提供渲染过程中使用的模板引擎支持。


3.面向二进制文档格式的view实现

该类的View实现主要指Excel和PDF形式的文档视图,通过设定合适的contentType,并且本地有相应的应用程序的话,这些文档将可以在浏览器中直接打开,而不是下载保存。

对于Excel形式的视图,Spring MVC提供了如下两个抽象类的视图实现。

  • AbstractExcelview。

使用Apache POI来构建Excel文档的View实现 类,支持读入Excel模板文件,子类需要实现buildExcelDocument模板方法,以给出具体的模型数据到模板文件的合并逻辑。

  • AbstractJExcelView。

该抽象类使用JExcel API作为视图的渲染API,同样支持现有Excel模板文件的读入,具体子类也需要通过实现buildExcelDocument模板方法,来实现具体的模型数据到Excel模板文件的合并过程。

两种面向Excel的View实现类都支持按照Locale读入不同的Excel模板文件,读入顺序类似于:

(1)fileLocation_zh_CN.xls;

(2)fileLocation_zh.xls;

(3)fileLocation.xls。

也就是说,我们可以为不同地区的用户提供不同的视图文件。


对应 PDF 形式的View实现类只有 Abstractpdfview ,它将使用 iText 来构建通终要输出的 PDF 文件。

应该是 API 的限制,该类无法读入 PDF 形式的模板文件(当然,没有 API 的支持,也不可能做到)。

我们只能通过该类创建新的 PDF 文件,然后将模型数据与要输入的格式一并纳入新创建的 POF 文件对象中。

该类也是抽象类,子类要实现buildPdfDocment模板方法提供具体的输出逻辑。

因为面向二进制文档格式的 view 实现没有一个统一的模板形式,所以, Spring MVC 无法提供通用的 view 实现类,只能在抽象父类中提供部分共同逻辑的实现,而具体的模型数据如何融入视图的显示逻辑,则需要子类在相应的模板方法给出。

有关面向二进制文档格式的 view 实现的使用,我们可能需要使用“面向多视图类型的 ViewResolver " ,因为没有特定于二进制文档格式 View 实现的 ViewResolver 可用。


4.面向JsperReport的view实现

面向JsperReport的view实现允许我们输出JasperReport生成的相应格式的报表文件,包括HTML格式、CSV格式、Excel格式以及PDF格式。

只要我们在ModelAndview中将要合并到报表的数据返回,面向JsperReport的view实现将把这些数据按照指定格式输出到客户端。

面向JsperReport的view实现主要包括如下几个。

  • AbstractJasperReportssingleFormatview

只负责输出单一类型的报表文件的View抽象类,实现了不同模板类型的读入以及数据的合并操作,将不同报表格式的输出通过模板方法下发给具体的子类实现,包括:

  • JasperReportsCsvView
  • JasperReportsHtmlView
  • JasperReportsPdfview
  • JasperReportsxlsview

如果只需要根据模型数据输出单一文档格式的报表视图,选择以上对应的View子类即可。

  • JasperReportsMultiFormatview

允许根据ModelAndview中的某个模型数据的值来决定输出何种格式的报表文档,默认使用“format”作为键。

当然,我们可以通过setFormatKey(String)来更改这一默认键的名称。

如果在ModelAndview中添加如下数据,并.且使用JasperReportsMultiFormatview作为将要使用的view实现:

代码语言:javascript复制
ModelAndView mav=new ModelAndView(...);
mav.addObject("format","pdf");
...
return mav;

那么,JasperReportsMultiFormatview最终将通过JasperReportsPdfview输出PDF格式的报表文档,关于format的值与具体View实现类之间的关系,如表所示:

当然,我们可以通过setFormatMappings(Properties)方法更改这一默认映射行为。

至于面向JsperReport的View实现类的使用,我们既可以使用特定的JasperReportsviewResolver 来映射逻辑视图名到具体view实现类,也可以使用ResourceBundleviewResolver之类“面向多视图 类型的ViewResolver”。

更多的信息可以参考Spring的参考文档,其中对各种视图的应用有详细的介绍,但对于JasperReport相关的view实现,主要设定的属性可能只有viewclass、url和reportDataKey,

如下所示:

其中reportDataKey作为ModelAndView中JasperReport需要的数据源(JRDataSource)的键,通常是必须的。


5. RedirectView和逻辑视图名前缀

Redirectviews会对指定的Uri做重定向操作。

如果设置Redirectview的http10Compatible属性为true,RedirectView:将直接通过HttpServletReponsel的sendRedirect(…)方法进行重定向操作.

否则通过设置HTTP状态码(303)以及HTTP Header(“Location”)达到同样的目的。不过在此之前,Redirectview会将ModelAndview中的模型数据附加到指定的URL后部,然后对URL进行编码。

使用Redirectviewl最多的地方是在Controller内,当然,通过相应的ViewResolver指定也是可以的,例如:

代码语言:javascript复制
ModelAndView mav=new ModelAndView();
RedirectView view=new RedirectView("addUser.do");
mav.setView(view);
...
return mav;

不过,不管是在Controller内直接实例化Redirectview使用,还是在相应的ViewResolver中配置,看起来都不是很简洁的样子。

所以,Spring MVC还提供了另外一种进行请求重定向的方法,那就是在逻辑视图名中使用redirect或者forword前缀。

实际上,我们在前面已经接触过这两个字符前缀的使用了。如果在刚才的代码中使用结合redirect前缀的逻辑视图名,代码看起来就是如下所示:

代码语言:javascript复制
ModelAndview mav new ModelAndview("redirect:adduser.do");
return mav;

RedirectView的核心源码如下:

代码语言:javascript复制
	@Override
	protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
			HttpServletResponse response) throws IOException {
       //将ModelAndview中的模型数据附加到指定的URL后部,然后对URL进行编码。
		String targetUrl = createTargetUrl(model, request);
		targetUrl = updateTargetUrl(targetUrl, model, request, response);
        ...
        //http10Compatible属性为true,RedirectView:将直接通过HttpServletReponsel的sendRedirect(..)方法进行重定向操作.
//否则通过设置HTTP状态码(303)以及HTTP Header(“Location”)达到同样的目的。
		sendRedirect(request, response, targetUrl, this.http10Compatible);
	}

 	protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
			String targetUrl, boolean http10Compatible) throws IOException {

		String encodedRedirectURL = response.encodeRedirectURL(targetUrl);
		if (http10Compatible) {
			if (this.statusCode != null) {
				response.setStatus(this.statusCode.value());
				response.setHeader("Location", encodedRedirectURL);
			}
			else {
				// Send status code 302 by default.
				response.sendRedirect(encodedRedirectURL);
			}
		}
		else {
			HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
			response.setStatus(statusCode.value());
			response.setHeader("Location", encodedRedirectURL);
		}
	}

关于Controller类方法中直接返回redirect:和forward:前缀的解析时机,是在UrlBasedViewResolver类的createView方法中完成的,不清楚的可以回看:

UrlBasedViewResolver的createView方法

代码语言:javascript复制
@Override
	protected View createView(String viewName, Locale locale) throws Exception {
		//判断当前视图解析器能否解析当前视图名,如果不能直接返回null,表示无法解析
		if (!canHandle(viewName, locale)) {
			return null;
		}
		//判断视图名是否以"redirect:"开头,表示重定向请求
		//重定义请求返回的是RedirectView
		//applyLifecycleMethods是调用RedirectView相关初始化方法以及相关后处理
		if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
			String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
			RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
			return applyLifecycleMethods(viewName, view);
		}
		// 判断视图名是否以"forward:"开头,表示转发请求
		if (viewName.startsWith(FORWARD_URL_PREFIX)) {
			String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
			return new InternalResourceView(forwardUrl);
		}
		//做完增加后,继续走父类的逻辑---父类方法只会去调用loadView
		return super.createView(viewName, locale);
	}

关于forward前缀的处理,则是直接交给了InternalResourceView完成,至于InternalResourceView的源码上面已经讲解过了,非常的简单,这里不再重新讲解。


自定义View

在目前前后端分离的大背景下,controller层的返回结果通常都是一个对象,然后需要我们通过JSON方式进行返回,因此,这里我们就来实现一个JsonView来完成这样的功能:

代码语言:javascript复制
@Slf4j
public class JsonView extends AbstractView {
    private ObjectMapper objectMapper = new ObjectMapper();
    private JsonEncoding encoding=JsonEncoding.UTF8;

    public JsonView() {
        setContentType("application/json");
    }

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        Object targetModel = model.get("JSON");
        if(targetModel==null){
            log.info("targetModel is null, nothing need to write");
            return;
        }else {
            log.info("detected targetModel: {}",targetModel);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            writeContent(outputStream,targetModel,null);
            //父类提供好的模板方法
            writeToResponse(response,outputStream);
        }
    }

    //关于FastJson的具体写出细节不是本节重点,大家感兴趣可以自行去研究
    protected void writeContent(OutputStream stream, Object value, String jsonPrefix) throws IOException {
        JsonGenerator generator = this.objectMapper.getJsonFactory().createJsonGenerator(stream, this.encoding);

        if (this.objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
            generator.useDefaultPrettyPrinter();
        }

        if (jsonPrefix != null) {
            generator.writeRaw(jsonPrefix);
        }

        this.objectMapper.writeValue(generator, value);
    }
}
代码语言:javascript复制
public class JsonController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ModelAndView modelAndView = new ModelAndView(new JsonView());
        if(request.getParameter("JSON")!=null){
            modelAndView.addObject("JSON", Customer.builder()
                    .name("大忽悠")
                    .address("1111")
                    .shopCard(Arrays.asList(ShopCard.builder().money(-1).build()))
                    .build());
        }
        return modelAndView;
    }
}

注意: 通常,自定义View实现类需要结合相应的ViewResolver才能使用,直接在Controller中实例化View并非大部分情况下的做法。对某类View来说,完全可以为其单独声明一个ViewResolver,指定合适的优先级别(通过order.属性)。即使现用的ViewResolver无法满足需要,为某类View实现类提供自定义的ViewResolver实现类也并非难事。

我们在controller中直接实例化了指定的View,这种做法不太常被使用,因此下面再给出增加自定义ViewResolver的做法演示:

代码语言:javascript复制
public class JsonViewResolver extends AbstractCachingViewResolver implements Ordered {
    @Override
    protected View loadView(String viewName, Locale locale) throws Exception {
        //如果viewName不是JSON,那么就不处理,让其他ViewResolver进行处理
        if(viewName.equals("JSON")){
            return new JsonView();
        }
        return null;
    }
    //让自定义的ViewResolver优先级最高
    @Override
    public int getOrder() {
        return 0;
    }
}
代码语言:javascript复制
public class JsonController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //不实例化View对象,只指定viewName
        ModelAndView modelAndView = new ModelAndView("JSON");
        if(request.getParameter("JSON")!=null){
            modelAndView.addObject("JSON", Customer.builder()
                    .name("大忽悠")
                    .address("1111")
                    .shopCard(Arrays.asList(ShopCard.builder().money(-1).build()))
                    .build());
        }
        return modelAndView;
    }
}
代码语言:javascript复制
    <bean id="viewResolver" class="com.example.controller.JsonViewResolver"/>
    
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/JSON/**">jsonController</prop>
            </props>
        </property>
    </bean>

    <bean id="jsonController" class="com.example.controller.JsonController"/>

前后端分离大背景

在前后端分离的大背景下,controller返回viewName映射到具体某个页面的场景已然不多,而是被返回JSON字符串所替代,上面给出的自定义View实现其实就是返回JSON字符串的场景应用,但是使用过SpringMVC的小伙伴都知道,只要我们在controller的方法上标注了@ResponseBoby注解,那么就会把返回值当做JSON字符串进行处理,那么这个是如何实现的呢?

  • 因为涉及到了注解版本Controller的内容,所以具体的后面再讲,我们先来看看Spring mvc为我们提供的处理JSON返回值的View和ViewResolver吧

SpringMVC为我们提供了用于处理Json返回值的View实现,如下所示,默认使用JackSon框架来进行JSON序列化处理:

ViewResolver采用的是基于内容协商机制来进行处理:

具体细节不多展开,后续会专门对JSON处理流程进行讲解


小结

HandlerMapping、Controller、ModelAndView、ViewResolver和view可以算是Spring MVC框架中的“五虎将”,它们共同组成了Spring MVC框架的强大躯干。

不过,这五个角色并非Spring MVC的全部,没有了其他角色的支持,Spring MVC也不会看起来这么饱满。下一节,我们将一起看一下Spring MVC家族中的其他成员。

0 人点赞