一、Spring MVC 纵览
Spring MVC就是Spring框架对MVC设计模式的实现,通过Spring MVC ,我们可以快速的构建灵活、松耦合的web服务。再具体介绍Spring MVC 之前,我们先看一下它的请求处理过程:
1.1 springMVC 的请求过程
1. 请求会首先发送到DispatchServlet,这是spring的前置Servlet,它会接收请求并转发给spring的MVC controller,也就是业务controller
2. DispatchServlet通过HandlerMapping确定将请求转发给哪个controller,HandlerMapping主要通过请求中的URL确定映射关系的
3. DispatchServlet将请求转发给确定的controller之后,controller负责处理这个请求,一般会通过调用service层进行业务逻辑处理
4. 当controller处理完请求后,它会把业务处理结果封装成model,为了使处理结果的model在页面上更好的展示,controller还会指定展示model对应的view(比如一个JSP页面),当controller确定了model和view之后,会把它们以请求的形式再转发给DispatchServlet
5. DispatchServlet通过查询ViewResolver找到view对应的页面
6. DispatchServlet最终把model交给页面进行渲染
7. 页面对model进行渲染,将结果展示到客户端,整个请求结束
1.2 配置Spring MVC
其实Spring MVC的核心就是DispatchServlet,配置Spring MVC的过程就是配置DispatchServlet的过程。
1.2.1 配置DispatchServlet
Spring的配置有两种方式,一种通过web.xml配置,另一种就是通过Java配置,在这里我们主要讲如何在web.xml中配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<listener>
<listener-class> org.springframework.web.context.ContextLoaderListener </listener-class>
</listener>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
可能有些人还不太清楚为什么要这样配置,在具体讲解之前,先介绍Spring MVC中的两个context(上下文)。
1.2.2 理解两个context
我们知道,Spring有一个核心容器也就是ApplicationContext,所有的Spring组件都由这个ApplicationContext进行管理。但是在Web项目中,Spring会维护两个context:
1. WebContext
第一个Context就是由DispatchServlet创建的WebContext,负责装载web组件相关的bean,比如controllers、view resolvers、handler mapping等,对应的配置文件是{servlet-name}-servlet.xml
,在上面例子中我们配置的Servlet的名字是spring,所以它默认加载spring-servlet.xml配置文件作为上下文。
但是我们也可以自己指定一个WebContext配置文件位置,比如指定/WEB-INF/spring/appServlet/servlet-context.xml路径下的配置文件:
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/appServlet/servlet-context.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
2. RootContext
RootContext是由ContextListener加载的,它主要装载除web组件之外的应用程序组件,比如jdbc、mybatis等组件。<context-param>
标签指定了RootContext配置文件的位置,并由<listener>
标签指定的Listener类进行装载。
1.2.3 启用Spring MVC
上面的配置其实已经基本ok了,但是一个完整的Spring MVC应用还需要controller、service、view等组件,这些后面会详细讲,现在我们先在spring-servlet.xml中配置好:
<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:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<mvc:annotation-driven/>
<context:component-scan base-package="com.springmvc.demo" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
<mvc:annotation-driven/>
标签作用是开启注解
<context:component-scan/>
标签目的是启用自动包扫描,这样spring框架就会自动扫描被注解的类,纳入到WebContext中。
通过上面的配置,Spring MVC的主要配置就已经完成了,我们现在就可以编写Spring MVC的组件了,我们先从controller开始。
二、写一个controller
2.1 写一个简单的controller
@Controller
public class HomeController {
@RequestMapping(value="/", method= RequestMethod.GET)
public String home() {
return "home";
}
}
声明controller组件:写一个controller很简单,@controller注解声明当前类是一个controller类,上面配置中我们开启了ComponentScan,被@controller注解的类会被自动装载到spring application context中。当然我们使用@component组件效果也是一样的,只不过@controller更能体现controller角色。
定义请求路径: HomeController类里面只有一个home()方法,并且还携带一个@RequestMapping注解,注解中的value属性定义了这个方法的访问路径是“/”,method属性定义了这个方法只处理get请求。
定义渲染view:我们可以看到,home()方法很简单,仅返回了一个“home”字符串。默认情况下,方法返回的字符串默认会被解析成view的名称,DispatcherServlet 会让ViewResolver解析这个view名称对应的真是view页面。我们上面已经配置了一个InternalResourceViewResolver,返回的home会被解析/WEB-INF/views/home.jsp。
写完controller之后我们可以通过测试类测试一下,写一个测试controller:
public class HomeControllerTest {
@Test
public void testHomePage() throws Exception {
HomeController controller = new HomeController();
Assert.assertEquals("home", controller.home());
}
@Test
public void testHomePageMVC() throws Exception {
HomeController controller = new HomeController();
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(MockMvcRequestBuilders.get("/")).andExpect(MockMvcResultMatchers.view().name("home"));
}
}
2.2 定义一个类层次的路径映射
其实@RequestMapping
注解既可以注解方法又可以注解类,当注解类时,访问类中所有的方法就必须加上类路径。另外@RequestMapping
中的值可以是一个路径数组,当传入一个数组时,我们可以通过数组中的任何一个路径访问到这个类.
@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
...
}
2.3 返回Model数据给view层
我们知道通常情况下,controller处理完业务后会返回结果数据给view层。为此,SpringMVC提供了Model类来封装结果数据,封装的过程是自动的,我们只需要在方法中加入Model参数即可:
public String getUsers(Model model) {
model.addAttribute( userService.findSpittles( Long.MAX_VALUE, 20));
return "userView";
}
Model其实就是一个Map,view层会自动解析model中的数据,然后渲染结果给client端。上面的model.addAttribute
方法没有指定key,key值会被默认设置为对象的类型名,比如上例子中对象类型是List<User>
,则key值默认为:userList
。方法最后返回的String值userView
会直接作为view的名称。
当然我们也可以明确指定返回数据模型的key值:
public String getUsers(Model model) {
model.addAttribute("userList", userService.findUsers( Long.MAX_VALUE, 20));
return "userView";
}
如果我们不想使用Spring的Model类,我们也可以用java.util.Map类替换Model,这两个效果是完全一样的:
public String getUsers(Map map) {
map.addAttribute("userList", userService.findUsers( Long.MAX_VALUE, 20));
return "userView";
}
除了上面两种方式之外,我们还可以这么写:
@RequestMapping(method=RequestMethod.GET)
public List<User> getUsers() {
return userService.findUsers(Long.MAX_VALUE, 20));
}
这种方式比较特殊,我们既没有设置返回的Model,又没有指定渲染Model的view,仅仅返回处理的结果对象。遇到这种情况时,Spring MVC会把返回的对象会自动放入Model中,其key就是对象的类型名,即userList
。而对应的view名称则默认与请求路径名一致,例如我们的请求路径是这样:
@Controller
@RequestMapping("/users")
public class UserController {
... ...
}
那么对应渲染结果的view就是users
。
三、 处理请求数据
SpringMVC提供了多种数据传输方式:
- QueryParameter:查询参数
- Form Parameters:表单参数
- Path variables:路径变量
下面我们逐一说明。
3.1 获取查询参数
首先什么是查询参数呢?比如我们有一个这样的请求http://localhost:8080/user/queryUsers?pageNo=1&count=5
,在这个请求中,参数值都是通过URL中?后面的参数传递过来的,这种方式就是查询参数传值。如果要解析查询参数中的值,我们需要用到@RequestParam注解,它会将请求参数映射到方法参数:
@RequestMapping(value = "/getUsersByPage", method = RequestMethod.GET)
public String getUsersByPage(@RequestParam(value = "pageNo",defaultValue = "1") long pageNo,@RequestParam(value = "count",defaultValue = "5") long count ,Model model) {
model.addAttribute(userService.getAllUsers());
return "userListPageView";
}
需要注意的是,我们在配置请求参数时可以指定参数的默认值,当client端传过来的参数值不存在或者为空时,就会采用这个默认值,还有一点,因为查询参数都是String类型,所以这里的默认值也都是String类型。
除了通过查询参数传递参数值之外,还有一种流行的方式就是通过请求路径传递参数值,特别是在讨论构建基于资源的服务时会经常用到这种方式。(注:基于资源的服务可以简单看做所有请求都是针对资源,所有的返回结果也是资源)
3.2 通过请求路经获取参数
比如我们有一个根据用户id查询用户信息的需求,通过上面介绍的方式我们可以这么做:
@RequestMapping(value = "/queryUser", method = RequestMethod.GET)
public String queryUser(@RequestParam(value = "employeeId") long employeeId,Model model){
model.addAttribute(manager.queryEmployeeVO(employeeId));
return "employee";
}
那么我们客户的的请求路径应该是这样:/employee/queryEmployee ?employeeId=12345
,尽管也可以满足需求,但这样不太符合基于资源的理念。理想的情况是,资源应该是由请求路径决定的,而不是由请求参数决定。或者说,请求参数不应该被用来描述一个资源。/employee/12345
这种请求方式显然比/employee/queryEmployee ?employeeId=12345
更能合适,前者定义了要查询的资源,而后者更强调了通过参数进行操作。
为了达到构建基于资源的controller这个目标,Spring MVC允许请求路径中包含一个占位符,占位符名称需要用一对{}括起来。在客户的请求资源时,请求路径的其它部分还是用来匹配资源路径,而占位符部分则直接用来传输参数值。下面就是通过占位符的方式实现路径中传递参数值:
@RequestMapping(value ="/{userId}", method = RequestMethod.GET)
public String queryUser(@PathVariable(value = "userId") long userId, Model model){
model.addAttribute(userService.queryUser(employeeId));
return "userView";
}
上面例子可以看到,方法参数中有一个@PathVariable
的注解,这个注解的意思就是不管请求路径中占位符处的值是什么,它都会被传递到@PathVariable
注解的变量中。比如按照上面的配置,如果我们的请求是/employee/12345
,则123456
便会传入变成userId
的值。需要注意的是,如果方法参数值和占位符名称一样,我们也可以省略@PathVariable
中的属性值:
@RequestMapping(value ="/{userId}", method = RequestMethod.GET)
public String queryUser(@PathVariable long userId, Model model){
model.addAttribute(userService.queryUser(employeeId));
return "userView";
}
在请求的数据量很小的时候使用查询参数和路径参数还是可行的,但是有时候我们请求的数据量会很大,再用上面两种方式就显得有点不太合适,这时就需要考虑用第三种方式:表单参数。
四、处理表单数据
一个Web应用不单单只是给用户展示数据,很多时候它还需要与用户交互获取用户数据,表单就是获取用户数据最常见的方式。
比如我们有个注册表单:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
<title>Spittr</title>
<link rel="stylesheet" type="text/css" >
</head>
<body>
<h1>Register</h1>
<form method="POST">
First Name: <input type="text" name="name" /><br/>
Last Name: <input type="text" name="accountNo" /><br/>
Password: <input type="password" name="password" /><br/>
<input type="submit" value="Register" />
</form>
</body>
</html>
4.1 写一个接收表单数据的controller
表单都是以post方式提交的,所以我们的controller接受的应该是个post请求:
@Controller
@RequestMapping("/user")
public class RegisterController {
@Autowired
private RegisterService registerService;
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(User user) {
registerService.register(user);
return "redirect:/user/" + user.getName();
}
}
在上面例子中,出了接收的请求是post之外,注册方法还有一个含有User对象的参数,这个User对象中含有name、password、accountNo属性,SpringMVC会根据名称从请求中解析相同名称参数并赋值到对象属性中。
当注册方法保存用户信息之后,会返回一个字符串redirect:/user/
,这个字符串与前面讲到的都不同,它返回的并不是一个view的名称,而是一个redirect重定向请求。因为返回的字符串中含有一个redirect:
重定向关键字,当InternalResourceViewResolver
类遇到这个关键字时,它将会拦截这个字符串并把它解析成一个重定向请求而不是view名称。
InternalResourceViewResolver
除了能够识别redirect:关键字之外,它还能识别forward:
关键字,并把包含forward:
关键字的字符串解析成forward
请求。
4.2 验证表单
在Spring3.0以后,SpringMVC便支持Java Validation API了,Java Validation API提供了一些注解来约束对象属性值,这些注解有:
注解 | 解释 |
---|---|
@AssertFalse | The annotated element must be a Boolean type and be false. |
@AssertTrue | The annotated element must be a Boolean type and be true. |
@DecimalMax | The annotated element must be a number whose value is less than or equal toa given BigDecimalString value. |
@DecimalMin | The annotated element must be a number whose value is greater than orequal to a given BigDecimalString value. |
@Digits | The annotated element must be a number whose value has a specified number of digits. |
@Future | The value of the annotated element must be a date in the future. |
@Max | The annotated element must be a number whose value is less than or equal to a given value. |
@Min | The annotated element must be a number whose value is greater than or equal to a given value. |
@NotNull | The value of the annotated element must not be null. |
@Null | The value of the annotated element must be null. |
@Past | The value of the annotated element must be a date in the past. |
@Pattern | The value of the annotated element must match a given regular expression. |
@Size | The value of the annotated element must be either a String, a collection, or an array whose length fits within the given range. |
我们可以利用这些注解给User对象添加一些验证,比如非空和字符串长度验证:
public class User {
@NotNull
@Size(min=3,max=20)
private String name;
@NotNull
@Size(min=6,max=20)
private String password;
@NotNull
@Size(min=3,max=20)
private String accountNo;
}
我们还需要通过@Valid
标签在方法中对User启用参数验证:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(@Valid User user, Errors errors) {
if(errors.hasErrors()){
return "register";
}
registerService.register(user);
return "redirect:/user/" + user.getName();
}
在上面例子中,我们在方法中的User参数前面添加了@valid注解,这个注解会告诉Spring框架需要启用对这个对象的验证。如果发现任何验证错误,错误信息都将会被封装到Errors对象中,我们可以通过errors.hasErrors()
方法判断是否验证通过。
未完待续……