方志朋 2020-02-14
Spring MVC 基于模型-视图-控制器(Model-View-Controller)模式实现,它能够帮你构建灵活和松耦合的应用程序。
每当用户在 Web 浏览器中点击链接或提交表单是,请求就开始工作,从离开浏览器开始到获取响应返回,请求在 Spring MVC 框架中会经过以下过程:
我们已经大概了解了 Spring MVC 的工作流程,但其间涉及到多种组件,它们的任务分别是:
DispatcherServlet 是 Spring MVC 的核心,它负责将请求转到其他组件中。但它是一个 Servlet,按照传统方式,需要在 web.xml 文件中配置 DispatcherServlet ,借助 Servlet 3 的规范和 Spring 3.1 的功能增强,我们可以使用 Java 的方式进行配置 DispatcherServlet。
package spitter.config; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; /** * 该类继承了 AbstractAnnotationConfigDispatcherServletInitializer ,会自动地配置 DispatcherServlet 和 Spring 应用上下文,Spring应用上下文会位于应用程序的 Servlet 上下文之中; */ public class SpitterWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { /* 用来装配非Web组件的bean */ @Override protected Class<?>[] getRootConfigClasses() { return new Class[]{RootConfig.class}; } /* 该方法指定配置类 当 DispatcherServlet 启动时,会创建Spring应用上下文,并加载配置文件或配置类中所声明的 bean。 */ @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{WebConfig.class}; } /* 该方法会将一个或多个路径映射到 DispatcherServlet 上 将 DispatcherServlet 映射到 “/”,表示应用的默认Servlet,会处理进入应用的所有请求 */ @Override protected String[] getServletMappings() { return new String[]{"/"}; } }
package spitter.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.view.InternalResourceViewResolver; /** * 该类为Spring MVC配置类,具有以下配置: * 1、启用Spring MVC; * 2、启用组件扫描;如果没有启用,Spring 只会找到显式声明在配置类中的控制器; * 3、配置 JSP 视图解析;如果没有配置,会默认使用 BeanNameViewResolver 解析器; * 4、配置静态资源的处理; */ @Configuration @EnableWebMvc //启用Spring MVC; @ComponentScan("spitter.config") public class WebConfig extends WebMvcConfigurerAdapter { /* 配置 JSP 视图解析器 */ @Bean public ViewResolver viewResolver(){ InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/views/"); resolver.setSuffix(".jsp"); resolver.setExposeContextBeansAsAttributes(true); return resolver; } /* 配置静态资源的处理 该配置是将对静态资源的请求转发到Servlet容器中默认的Servlet上, 而不是使用 DispatcherServlet 来处理此类请求 */ @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer){ configurer.enable(); } }
package spitter.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * 用来装配非Web组件的配置; */ @Configuration @ComponentScan(basePackages = {"spitter"}, excludeFilters = {@Filter(type = FilterType.ANNOTATION,value = EnableWebMvc.class) }) public class RootConfig { }
基于 Java 配置虽然新颖,但对于初学者很不友好。所以,这里也提供了在 web.xml 中装配核心控制器 DispatcherServlet
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- SpringMVC的核心控制器 --> <servlet> <!-- 配置Servlet的初始化参数 --> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--读取spring mvc的配置文件,创建spring容器--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springMvcConfig.xml</param-value> </init-param> <!-- 配置servlet启动时加载对象 --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
Spring MVC配置文件:springMvcConfig.xml
<?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:aop="http://www.springframework.org/schema/aop" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 配置spring开启注解mvc的支持 --> <mvc:annotation-driven/> <!-- 配置spring创建容器时要扫描的包 --> <context:component-scan base-package="spitter.controller"/> <!-- 配置视图解析器 --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/views/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
在接下来的实践中,本人是根据 XML 配置的。
如何创建控制器?如何根据请求路径找到控制器?
声明控制器
在类级别上使用注解 @Controller
,该注解与注解 @Component
作用一样,目标是辅助实现组件扫描;
定义方法
方法内进行请求处理,返回值类型为 String,该字符串为要渲染的视图名,提供给视图解析器进行解析。当然返回值类型也可以是其它类型,稍后再讲。
使用注解 @RequestMapping
设置请求路径映射控制器;
RequestMapping注解:指定方法要处理的请求路径,和细化所处理的HTTP方法等,注解内有以下属性:
name:为此 mapping 设置名称;
path/value:复数形式,限制请求路径匹配时调用;当请求路径与该属性内容匹配,则访问;
可同时在类级别和方法级别标注,相当于两级路径:/类设置路径/方法设置路径;
method:限制请求方法匹配时调用;使用 RequestMethod 枚举的属性;
params:限制请求参数;当请求中包含相同的参数名和相同参数值才匹配;
headers:限制请求头;当请求头中包含相同的请求头才匹配;
下面是一个简单的例子
package spitter.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping({"/home"}) public class HomeController { //访问以下方法的请求路径为:/home/homepage @RequestMapping(value = {"/homepage"}, method = RequestMethod.GET) public String home() { return "home"; // 被Spring MVC解读为要渲染的视图名称 } }
当请求到控制器,如何将请求中的参数传递到控制器的方法中?Spring MVC提供了请求参数绑定机制。
@ResponseBody
实现;HttpServletRequest
对象和 HttpServletResponse
对象获取请求参数和响应结果;案例
下面是表单数据提交的 JSP 代码,例子中所使用的 POJO 类并未展示。
<a href="<c:url value="/reqParamBind/get?method=GET&num=10"/>" >GET方式</a><br/><br/> <!--POST方式--> <form action="reqParamBind/post?method=POST" method="post"> 用户名:<input type="text" name="userName"/><br/> 密码:<input type="password" name="password"/><br/> <input type="submit" value="提交"/> </form> <!--POJO类型参数绑定,name属性的值必须与POJO类属性名一致--> <form action="reqParamBind/pojoBind" method="post"> 用户名:<input type="text" name="userName"/><br> 密码:<input type="password" name="password"/><br> 金额:<input type="text" name="money"/><br> <!--POJO对象内引用另一个POJO对象--> 真实姓名:<input type="text" name="user.realName"/><br> 年龄:<input type="text" name="user.age"/><br> <input type="submit" value="提交"/> </form> <!--集合类型--> <form action="reqParamBind/collectionBind" method="post"> <!--List集合,name属性的值为:集合属性名[下标].属性名--> 用户:<input type="text" name="list[0].realName"/><br> 年龄:<input type="text"name="list[0].age"/><br> 用户:<input type="text" name="list[1].realName"/><br> 年龄:<input type="text" name="list[1].age"/><br> <!--Map集合,name属性的值为:集合属性名['key'].属性名--> 用户:<input type="text" name="map['one'].realName"/><br> 年龄:<input type="text"name="map['one'].age"/><br> 用户:<input type="text" name="map['two'].realName"/><br> 年龄:<input type="text" name="map['two'].age"/><br> <input type="submit" value="提交"/> </form>
表单提交数据访问的控制器代码
package spitter.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import spitter.pojo.Account; import spitter.pojo.UserList; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Date; @Controller @RequestMapping(value = {"/reqParamBind"}) public class ParamController { /** * get方式请求参数传递 * @param method 请求参数 * @param num 请求参数 * @return 要解析的视图名称 */ @RequestMapping(value = {"/get"},method = RequestMethod.GET) public String reqParamBind(String method,int num){ System.out.println("Method:"+method+"---num:"+num); return "paramAccept"; } /** * post方式请求参数传递 * @param method 请求参数 * @param userName 请求参数 * @param password 请求参数 * @return 要解析的视图名称 */ @RequestMapping(value = {"/post"},method = RequestMethod.POST) public String reqParamBind(String method,String userName,String password){ System.out.println("Method:"+method+"---userName:"+userName+"---密码:"+password); return "paramAccept"; } /** * POJO实体类型参数绑定 * @param account POJO类 * @return 要解析的视图名称 */ @RequestMapping(value = {"/pojoBind"},method = RequestMethod.POST) public String pojoBind(Account account){ System.out.println(account); return "paramAccept"; } /** * List、Map等集合类型参数绑定 * @param userList 实体类;实体类中封装了List集合和Map集合进行绑定 * @return 要解析的视图名称 */ @RequestMapping(value = {"/collectionBind"},method = RequestMethod.POST) public String collectionBind(UserList userList,HashMap<String,>){ System.out.println(userList); return "paramAccept"; } /** * 获取Servlet的请求和响应对象 * @param request HttpServletRequest * @param response HttpServletResponse * @return 要解析的视图名称 */ @RequestMapping(value = {"/getReqAndResp"},method = RequestMethod.GET) public String getReqAndResp(HttpServletRequest request, HttpServletResponse response){ System.out.println(request); HttpSession session = request.getSession(); System.out.println(session); ServletContext servletContext = request.getServletContext(); System.out.println(servletContext); System.out.println(response); return "paramAccept"; } }
正如我们所知,浏览器提交的数据都是以字符串类型传递的,而 Spring MVC 为我们封装好了常用的类型转换器。但有时,依然会有一些特殊情况。比如:日期格式转换。Spring MVC 只接受 yyyy/MM/dd 类型的日期格式;如果换成 yyyy-MM-dd 类型的日期格式就会出现异常;
这是,我们可以自定义一个转换器,请求参数会根据我们定义的格式进行转换。
定义转换器步骤
org.springframework.core.convert.converter.Converter<S,T>
接口;重写 convert 方法,方法中编写类型转换的代码;org.springframework.context.support.ConversionServiceFactoryBean
;再把刚创建的转换器(bean)填充到该 bean 的属性 converters(为Set集合);案例
转换器类
package spitter.converter; import org.springframework.core.convert.converter.Converter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; public class StringToDate implements Converter<String, Date> { @Override public Date convert(String source) { if (source == null && "".equals(source)) { throw new RuntimeException("请输入数值"); } else { DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); try { return df.parse(source); } catch (Exception e) { throw new RuntimeException("数据类型转换错误"); } } } }
在 Spring MVC 配置文件进行配置
<!--配置自定义类型转换器--> <bean id="conversionService2" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <!--装配转换器 Bean对象--> <bean class="spitter.converter.StringToDate"/> </set> </property> </bean> <!-- 配置spring开启注解mvc的支持,并装配转换器--> <mvc:annotation-driven conversion-service="conversionService2"/>
但是:日期转换器转换 yyyy-MM-dd 类型后,再使用 yyyy/MM/dd类型会出现错误:Bad Request。
控制器方法中处理后的数据信息,如何传递到视图,展示在浏览器?
org.springframework.ui.Model
,它会调用子类 BindingAwareModelMap
对象的 addAttribute(key,value)
方法传递;HttpServletRequest
对象和 HttpServletResponse
对象进行数据传递;/* 将控制器的数据传递到视图中 需要传递一个参数:Model或 Map对象,调用对应的addAttribute方法或put方法传递; */ @RequestMapping(value = {"/homeParam"}, method = RequestMethod.GET) public String home(/*Map<String,String>*/ Model model) { //model.put("name","Chan"); model.addAttribute("name", "Chan"); return "home"; }
当控制器方法的返回值类型为以下类型时,实现页面跳转的方式:
String类型
Spring MVC 会根据返回的字符串进行视图名称解析,比如:jsp的视图解析后为:路径/字符串.jsp,并跳转到解析后的 JSP 页面;
返回值是void类型
ModelAndView类型
ModelAndView 是 SpringMVC 为我们提供的一个对象,该对象也可以用作控制器方法的返回值;
该对象中有两个常用的方法:
addObject(String key,Object value)
:将数据以键值对的方式存储到request作用域中;底层也是使用 Model 对象进行传递数据;setViewName(String viewName)
:跳转页面,给定视图名称,经过解析器解析;关键字进行请求转发、重定向;返回值是String类型,字符串内容如下:
forward:URI路径 :请求转发;/
表示当前项目路径;
redirect:URI路径 :重定向;不需要加项目名称;
案例
package spitter.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller @RequestMapping(value = {"/returnType"}) public class ReturnTypeController { /** * 返回值为字符串 * @return 字符串为要解析的视图名 */ @RequestMapping(value = "/stringType",method = RequestMethod.GET) public String stringType(){ return "returnType"; } /** * 返回值为 void */ @RequestMapping(value = "/voidType",method = RequestMethod.GET) public void voidType(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //请求转发 //request.getRequestDispatcher("/index.jsp").forward(request,response); //重定向 //response.sendRedirect(request.getContextPath()+"/index.jsp"); //直接返回数据 //request.setCharacterEncoding("UTF-8"); //response.setContentType("text/html;charset=utf-8"); //response.getWriter().write("数据..."); } /** * 返回值为 ModelAndView 类型 * @return ModelAndView对象 */ @RequestMapping(value = "/modelAndView",method = RequestMethod.GET) public ModelAndView modelAndView(){ //创建 modelAndView 对象 ModelAndView modelAndView = new ModelAndView(); //设置Request作用域属性 modelAndView.addObject("name","Chan"); //跳转页面 modelAndView.setViewName("../index"); return modelAndView; } @RequestMapping(value = "/forwardOrRedirect") public String forwardOrRedirect(){ //请求转发 return "forward:/views/page.jsp"; //重定向 //return "redirect:/index.jsp"; } }
@RequestParam():将指定的请求参数赋给控制器中的参数;
用于请求参数名称与控制器方法形参名称不一致时,标注在控制器方法的形参前;
/** * RequestParam注解匹配请求参数名和控制器方法形参名不一致 * @param userName 控制器方法形参名 * @param model 数据传递对象,把数据传递到视图层 * @return 要解析的视图名称 */ @RequestMapping(value = "/requestParam",method = RequestMethod.GET) public String requestParam(@RequestParam(value = "uname") String userName, Model model){ }
@RequestBody()
读取 Request
请求的 body
部分数据,直接使用得到是 key=value&key=value... 结构的数据;get 请求方式不适用;
HttpMessageConverter
解析数据,然后把相应的数据绑定到对象上;@ResponseBody()
将 Controller
的方法返回的对象,通过适当的HttpMessageConverter
转换为指定格式后,写入到Response
对象的body
数据区;
@RequestBody()
和 @ResponseBody()
可以接收和响应 JSON 数据。例如:Ajax 请求中,能够接收 JSON 数据封装到对象中,也可以将对象解析为 JSON 数据传送。
@RequestMapping(value = "/ajaxRequest") public @ResponseBody User ajaxRequest(@RequestBody User user){ user.setRealName("xiaoyao"); user.setAge(30); return user; }
@PathVariable():用于从 url 中获取数据
请求 url 中使用占位符标记要获取的数据,例如: /delete/{id},这个 {}
就是 url 占位符;url 支持占位符是 spring3.0 之后加入的;是 spring mvc 支持 rest 风格 URL 的一个重要标志。
@RequestHeader():用于获取请求消息头;
@CookieValue():用于把指定 cookie 名称的值传入控制器方法参数;
@ModelAttribute():可以用于修饰方法和参数;
也就是 Model 对象中 Attribute 操作。
出现在方法上,表示当前方法会在控制器方法执行之前,先执行。方法有返回值则把返回值放到 Model中;
出现在参数上,获取指定的数据赋给方法参数;如果存在表单提交的数据,会优先绑定请求参数;
value属性:获取 Model 中数据的 key 。key 可以是POJO的属性名称,也可以是map结构的 key;
案例:当表单提交的数据不是完整的实体类数据时,为了保证没有提交数据的字段使用数据库对象原来的数据,有数据的字段是不允许修改的,即使用set方法赋值;
@SessionAttribute():用于多次执行控制器方法间的参数共享;
只能作用在类级别上;将数据存储在Session作用域;
package spitter.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.support.SessionStatus; import spitter.pojo.User; import java.io.UnsupportedEncodingException; @Controller @RequestMapping(value = {"/anno"}) //(1)将控制器方法中添加到 Model 中的数据存储到Session域。value中指定数据的 key @SessionAttributes(value = {"session"}) public class AnnoController { //(2) @RequestMapping(value = "/setSessionAttr") public String setSessionAttr(Model model){ model.addAttribute("session","控制器类使用 @SessionAttribute 存储数据"); return "paramAccept"; } @RequestMapping(value = "/getSessionAttr") public String getSessionAttr(ExtendedModelMap model){ //(3)获取Session作用域属性 String session = (String) model.get("session"); System.out.println(session); return "paramAccept"; } @RequestMapping(value = "/delSessionAttr") public String delSessionAttr(SessionStatus status){ //(4)清空session域 status.setComplete(); return "paramAccept"; } }