springMVC

一、Spring Web MVC优势

1、清晰的角色划分:前端控制器(DispatcherServlet)、请求到处理器映射(HandlerMapping)、处理器适配器(HandlerAdapter)、视图解析器(ViewResolver)、处理器或页面控制器(Controller)、验证器(Validator)、命令对象(Command 请求参数绑定到的对象就叫命令对象)、表单对象(Form Object 提供给表单展示和提交到的对象就叫表单对象)。

2、分工明确,而且扩展点相当灵活,可以很容易扩展,虽然几乎不需要;

3、由于命令对象就是一个POJO,无需继承框架特定API,可以使用命令对象直接作为业务对象;
4、和Spring 其他框架无缝集成,是其它Web框架所不具备的;
5、可适配,通过HandlerAdapter可以支持任意的类作为处理器;
6、可定制性,HandlerMapping、ViewResolver等能够非常简单的定制;
7、功能强大的数据验证、格式化、绑定机制;
8、利用Spring提供的Mock对象能够非常简单的进行Web层单元测试;
9、本地化、主题的解析的支持,使我们更容易进行国际化和主题的切换。
10、强大的JSP标签库,使JSP编写更容易。
还有比如RESTful风格的支持、简单的文件上传、约定大于配置的契约式编程支持、基于注解的零配置支持等等。

二、Spring3.1新特性

一、Spring2.5之前,我们都是通过实现Controller接口或其实现来定义我们的处理器类。
二、Spring2.5引入注解式处理器支持,通过@Controller 和 @RequestMapping注解定义我们的处理器类。并且提供了一组强大的注解:

需要通过处理器映射DefaultAnnotationHandlerMapping和处理器适配器AnnotationMethodHandlerAdapter来开启支持@Controller 和 @RequestMapping注解的处理器。
@Controller:用于标识是处理器类;
@RequestMapping:请求到处理器功能方法的映射规则;
@RequestParam:请求参数到处理器功能处理方法的方法参数上的绑定;
@ModelAttribute:请求参数到命令对象的绑定;
@SessionAttributes:用于声明session级别存储的属性,放置在处理器类上,通常列出模型属性(如
@ModelAttribute)对应的名称,则这些属性会透明的保存到session中;
@InitBinder:自定义数据绑定注册支持,用于将请求参数转换到命令对象属性的对应类型;
三、Spring3.0引入RESTful架构风格支持(通过@PathVariable注解和一些其他特性支持),且又引入了更多的注解支持:
@CookieValue:cookie数据到处理器功能处理方法的方法参数上的绑定;
@RequestHeader:请求头(header)数据到处理器功能处理方法的方法参数上的绑定;
@RequestBody:请求的body体的绑定(通过HttpMessageConverter进行类型转换);
@ResponseBody:处理器功能处理方法的返回值作为响应体(通过HttpMessageConverter进行类型转换);
@ResponseStatus:定义处理器功能处理方法/异常处理器返回的状态码和原因;
@ExceptionHandler:注解式声明异常处理器;
@PathVariable:请求URI中的模板变量部分到处理器功能处理方法的方法参数上的绑定,从而支持RESTful架构风格的URI;
四、还有比如:
JSR-303验证框架的无缝支持(通过@Valid注解定义验证元数据);

使用Spring 3开始的ConversionService进行类型转换(PropertyEditor依然有效),支持使用@NumberFormat 和@DateTimeFormat来进行数字和日期的格式化;
HttpMessageConverter(Http输入/输出转换器,比如JSON、XML等的数据输出转换器);
ContentNegotiatingViewResolver,内容协商视图解析器,它还是视图解析器,只是它支持根据请求信息将同一模型数据以不同的视图方式展示(如json、xml、html等),RESTful架构风格中很重要的概念(同一资源,多种表现形式);
Spring 3 引入 一个mvc XML的命名空间用于支持mvc配置,包括如:
<mvc:annotation-driven>:自动注册基于注解风格的处理器需要的DefaultAnnotationHandlerMapping、AnnotationMethodHandlerAdapter支持Spring3的ConversionService自动注册
支持JSR-303验证框架的自动探测并注册(只需把JSR-303实现放置到classpath)
自动注册相应的HttpMessageConverter(用于支持@RequestBody 和 @ResponseBody)(如XML输入输出转换器(只需将JAXP实现放置到classpath)、JSON输入输出转换器(只需将Jackson实现放置到classpath))等。
<mvc:interceptors>:注册自定义的处理器拦截器;
<mvc:view-controller>:和ParameterizableViewController类似,收到相应请求后直接选择相应的视图;
<mvc:resources>:逻辑静态资源路径到物理静态资源路径的支持;
<mvc:default-servlet-handler>:当在web.xml 中DispatcherServlet使用<url-pattern>/</url-pattern> 映射时,能映射静态资源(当Spring Web MVC框架没有处理请求对应的控制器时(如一些静态资源),转交给默认的Servlet来响应静态文件,否则报404找不到资源错误,)。

五、对Servlet 3.0的全面支持。

@EnableWebMvc:用于在基于Java类定义Bean配置中开启MVC支持,和XML中的<mvc:annotation-driven>功能一样;
新的@Contoller和@RequestMapping注解支持类:处理器映射RequestMappingHandlerMapping 和 处理器适配器RequestMappingHandlerAdapter组合来代替Spring2.5开始的处理器映射DefaultAnnotationHandlerMapping和处理器适配器AnnotationMethodHandlerAdapter,提供更多的扩展点,它们之间的区别我们在处理器映射一章介绍。
新的@ExceptionHandler 注解支持类:ExceptionHandlerExceptionResolver来代替Spring3.0的AnnotationMethodHandlerExceptionResolver,在异常处理器一章我们再详细讲解它们的区别。
@RequestMapping的"consumes" 和 "produces" 条件支持:用于支持@RequestBody 和 @ResponseBody,

1、consumes指定请求的内容是什么类型的内容,即本处理方法消费什么类型的数据,如consumes="application/json"表示JSON类型的内容,Spring会根据相应的HttpMessageConverter进行请求内容区数据到@RequestBody注解的命令对象的转换;
2、produces指定生产什么类型的内容,如produces="application/json"表示JSON类型的内容,Spring的根据相应的HttpMessageConverter进行请求内容区数据到RequestBody注解的命令对象的转换,Spring会根据相应的HttpMessageConverter进行模型数据(返回值)到JSON响应内容的转换3以上内容,本章第×××节详述。
URI模板变量增强:URI模板变量可以直接绑定到@ModelAttribute指定的命令对象、@PathVariable方法参数在视图渲染之前被合并到模型数据中(除JSON序列化、XML混搭场景下)。
@Validated:JSR-303的javax.validation.Valid一种变体(非JSR-303规范定义的,而是Spring自定义的),用于提供对Spring的验证器(org.springframework.validation.Validator)支持,需要Hibernate Validator
4.2及更高版本支持;
@RequestPart:提供对“multipart/form-data”请求的全面支持,支持Servlet 3.0文件上传(javax.servlet.http.Part)、支持内容的HttpMessageConverter(即根据请求头的Content-Type,来判断内容区数据是什么类型,如JSON、XML,能自动转换为命令对象),比@RequestParam更强大(只能对请求参数数据绑定,key-alue格式),而@RequestPart支持如JSON、XML内容区数据的绑定;详见本章的第×××节;

Flash 属性 和 RedirectAttribute:通过FlashMap存储一个请求的输出,当进入另一个请求时作为该请求的输入,典型场景如重定向(POST-REDIRECT-GET模式,

1、POST时将下一次需要的数据放在FlashMap;

2、重定向;

3、通过GET访问重定向的地址,此时FlashMap会把1放到FlashMap的数据取出放到请求中,并从FlashMap中删除;从而支持在两次请求之间保存数据并防止了重复表单提交)。
Spring Web MVC提供FlashMapManager用于管理FlashMap,默认使用SessionFlashMapManager,即数据默认存储在session中。

三、springMVC的实现

springMVC大概的流程:

springMVC

SpringMVC对请求处理期间涉及的各种关注点进行了合理而完全的分离,并且明确设置了相应的角色用于建模并处理整个生命周期中的各个关注点
DispatcherServlet充当MVC中的C控制器角色
设置HandleMapping用于处理web请求与具体请求处理控制器之间的映射匹配
设置LocalResolver用于国际化处理
设置ViewReslover用于灵活的视图选择
 

3.1DispatcherServlet作用

DispatcherServlet是前端控制器设计模式的实现,提供Spring Web MVC的集中访问点,而且负责职责的分派,而且与Spring IoC容器无缝集成,从而可以获得Spring的所有好处。

springMVC
DispatcherServlet主要用作职责调度工作,本身主要用于控制流程,主要职责如下:
1、文件上传解析,如果请求类型是multipart将通过MultipartResolver进行文件上传解析;
2、通过HandlerMapping,将请求映射到处理器(返回一个HandlerExecutionChain,它包括一个处理器、多个HandlerInterceptor拦截器);
<!--[if !supportLists]-->

3、<!--[endif]-->通过HandlerAdapter支持多种类型的处理器(HandlerExecutionChain中的处理器);
4、通过ViewResolver解析逻辑视图名到具体视图实现;
5、本地化解析;
6、渲染具体的视图等;
7、如果执行过程中遇到异常将交给HandlerExceptionResolver来解析。

从以上我们可以看出DispatcherServlet主要负责流程的控制(而且在流程中的每个关键点都是很容易扩展的)。

3.2dispatcher在web.xml中配置

3.3、上下文关系

集成Web环境的通用配置:

<context-param>  
      <param-name>contextConfigLocation</param-name>  
      <param-value>  
          classpath:spring-common-config.xml,  
          classpath:spring-budget-config.xml  
      </param-value>  
</context-param>
<!-- 指定WebApplicationContext初始化时使用的类 -->
<context-param>
  <param-name>contextClass</param-name>
  <param-value>com.dxz.SpringContext</param-value>
</context-param>
<listener>  
   <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
</listener> 

如上配置是Spring集成Web环境的通用配置;一般用于加载除Web层的Bean(如DAO、Service等),以便于与其他任何Web框架集成。

contextConfigLocation:表示用于加载Bean的配置文件;

contextClass:表示用于加载Bean的ApplicationContext实现类,默认WebApplicationContext。

 

创建完毕后会将该上下文放在ServletContext:

servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,this.context);

  

ContextLoaderListener初始化的上下文和DispatcherServlet初始化的上下文关系,如下图:

springMVC

从图中可以看出:

ContextLoaderListener初始化的上下文加载的Bean是对于整个应用程序共享的,不管是使用什么表现层技术,一般如DAO层、Service层Bean;

DispatcherServlet初始化的上下文加载的Bean是只对Spring Web MVC有效的Bean,如Controller、HandlerMapping、HandlerAdapter等等,该初始化上下文应该只加载Web相关组件。

3.4、DispatcherServlet初始化顺序

继承体系结构如下所示:


springMVC

 

1、HttpServletBean继承HttpServlet因此在Web容器启动时将调用它的init方法,该初始化方法的主要作用

:::将Servlet初始化参数(init-param)设置到该组件上(如contextAttribute、contextClass、namespace、contextConfigLocation),通过BeanWrapper简化设值过程,方便后续使用;

:::提供给子类初始化扩展点,initServletBean(),该方法由FrameworkServlet覆盖。

