这篇blog主要是介绍如何实现一个超级灵活的SpringMVC多视图协商配置,试想如果一个项目需要使用jsp、freemaker、jasper、velosity等,而返回客服端数据需要用到json、xml。那么改如果配置SpringMVC视图呢?
在本例中,我们提供了几种视图:JSP 文件、freemaker、jasper、SWF 文件(flash 文件)以及一个自定义后缀名(.config和.txt)的文件,为了更方便的支持 SWF 视图和自定义文件后缀名的视图,我们开发了自定义的视图对象,希望能够使用该视图对象来支持 SWF 文件和 .config .txt 文件,另外还开发了两个视图解析器来实现本例。
实际上,Spring 已经提供了RESTful的多视图解析器-ContentNegotiatingViewResolver (以下简称CNV),它可以根据请求的文件后缀名或请求的 Accept 头或者请求的参数来查找视图。CNV本身并不负责查找视图,它只是将视图查找工作代理给所注册的视图解析器,但是在实际开发中如果把所以视图都交给CNV处理,CVN就显得有点笨拙,比如我在测试中发现把freemarker和InternalResourceViewResolver都放到CNV的viewResolvers里,所有返回freemarker的视图都跑到InternalResourceViewResolver里去处理了,结果导致404。但是用CNV来处理json和xml可以说是相当的方便。本例中的RESTful MVC就是采用CNV来处理json和xml。比如访问http://localhost/sample/message.json 那么返回客服端的数据内容格式为json,数据为ModelMap里携带的数据。
一、自定义视图类代码
为了简化程序该视图类只能用于处理浏览器能直接显示的请求文件资源,如文本文件、SWF 文件等
public class GenericFileView extends AbstractUrlBasedView { // default content type private final static String CONTENT_TYPE = "text/plain"; //content of http response private String responseContent; public GenericFileView() { super(); setContentType(CONTENT_TYPE); } @Override public void setContentType(String contentType) { super.setContentType(contentType); } @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType(getContentType()); response.getWriter().write(this.responseContent); response.getWriter().close(); } /** * Set http request content * @param responseContent */ public void setResponseContent(String responseContent) { this.responseContent = responseContent; } }
二、Spring视图解析器
有了视图类,我们还需要查找该视图类的视图解析器。所有的视图解析器都需要实现接口 org.springframework.web.servlet.ViewResolver,但同视图的实现一样,Spring 还提供了一个抽象类AbstractCachingViewResolver常用的 Spring 中的视图解析器都继承了该抽象类,我们同样可以通过实现该抽象类来节省开发工作。
public class GenericFileViewResolver extends AbstractCachingViewResolver implements Ordered { private Logger logger = Logger.getLogger(GenericFileViewResolver.class .getName()); private int order = Integer.MIN_VALUE; // requested file location under web app private String location; // View private String viewName; public void setViewName(String viewName) { this.viewName = viewName; } public void setOrder(int order) { this.order = order; } public void setLocation(String location) { this.location = location; } public int getOrder() { return this.order; } @Override protected View loadView(String viewName, Locale locale) throws Exception { if (location == null) { throw new Exception( "No location specified for GenericFileViewResolver."); } String requestedFilePath = location + viewName; Resource resource = null; //String url = ""; try { logger.finest(requestedFilePath); resource = getApplicationContext().getResource(requestedFilePath); //url = resource.getURI().toString(); } catch (Exception e) { // this exception should be catched and return null in order to call // next view resolver logger.finest("No file found for file: " + requestedFilePath); return null; } logger.fine("Requested file found: " + requestedFilePath + ", viewName:" + viewName); //get view from application content, scope=prototype GenericFileView view = this.getApplicationContext().getBean(this.viewName, GenericFileView.class); view.setUrl(requestedFilePath); view.setResponseContent(inputStreamTOString(resource.getInputStream(), "UTF-8")); return view; } final static int BUFFER_SIZE = 4096; /** * Convert Input to String based on specific encoding * * @param in * @param encoding * @return * @throws Exception */ public static String inputStreamTOString(InputStream in, String encoding) throws Exception { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); byte[] data = new byte[BUFFER_SIZE]; int count = -1; while ((count = in.read(data, 0, BUFFER_SIZE)) != -1) outStream.write(data, 0, count); data = null; return new String(outStream.toByteArray(), encoding); } }
三、自定义多视图解析器
package org.mkwu.web.view.viewresolver; import java.util.Iterator; import java.util.Locale; import java.util.Map; import org.springframework.core.Ordered; import org.springframework.util.StringUtils; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.view.AbstractCachingViewResolver; public class MultipleViewResolver extends AbstractCachingViewResolver implements Ordered{ private int order = Integer.MIN_VALUE; public int getOrder() { return order; } public void setOrder(int order) { this.order = order; } private Map<String, ViewResolver> resolvers; public void setResolvers(Map<String, ViewResolver> resolvers) { this.resolvers = resolvers; } private String getViewResolverKey(String fileExtension){ Iterator<String> keyIt = resolvers.keySet().iterator(); while (keyIt.hasNext()) { String viewResolverKey = (String) keyIt.next(); String[] arr = viewResolverKey.split(","); for (String subKey : arr) { if(subKey.equals(fileExtension)) return viewResolverKey; } } return null; } @Override protected View loadView(String viewName, Locale locale) throws Exception { String fileExtension = StringUtils.getFilenameExtension(viewName); // return null to invoke next resolver if no extension found if (fileExtension == null) { return null; } String viewResolverKey = getViewResolverKey(fileExtension); ViewResolver resolver = resolvers.get(viewResolverKey); // get resolver by extension //return null to invoke next resolver if no resolver found return resolver == null ? null : resolver.resolveViewName(viewName,locale); } }
四、SpringMVC配置 也是本文的重点
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd"> <!-- 自动扫描且只扫描@Controller --> <context:component-scan base-package="com.martinwu.example" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <mvc:annotation-driven/> <!-- --> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <!-- 解析json请求数据,将json转换为java对象 --> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> </list> </property> </bean> <!-- 解析xml请求数据,将xml转换为java对象 可以与xml互换的对象 --> <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter"p:marshaller-ref="xstreamMarshaller" p:unmarshaller-ref="xstreamMarshaller" /> <bean class="org.springframework.http.converter.FormHttpMessageConverter"></bean> <bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter"></bean> <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"></bean> <bean class="org.springframework.http.converter.StringHttpMessageConverter"></bean> <bean class="org.springframework.http.converter.ResourceHttpMessageConverter"></bean> </list> </property> </bean> <!-- 如果内容协商视图选用Jaxb2Marshaller,controller返回的内容必须有对应的shema. 如果返回pojo,pojo必须加@XmlRootElement注解,注:速度比xstream快,但是需要固定的shema--> <bean id="jaxbMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="classesToBeBound"> <list> <value>com.martinwu.example.entity.User</value> </list> </property> </bean> <!-- 可应用于格式不固定返回内容--> <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> <property name="streamDriver"> <bean class="com.thoughtworks.xstream.io.xml.StaxDriver" /> </property> <property name="annotatedClasses"> <list> <value>com.martinwu.example.entity.User</value> </list> </property> </bean> <!--国际化资源 --> <bean id="messageSource"class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>i18n/messages</value> </list> </property> <property name="useCodeAsDefaultMessage" value="true"/> </bean> <!-- 将无法mapping到Controller的path交给default servlet handler处理 --> <!-- 当在web.xml 中 DispatcherServlet使用 <url-pattern>/</url-pattern> 映射时,能映射静态资源 --> <mvc:default-servlet-handler/> <!-- 静态资源映射 <mvc:resources mapping="/images/**" location="/WEB-INF/images/" /> <mvc:resources mapping="/css/**" location="/WEB-INF/css/" /> <mvc:resources mapping="/js/**" location="/WEB-INF/js/" /> --> <!-- ========================= VIEW定义 ========================= --> <!-- FreeMarker基础设施及视图解析器配置 --> <!-- freemarker的配置 --> <bean id="freemarkerConfigurer" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"> <property name="templateLoaderPath" value="/WEB-INF/freemarker/" /> <property name="defaultEncoding" value="UTF-8" /> <property name="freemarkerSettings"> <props> <prop key="template_update_delay">10</prop> <prop key="locale">zh_CN</prop> <prop key="datetime_format">yyyy-MM-dd HH:mm:ss</prop> <prop key="date_format">yyyy-MM-dd</prop> <prop key="number_format">#.##</prop> </props> </property> </bean> <!-- 自定义多视图解析器,根据请求后缀调用相应的视图解析器 --> <bean id="multipleViewResolver" class="com.martinwu.example.view.viewresolver.MultipleViewResolver" p:order="0"> <property name="resolvers"> <map> <entry key="config,txt"> <bean class="com.martinwu.example.view.viewresolver.GenericFileViewResolver" p:location="/WEB-INF/config/" p:cache="false"> <property name="viewName" value="configFileView"/> </bean> </entry> <entry key="swf"> <bean class="com.martinwu.example.view.viewresolver.GenericFileViewResolver" p:location="/WEB-INF/swf/" p:cache="false"> <property name="viewName" value="swfFileView"/> </bean> </entry> <entry key="ftl"> <bean id="freeMarkerResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver" p:order="2"> <property name="viewClass" value="org.springframework.web.servlet.view.freemarker.FreeMarkerView" /> <property name="contentType" value="text/html;charset=UTF-8" /> <property name="exposeRequestAttributes" value="true" /> <property name="exposeSessionAttributes" value="true" /> <property name="exposeSpringMacroHelpers" value="true" /> </bean> </entry> <entry key="jasper"> <!-- Jasper Report 视图--> <bean id="jasperReportsViewResolver" class="org.springframework.web.servlet.view.jasperreports.JasperReportsViewResolver" p:viewClass="com.scinvest.util.support.JasperView" p:requestContextAttribute="rc" p:prefix="/WEB-INF/reportView/"> <!-- dataSources参数的key --> <property name="reportDataKey"> <value>datasource</value> </property> <property name="exporterParameters"> <map> <!--参数对于生成html时,jasperReport查找图片的位置 --> <entry key="net.sf.jasperreports.engine.export.JRHtmlExporterParameter.IMAGES_URI"> <value>#{rpe.resourceRoot}/images/reports/</value> </entry> <entry key="net.sf.jasperreports.engine.export.JRHtmlExporterParameter.CHARACTER_ENCODING"> <value>UTF-8</value> </entry> <entry key="net.sf.jasperreports.engine.export.JRXlsExporterParameter.IS_ONE_PAGE_PER_SHEET"> <bean id="java.lang.Boolean.TRUE" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/> </entry> </map> </property> </bean> </entry> <entry key="jsp"> <bean id="defaultViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="contentType" value="text/html"/> <property name="prefix" value="/WEB-INF/views/"/> </bean> </entry> </map> </property> </bean> <!-- 根据客户端的不同的请求决定不同的 view进行响应, 如 /blog/1.json /blog/1.xml --> <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver" p:order="1"> <!-- 设置为true以忽略对Accept Header的支持--> <property name="ignoreAcceptHeader" value="true"/> <!-- 扩展名至mimeType的映射,即 /user.json => application/json --> <property name="favorPathExtension" value="true"/> <!-- 用于开启 /userinfo/123?format=json 的支持 --> <property name="favorParameter" value="false"/> <property name="parameterName" value="format"/> <property name="defaultContentType" value="text/html"/> <property name="mediaTypes"> <!--favorPathExtension, favorParameter是true时才起作用 --> <map> <!-- <entry key="pdf" value="application/pdf"/> --> <!-- <entry key="xls" value="application/vnd.ms-excel"/> --> <entry key="html" value="text/html;charset=UTF-8"/> <entry key="xml" value="application/xml" /> <entry key="json" value="application/json" /> </map> </property> <property name="defaultViews"> <list> <!-- for application/json --> <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" /> <!-- for application/xml --> <bean class="org.springframework.web.servlet.view.xml.MarshallingView" > <constructor-arg> <ref bean="xstreamMarshaller"/> </constructor-arg> </bean> </list> </property> </bean> <bean id="configFileView" class="com.martinwu.example.view.GenericFileView" p:contentType="text/plain" p:url="" scope="prototype"/> <bean id="swfFileView" class="com.martinwu.example.view.GenericFileView" p:contentType="application/x-shockwave-flash" p:url="" scope="prototype"/> <!-- FreeMarker视图解析,以便能访问controller里返回不带后缀ftl的freemarker视图,注: 前面定义的multipleViewResolver里只能解析controller里返回带后缀.ftl的freemaker视图 --> <bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver" p:order="3"> <property name="viewClass" value="org.springframework.web.servlet.view.freemarker.FreeMarkerView" /> <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" /> </bean> <!-- bean name view resolver <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="4"/> --> <!-- 默认的视图解析器 应该给InternalResourceViewResolver分配最低的优先级别,因为无论它是否存在,始终都会用它来解析视图。因此,如果其他解析器的优先级别比较低,就没有机会解析视图了 --> <bean id="defaultViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:order="5"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="contentType" value="text/html"/> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 文件上传相关 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!--one of the properties available;the maximum file size in bytes--> <property name="maxUploadSize" value="100000"/> </bean> <!-- 定义无Controller的path<->view直接映射 --> <mvc:view-controller path="/" view-name="redirect:/welcome"/> </beans>
参考:http://www.ibm.com/developerworks/cn/java/j-lo-springview/