- 所谓配置式开发是指,“处理器类是程序员手工定义的、实现了特定接口的类,然后再在SpringMVC配置文件中对该类进行显式的、明确的注册”的开发方式。
1 处理器映射器HandlerMapping
- HandlerMapping接口负责根据request请求找到对应的Handler处理器以及Inteceptor拦截器,并将它们封装在HandlerExcecutionChain对象中,返回给中央调度器。
- 其常用的实现类有两种:BeanNameUrlHandlerMapping和SimpleHandlerMapping。
1.1 BeanNameUrlHandlerMapping
- BeanNameUrlHandlerMapping处理器映射器,会根据请求的url与spring容器中定义的处理器bean的name属性值进行匹配,从而在spring容器中找到处理器bean的实例。
<!-- 注册处理器映射器 -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"></bean>
<!-- 注册处理器 -->
<bean id="/hello.do" class="com.eason.springmvc.handlers.MyController"></bean>
- 打开类的源码,从处理器映射器的方法中可以看出,对于处理器的Bean的名称,必须以“/”开头,否则无法加入到urls数组中。而urls数组中的url则是中央调度器用于判定“该url所对应的类是否作为处理器交给处理器适配器进行适配”的依据。这也是处理器与其他普通Bean的区别。
1.2 SimpleUrlHandlerMapping
- 使用BeanNameUrlHandlerMapping映射器有两点明显不足:
1、处理器Bean的id为一个url请求路径,而不是Bean的名称,有些不伦不类。
2、处理器Bean的定义与请求url绑定在一起。若出现多个url请求同一个处理器的情况,就需要在Spring容器中配置多个该处理器类的<bean/>,这将导致容器会创建多个该处理器类实例。
- SimpleUrlHandlerMapping处理器映射器,不仅可以将url与处理器的定义分离,还可以对url进行统一映射管理。
- SimpleUrlHandlerMapping处理器映射器,会根据请求的url与Spring容器中定义的处理器映射器子标签的key属性进行匹配。匹配上之后,再将该key的value值与处理器bean的id值进行匹配,从而在spring容器中找到处理器bean。
<!-- 配置处理器映射器 -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/hello.do">myController</prop>
</props>
</property>
</bean>
<!-- 注册处理器 -->
<bean id="myController" class="com.eason.springmvc.handlers.MyController"></bean>
<!-- 配置处理器映射器 -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="urlMap">
<map>
<entry key="/hello.do" value-ref="myController"></entry>
</map>
</property>
</bean>
2 处理器适配器HandlerAdapter
- 适配器模式解决的问题是,使得原来由于接口不兼容而不能够在一起工作的那些类可以在一起工作,所以处理器适配器所起的作用是,将多种处理器(实现了不同接口的处理器),通过处理器适配器的适配,使得它们可以进行统一标准的工作,对请求进行统一方式的处理。
- 之所以要将Handler定义为Controller接口的实现类,就是因为这里使用的处理器适配器是SimpleControllerHandlerAdapter。打开其源码,可以看到将handler强转为Controller。在定义Handler时,若不将其定义为Controller接口的实现类,这里的强转是要出错的。
- 当然,中央调度器首先会调用该适配器的supports()方法,判断该Handler是否与Controller具有is-a关系,在具有is-a关系的前提下,才会强转。
- 当然,强转后,就会调用我们自己定义的处理器的handleRequest()方法:
- HandlerAdapter接口会根据处理器所实现的接口的不同,对处理器进行适配,适配后即可对处理器进行执行。通过扩展处理器适配器,可以执行多种类型的处理器。常用的HandlerAdapter接口实现类有两种:
2.1 SimpleControllerHandlerAdapter
- 所有实现了Controller接口的处理器Bean,均是通过此适配器进行适配、执行的。
- Controller接口中有一个方法:
- 该方法用于处理用户提交的请求。通过调用Service层代码,实现对用户请求的计算响应,并最终将计算所得数据以及要响应的页面,封装为一个对象ModelAndView,返回给中央调度器。
2.1 HttpRequestHandlerAdapter
- 所有实现了HttpRequestHandler接口的处理器bean,均是通过此适配器进行适配,执行的。HttpRequestHandler接口中有一个方法:
- 该方法没有返回值,不能够像ModelAndView一样,将数据以及目标视图封装为一个对象,但是可以将数据直接放入到request、session等域属性中,并由request或者response完成到目标页面的跳转。
- 当然,此时视图解析器也无需再配置前缀和后缀了,即在springmvc.xml中无需声明视图解析器的Bean。
3 处理器
- 处理器除了实现Controller接口外,还可以继承自一些其他的类来完成一些特殊的功能。
3.1 继承自AbstractController类
- 若处理器继承自AbstractController类,那么该控制器就具有一些新的功能。因为AbstractController类还可以继承自一个父类WebContentGenerator。
- WebContentGenerator类具有supportedMethods属性,可以设置支持的HTTP数据提交方式。默认支持GET、POST。
- 若处理器继承自AbstractController类,那么处理器就可以通过属性supportedMethods来限制HTTP请求提交方式。例如,指定只支持POST的HTTP请求提交方式。
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
ModelAndView mv = new ModelAndView();
mv.addObject("welcome", "Hello SpringMVC World!");
mv.setViewName("welcome");
return mv;
}
<bean id="/hello.do" class="com.eason.springmvc.handlers.MyController">
<property name="supportedMethods" value="POST"></property>
</bean>
- 注意,这里的POST必须写为大写。
- 那就意味着该请求只能够通过表单或者AJAX请求方式进行提交,而不能够通过地址栏,超链接、HTML标签中的src方式进行提交。因为地址栏、超链接、Html标签中的src方式都是GET提交。否则,会给出请求方法不允许的405错误:
- 客户端浏览器常用的请求方式,以及提交方式有以下几种:
- 不过,需要注意的是,AbstractController类中有一个抽象方法需要实现:
- 即定义处理器时,就需要实现其抽象方法handleRequestInternal()。那么我们的处理器中实现的这个方法时什么时候被调用执行的呢?
- AbstractController类实现了Controller接口:
- 所以AbstractController类就实现了Controller接口中的handleRequest()方法。但是查看AbstractController类的handleRequest()方法源码,发现该方法最终返回的确实抽象方法handleRequestInternal()。
- 而AbstractController抽象类的实现类即我们自定义的子类,实现了该抽象方法。即,我们自定义的处理器的处理器方法handleRequestInternal()最终被AbstractController类的handleRequest()方法调用执行。而AbstractController类的handleRequest()方法是被处理器适配器调用执行的。
3.2 继承自MultiActionController类
- MultiActionController类继承自AbstractController,所以继承自MultiActionController类的子类也可以设置HTTP请求提交方式。
- 除此之外,继承自该类的处理器中可以定义多个处理方法。这些方法的签名为公共的方法,返回值为ModelAndView,包括参数HttpServletRequest与HttpServletResponse,抛出Exception异常,方法名随意。
- 修改处理器类
public class MyController extends MultiActionController{
public ModelAndView doFirst(HttpServletRequest request, HttpServletResponse response)
throws Exception {
ModelAndView mv = new ModelAndView();
mv.addObject("method", "doFirst()");
mv.setViewName("welcome");
return mv;
}
public ModelAndView doSecond(HttpServletRequest request, HttpServletResponse response)
throws Exception {
ModelAndView mv = new ModelAndView();
mv.addObject("method", "doSecond()");
mv.setViewName("welcome");
return mv;
}
}
- 修改springmvc.xml:注意处理器类的映射路径的写法,要求必须以/xxx/的路径方式定义映射路径。其中为通配符,在访问时使用要访问的方法名代替。
<!-- 配置处理器映射器 -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/my/*.do">myController</prop>
</props>
</property>
</bean>
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!-- 注册处理器 -->
<bean id="myController" class="com.eason.springmvc.handlers.MyController"></bean>
- 对doFist()方法的访问请求是:
- 对doSecond()方法的访问请求是:
- 之所以通过在请求URI中写上方法名就可以访问到指定方法,是因为在MultiActionController类中有一个专门处理方法名称的解析器MethodNameResolver。该解析器作为一个属性出现,具有get与set方法。MethodNameResolver是一个接口,不同的解析器实现类,其对方法名在URI中的写法要求也是不同的。
1.3.1 IntervalPathMethodNameResolver方法名解析器(默认)
- MultiActionController类具有一个默认的MethodNameResolver解析器。
- 该方法名解析器要求方法名为URI中资源名称的身份出现,即方法作为一种可以被请求的资源出现,也就是前面的写法:/xxx/方法名。
1.3.2 PropertiesMethodNameResolver方法名解析器
- 该方法名解析器中的方法名是作为URI资源名称中的一部分出现的。即方法名并非单独作为一种资源名称出现的。例如请求时可以写为xxx_doFirst,则会访问xxx所映射的处理器的doFirst()方法。
- 直接修改springmvc.xml文件即可,视图解析器保持不变。
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
- 此时,处理器映射器的请求格式发生了改变。另外,还需要配置一个PropertiesMethodNameResolver方法名解析器,并指定请求和要执行的方法名之间的映射关系。注意,这里的指定的请求,必须要加上.do。否则,无法完成匹配,将报404错误。
- 最后,将配置好的方法名解析器,注入给处理器。
<!-- 配置处理器映射器 -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/my_*.do">myController</prop>
</props>
</property>
</bean>
<!-- 配置方法名解析器 -->
<bean id="propertiesMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
<property name="mappings">
<props>
<prop key="/my_doFirst.do">doFirst</prop>
<prop key="/my_doSecond.do">doSecond</prop>
</props>
</property>
</bean>
<!-- 注册处理器 -->
<bean id="myController" class="com.eason.springmvc.handlers.MyController">
<property name="methodNameResolver" ref="propertiesMethodNameResolver"></property>
</bean>
- 对doFirst()方法的访问请求时:
1.3.3 ParameterMethodNameResolver方法名解析器
- 该方法名解析器中的方法名作为请求参数的值出现。例如请求时可以写为/xxx?ooo=doFirst,则会访问xxx所映射的处理器的doFirst()方法。其中ooo为该请求所携带的参数名,而doFirst则作为其参数值出现。
- 直接修改springmvc.xml文件即可。
<!-- 配置处理器映射器 -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/my.do">myController</prop>
</props>
</property>
</bean>
<!-- 配置方法名解析器 -->
<bean id="parameterMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
<property name="paramName" value="method"></property>
</bean>
<!-- 注册处理器 -->
<bean id="myController" class="com.eason.springmvc.handlers.MyController">
<property name="methodNameResolver" ref="parameterMethodNameResolver"></property>
</bean>
- 对doFirst()方法的访问请求是:
- 不过,打开ParameterMethodNameResolver源码,发现该类中有一个默认的参数action。即若不指定参数名称,则可以使用action作为参数。
- 也就是说,对于方法名称解析器ParameterMethodNameResolver的注册,可以修改为如下形式,即不指定paramName的属性值:
<!-- 配置方法名解析器 -->
<bean id="parameterMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
</bean>
- 对于doFirst()方法的访问请求是:
4 ModelAndView
- ModelAndView即模型与视图,通过addObject()方法向模型中添加数据,通过setViewName()方法向模型添加视图名称。
4.1 模型
4.1.1 模型的本质就是HashMap
- 跟踪addObject()方法,可以知道这里的额模型就是ModelMap,而ModelMap的本质就是个HashMap,向模型中添加数据,就是向HashMap中添加数据。
- 从以上代码跟踪可知,向ModelAndView添加数据,就是向HashMap中添加数据。
4.1.2 HashMap是一个单向查找数组
- 而我们知道,HashMap的本质是一个单向链表数组。从以下源码跟踪中可以知道这点。
- 查看HashMap的源码,其用于存放数据的底层数据结构是一个数组,而数组元素为一个Entry对象。
- 跟踪Entry类可知,其为HashMap类的内部类,为一个可单向链表的数据结构:因为其只能够通过next查找下一个元素,而无法查找上一个元素。
4.1.3 LinkedHashMap
- LinkedHashMap的本质是一个HashMap,但其将Entry内部类进行了扩展。HashMap中的Entry是单向的,只能够通过next查找下一个元素。而LinkedHashMap中的Entry变为了双向的,可以通过before查找上一个元素,通过after查找下一个元素。即从性能上说,LinkedHashMap的操作性能要高于HashMap。
- 当然,这些是底层的数据结构,我们了解即可。当时通过以上源码查看,我们应该掌握的是ModelAndView中的模型对象是ModelMap,其本质是一个HashMap,向ModelMap中添加数据就是向HashMap中添加数据。只不过,这个ModelMap要比HashMap的性能要高。
4.2 视图
- 通过setViewName()指定视图名称。注意,这里的视图名称将会对应一个视图对象,一般是不会在这里直接写上要跳转的页面的。这个视图对象,将会封装在ModelAndView中,传给视图解析器来解析,最终转换成相应的页面。但是需要注意的是,这里的View对象本质仅仅是一个String而已。后续的步骤中,还会继续对这个View对象进行进一步的封装。
- 当然,若处理器方法返回的ModelAndView中并没有数据要携带,则可以直接通过ModelAndView的带参构造器将视图名称放入ModelAndView中。
5 视图解析器ViewResolver
- 视图解析器ViewResolver接口负责将处理结果生成View视图。常用的实现类有四种。
5.1 InternalResourceViewResolver视图解析器
- 该视图解析器用于完成对当前Web应用内部资源的封装与跳转。而对于内部资源的查找规则是,将ModelAndView中指定的视图名称与为视图解析器配置的前缀与后缀相结合的方式,拼接成一个Web应用内部资源路径。拼接规则是:前缀 + 视图名称 + 后缀。
- InternalResourceView解析器会把处理器方法返回的模型属性都存放到对应的request中,然后将请求转发到目标URL。
- 配置视图解析器:
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
- 当然,若不指定前缀和后缀,直接将内部资源路径写到setViewName()中也可以。相当于前缀与后缀为空串。
5.2 BeanNameViewResolver视图解析器
- InternalResourceViewResolver解析器存在两个问题,使其使用很不灵活:
1、只可以完成内部资源封装后的跳转。但是无法转向外部资源,如外部网页;
2、对于内部资源的定义,也只能定义一种格式的资源:存放于同一目录的同一文件类型的资源文件;
- BeanNameViewResolver视图解析器,顾名思义就是将资源封装为“Spring容器中注册的Bean实例”,ModelAndView通过设置视图名称为该Bean的id属性值来完成对该资源的访问。所以在springmvc.xml中,可以定义多个View视图Bean,让处理器中ModelAndView通过对这些Bean的id引用来完成向View中封装资源的跳转。
- 修改springmvc.xml:对于配置文件,需要做两处修改:修改视图解析器的类型为BeanNameViewResolver;注册多个视图对象Bean。
1、RedirectView:定义外部资源视图对象;2、JstView:定义内部资源视图对象。
<!-- 定义一个内部资源视图 -->
<bean id="internalResource" class="org.springframework.web.servlet.view.JstlView">
<property name="url" value="/WEB-INF/jsp/welcome.jsp"/>
</bean>
<!-- 定义一个外部资源视图 -->
<bean id="taobao" class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="http://www.taobao.com"/>
</bean>
<!-- 定义一个外部资源视图 -->
<bean id="jd" class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="http://www.jd.com"/>
</bean>
<!-- 注册视图解析器 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<!-- 注册处理器 -->
<bean id="/hello.do" class="com.eason.springmvc.handlers.MyController"/>
- 修改处理器类:在处理器中,只要将ModelAndView的视图名称指定为Spring容器中定义好的Bean的id,则可完成对该资源的访问。
public class MyController implements Controller {
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
ModelAndView mv = new ModelAndView("internalResource");
mv.addObject("welcome", "welcome to SpringMVC world!");
return mv;
//return new ModelAndView("taobao");
}
}
- 此处需要注意的是,如出现以下500错误,是因为没有导入jstl-1.2.jar和standard-1.1.2.jar两jar包。
5.3 XmlViewResolver视图解析器
- 当需要定义的View视图对象很多时,就会使springmvc.xml文件变得很大,很臃肿,不便于管理。所以可以将这些View视图对象专门抽取出来,单独定义为xml文件。这时就需要使用XmlViewResolver解析器了。
- 定义存放View对象的xml文件:直接将原来的springmvc.xml中的View对象的注册Bean,全部复制到单独的一个xml文件中。当然,该xml文件需要Spring配置文件中bean的约束。假设指定这个文件名为myViews,存放于类路径下:
<!-- 定义一个内部资源视图 -->
<bean id="internalResource" class="org.springframework.web.servlet.view.JstlView">
<property name="url" value="/WEB-INF/jsp/welcome.jsp"/>
</bean>
<!-- 定义一个外部资源视图 -->
<bean id="taobao" class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="http://www.taobao.com"/>
</bean>
<!-- 定义一个外部资源视图 -->
<bean id="jd" class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="http://www.jd.com"/>
</bean>
- 修改springmvc.xml:将原来对于View的Bean的注册删除,将试图解析器修改为XMLViewResolver解析器。该解析器的配置中需要指定用于存放View试图Bean的注册的xml文件的位置以及文件名。
<!-- 注册视图解析器 -->
<bean class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="location" value="classpath:myViews.xml"></property>
</bean>
<!-- 注册处理器 -->
<bean id="/hello.do" class="com.eason.springmvc.handlers.MyController"/>
5.4 ResourceBundleViewResolver视图解析器
- 对于View视图对象的注册,除了使用xml文件外,也可以在properties文件中进行注册。只不过,此时的视图解析器需要更换为ResourceBundleViewResolver解析器。
- 该属性文件需要定义在类路径下,即src下。而对于属性文件的写法,是由格式要求的:
- 资源名称.(class)=封装资源的View全限定性类名;资源名称.url=资源路径。
- 定义properties文件:在src下定义一个properties文件,文件名随意,这里定义为views.properties。文件内容如下:
#define an internal view named internalResource
internalResource.(class)=org.springframework.web.servlet.view.JstlView
internalResource.url=/WEB-INF/jsp/welcome.jsp
#define an internal view named taobao
taobao.(class)=org.springframework.web.servlet.view.RedirectView
taobao.url=http://www.taobao.com
#define an internal view named jd
jd.(class)=org.springframework.web.servlet.view.RedirectView
jd.url=http://www.jd.com
- 修改springmv.xml:这里使用的视图解析器为ResourceBundleViewResolver,其需要设置一个属性basename,用于指定类路径下的properties文件的基本名。
<!-- 注册视图解析器 -->
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"></property>
</bean>
<!-- 注册处理器 -->
<bean id="/hello.do" class="com.eason.springmvc.handlers.MyController"/>
5.5 视图解析器的优先级
- 有时经常需要应用一些视图解析器策略来解析视图名称,即当同时存在多个视图解析器均可解析ModelAndView中的同一视图名称时,哪个视图解析器会其作用呢?
- 视图解析器有一个order属性,专门用于设置多个视图解析器的优先级。数字越小,优先级越高;数字越相同,先注册的优先级高。一般不为InternalResourceViewResolver解析器指定优先级,即让其优先级是最低的。