public abstract class HttpServletBean extends HttpServlet implements EnvironmentAware{  
@Override  
    public final void init() throws ServletException {  
       //省略部分代码  
       //1、如下代码的作用是将Servlet初始化参数设置到该组件上  
       //如contextAttribute、contextClass、namespace、contextConfigLocation;  
       try {  
           PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);  
           BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);  
           ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());  
           bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));  
           initBeanWrapper(bw);  
           bw.setPropertyValues(pvs, true);  
       }  
       catch (BeansException ex) {  
           //…………省略其他代码  
       }  
       //2、提供给子类初始化的扩展点,该方法由FrameworkServlet覆盖  
       initServletBean();  
       if (logger.isDebugEnabled()) {  
           logger.debug("Servlet ‘" + getServletName() + "‘ configured successfully");  
       }  
    }  
    //…………省略其他代码  
}  

2、FrameworkServlet继承HttpServletBean通过initServletBean()进行Web上下文初始化,该方法主要覆盖一下两件事情:

    初始化web上下文;

    提供给子类初始化扩展点;

public abstract class FrameworkServlet extends HttpServletBean {  
@Override  
    protected final void initServletBean() throws ServletException {  
        //省略部分代码  
       try {  
             //1、初始化Web上下文  
           this.webApplicationContext = initWebApplicationContext();  
             //2、提供给子类初始化的扩展点  
           initFrameworkServlet();  
       }  
        //省略部分代码  
    }  
}
protected WebApplicationContext initWebApplicationContext() {  
        //ROOT上下文(ContextLoaderListener加载的)  
       WebApplicationContext rootContext =  
              WebApplicationContextUtils.getWebApplicationContext(getServletContext());  
       WebApplicationContext wac = null;  
       if (this.webApplicationContext != null) {  
           // 1、在创建该Servlet注入的上下文  
           wac = this.webApplicationContext;  
           if (wac instanceof ConfigurableWebApplicationContext) {  
              ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;  
              if (!cwac.isActive()) {  
                  if (cwac.getParent() == null) {  
                      cwac.setParent(rootContext);  
                  }  
                  configureAndRefreshWebApplicationContext(cwac);  
              }  
           }  
       }  
       if (wac == null) {  
             //2、查找已经绑定的上下文  
           wac = findWebApplicationContext();  
       }  
       if (wac == null) {  
            //3、如果没有找到相应的上下文,并指定父亲为ContextLoaderListener  
           wac = createWebApplicationContext(rootContext);  
       }  
       if (!this.refreshEventReceived) {  
             //4、刷新上下文(执行一些初始化)  
           onRefresh(wac);  
       }  
       if (this.publishContext) {  
           // Publish the context as a servlet context attribute.  
           String attrName = getServletContextAttributeName();  
           getServletContext().setAttribute(attrName, wac);  
           //省略部分代码  
       }  
       return wac;  
}

从initWebApplicationContext()方法可以看出,基本上如果ContextLoaderListener加载了上下文将作为根上下文(DispatcherServlet的父容器)。

最后调用了onRefresh()方法执行容器的一些初始化,这个方法由子类实现,来进行扩展。

3、DispatcherServlet继承FrameworkServlet,并实现了onRefresh()方法提供一些前端控制器相关的配置:

public class DispatcherServlet extends FrameworkServlet {  
     //实现子类的onRefresh()方法,该方法委托为initStrategies()方法。  
    @Override  
    protected void onRefresh(ApplicationContext context) {  
       initStrategies(context);  
    }  
   
    //初始化默认的Spring Web MVC框架使用的策略(如HandlerMapping)  
    protected void initStrategies(ApplicationContext context) {  
       initMultipartResolver(context);  
       initLocaleResolver(context);  
       initThemeResolver(context);  
       initHandlerMappings(context);  
       initHandlerAdapters(context);  
       initHandlerExceptionResolvers(context);  
       initRequestToViewNameTranslator(context);  
       initViewResolvers(context);  
       initFlashMapManager(context);  
    }  
} 

从如上代码可以看出,DispatcherServlet启动时会进行我们需要的Web层Bean的配置,如HandlerMapping、HandlerAdapter等,而且如果我们没有配置,还会给我们提供默认的配置。

 

从如上代码我们可以看出,整个DispatcherServlet初始化的过程和做了些什么事情,具体主要做了如下两件事情:

1、初始化Spring Web MVC使用的Web上下文,并且可能指定父容器为(ContextLoaderListener加载了根上下文);

2、初始化DispatcherServlet使用的策略,如HandlerMapping、HandlerAdapter等。

 

服务器启动时的日志分析(此处加上了ContextLoaderListener从而启动ROOT上下文容器):

 

 信息: Initializing Spring root WebApplicationContext //ContextLoaderListener启动ROOT上下文

 

2012-03-12 13:33:55 [main] INFO  org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started

2012-03-12 13:33:55 [main] INFO  org.springframework.web.context.support.XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Mon Mar 12 13:33:55 CST 2012]; root of context hierarchy

2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader - Loading bean definitions

2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 0 bean definitions from location pattern [/WEB-INF/ContextLoaderListener.xml]

2012-03-12 13:33:55 [main] DEBUG org.springframework.web.context.support.XmlWebApplicationContext - Bean factory for Root WebApplicationContext: org.springframework.beans.factory.support.DefaultListableBeanFactory@1c05ffd: defining beans []; root of factory hierarchy

2012-03-12 13:33:55 [main] DEBUG org.springframework.web.context.support.XmlWebApplicationContext - Bean factory for Root WebApplicationContext:

2012-03-12 13:33:55 [main] DEBUG org.springframework.web.context.ContextLoader - Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT] //ROOT上下文绑定到ServletContext

2012-03-12 13:33:55 [main] INFO  org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 438 ms  //到此ROOT上下文启动完毕

 

 2012-03-12 13:33:55 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Initializing servlet ‘chapter2‘

信息: Initializing Spring FrameworkServlet ‘chapter2‘  //开始初始化FrameworkServlet对应的Web上下文

2012-03-12 13:33:55 [main] INFO  org.springframework.web.servlet.DispatcherServlet - FrameworkServlet ‘chapter2‘: initialization started

2012-03-12 13:33:55 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Servlet with name ‘chapter2‘ will try to create custom WebApplicationContext context of class ‘org.springframework.web.context.support.XmlWebApplicationContext‘, using parent context [Root WebApplicationContext: startup date [Mon Mar 12 13:33:55 CST 2012]; root of context hierarchy]

//此处使用Root WebApplicationContext作为父容器。

2012-03-12 13:33:55 [main] INFO  org.springframework.web.context.support.XmlWebApplicationContext - Refreshing WebApplicationContext for namespace ‘chapter2-servlet‘: startup date [Mon Mar 12 13:33:55 CST 2012]; parent: Root WebApplicationContext

2012-03-12 13:33:55 [main] INFO  org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource [/WEB-INF/chapter2-servlet.xml]

2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader - Loading bean definitions

2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDelegate - Neither XML ‘id‘ nor ‘name‘ specified - using generated bean name[org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping#0]  //我们配置的HandlerMapping

2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDelegate - Neither XML ‘id‘ nor ‘name‘ specified - using generated bean name[org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter#0] //我们配置的HandlerAdapter

2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDelegate - Neither XML ‘id‘ nor ‘name‘ specified - using generated bean name [org.springframework.web.servlet.view.InternalResourceViewResolver#0] //我们配置的ViewResolver

2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDelegate - No XML ‘id‘ specified - using ‘/hello‘ as bean name and [] as aliases 

//我们的处理器(HelloWorldController

2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 4 bean definitions from location pattern [/WEB-INF/chapter2-servlet.xml]

2012-03-12 13:33:55 [main] DEBUG org.springframework.web.context.support.XmlWebApplicationContext - Bean factory for WebApplicationContext for namespace ‘chapter2-servlet‘: org.springframework.beans.factory.support.DefaultListableBeanFactory@1372656: defining beans [org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping#0,org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter#0,org.springframework.web.servlet.view.InternalResourceViewResolver#0,/hello]; parent: org.springframework.beans.factory.support.DefaultListableBeanFactory@1c05ffd

//到此容器注册的Bean初始化完毕

 

2012-03-12 13:33:56 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Unable to locate MultipartResolver with name ‘multipartResolver‘: no multipart request handling provided

 

2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean ‘org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver‘

//默认的LocaleResolver注册

2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean ‘org.springframework.web.servlet.theme.FixedThemeResolver‘

//默认的ThemeResolver注册

 

2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean ‘org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping#0‘

//发现我们定义的HandlerMapping 不再使用默认的HandlerMapping

 

2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean ‘org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter#0‘

//发现我们定义的HandlerAdapter 不再使用默认的HandlerAdapter

 

2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean ‘org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver‘

//异常处理解析器ExceptionResolver

2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean ‘org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver‘

 

2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean ‘org.springframework.web.servlet.view.InternalResourceViewResolver#0‘

 

2012-03-12 13:33:56 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Published WebApplicationContext of servlet ‘chapter2‘ as ServletContext attribute with name [org.springframework.web.servlet.FrameworkServlet.CONTEXT.chapter2]

//绑定FrameworkServlet初始化的Web上下文到ServletContext

2012-03-12 13:33:56 [main] INFO  org.springframework.web.servlet.DispatcherServlet - FrameworkServlet ‘chapter2‘: initialization completed in  297 ms

2012-03-12 13:33:56 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Servlet ‘chapter2‘ configured successfully

//到此完整流程结束 

 

 

 

从如上日志我们也可以看出,DispatcherServlet会进行一些默认的配置。接下来我们看一下默认配置吧。

 

3.5、DispatcherServlet默认配置

DispatcherServlet的默认配置在DispatcherServlet.properties(和DispatcherServlet类在一个包下)中,而且是当Spring配置文件中没有指定配置时使用的默认策略:

 springMVC

# Default implementation classes for DispatcherServlet‘s strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,    org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,    org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,    org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

从如上配置可以看出DispatcherServlet在启动时会自动注册这些特殊的Bean,无需我们注册,如果我们注册了,默认的将不会注册。

因此如第二章的BeanNameUrlHandlerMapping、SimpleControllerHandlerAdapter是不需要注册的,DispatcherServlet默认会注册这两个Bean。

从DispatcherServlet.properties可以看出有许多特殊的Bean,那接下来我们就看看Spring Web MVC主要有哪些特殊的Bean。

3.6、DispatcherServlet中使用的特殊的Bean

DispatcherServlet默认使用WebApplicationContext作为上下文,因此我们来看一下该上下文中有哪些特殊的Bean:

1、Controller处理器/页面控制器,做的是MVC中的C的事情,但控制逻辑转移到前端控制器了,用于对请求进行处理;

2、HandlerMapping请求到处理器的映射,如果映射成功返回一个HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象、多个HandlerInterceptor拦截器)对象;如BeanNameUrlHandlerMapping将URL与Bean名字映射,映射成功的Bean就是此处的处理器;

3、HandlerAdapterHandlerAdapter将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器;如SimpleControllerHandlerAdapter将对实现了Controller接口的Bean进行适配,并且调处理器的handleRequest方法进行功能处理;

4、ViewResolverViewResolver将把逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术;如InternalResourceViewResolver将逻辑视图名映射为jsp视图;

5、LocalResover本地化解析,因为Spring支持国际化,因此LocalResover解析客户端的Locale信息从而方便进行国际化;

6、ThemeResovler主题解析,通过它来实现一个页面多套风格,即常见的类似于软件皮肤效果;

7、MultipartResolver文件上传解析,用于支持文件上传;

8HandlerExceptionResolver处理器异常解析,可以将异常映射到相应的统一错误界面,从而显示用户友好的界面(而不是给用户看到具体的错误信息);

9RequestToViewNameTranslator当处理器没有返回逻辑视图名等相关信息时,自动将请求URL映射为逻辑视图名;

10FlashMapManager用于管理FlashMap的策略接口,FlashMap用于存储一个请求的输出,当进入另一个请求时作为该请求的输入,通常用于重定向场景,后边会细述。

  

到此DispatcherServlet我们已经了解了,接下来我们就需要把上边提到的特殊Bean挨个击破,那首先从控制器开始吧。

4.1、Controller简介

Controller控制器,是MVC中的部分C,为什么是部分呢?因为此处的控制器主要负责功能处理部分:
1、收集、验证请求参数并绑定到命令对象;
2、将命令对象交给业务对象,由业务对象处理并返回模型数据;
3、返回ModelAndView(Model部分是业务对象返回的模型数据,视图部分为逻辑视图名)。
还记得DispatcherServlet吗?主要负责整体的控制流程的调度部分:
1、负责将请求委托给控制器进行处理;
2、根据控制器返回的逻辑视图名选择具体的视图进行渲染(并把模型数据传入)。
因此MVC中完整的C(包含控制逻辑+功能处理)由(DispatcherServlet + Controller)组成。
因此此处的控制器是Web MVC中部分,也可以称为页面控制器、动作、处理器。

4.2、Controller接口

package org.springframework.web.servlet.mvc;
public interface Controller {
   ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws ...
}

这是控制器接口,此处只有一个方法handleRequest,用于进行请求的功能处理,处理完请求后返回ModelAndView(Model模型数据部分和View视图部分)。

Spring默认提供了一些Controller接口的实现以方便我们使用,具体继承体系如图4-1:

springMVC

 

4.3、WebContentGenerator

用于提供如浏览器缓存控制、是否必须有session开启、支持的请求方法类型(GET、POST等)等,该类主要有如下属性:

Set<String>   supportedMethods设置支持的请求方法类型,默认支持“GET”、“POST”、“HEAD”,如果我们想支持“PUT”,则可以加入该集合“PUT”。

boolean requireSession = false是否当前请求必须有session,如果此属性为true,但当前请求没有打开session将抛出HttpSessionRequiredException异常;

boolean useExpiresHeader = true是否使用HTTP1.0协议过期响应头:如果true则会在响应头添加:“Expires:”;需要配合cacheSeconds使用;

boolean useCacheControlHeader = true是否使用HTTP1.1协议的缓存控制响应头,如果true则会在响应头添加;需要配合cacheSeconds使用;

boolean useCacheControlNoStore = true是否使用HTTP 1.1协议的缓存控制响应头,如果true则会在响应头添加;需要配合cacheSeconds使用;

private int cacheSeconds = -1缓存过期时间,正数表示需要缓存,负数表示不做任何事情(也就是说保留上次的缓存设置),

      1、cacheSeconds =0时,则将设置如下响应头数据:

        Pragma:no-cache             // HTTP 1.0的不缓存响应头

        Expires:1L                  // useExpiresHeader=true时,HTTP 1.0

        Cache-Control :no-cache      // useCacheControlHeader=true时,HTTP 1.1

        Cache-Control :no-store       // useCacheControlNoStore=true时,该设置是防止Firefox缓存

      2、cacheSeconds>0时,则将设置如下响应头数据:

        Expires:System.currentTimeMillis() + cacheSeconds * 1000L    // useExpiresHeader=true时,HTTP 1.0

        Cache-Control :max-age=cacheSeconds                    // useCacheControlHeader=true时,HTTP 1.1

      3、cacheSeconds<0时,则什么都不设置,即保留上次的缓存设置。

此处简单说一下以上响应头的作用,缓存控制已超出本书内容:

HTTP1.0缓存控制响应头

  Pragma:no-cache:表示防止客户端缓存,需要强制从服务器获取最新的数据;

  Expires:HTTP1.0响应头,本地副本缓存过期时间,如果客户端发现缓存文件没有过期则不发送请求,HTTP的日期时间必须是格林威治时间(GMT), 如“Expires:Wed, 14 Mar 2012 09:38:32 GMT”;

 

HTTP1.1缓存控制响应头

  Cache-Control no-cache       强制客户端每次请求获取服务器的最新版本,不经过本地缓存的副本验证;

  Cache-Control no-store       强制客户端不保存请求的副本,该设置是防止Firefox缓存

  Cache-Control:max-age=[秒]    客户端副本缓存的最长时间,类似于HTTP1.0的Expires,只是此处是基于请求的相对时间间隔来计算,而非绝对时间。

 

还有相关缓存控制机制如Last-Modified(最后修改时间验证,客户端的上一次请求时间 在 服务器的最后修改时间 之后,说明服务器数据没有发生变化 返回304状态码)、ETag(没有变化时不重新下载数据,返回304)。

 

该抽象类默认被AbstractController和WebContentInterceptor继承。

4.4、AbstractController

该抽象类实现了Controller,并继承了WebContentGenerator(具有该类的特性,具体请看4.3),该类有如下属性:

boolean synchronizeOnSession = false表示该控制器是否在执行时同步session,从而保证该会话的用户串行访问该控制器。

public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
              //委托给WebContentGenerator进行缓存控制
              checkAndPrepare(request, response, this instanceof LastModified);
              //当前会话是否应串行化访问.
              if (this.synchronizeOnSession) {
                     HttpSession session = request.getSession(false);
                     if (session != null) {
                            Object mutex = WebUtils.getSessionMutex(session);
                            synchronized (mutex) {
                                   return handleRequestInternal(request, response);
                            }
                     }
              }
              return handleRequestInternal(request, response);
}

可以看出AbstractController实现了一些特殊功能,如继承了WebContentGenerator缓存控制功能,并提供了可选的会话的串行化访问功能。而且提供了handleRequestInternal方法,因此我们应该在具体的控制器类中实现handleRequestInternal方法,而不再是handleRequest。

AbstractController使用方法:

首先让我们使用AbstractController来重写第二章的HelloWorldController:

public class HelloWorldController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        //1、收集参数
        //2、绑定参数到命令对象
        //3、调用业务对象
        //4、选择下一个页面
        ModelAndView mv = new ModelAndView();
        //添加模型数据 可以是任意的POJO对象
        mv.addObject("message", "Hello World!");
        //设置逻辑视图名,视图解析器会根据该名字解析到具体的视图页面
        mv.setViewName("hello");
        return mv;
    }
}

 

<!— 在chapter4-servlet.xml配置处理器 -->
<bean name="/hello" class="cn.javass.chapter4.web.controller.HelloWorldController"/>

从如上代码我们可以看出:

1、继承AbstractController

2、实现handleRequestInternal方法即可。

 

直接通过response写响应

如果我们想直接在控制器通过response写出响应呢,以下代码帮我们阐述:

public class HelloWorldWithoutReturnModelAndViewController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {

        resp.getWriter().write("Hello World!!");        
        //如果想直接在该处理器/控制器写响应 可以通过返回null告诉DispatcherServlet自己已经写出响应了,不需要它进行视图解析
        return null;
    }
}
<!— 在chapter4-servlet.xml配置处理器 -->
<bean name="/helloWithoutReturnModelAndView" class="cn.javass.chapter4.web.controller.HelloWorldWithoutReturnModelAndViewController"/>

从如上代码可以看出如果想直接在控制器写出响应,只需要通过response写出,并返回null即可。

 

强制请求方法类型:

<!— 在chapter4-servlet.xml配置处理器 -->
<bean name="/helloWithPOST" class="cn.javass.chapter4.web.controller.HelloWorldController">
        <property name="supportedMethods" value="POST"></property>
</bean>

 以上配置表示只支持POST请求,如果是GET请求客户端将收到“HTTP Status 405 - Request method ‘GET‘ not supported”。

 

比如注册/登录可能只允许POST请求。

 

当前请求的session前置条件检查,如果当前请求无session将抛出HttpSessionRequiredException异常:

<!— 在chapter4-servlet.xml配置处理器 -->
<bean name="/helloRequireSession"
class="cn.javass.chapter4.web.controller.HelloWorldController">
        <property name="requireSession" value="true"/>
</bean>
在进入该控制器时,一定要有session存在,否则抛出HttpSessionRequiredException异常。

 

Session同步:

即同一会话只能串行访问该控制器。

 

客户端端缓存控制:

1、缓存5秒,cacheSeconds=5

package cn.javass.chapter4.web.controller;
//省略import
public class HelloWorldCacheController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        
        //点击后再次请求当前页面
        resp.getWriter().write("<a href=‘‘>this</a>");
        return null;
    }
}
<!— 在chapter4-servlet.xml配置处理器 -->
<bean name="/helloCache" 
class="cn.javass.chapter4.web.controller.HelloWorldCacheController">
<property name="cacheSeconds" value="5"/>
</bean>

如上配置表示告诉浏览器缓存5秒钟:

 

开启chrome浏览器调试工具:

 springMVC

服务器返回的响应头如下所示:

 springMVC

添加了“Expires:Wed, 14 Mar 2012 09:38:32 GMT” 和“Cache-Control:max-age=5” 表示允许客户端缓存5秒,当你点“this”链接时,会发现如下:

 springMVC

而且服务器也没有收到请求,当过了5秒后,你再点“this”链接会发现又重新请求服务器下载新数据。

 

注:下面提到一些关于缓存控制的一些特殊情况:

    1、对于一般的页面跳转(如超链接点击跳转、通过js调用window.open打开新页面都是会使用浏览器缓存的,在未过期情况下会直接使用浏览器缓存的副本,在未过期情况下一次请求也不发送);

    2、对于刷新页面(如按F5键刷新),会再次发送一次请求到服务器的;

 

2、不缓存,cacheSeconds=0

<!— 在chapter4-servlet.xml配置处理器 -->
<bean name="/helloNoCache"
class="cn.javass.chapter4.web.controller.HelloWorldCacheController">
<property name="cacheSeconds" value="0"/>
</bean>

 

以上配置会要求浏览器每次都去请求服务器下载最新的数据:

 springMVC

 

3、cacheSeconds<0,将不添加任何数据

响应头什么缓存控制信息也不加。

 

4、Last-Modified缓存机制

(1、在客户端第一次输入url时,服务器端会返回内容和状态码200表示请求成功并返回了内容;同时会添加一个“Last-Modified”的响应头表示此文件在服务器上的最后更新时间,如“Last-Modified:Wed, 14 Mar 2012 10:22:42 GMT”表示最后更新时间为(2012-03-14 10:22);

(2、客户端第二次请求此URL时,客户端会向服务器发送请求头 “If-Modified-Since”,询问服务器该时间之后当前请求内容是否有被修改过,如“If-Modified-Since: Wed, 14 Mar 2012 10:22:42 GMT”,如果服务器端的内容没有变化,则自动返回 HTTP 304状态码(只要响应头,内容为空,这样就节省了网络带宽)。

 

客户端强制缓存过期:

(1、可以按ctrl+F5强制刷新(会添加请求头 HTTP1.0 Pragma:no-cache和 HTTP1.1 Cache-Control:no-cache、If-Modified-Since请求头被删除)表示强制获取服务器内容,不缓存。

(2、在请求的url后边加上时间戳来重新获取内容,加上时间戳后浏览器就认为不是同一份内容:

http://test.com/?2343243243 和 http://test.com/?34334344 是两次不同的请求。

 

Spring也提供了Last-Modified机制的支持,只需要实现LastModified接口,如下所示:

package cn.javass.chapter4.web.controller;
public class HelloWorldLastModifiedCacheController extends AbstractController implements LastModified {
    private long lastModified;
    protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        //点击后再次请求当前页面
        resp.getWriter().write("<a href=‘‘>this</a>");
        return null;
    }
    public long getLastModified(HttpServletRequest request) {
        if(lastModified == 0L) {
            //TODO 此处更新的条件:如果内容有更新,应该重新返回内容最新修改的时间戳
            lastModified = System.currentTimeMillis();
        }
        return lastModified;
    }    
}
<!— 在chapter4-servlet.xml配置处理器 -->   
<bean name="/helloLastModified" 
class="cn.javass.chapter4.web.controller.HelloWorldLastModifiedCacheController"/>

HelloWorldLastModifiedCacheController只需要实现LastModified接口的getLastModified方法,保证当内容发生改变时返回最新的修改时间即可。

 

分析:

(1、发送请求到服务器,如(http://localhost:9080/springmvc-chapter4/helloLastModified),则服务器返回的响应为:

springMVC

(2、再次按F5刷新客户端,返回状态码304表示服务器没有更新过:

 springMVC

(3、重启服务器,再次刷新,会看到200状态码(因为服务器的lastModified时间变了)。

 

Spring判断是否过期,通过如下代码,即请求的“If-Modified-Since” 大于等于当前的getLastModified方法的时间戳,则认为没有修改:

this.notModified = (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));

 

5、ETag(实体标记)缓存机制

(1:浏览器第一次请求,服务器在响应时给请求URL标记,并在HTTP响应头中将其传送到客户端,类似服务器端返回的格式:“ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"”

(2:浏览器第二次请求,客户端的查询更新格式是这样的:“If-None-Match:"0f8b0c86fe2c0c7a67791e53d660208e3"”,如果ETag没改变,表示内容没有发生改变,则返回状态304。

 

 

Spring也提供了对ETag的支持,具体需要在web.xml中配置如下代码:

 

<filter>
   <filter-name>etagFilter</filter-name>
   <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>etagFilter</filter-name>
   <servlet-name>chapter4</servlet-name>
</filter-mapping>

 

此过滤器只过滤到我们DispatcherServlet的请求。

 

分析:

1):发送请求到服务器:“http://localhost:9080/springmvc-chapter4/hello”,服务器返回的响应头中添加了(ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"):

 springMVC

2):浏览器再次发送请求到服务器(按F5刷新),请求头中添加了“If-None-Match:

"0f8b0c86fe2c0c7a67791e53d660208e3"”,响应返回304代码,表示服务器没有修改,并且响应头再次添加了“ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"”(每次都需要计算):

 springMVC

那服务器端是如何计算ETag的呢?

 

protected String generateETagHeaderValue(byte[] bytes) {
              StringBuilder builder = new StringBuilder("\"0");
              DigestUtils.appendMd5DigestAsHex(bytes, builder);
              builder.append(‘"‘);
              return builder.toString();
}

 

bytes是response要写回到客户端的响应体(即响应的内容数据),是通过MD5算法计算的内容的摘要信息。也就是说如果服务器内容不发生改变,则ETag每次都是一样的,即服务器端的内容没有发生改变。

 

此处只列举了部分缓存控制,详细介绍超出了本书的范围,强烈推荐: http://www.mnot.net/cache_docs/(中文版http://www.chedong.com/tech/cache_docs.html) 详细了解HTTP缓存控制及为什么要缓存。

 

缓存的目的是减少相应延迟 和 减少网络带宽消耗,比如css、js、图片这类静态资源应该进行缓存。

实际项目一般使用反向代理服务器(如nginx、apache等)进行缓存。

4.5、ServletForwardingController

将接收到的请求转发到一个命名的servlet,具体示例如下:

 

java代码:
package cn.javass.chapter4.web.servlet;  
public class ForwardingServlet extends HttpServlet {      
    @Override  
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)  
    throws ServletException, IOException {  
  
        resp.getWriter().write("Controller forward to Servlet");  
          
    }  
}  
java代码:
<servlet>  
    <servlet-name>forwarding</servlet-name>  
    <servlet-class>cn.javass.chapter4.web.servlet.ForwardingServlet</servlet-class>  
</servlet>
java代码:
<!— 在chapter4-servlet.xml配置处理器 -->     
<bean name="/forwardToServlet"   
class="org.springframework.web.servlet.mvc.ServletForwardingController">  
        <property name="servletName" value="forwarding"></property>  
</bean> 

 

当我们请求/forwardToServlet时,会被转发到名字为“forwarding”的servlet处理,该sevlet的servlet-mapping标签配置是可选的。

 

4.6、BaseCommandController

命令控制器通用基类,提供了以下功能支持:

1、数据绑定:请求参数绑定到一个command object(命令对象,非GoF里的命令设计模式),这里的命令对象是指绑定请求参数的任何POJO对象;

   commandClass:表示命令对象实现类,如UserModel;

   commandName:表示放入请求的命令对象名字(默认command),request.setAttribute(commandName, commandObject);

 

2、验证功能:提供Validator注册功能,注册的验证器会验证命令对象属性数据是否合法;

   validators:通过该属性注入验证器,验证器用来验证命令对象属性是否合法;

 

该抽象类没有没有提供流程功能,只是提供了一些公共的功能,实际使用时需要使用它的子类。

4.7、AbstractCommandController

命令控制器之一,可以实现该控制器来创建命令控制器,该控制器能把自动封装请求参数到一个命令对象,而且提供了验证功能。

 

1、创建命令类(就是普通的JavaBean类/POJO)

java代码:
package cn.javass.chapter4.model;  
public class UserModel {  
    private String username;  
    private String password;  
        //省略setter/getter  
} 

2、实现控制器

 

java代码:
package cn.javass.chapter4.web.controller;  
//省略import  
public class MyAbstractCommandController extends AbstractCommandController {  
    public MyAbstractCommandController() {  
        //设置命令对象实现类  
        setCommandClass(UserModel.class);  
    }  
    @Override  
    protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception {  
        //将命令对象转换为实际类型  
        UserModel user = (UserModel) command;  
        ModelAndView mv = new ModelAndView();  
        mv.setViewName("abstractCommand");  
        mv.addObject("user", user);  
        return mv;  
    }  
}  
java代码:
<!— 在chapter4-servlet.xml配置处理器 -->     
<bean name="/abstractCommand"   
class="cn.javass.chapter4.web.controller.MyAbstractCommandController">  
        <!-- 也可以通过依赖注入 注入命令实现类 -->  
        <!-- property name="commandClass" value="cn.javass.chapter4.model.UserModel"/-->  
</bean>

 

java代码:
<!— WEB-INF/jsp/abstractCommand.jsp视图下的主要内容 -->     
  
${user.username }-${user.password }  

当我们在浏览器中输入“http://localhost:9080/springmvc-chapter4/abstractCommand?username=123&password=123”,会自动将请求参数username和password绑定到命令对象;绑定时按照JavaBean命名规范绑定;

springMVC

4.8、AbstractFormController

用于支持带步骤的表单提交的命令控制器基类,使用该控制器可以完成:

1、定义表单处理(表单的渲染),并从控制器获取命令对象构建表单;

2、提交表单处理,当用户提交表单内容后,AbstractFormController可以将用户请求的数据绑定到命令对象,并可以验证表单内容、对命令对象进行处理。

 

java代码:
      @Override  
rotected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)  
    throws Exception {  
     //1、是否是表单提交? 该方法实现为("POST".equals(request.getMethod())),即POST表示表单提交  
if (isFormSubmission(request)) {  
    try {  
        Object command = getCommand(request);  
        ServletRequestDataBinder binder = bindAndValidate(request, command);  
        BindException errors = new BindException(binder.getBindingResult());  
              //表单提交应该放到该方法实现  
        return processFormSubmission(request, response, command, errors);  
    }  
    catch (HttpSessionRequiredException ex) {  
              //省略部分代码  
        return handleInvalidSubmit(request, response);  
    }  
}  
else {  
    //2、表示是表单展示,该方法又转调showForm方法,因此我们需要覆盖showForm来完成表单展示  
    return showNewForm(request, response);  
} 

    bindOnNewForm:是否在进行表单展示时绑定请求参数到表单对象,默认false,不绑定;

    sessionForm:session表单模式,如果开启(true)则会将表单对象放置到session中,从而可以跨越多次请求保证数据不丢失(多步骤表单常使用该方式,详解AbstractWizardFormController),默认false;

 

    Object formBackingObject(HttpServletRequest request) :提供给表单展示时使用的表单对象(form object表单要展示的默认数据),默认通过commandName暴露到请求给展示表单;

    Map referenceData(HttpServletRequest request, Object command, Errors errors):展示表单时需要的一些引用数据(比如用户注册,可能需要选择工作地点,这些数据可以通过该方法提供),如:

java代码:
protected Map referenceData(HttpServletRequest request) throws Exception {  
             Map model = new HashMap();  
             model.put("cityList", cityList);  
             return model;  
} 

    这样就可以在表单展示页面获取cityList数据。

 

SimpleFormController继承该类,而且提供了更简单的表单流程控制。

4.9、SimpleFormController

提供了更好的两步表单支持:

1、准备要展示的数据,并到表单展示页面;

2、提交数据数据进行处理。

 

第一步,展示:

springMVC 

第二步,提交表单:

springMVC

接下来咱们写一个用户注册的例子学习一下:

(1、控制器

 

java代码:
package cn.javass.chapter4.web.controller;  
//省略import  
public class RegisterSimpleFormController extends SimpleFormController {      
    public RegisterSimpleFormController() {  
        setCommandClass(UserModel.class); //设置命令对象实现类  
        setCommandName("user");//设置命令对象的名字  
    }  
    //form object 表单对象,提供展示表单时的表单数据(使用commandName放入请求)  
    protected Object formBackingObject(HttpServletRequest request) throws Exception {  
        UserModel user = new UserModel();  
        user.setUsername("请输入用户名");  
        return user;  
    }  
    //提供展示表单时需要的一些其他数据    
    protected Map referenceData(HttpServletRequest request) throws Exception {  
        Map map = new HashMap();  
        map.put("cityList", Arrays.asList("山东", "北京", "上海"));  
        return map;  
    }  
    protected void doSubmitAction(Object command) throws Exception {  
        UserModel user = (UserModel) command;  
        //TODO 调用业务对象处理  
        System.out.println(user);  
    }  
}  

setCommandClasssetCommandName分别设置了命令对象的实现类和名字;

formBackingObjectreferenceData提供了表单展示需要的视图;

doSubmitAction用于执行表单提交动作,由onSubmit方法调用,如果不需要请求/响应对象或进行数据验证,可以直接使用doSubmitAction方法进行功能处理。

(2、spring配置(chapter4-servlet.xml

 

java代码:
<bean name="/simpleForm"   
class="cn.javass.chapter4.web.controller.RegisterSimpleFormController">  
        <property name="formView" value="register"/>  
        <property name="successView" value="redirect:/success"/>  
</bean>  
<bean name="/success" class="cn.javass.chapter4.web.controller.SuccessController"/> 

 

formView表示展示表单时显示的页面;

successView表示处理成功时显示的页面;“redirect:/success”表示成功处理后重定向到/success控制器;防止表单重复提交;

/success” bean的作用是显示成功页面,此处就不列举了。

 

(3、视图页面

 

java代码:
<!-- register.jsp 注册展示页面-->  
<form method="post">  
username:<input type="text" name="username" value="${user.username}"><br/>  
password:<input type="password" name="username"><br/>  
city:<select>  
  <c:forEach items="${cityList }" var="city">  
   <option>${city}</option>  
  </c:forEach>  
</select><br/>  
<input type="submit" value="注册"/>  
</form> 

此处可以使用${user.username}获取到formBackingObject设置的表单对象、使用${cityList}获取referenceData设置的表单支持数据;

 

到此一个简单的两步表单到此结束,但这个表单有重复提交表单的问题,而且表单对象到页面的绑定是通过手工绑定的,后边我们会学习spring标签库(提供自动绑定表单对象到页面)。

4.10、CancellableFormController

一个可取消的表单控制器,继承SimpleFormController,额外提供取消表单功能。

 

1、表单展示:和SimpleFormController一样;

2、表单取消:和SimpleFormController一样;

3、表单成功提交:取消功能处理方法为:onCancel(Object command),而且默认返回cancelView属性指定的逻辑视图名。

   那如何判断是取消呢?如果请求中有参数名为“_cancel”的参数,则表示表单取消。也可以通过cancelParamKey来修改参数名(如“_cancel.x”等)。

springMVC

示例:

 

(1、控制器

复制RegisterSimpleFormController一份命名为CanCancelRegisterSimpleFormController,添加取消功能处理方法实现:

 

java代码:
@Override  
protected ModelAndView onCancel(Object command) throws Exception {  
    UserModel user = (UserModel) command;  
    //TODO 调用业务对象处理  
    System.out.println(user);  
    return super.onCancel(command);  
}

onCancel在该功能方法内实现取消逻辑,父类的onCancel方法默认返回cancelView属性指定的逻辑视图名。

 

(2、spring配置(chapter4-servlet.xml

 

java代码:
<bean name="/canCancelForm"   
class="cn.javass.chapter4.web.controller.CanCancelRegisterSimpleFormController">  
        <property name="formView" value="register"/>  
        <property name="successView" value="redirect:/success"/>  
        <property name="cancelView" value="redirect:/cancel"/>  
</bean>  
<bean name="/cancel" class="cn.javass.chapter4.web.controller.CancelController"/>  

cancelParamKey用于判断是否是取消的请求参数名,默认是_cancel,即如果请求参数数据中含有名字_cancel则表示是取消,将调用onCancel功能处理方法;

cancelView表示取消时时显示的页面;“redirect:/cancel”表示成功处理后重定向到/cancel控制器;防止表单重复提交;

/cancel” bean的作用是显示取消页面,此处就不列举了(详见代码)。

(3、视图页面(修改register.jsp

 

java代码:
<input type="submit" name="_cancel" value="取消"/>  

该提交按钮的作用是取消,因为name="_cancel"即请求后会有一个名字为_cancel的参数,因此会执行onCancel功能处理方法。

(4、测试:

在浏览器输入“http://localhost:9080/springmvc-chapter4/canCancelForm”,则首先到展示视图页面,点击“取消按钮”将重定向到“http://localhost:9080/springmvc-chapter4/cancel”,说明取消成功了。

 

 

实际项目可能会出现比如一些网站的完善个人资料都是多个页面(即多步),那应该怎么实现呢?接下来让我们看一下spring Web MVC提供的对多步表单的支持类AbstractWizardFormController。

 

4.11、AbstractWizardFormController

向导控制器类提供了多步骤(向导)表单的支持(如完善个人资料时分步骤填写基本信息、工作信息、学校信息等)

假设现在做一个完善个人信息的功能,分三个页面展示:

1、页面1完善基本信息;

2、页面2完善学校信息;

3、页面3完善工作信息。

这里我们要注意的是当用户跳转到页面2时页面1的信息是需要保存起来的,还记得AbstractFormController中的sessionForm吗? 如果为true则表单数据存放到session中,哈哈,AbstractWizardFormController就是使用了这个特性。

springMVC
向导中的页码从0开始;

PARAM_TARGET = "_target":

用于选择向导中的要使用的页面参数名前缀,如“_target0”则选择第0个页面显示,即图中的“wizard/baseInfo”,以此类推,如“_target1”将选择第1页面,要得到的页码为去除前缀“_target”后的数字即是;

PARAM_FINISH = "_finish":

如果请求参数中有名为“_finish”的参数,表示向导成功结束,将会调用processFinish方法进行完成时的功能处理;

PARAM_CANCEL = "_cancel":

如果请求参数中有名为“_cancel”的参数,表示向导被取消,将会调用processCancel方法进行取消时的功能处理;

向导中的命令对象:

向导中的每一个步骤都会把相关的参数绑定到命令对象,该表单对象默认放置在session中,从而可以跨越多次请求得到该命令对象。

 

接下来具体看一下如何使用吧。

(1、修改我们的模型数据以支持多步骤提交:

 

Java代码
public class UserModel {
private String username;
private String password;
private String realname; //真实姓名
private WorkInfoModel workInfo;
private SchoolInfoModel schoolInfo;
//省略getter/setter
}
Java代码
public class SchoolInfoModel {
private String schoolType; //学校类型:高中、中专、大学
private String schoolName; //学校名称
private String specialty; //专业
//省略getter/setter
}
Java代码
public class WorkInfoModel {
private String city; //所在城市
private String job; //职位
private String year; //工作年限
//省略getter/setter
}
(2、控制器

Java代码
package cn.javass.chapter4.web.controller;
//省略import
public class InfoFillWizardFormController extends AbstractWizardFormController {
public InfoFillWizardFormController() {
setCommandClass(UserModel.class);
setCommandName("user");
}
protected Map referenceData(HttpServletRequest request, int page) throws Exception {
Map map = new HashMap();
if(page==1) { //如果是填写学校信息页 需要学校类型信息
map.put("schoolTypeList", Arrays.asList("高中", "中专", "大学"));
}
if(page==2) {//如果是填写工作信息页 需要工作城市信息
map.put("cityList", Arrays.asList("济南", "北京", "上海"));
}
return map;
}
protected void validatePage(Object command, Errors errors, int page) {
//提供每一页数据的验证处理方法
}
protected void postProcessPage(HttpServletRequest request, Object command, Errors errors, int page) throws Exception {
//提供给每一页完成时的后处理方法
}
protected ModelAndView processFinish(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception {
//成功后的处理方法
System.out.println(command);
return new ModelAndView("redirect:/success");
}
protected ModelAndView processCancel(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception {
//取消后的处理方法
System.out.println(command);
return new ModelAndView("redirect:/cancel");
}
}
page页码:是根据请求中以“_target”开头的参数名来确定的,如“_target0”,则页码为0;

referenceData:提供每一页需要的表单支持对象,如完善学校信息需要学校类型,page页码从0开始(而且根据请求参数中以“_target”开头的参数来确定当前页码,如_target1,则page=1);

validatePage:验证当前页的命令对象数据,验证应根据page页码来分步骤验证;

postProcessPage:验证成功后的后处理;

processFinish:成功时执行的方法,此处直接重定向到/success控制器(详见CancelController);

processCancel:取消时执行的方法,此处直接重定向到/cancel控制器(详见SuccessController);

 

其他需要了解:

allowDirtyBack和allowDirtyForward:决定在当前页面验证失败时,是否允许向导前移和后退,默认false不允许;

onBindAndValidate(HttpServletRequest request, Object command, BindException errors, int page):允许覆盖默认的绑定参数到命令对象和验证流程。

(3、spring配置文件(chapter4-servlet.xml)

Java代码
<bean name="/infoFillWizard"
class="cn.javass.chapter4.web.controller.InfoFillWizardFormController">
<property name="pages">
<list>
<value>wizard/baseInfo</value>
<value>wizard/schoolInfo</value>
<value>wizard/workInfo</value>
</list>
</property>
</bean>
pages:表示向导中每一个步骤的逻辑视图名,当InfoFillWizardFormController的page=0,则将会选择“wizard/baseInfo”,以此类推,从而可以按步骤选择要展示的视图。

 

(4、向导中的每一步视图

(4.1、基本信息页面(第一步) baseInfo.jsp:

Java代码
<form method="post">
真实姓名:<input type="text" name="realname" value="${user.realname}"><br/>
<input type="submit" name="_target1" value="下一步"/>
</form>
当前页码为0;

name="_target1":表示向导下一步要显示的页面的页码为1;

 

(4.2、学校信息页面(第二步) schoolInfo.jsp:

 

Java代码
<form method="post">
学校类型:<select name="schoolInfo.schoolType">
<c:forEach items="${schoolTypeList }" var="schoolType">
<option value="${schoolType }"
<c:if test="${user.schoolInfo.schoolType eq schoolType}">
selected="selected"
</c:if>
>
${schoolType}
</option>
</c:forEach>
</select><br/>
学校名称:<input type="text" name="schoolInfo.schoolName" value="${user.schoolInfo.schoolName}"/><br/>
专业:<input type="text" name="schoolInfo.specialty" value="${user.schoolInfo.specialty}"/><br/>
<input type="submit" name="_target0" value="上一步"/>
<input type="submit" name="_target2" value="下一步"/>
</form>
(4.3、工作信息页面(第三步) workInfo.jsp:

 

Java代码
<form method="post">
所在城市:<select name="workInfo.city">
<c:forEach items="${cityList }" var="city">
<option value="${city }"
<c:if test="${user.workInfo.city eq city}">selected="selected"</c:if>
>
${city}
</option>
</c:forEach>
</select><br/>
职位:<input type="text" name="workInfo.job" value="${user.workInfo.job}"/><br/>
工作年限:<input type="text" name="workInfo.year" value="${user.workInfo.year}"/><br/>
<input type="submit" name="_target1" value="上一步"/>
<input type="submit" name="_finish" value="完成"/>
<input type="submit" name="_cancel" value="取消"/>
</form>
当前页码为2;

name="_target1":上一步,表示向导上一步要显示的页面的页码为1;

name="_finish":向导完成,表示向导成功,将会调用向导控制器的processFinish方法;

name="_cancel":向导取消,表示向导被取消,将会调用向导控制器的processCancel方法;

 springMVC

到此向导控制器完成,此处的向导流程比较简单,如果需要更复杂的页面流程控制,可以选择使用Spring Web Flow框架。

 

4.12、ParameterizableViewController

参数化视图控制器,不进行功能处理(即静态视图),根据参数的逻辑视图名直接选择需要展示的视图。

 

Java代码
<bean name="/parameterizableView"
class="org.springframework.web.servlet.mvc.ParameterizableViewController">
<property name="viewName" value="success"/>
</bean>
该控制器接收到请求后直接选择参数化的视图,这样的好处是在配置文件中配置,从而避免程序的硬编码,比如像帮助页面等不需要进行功能处理,因此直接使用该控制器映射到视图。

4.13、AbstractUrlViewController

提供根据请求URL路径直接转化为逻辑视图名的支持基类,即不需要功能处理,直接根据URL计算出逻辑视图名,并选择具体视图进行展示:

urlDecode:是否进行url解码,不指定则默认使用服务器编码进行解码(如Tomcat默认ISO-8859-1);

urlPathHelper:用于解析请求路径的工具类,默认为org.springframework.web.util.UrlPathHelper。

 

UrlFilenameViewController是它的一个实现者,因此我们应该使用UrlFilenameViewController。

 

4.14、UrlFilenameViewController

将请求的URL路径转换为逻辑视图名并返回的转换控制器,即不需要功能处理,直接根据URL计算出逻辑视图名,并选择具体视图进行展示:

 

根据请求URL路径计算逻辑视图名;

Java代码
<bean name="/index1/*"
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
<bean name="/index2/**"
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
<bean name="/*.html"
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
<bean name="/index3/*.html"
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
/index1/*:可以匹配/index1/demo,但不匹配/index1/demo/demo,如/index1/demo逻辑视图名为demo;

/index2/**:可以匹配/index2路径下的所有子路径,如匹配/index2/demo,或/index2/demo/demo,“/index2/demo”的逻辑视图名为demo,而“/index2/demo/demo”逻辑视图名为demo/demo;

/*.html:可以匹配如/abc.html,逻辑视图名为abc,后缀会被删除(不仅仅可以是html);

/index3/*.html:可以匹配/index3/abc.html,逻辑视图名也是abc;

 

上述模式为Spring Web MVC使用的Ant-style 模式进行匹配的:

 

Java代码
? 匹配一个字符,如/index? 可以匹配 /index1 , 但不能匹配 /index 或 /index12
* 匹配零个或多个字符,如/index1/*,可以匹配/index1/demo,但不匹配/index1/demo/demo
** 匹配零个或多个路径,如/index2/**:可以匹配/index2路径下的所有子路径,如匹配/index2/demo,或/index2/demo/demo

如果我有如下模式,那Spring该选择哪一个执行呢?当我的请求为“/long/long”时如下所示:
/long/long
/long/**/abc
/long/**
/**
Spring的AbstractUrlHandlerMapping使用:最长匹配优先;
如请求为“/long/long” 将匹配第一个“/long/long”,但请求“/long/acd” 则将匹配 “/long/**”,如请求“/long/aa/abc”则匹配“/long/**/abc”,如请求“/abc”则将匹配“/**”
UrlFilenameViewController还提供了如下属性:

prefix:生成逻辑视图名的前缀;

suffix:生成逻辑视图名的后缀;

 

Java代码
protected String postProcessViewName(String viewName) {
return getPrefix() + viewName + getSuffix();
}
Java代码
<bean name="/*.htm" class="org.springframework.web.servlet.mvc.UrlFilenameViewController">
<property name="prefix" value="test"/>
<property name="suffix" value="test"/>
</bean>
当prefix=“test”,suffix=“test”,如上所示的/*.htm:可以匹配如/abc.htm,但逻辑视图名将变为testabctest。

 

4.15、MultiActionController

之前学过的控制器如AbstractCommandController、SimpleFormController等一般对应一个功能处理方法(如新增),如果我要实现比如最简单的用户增删改查(CRUD Create-Read-Update-Delete),那该怎么办呢?
4.15.1 解决方案
1、每一个功能对应一个控制器,如果是CRUD则需要四个控制器,但这样我们的控制器会暴增,肯定不可取;
2、使用Spring Web MVC提供的MultiActionController,用于支持在一个控制器里添加多个功能处理方法,即将多个请求的处理方法放置到一个控制器里,这种方式不错。

4.15.2 问题
1、 MultiActionController如何将不同的请求映射不同的请求的功能处理方法呢?
Spring Web MVC提供了MethodNameResolver(方法名解析器)用于解析当前请求到需要执行的功能处理方法的方法名。默认使用InternalPathMethodNameResolver实现类,另外还提供了ParameterMethodNameResolverPropertiesMethodNameResolver,当然我们也可以自己来实现,稍候我们仔细研究下它们是如何工作的。

2、那我们的功能处理方法应该怎么写呢?

public (ModelAndView | Map | String | void) actionName(HttpServletRequest request, HttpServletResponse response, [,HttpSession session] [,AnyObject]);

哦,原来如此,我们只需要按照如上格式写我们的功能处理方法即可;此处需要注意一下几点:
1、返回值:即模型和视图部分;
ModelAndView:模型和视图部分,之前已经见过了;
Map:只返回模型数据,逻辑视图名会根据RequestToViewNameTranslator实现类来计算,稍候讨论;
String:只返回逻辑视图名;
void:表示该功能方法直接写出response响应(如果其他返回值类型(如Map)返回null则和void进行相同的处理);
2、actionName:功能方法名字;由methodNameResolver根据请求信息解析功能方法名,通过反射调用;
3、形参列表:顺序固定,“[]”表示可选,我们来看看几个示例吧:

//表示到新增页面
public ModelAndView toAdd(HttpServletRequest request, HttpServletResponse response);
//表示新增表单提交,在最后可以带着命令对象
public ModelAndView add(HttpServletRequest request, HttpServletResponse response, UserModel user);
//列表,但只返回模型数据,视图名会通过RequestToViewNameTranslator实现来计算
public Map list(HttpServletRequest request, HttpServletResponse response);
//文件下载,返回值类型为void,表示该功能方法直接写响应
public void fileDownload(HttpServletRequest request, HttpServletResponse response)
//第三个参数可以是session
public ModelAndView sessionWith(HttpServletRequest request, HttpServletResponse response, HttpSession session);
//如果第三个参数是session,那么第四个可以是命令对象,顺序必须是如下顺序
public void sessionAndCommandWith(HttpServletRequest request, HttpServletResponse response, HttpSession session, UserModel user)

4、异常处理方法,MultiActionController提供了简单的异常处理,即在请求的功能处理过程中遇到异常会交给异常处理方法进行处理,式如下所示:

public ModelAndView anyMeaningfulName(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception)

MultiActionController会使用最接近的异常类型来匹配对应的异常处理方法,示例如下所示:

//处理PayException
public ModelAndView processPayException(HttpServletRequest request, HttpServletResponse response, PayException ex)
//处理Exception
public ModelAndView processException(HttpServletRequest request, HttpServletResponse response, Exception ex)

4.15.3 MultiActionController类实现
类定义:public class MultiActionController extends AbstractController implements LastModified ,继承了AbstractController,并实现了LastModified接口,默认返回-1;

核心属性:
delegate:功能处理的委托对象,即我们要调用请求处理方法所在的对象,默认是this;
methodNameResolver:功能处理方法名解析器,即根据请求信息来解析需要执行的delegate的功能处理方法的方法名。
核心方法:

Java代码

//判断方法是否是功能处理方法 
private boolean isHandlerMethod(Method method) { 
//得到方法返回值类型 
Class returnType = method.getReturnType(); 
//返回值类型必须是ModelAndView、Map、String、void中的一种,否则不是功能处理方法 
if (ModelAndView.class.equals(returnType) || Map.class.equals(returnType) || String.class.equals(returnType) || 
void.class.equals(returnType)) { 
Class[] parameterTypes = method.getParameterTypes(); 
//功能处理方法参数个数必须>=2,且第一个是HttpServletRequest类型、第二个是HttpServletResponse 
//且不能Controller接口的handleRequest(HttpServletRequest request, HttpServletResponse response),这个方法是由系统调用 
return (parameterTypes.length >= 2 && 
HttpServletRequest.class.equals(parameterTypes[0]) && 
HttpServletResponse.class.equals(parameterTypes[1]) && 
!("handleRequest".equals(method.getName()) && parameterTypes.length == 2)); 
} 
return false; 
} 

Java代码

//是否是异常处理方法 
private boolean isExceptionHandlerMethod(Method method) { 
//异常处理方法必须是功能处理方法 且 参数长度为3、第三个参数类型是Throwable子类 
return (isHandlerMethod(method) && 
method.getParameterTypes().length == 3 && 
Throwable.class.isAssignableFrom(method.getParameterTypes()[2])); 
} 

Java代码

private void registerHandlerMethods(Object delegate) { 
//缓存Map清空 
this.handlerMethodMap.clear(); 
this.lastModifiedMethodMap.clear(); 
this.exceptionHandlerMap.clear(); 

//得到委托对象的所有public方法 
Method[] methods = delegate.getClass().getMethods(); 
for (Method method : methods) { 
//验证是否是异常处理方法,如果是放入exceptionHandlerMap缓存map 
if (isExceptionHandlerMethod(method)) { 
registerExceptionHandlerMethod(method); 
} 
//验证是否是功能处理方法,如果是放入handlerMethodMap缓存map 
else if (isHandlerMethod(method)) { 
registerHandlerMethod(method); 
registerLastModifiedMethodIfExists(delegate, method); 
} 
} 
} 

Java代码

protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) 
throws Exception { 
try { 
//1、使用methodNameResolver 方法名解析器根据请求解析到要执行的功能方法的方法名 
String methodName = this.methodNameResolver.getHandlerMethodName(request); 
//2、调用功能方法(通过反射调用,此处就粘贴代码了) 
return invokeNamedMethod(methodName, request, response); 
} 
catch (NoSuchRequestHandlingMethodException ex) { 
return handleNoSuchRequestHandlingMethod(ex, request, response); 
} 
} 

接下来,我们看一下MultiActionController如何使用MethodNameResolver来解析请求到功能处理方法的方法名。
4.15.4 MethodNameResolver
1、InternalPathMethodNameResolver:MultiActionController的默认实现,提供从请求URL路径解析功能方法的方法名,从请求的最后一个路径(/)开始,并忽略扩展名;如请求URL是“/user/list.html”,则解析的功能处理方法名为“list”,即调用list方法。该解析器还可以指定前缀和后缀,通过prefix和suffix属性,如指定prefix=”test_”,则功能方法名将变为test_list;

2、ParameterMethodNameResolver:提供从请求参数解析功能处理方法的方法名,并按照如下顺序进行解析:
(1、 methodParamNames:根据请求的参数名解析功能方法名(功能方法名和参数名同名);
Java代码

<property name="methodParamNames" value="list,create,update"/> 

如上配置时,如果请求中含有参数名list、create、update时,则功能处理方法名为list、create、update,这种方式的可以在当一个表单有多个提交按钮时使用,不同的提交按钮名字不一样即可。
ParameterMethodNameResolver也考虑到图片提交按钮提交问题:
<input type="image" name="list"> 和submit类似可以提交表单,单击该图片后会发送两个参数“list.x=x轴坐标”和“list.y=y轴坐标”(如提交后会变为list.x=7&list.y=5);因此我们配置的参数名(如list)在会加上“.x” 和 “.y”进行匹配。

Java代码

for (String suffix : SUBMIT_IMAGE_SUFFIXES) {//SUBMIT_IMAGE_SUFFIXES {“.x”, “.y”} 
   if (request.getParameter(name + suffix) != null) {// name是我们配置的methodParamNames 
      return true; 
   } 
} 

(2、paramName:根据请求参数名的值解析功能方法名,默认的参数名是action,即请求的参数中含有“action=query”,则功能处理方法名为query;
(3、logicalMappings:逻辑功能方法名到真实功能方法名映射,如下所示:
Java代码

<property name="logicalMappings"> 
   <props> 
      <prop key="doList">list</prop> 
   </props> 
</property> 

即如果步骤1或2解析出逻辑功能方法名为doList(逻辑的),将会被重新映射为list功能方法名(真正执行的)。
(4、defaultMethodName:默认的方法名,当以上策略失败时默认调用的方法名。

3、PropertiesMethodNameResolver:提供自定义的从请求URL解析功能方法的方法名,使用一组用户自定义的模式到功能方法名的映射,映射使用Properties对象存放,具体配置示例如下:
Java代码

<bean id="propertiesMethodNameResolver" 
class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver"> 
   <property name="mappings"> 
   <props> 
      <prop key="/create">create</prop> 
      <prop key="/update">update</prop> 
      <prop key="/delete">delete</prop> 
      <prop key="/list">list</prop> 
      <!-- 默认的行为 --> 
      <prop key="/**">list</prop> 
   </props> 
</property> 
</bean> 

对于/create请求将调用create方法,Spring内部使用PathMatcher进行匹配(默认实现是AntPathMatcher)。
4.15.5 RequestToViewNameTranslator
用于直接将请求转换为逻辑视图名。默认实现为DefaultRequestToViewNameTranslator。

1、DefaultRequestToViewNameTranslator:将请求URL转换为逻辑视图名,默认规则如下:
http://localhost:9080/web上下文/list -------> 逻辑视图名为list
http://localhost:9080/web上下文/list.html -------> 逻辑视图名为list(默认删除扩展名)
http://localhost:9080/web上下文/user/list.html -------> 逻辑视图名为user/list
4.15.6 示例
(1、控制器UserController
Java代码

package cn.javass.chapter4.web.controller; 
//省略import 
public class UserController extends MultiActionController { 
//用户服务类 
private UserService userService; 
//逻辑视图名 通过依赖注入方式注入,可配置 
private String createView; 
private String updateView; 
private String deleteView; 
private String listView; 
private String redirectToListView; 
//省略setter/getter 

public String create(HttpServletRequest request, HttpServletResponse response, UserModel user) { 
if("GET".equals(request.getMethod())) { 
//如果是get请求 我们转向 新增页面 
return getCreateView(); 
} 
userService.create(user); 
//直接重定向到列表页面 
return getRedirectToListView(); 
} 
public ModelAndView update(HttpServletRequest request, HttpServletResponse response, UserModel user) { 
if("GET".equals(request.getMethod())) { 
//如果是get请求 我们转向更新页面 
ModelAndView mv = new ModelAndView(); 
//查询要更新的数据 
mv.addObject("command", userService.get(user.getUsername())); 
mv.setViewName(getUpdateView()); 
return mv; 
} 
userService.update(user); 
//直接重定向到列表页面 
return new ModelAndView(getRedirectToListView()); 
} 


public ModelAndView delete(HttpServletRequest request, HttpServletResponse response, UserModel user) { 
if("GET".equals(request.getMethod())) { 
//如果是get请求 我们转向删除页面 
ModelAndView mv = new ModelAndView(); 
//查询要删除的数据 
mv.addObject("command", userService.get(user.getUsername())); 
mv.setViewName(getDeleteView()); 
return mv; 
} 
userService.delete(user); 
//直接重定向到列表页面 
return new ModelAndView(getRedirectToListView()); 
} 

public ModelAndView list(HttpServletRequest request, HttpServletResponse response) { 
ModelAndView mv = new ModelAndView(); 
mv.addObject("userList", userService.list()); 
mv.setViewName(getListView()); 
return mv; 
} 

//如果使用委托方式,命令对象名称只能是command 
protected String getCommandName(Object command) { 
//命令对象的名字 默认command 
return "command"; 
} 
} 

增删改:如果是GET请求方法,则表示到展示页面,POST请求方法表示真正的功能操作;
getCommandName:表示是命令对象名字,默认command,对于委托对象实现方式无法改变,因此我们就使用默认的吧。

(2、spring配置文件chapter4-servlet.xml

Java代码

<bean id="userService" class="cn.javass.chapter4.service.UserService"/> 
<bean name="/user/**" class="cn.javass.chapter4.web.controller.UserController"> 
<property name="userService" ref="userService"/> 
<property name="createView" value="user/create"/> 
<property name="updateView" value="user/update"/> 
<property name="deleteView" value="user/delete"/> 
<property name="listView" value="user/list"/> 
<property name="redirectToListView" value="redirect:/user/list"/> 
<!-- 使用PropertiesMethodNameResolver来解析功能处理方法名 --> 
<!--property name="methodNameResolver" ref="propertiesMethodNameResolver"/--> 
</bean> 

userService:用户服务类,实现业务逻辑;
依赖注入:对于逻辑视图页面通过依赖注入方式注入,redirectToListView表示增删改成功后重定向的页面,防止重复表单提交;
默认使用InternalPathMethodNameResolver解析请求URL到功能方法名。

(3、视图页面
(3.1、list页面(WEB-INF/jsp/user/list.jsp)

Java代码

<a href="${pageContext.request.contextPath}/user/create">用户新增</a><br/> 
<table border="1" width="50%"> 
<tr> 
<th>用户名</th> 
<th>真实姓名</th> 
<th>操作</th> 
</tr> 
<c:forEach items="${userList}" var="user"> 
<tr> 
<td>${user.username }</td> 
<td>${user.realname }</td> 
<td> 
<a href="${pageContext.request.contextPath}/user/update?username=${user.username}">更新</a> 
| 
<a href="${pageContext.request.contextPath}/user/delete?username=${user.username}">删除</a> 
</td> 
</tr> 
</c:forEach> 
</table> 

(3.2、update页面(WEB-INF/jsp/user/update.jsp)
Java代码

<form action="${pageContext.request.contextPath}/user/update" method="post"> 
用户名: <input type="text" name="username" value="${command.username}"/><br/> 
真实姓名:<input type="text" name="realname" value="${command.realname}"/><br/> 
<input type="submit" value="更新"/> 
</form> 

(4、测试:
默认的InternalPathMethodNameResolver将进行如下解析:
http://localhost:9080/springmvc-chapter4/user/list————>list方法名;
http://localhost:9080/springmvc-chapter4/user/create————>create方法名;
http://localhost:9080/springmvc-chapter4/user/update————>update功能处理方法名;
http://localhost:9080/springmvc-chapter4/user/delete————>delete功能处理方法名。

我们可以将默认的InternalPathMethodNameResolver改为PropertiesMethodNameResolver:

Java代码

<bean id="propertiesMethodNameResolver" 
class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver"> 
<property name="mappings"> 
<props> 
<prop key="/user/create">create</prop> 
<prop key="/user/>update">update</prop>
<prop key="/user/delete">delete</prop>
<prop key="/user/list">list</prop>  
<prop key="/**">list</prop><!-- 默认的行为 --> 
</props> 
</property> 
<property name="alwaysUseFullPath" value="false"/><!-- 不使用全路径 --> 
</bean> 
<bean name="/user/**" class="cn.javass.chapter4.web.controller.UserController"> 
<!—省略其他配置,详见配置文件--> 
<!-- 使用PropertiesMethodNameResolver来解析功能处理方法名 --> 
<property name="methodNameResolver" ref="propertiesMethodNameResolver"/> 
</bean> 

/**表示默认解析到list功能处理方法。
如上配置方式可以很好的工作,但必须继承MultiActionController,Spring Web MVC提供给我们无需继承MultiActionController实现方式,即使有委托对象方式,继续往下看吧。
4.15.7、委托方式实现
(1、控制器UserDelegate
将UserController复制一份,改名为UserDelegate,并把继承MultiActionController去掉即可,其他无需改变。
(2、spring配置文件chapter4-servlet.xml
Java代码

<!—委托对象--> 
<bean id="userDelegate" class="cn.javass.chapter4.web.controller.UserDelegate"> 
<property name="userService" ref="userService"/> 
<property name="createView" value="user2/create"/> 
<property name="updateView" value="user2/update"/> 
<property name="deleteView" value="user2/delete"/> 
<property name="listView" value="user2/list"/> 
<property name="redirectToListView" value="redirect:/user2/list"/> 
</bean> 
<!—控制器对象--> 
<bean name="/user2/**" 
class="org.springframework.web.servlet.mvc.multiaction.MultiActionController"> 
<property name="delegate" ref="userDelegate"/> 
<property name="methodNameResolver" ref="parameterMethodNameResolver"/> 
</bean> 

delegate:控制器对象通过delegate属性指定委托对象,即实际调用delegate委托对象的功能方法。
methodNameResolver:此处我们使用ParameterMethodNameResolver解析器;
Java代码

<!—ParameterMethodNameResolver --> 
<bean id="parameterMethodNameResolver" 
class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver"> 
<!-- 1、根据请求参数名解析功能方法名 --> 
<property name="methodParamNames" value="create,update,delete"/> 
<!-- 2、根据请求参数名的值解析功能方法名 --> 
<property name="paramName" value="action"/> 
<!-- 3、逻辑方法名到真实方法名的映射 --> 
<property name="logicalMappings"> 
<props> 
<prop key="doList">list</prop> 
</props> 
</property> 
<!—4、默认执行的功能处理方法 --> 
<property name="defaultMethodName" value="list"/> 
</bean> 

1、methodParamNames:create,update,delete,当请求中有参数名为这三个的将被映射为功能方法名,如“<input type="submit" name="create" value="新增"/>”提交后解析得到的功能方法名为create;
2、paramName:当请求中有参数名为action,则将值映射为功能方法名,如“<input type="hidden" name="action" value="delete"/>”,提交后解析得到的功能方法名为delete;
3、logicalMappings:逻辑功能方法名到真实功能方法名的映射,如:
http://localhost:9080/springmvc-chapter4/user2?action=doList;
首先请求参数“action=doList”,则第二步解析得到逻辑功能方法名为doList;
本步骤会把doList再转换为真实的功能方法名list。
4、defaultMethodName:以上步骤如果没有解析到功能处理方法名,默认执行的方法名。

(3、视图页面
(3.1、list页面(WEB-INF/jsp/user2/list.jsp)
Java代码

<a href="${pageContext.request.contextPath}/user2?action=create">用户新增</a><br/> 
<table border="1" width="50%"> 
<tr> 
<th>用户名</th> 
<th>真实姓名</th> 
<th>操作</th> 
</tr> 
<c:forEach items="${userList}" var="user"> 
<tr> 
<td>${user.username }</td> 
<td>${user.realname }</td> 
<td> 
<a href="${pageContext.request.contextPath}/user2?action=update&username=${user.username}">更新</a> 
| 
<a href="${pageContext.request.contextPath}/user2?action=delete&username=${user.username}">删除</a> 
</td> 
</tr> 
</c:forEach> 
</table>

(3.2、update页面(WEB-INF/jsp/user2/update.jsp)
Java代码

<form action="${pageContext.request.contextPath}/user2" method="post"> 
<input type="hidden" name="action" value="update"/> 
用户名: <input type="text" name="username" value="${command.username}"/><br/> 
真实姓名:<input type="text" name="realname" value="${command.realname}"/><br/> 
<input type="submit" value="更新"/> 
</form> 

通过参数name="action" value="update"来指定要执行的功能方法名update。

(3.3、create页面(WEB-INF/jsp/user2/create.jsp)
Java代码

<form action="${pageContext.request.contextPath}/user2" method="post"> 
用户名: <input type="text" name="username" value="${command.username}"/><br/> 
真实姓名:<input type="text" name="realname" value="${command.realname}"/><br/> 
<input type="submit" name="create" value="新增"/> 
</form> 

通过参数name="create"来指定要执行的功能方法名create。
(4、测试:
使用ParameterMethodNameResolver将进行如下解析:
http://localhost:9080/springmvc-chapter4/user2?create ————>create功能处理方法名(参数名映射);
http://localhost:9080/springmvc-chapter4/user2?action=create————>create功能处理方法名(参数值映射);

http://localhost:9080/springmvc-chapter4/user2?update ————>update功能处理方法名;
http://localhost:9080/springmvc-chapter4/user2?action=update————>update功能处理方法名;

http://localhost:9080/springmvc-chapter4/user2?delete ————>delete功能处理方法名;
http://localhost:9080/springmvc-chapter4/user2?action=delete————>delete功能处理方法名;

http://localhost:9080/springmvc-chapter4/user2?doList ————>通过logicalMappings解析为list功能处理方法。
http://localhost:9080/springmvc-chapter4/user2?action=doList————>通过logicalMappings解析为list功能处理方法。

http://localhost:9080/springmvc-chapter4/user2————>默认的功能处理方法名list(默认)。

 

4.16、数据类型转换和数据验证

springMVC

流程:
1、首先创建数据绑定器,在此此会创建ServletRequestDataBinder类的对象,并设置messageCodesResolver(错误码解析器);
2、提供第一个扩展点,初始化数据绑定器,在此处我们可以覆盖该方法注册自定义的PropertyEditor(请求参数——>命令对象属性的转换);
3、进行数据绑定,即请求参数——>命令对象的绑定;
4、提供第二个扩展点,数据绑定完成后的扩展点,此处可以实现一些自定义的绑定动作;
5、验证器对象的验证,验证器通过validators注入,如果验证失败,需要把错误信息放入Errors(此处使用BindException实现);
6、提供第三个扩展点,此处可以实现自定义的绑定/验证逻辑;
7、将errors传入功能处理方法进行处理,功能方法应该判断该错误对象是否有错误进行相应的处理。

4.16.1、数据类型转换
请求参数(String)——>命令对象属性(可能是任意类型)的类型转换,即数据绑定时的类型转换,使用PropertyEditor实现绑定时的类型转换。

一、Spring内建的PropertyEditor如下所示:

类名

说明

默认是否注册

ByteArrayPropertyEditor

String<——>byte[]

ClassEditor

String<——>Class

当类没有发现抛出IllegalArgumentException

CustomBooleanEditor

String<——>Boolean

true/yes/on/1转换为true,false/no/off/0转换为false

CustomCollectionEditor

数组/Collection——>Collection

普通值——>Collection(只包含一个对象)

如String——>Collection

不允许Collection——>String(单方向转换)

CustomNumberEditor

String<——>Number(Integer、Long、Double)

FileEditor

String<——>File

InputStreamEditor

String——>InputStream

单向的,不能InputStream——>String

LocaleEditor

String<——>Locale,

(String的形式为[语言]_[国家]_[变量],这与Local对象的toString()方法得到的结果相同)

PatternEditor

String<——>Pattern

PropertiesEditor

String<——>java.lang.Properties

URLEditor

String<——>URL

StringTrimmerEditor

一个用于trim 的 String类型的属性编辑器

如默认删除两边的空格,charsToDelete属性:可以设置为其他字符

emptyAsNull属性:将一个空字符串转化为null值的选项。

×

CustomDateEditor

String<——>java.util.Date

×

 

二、Spring内建的PropertyEditor支持的属性(符合JavaBean规范)操作:

表达式

设值/取值说明

username

属性username

设值方法setUsername()/取值方法getUsername() 或 isUsername()

schooInfo.schoolType

属性schooInfo的嵌套属性schoolType

设值方法getSchooInfo().setSchoolType()/取值方法getSchooInfo().getSchoolType()

hobbyList[0]

属性hobbyList的第一个元素

索引属性可能是一个数组、列表、其它天然有序的容器。

map[key]

属性map(java.util.Map类型)

map中key对应的值



三、示例:
接下来我们写自定义的属性编辑器进行数据绑定:
(1、模型对象:

java代码:

package cn.javass.chapter4.model; 
//省略import 
public class DataBinderTestModel { 
   private String username; 
   private boolean bool;//Boolean值测试 
   private SchoolInfoModel schooInfo; 
   private List hobbyList;//集合测试,此处可以改为数组/Set进行测试 
   private Map map;//Map测试 
   private PhoneNumberModel phoneNumber;//String->自定义对象的转换测试 
   private Date date;//日期类型测试 
   private UserState state;//String——>Enum类型转换测试 
   //省略getter/setter 
} 
package cn.javass.chapter4.model; 
   //如格式010-12345678 
   public class PhoneNumberModel { 
      private String areaCode;//区号 
      private String phoneNumber;//电话号码 
      //省略getter/setter 
   }
} 

(2、PhoneNumber属性编辑器
前台输入如010-12345678自动转换为PhoneNumberModel。

java代码:

package cn.javass.chapter4.web.controller.support.editor; 
//省略import 
public class PhoneNumberEditor extends PropertyEditorSupport { 
   Pattern pattern = Pattern.compile("^(\\d{3,4})-(\\d{7,8})$"); 
   @Override    
   public void setAsText(String text) throws IllegalArgumentException { 
      if(text == null || !StringUtils.hasLength(text)) { 
         setValue(null); //如果没值,设值为null 
      } 
      Matcher matcher = pattern.matcher(text); 
      if(matcher.matches()) { 
         PhoneNumberModel phoneNumber = new    PhoneNumberModel(); 
         phoneNumber.setAreaCode(matcher.group(1)); 
         phoneNumber.setPhoneNumber(matcher.group(2)); 
         setValue(phoneNumber); 
      } else { 
         throw new IllegalArgumentException(String.format("类型转换失败,需要格式[010-12345678],但格式是[%s]", text)); 
      } 
} 
@Override 
public String getAsText() { 
   PhoneNumberModel phoneNumber = ((PhoneNumberModel)getValue()); 
   return phoneNumber == null ? "" :    phoneNumber.getAreaCode() + "-" + phoneNumber.getPhoneNumber(); 
} 
} 

PropertyEditorSupport:一个PropertyEditor的支持类;
setAsText:表示将String——>PhoneNumberModel,根据正则表达式进行转换,如果转换失败抛出异常,则接下来的验证器会进行验证处理;
getAsText:表示将PhoneNumberModel——>String。

(3、控制器
需要在控制器注册我们自定义的属性编辑器。
此处我们使用AbstractCommandController,因为它继承了BaseCommandController,拥有绑定流程。

java代码:

package cn.javass.chapter4.web.controller; 
//省略import 
public class DataBinderTestController extends AbstractCommandController { 
public DataBinderTestController() { 
   setCommandClass(DataBinderTestModel.class); //设置命令对象 
   setCommandName("dataBinderTest");//设置命令对象的名字 
} 
@Override 
protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { 
   //输出command对象看看是否绑定正确 
   System.out.println(command); 
   return new ModelAndView("bindAndValidate/success").addObject("dataBinderTest", command); 
} 
@Override 
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { 
   super.initBinder(request, binder); 
   //注册自定义的属性编辑器 
   //1、日期 
   DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
   CustomDateEditor dateEditor = new CustomDateEditor(df, true); 
   //表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 
   binder.registerCustomEditor(Date.class, dateEditor); 
//自定义的电话号码编辑器 
   binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()); 
} 
} 

initBinder:第一个扩展点,初始化数据绑定器,在此处我们注册了两个属性编辑器;
CustomDateEditor:自定义的日期编辑器,用于在String<——>日期之间转换;
binder.registerCustomEditor(Date.class, dateEditor):表示如果命令对象是Date类型,则使用dateEditor进行类型转换;
PhoneNumberEditor:自定义的电话号码属性编辑器用于在String<——> PhoneNumberModel之间转换;
binder.registerCustomEditor(PhoneNumberModel.class, newPhoneNumberEditor()):表示如果命令对象是PhoneNumberModel类型,则使用PhoneNumberEditor进行类型转换;
(4、spring配置文件chapter4-servlet.xml

java代码:

<bean name="/dataBind" 
class="cn.javass.chapter4.web.controller.DataBinderTestController"/> 

(5、视图页面(WEB-INF/jsp/bindAndValidate/success.jsp)

java代码:

EL phoneNumber:${dataBinderTest.phoneNumber}<br/> 
EL state:${dataBinderTest.state}<br/> 
EL date:${dataBinderTest.date}<br/> 

视图页面的数据没有预期被格式化,如何进行格式化显示呢?请参考【第七章 注解式控制器的数据验证、类型转换及格式化】。

(6、测试:
1、在浏览器地址栏输入请求的URL,如
http://localhost:9080/springmvc-chapter4/dataBind?username=zhang&bool=yes&schooInfo.specialty=computer&hobbyList[0]=program&hobbyList[1]=music&map[key1]=value1&map[key2]=value2&phoneNumber=010-12345678&date=2012-3-18 16:48:48&state=blocked

2、控制器输出的内容:
DataBinderTestModel [username=zhang, bool=true, schooInfo=SchoolInfoModel [schoolType=null, schoolName=null, specialty=computer], hobbyList=[program, music], map={key1=value1, key2=value2}, phoneNumber=PhoneNumberModel [areaCode=010, phoneNumber=12345678], date=Sun Mar 18 16:48:48 CST 2012, state=锁定]

类型转换如图所示:

springMVC
四、注册PropertyEditor
1、使用WebDataBinder进行控制器级别注册PropertyEditor(控制器独享)
如“【三、示例】”中所使用的方式,使用WebDataBinder注册控制器级别的PropertyEditor,这种方式注册的PropertyEditor只对当前控制器独享,即其他的控制器不会自动注册这个PropertyEditor,如果需要还需要再注册一下。

2、使用WebBindingInitializer批量注册PropertyEditor
如果想在多个控制器同时注册多个相同的PropertyEditor时,可以考虑使用WebBindingInitializer。

示例:
(1、实现WebBindingInitializer

java代码:

package cn.javass.chapter4.web.controller.support.initializer; 
//省略import 
public class MyWebBindingInitializer implements WebBindingInitializer { 
@Override 
public void initBinder(WebDataBinder binder, WebRequest request) { 
//注册自定义的属性编辑器 
//1、日期 
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
CustomDateEditor dateEditor = new CustomDateEditor(df, true); 
//表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 
binder.registerCustomEditor(Date.class, dateEditor); 
//自定义的电话号码编辑器 
binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()); 
} 
} 


通过实现WebBindingInitializer并通过binder注册多个PropertyEditor。

(2、修改【三、示例】中的DataBinderTestController,注释掉initBinder方法;

(3、修改chapter4-servlet.xml配置文件:

java代码:

<!-- 注册WebBindingInitializer实现 --> 
<bean id="myWebBindingInitializer" class="cn.javass.chapter4.web.controller.support.initializer.MyWebBindingInitializer"/> 
<bean name="/dataBind" class="cn.javass.chapter4.web.controller.DataBinderTestController"> 
<!-- 注入WebBindingInitializer实现 --> 
<property name="webBindingInitializer" ref="myWebBindingInitializer"/> 
</bean> 


(4、尝试访问“【三、示例】”中的测试URL即可成功。

使用WebBindingInitializer的好处是当你需要在多个控制器中需要同时使用多个相同的PropertyEditor可以在WebBindingInitializer实现中注册,这样只需要在控制器中注入WebBindingInitializer即可注入多个PropertyEditor。

3、全局级别注册PropertyEditor(全局共享)
只需要将我们自定义的PropertyEditor放在和你的模型类同包下即可,且你的Editor命名规则必须是“模型类名Editor”,这样Spring会自动使用标准JavaBean架构进行自动识别,如图所示:

springMVC

此时我们把“DataBinderTestController”的“binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor());”注释掉,再尝试访问“【三、示例】”中的测试URL即可成功。

这种方式不仅仅在使用Spring时可用,在标准的JavaBean等环境都是可用的,可以认为是全局共享的(不仅仅是Spring环境)。

PropertyEditor被限制为只能String<——>Object之间转换,不能Object<——>Object,Spring3提供了更强大的类型转换(TypeConversion)支持,它可以在任意对象之间进行类型转换,不仅仅是String<——>Object。

如果我在地址栏输入错误的数据,即数据绑定失败,Spring Web MVC该如何处理呢?如果我输入的数据不合法呢?如用户名输入100个字符(超长了)那又该怎么处理呢?出错了需要错误消息,那错误消息应该是硬编码?还是可配置呢?

接下来我们来学习一下数据验证器进行数据验证吧。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

springMVC,布布扣,bubuko.com

springMVC

上一篇:javascript 跨域解决方案


下一篇:spring(7)--注解式控制器的数据验证、类型转换及格式化