SpringMVC 概述
概述
- Spring 为展现层提供了基于 MVC 设计理念的优秀的 web 框架,是目前最主流的 MVC 框架之一
- Spring 3.0 后全面超越 struts2,成为最优秀的 MVC 框架
- Spring MVC 通过一套 MVC 注解,让 POJO(Plain Old Java Object) 成为处理请求的控制器,而无需实现任何接口
- 支持 REST(Restful) 风格的RUL
- 采用了松散耦合可插拔组件结构,比其他 MVC 框架更具扩展性和灵活性
是什么
一种轻量级、基于 MVC 的 web 层应用框架。偏前端而不是基于业务逻辑层。Spring 框架的一个后续产品。
Spring 框架结构图:
能干啥
- 天生与 Spring 框架集成,如:IOC、AOP
- 支持 Restful 风格
- 进行更简洁的 web 层开发
- 支持灵活的 URL 到页面控制器的映射
- 非常容易与其他视图技术集成,如:Velocity、FreeMarker 等等
- 因为模型数据不存放在特定的 API 里,而是放在一个 Model 里(Map 数据结构实现,因此很容易被其它框架使用)
- 非常灵活的数据验证、格式化和数据绑定机制,能使用任何对象进行数据绑定,不必实现特定框架的 API
- 更加简单、强大的异常处理
- 对静态资源的支持
- 支持灵活的本地化、主题等解析
怎么用
将 web 层进行了职责解耦,基于请求-响应模型。
常用主要组件:
HelloWorld
开发过程
(1) 新建 web 工程,导入需要的 jar 包
(2) 在 web.xml 中配置 SpringMVC 的核心(前端)控制器 DispatcherServlet
DispatcherServlet 会自动从 contextConfigLocation 所指定的位置加载 SpringMVC 的配置文件。
public class DispatcherServlet extends FrameworkServlet {...}
public abstract class FrameworkServlet ... {
/** Explicit context config location */
private String contextConfigLocation;
}
配置 SpringMVC 核心控制器:
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置 DispatcherServlet 的初始化参数:设置文件的路径和文件名称 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- 项目启动时加载;值越小,优先级越高 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
【补充】也可以不通过初始化参数 contextConfigLocation 来配置 SpringMVC 的配置文件, 而使用默认的:/WEB-INF/<servlet-name>-servlet.xml
(3) 加入 Spring MVC 的配置文件:springmvc.xml
<!-- 扫描组件,将带有 @Controller 注解的类作为 SpringMVC 的控制层加载 -->
<context:component-scan base-package="cn.edu.nuist.test"></context:component-scan>
<!--
配置 [视图解析器]
作用:将 prefix + 视图名称 + suffix 拼接得到实际的物理视图,然后做转发操作
举例:/WEB-INF/views/success.jsp
-->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
(4) 需要创建一个入口页面:index.jsp
<a href="${pageContext.request.contextPath }/helloworld">Hello World</a>
(5) 编写处理请求的处理器,并标识为处理器
@Controller // 声明 Bean 对象为一个控制器组件
public class TestController {
// 使用 @RequestMapping 注解来映射请求的 URL
// 请求的资源要与注解的 value 属性值一致 (与方法名无关)
@RequestMapping("helloWorld")
public String helloWorld() {
System.out.println("SUCCESS");
// 返回值会通过 [视图解析器] 解析为实际的物理视图
return "success";
}
}
(6) 编写视图:/WEB-INF/views/success.jsp
<h1>SUCCESS!</h1>
执行流程
- 客户端请求提交到 DispatcherServlet
- 由 DispatcherServlet 控制器查询一个或多个 HandlerMapping,找到处理请求的 Controller
- DispatcherServlet 将请求提交到 Controller(也称为Handler)
- Controller 调用业务逻辑处理后,返回 ModelAndView
- DispatcherServlet 查询一个或多个 ViewResoler 视图解析器,找到 ModelAndView 指定的视图
- 视图负责将结果显示到客户端
@RequestMapping
简述
- 是映射请求的注解,SpringMVC 使用 @RequestMapping 注解为控制器指定可以处理哪些 URL 请求
- 在控制器的类定义及方法定义处都可标注 @RequestMapping
- 标记在类上:提供初步的请求映射信息。相对于 WEB 应用的根目录
- 标记在方法上:提供进一步的细分映射信息。相对于标记在类上的 URL。若类上未标注 @RequestMapping,则方法处标记的 URL 相对于 WEB 应用的根目录
- 【作用】DispatcherServlet 截获请求后,就通过控制器上 @RequestMapping 提供的映射信息确定请求所对应的处理方法
一个 HTTP 请求报文:
源码
package org.springframework.web.bind.annotation;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String[] value() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
映射参数、方法和头
@RequestMapping 除了可以使用请求 URL 映射请求外,还可以使用请求方法、请求参数及请求头映射请求。
@RequestMapping 的 value、method、params 及 headers 分别表示请求 URL、请求方法、请求参数及请求头的映射条件,他们之间是与的关系,联合使用多个条件可让请求映射更加精确化。
method
用来设置请求方式,只有客户端发送请求的方式 IN {method 属性值数组} 中,才会处理请求。常用的 4 个:GET(查询)、POST(添加)、PUT(修改)、DELETE(删除)
public enum RequestMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}
- 定义控制器方法
@Controller @RequestMapping("/springmvc") public class SpringMVCController { @RequestMapping(value="/testMethod", method=RequestMethod.POST) public String testMethod(){ System.out.println("testMethod..."); return "success"; } }
- 以 GET 方式请求
<a href="springmvc/testMethod">testMethod</a>
- 发生错误;当以 POST 方式请求,则 200
params
- 用来设置客户端传到服务器的数据,支持表达式。
- 举例:
@RequestMapping(value="teset", params={"username", "!sex", "score=100", "age!=18"})
- 要求请求中必须包含名为 username 的请求参数
- 要求请求中不能带有名为 sex 的请求参数
- 要求请求中必须包含名为 score 的请求参数,且其值必须等于 100
- 要求请求中必须包含名为 age 的请求参数,但其值不能等于 18
headers
用来设置请求头信息,所发送的请求的请求头信息一定要和 headers 属性中设置的一致。
支持 Ant 路径风格
- Ant 风格资源地址支持 3 种匹配符:
?: 匹配文件名中的一个字符 *: 匹配文件名中的任意字符 **: 匹配多层路径
- @RequestMapping 还支持 Ant 风格的 URL
占位符 @PathVariable
带占位符的 URL 是 Spring 3.0 新增功能,该功能在 SpringMVC 向 REST 目标挺进发展中具有里程碑的意义。通过使用 @PathVariable 注解可以将 URL 中占位符参数绑定到控制器处理方法的形参中。
/*
* 之前: localhost:8080/pName/testREST?id=1101&username=TREE
* 之后: localhost:8080/pName/testREST/1101/TREE
*/
@RequestMapping("/testREST/{id}/{username}")
public String testREST(@PathVariable("id")Integer id
, @PathVariable("username")String username) {
System.out.println(id + ":" + username);
return "success";
}
REST
REST 简述
REST —— (Resource) REpresentational State Transfer。(资源)表现层状态转化。通俗来讲就是:资源在网络中以某种表现形式进行状态转移
- Resource:资源,即数据(前面说过网络的核心)。可以用一个 URI(统一资源定位符)指向它,每种资源对应一个特定的 URI 。获取这个资源,访问它的 URI 就可以,因此 URI 即为每一个资源的独一无二的识别符
- Representational:把资源具体呈现出来的某种表现形式。比如用 HTML,JSON,XML,JPEG 等
- State Transfer:状态转换。每发出一个请求,就代表了客户端和服务器的一次交互过程。HTTP 协议,是一个无状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”。通过 HTTP 动词实现
首先为什么要用 RESTful 结构呢?大家都知道"古代"网页是前端后端融在一起的,比如之前的 PHP,JSP 等。在之前的桌面时代问题不大,但是近年来移动互联网的发展,各种类型的 Client 层出不穷,RESTful 可以通过一套统一的接口为 Web、iOS 和 Android 提供服务。另外对于广大平台来说,比如 Facebook platform,微博开放平台,微信公共平台等,它们不需要有显式的前端,只需要一套提供服务的接口,于是 RESTful 更是它们最好的选择。在 RESTful 架构下:
- REST 描述的是在网络中 Client 和 Server 的一种交互形式;REST 本身不实用,实用的是如何设计 RESTful API(REST 风格的网络接口)
- Server 提供的 RESTful API 中,URL 中只使用名词来指定资源,原则上不使用动词。“资源”是 REST 架构或者说整个网络处理的核心。比如:
http://api.qc.com/v1/newsfeed
获取某人的新闻;http://api.qc.com/v1/friends
获取某人的好友列表;http://api.qc.com/v1/profile
获取某人的详细信息 - 用 HTTP 协议里的动词来实现资源的添加,修改,删除等操作。即通过 HTTP 动词来实现资源的状态扭转:GET 用来获取资源;POST 用来新建资源(也可以用于更新资源);PUT 用来更新资源;DELETE 用来删除资源
- [DELETE]
http://api.qc.com/v1/friends
删除某人的好友(在 HTTP parameter 指定好友 id) - [POST]
http://api.qc.com/v1/friends
添加好友 - [UPDATE]
http://api.qc.com/v1/profile
更新个人资料
- [DELETE]
- Server 和 Client 之间传递某资源的一个表现形式,比如用 JSON,XML 传输文本,或者用 JPG,WebP 传输图片等。当然还可以压缩 HTTP 传输时的数据(on-wire data compression)
- 用 HTTP Status Code 传递 Server 的状态信息。比如最常用的 200 表示成功,500 表示 Server 内部错误等。主要信息就这么点。最后是要解放思想,Web 端不再用之前典型的 PHP 或 JSP 架构,而是改为前端渲染和附带处理简单的商务逻辑(比如 AngularJS 或者 BackBone 的一些样例)。Web 端和 Server 只使用上述定义的 API 来传递数据和改变数据状态。格式一般是 JSON。iOS 和 Android 同理可得。由此可见,Web,iOS,Android 和第三方开发者变为平等的角色通过一套 API 来共同消费 Server 提供的服务。
[总结] URL 定位资源,用 HTTP 动词(GET,POST,DELETE,PUT)描述操作
- 看 URL 就知道要什么
- 看 HTTP Method 就知道干什么
- 看 HTTP status code 就知道结果如何
HiddenHttpMethodFilter
浏览器 <form>
表单只支持 GET 与 POST 请求,而 DELETE、PUT 等 method 并不支持,Spring 3.0 添加了一个过滤器,可以将这些请求转换为标准的 HTTP 方法,使得支持 GET、POST、PUT 与 DELETE 请求。
图解
源码
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = DEFAULT_METHOD_PARAM;
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response
, FilterChain filterChain) throws ServletException, IOException {
String paramValue = request.getParameter(this.methodParam);
if ("POST".equals(request.getMethod()) && StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
HttpServletRequest wrapper = new HttpMethodRequestWrapper(request, method);
filterChain.doFilter(wrapper, response);
} else {
filterChain.doFilter(request, response);
}
}
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
@Override
public String getMethod() {
return this.method;
}
}
}
配置
<!-- 配置支持 REST 风格的过滤器:可以将 POST 请求转换为 PUT/DELETE 请求 -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
测试
测试 REST 风格的 GET,POST,PUT,DELETE 操作:
- 新增:/order → POST
- 修改:/order/1101 → PUT ? id=1101
- 获取:/order/1101 → GET ? id=1101
- 删除:/order/1101 → DELETE ? id=1101
rest.jsp
<!-- Test1: 测试 REST 风格 GET 请求 -->
<a href="testRESTGet/1101">testREST GET</a><br/>
<!-- Test2: 测试 REST 风格 POST 请求 -->
<form action="testRESTPost" method="POST">
<input type="submit" value="testRESTPost">
</form>
<!-- Test3: 测试 REST 风格 PUT 请求 -->
<form action="testRESTPut/1101" method="POST">
<input type="hidden" name="_method" value="PUT">
<input type="submit" value="testRESTPut">
</form>
<!-- Test4: 测试 REST 风格 DELETE 请求 -->
<form action="testRESTDelete/1101" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="submit" value="testRESTDelete">
</form>
<!-- 超链接肯定是 GET 方式,条件1 都不满足! -->
<a href="testRESTDelete?_method=DELETE&id=1101">错误方式</a>
RestController.java
@RequestMapping(value="/testRESTGet/{id}", method=RequestMethod.GET)
public String testRESTGet(@PathVariable(value="id") Integer id){
System.out.println("testRESTGet id=" + id);
return "success";
}
@RequestMapping(value="/testRESTPost", method=RequestMethod.POST)
public String testRESTPost(){
System.out.println("testRESTPost");
return "success";
}
@RequestMapping(value="/testRESTPut/{id}", method=RequestMethod.PUT)
public String testRESTPut(@PathVariable("id") Integer id){
System.out.println("testRESTPut id=" + id);
return "success";
}
@RequestMapping(value="/testRESTDelete/{id}", method=RequestMethod.DELETE)
public String testRESTDelete(@PathVariable("id") Integer id){
System.out.println("testRESTDelete id=" + id);
return "success";
}
处理请求数据
Review:JavaWEB 处理请求数据
- 请求参数:Request.getParameter(paramName); / Request.getParameterMap();
- Cookie 信息:Request.getCookies();
- 请求头信息:Request.getHeader();
请求处理方法签名
- Spring MVC 通过分析处理方法的签名(方法名+ 参数列表),将 HTTP 请求信息绑定到处理方法的相应形参中
- Spring MVC 对控制器处理方法签名的限制是很宽松的,几乎可以按喜欢的任何方式对方法进行签名
- 必要时可以对方法及方法形参标注相应的注解(@PathVariable 、@RequestParam、@RequestHeader 等)
- Spring MVC 会将 HTTP 请求的信息绑定到相应的方法形参中,并根据方法的返回值类型做出相应的后续处理
@RequestParam
在处理方法形参处使用 @RequestParam 可以把请求参数传递给请求方法(若方法形参名和请求参数名一致,那么就算不使用该注解,也能实现自动赋值)。
- value:请求参数的参数名
- required:是否必须。默认为 true,表示请求参数中必须包含对应的参数;若不存在,将抛出异常
- defaultValue:默认值。当没有传递该参数或者该参数的值为空字符串时使用该值进行赋值
@RequestMapping(value="/param", method=RequestMethod.POST)
public String registUser(@RequestParam(value="name", required=false
, defaultValue="admin")String username, String password) {
System.out.println(username + "=" + password);
return "success";
}
@RequestHeader
- 使用 @RequestHeader 绑定请求报头的属性值
- 请求头包含了若干个属性,服务器可据此获知客户端的信息,通过 @RequestHeader 即可将请求头中的属性值绑定到处理方法形参中
- 用法同 @RequestParam 一致
@RequestMapping(value="/testRequestHeader")
public String testRequestHeader(@RequestHeader(value="Accept-Language") String al){
System.out.println("testRequestHeader - Accept-Language:" + al);
return "success";
}
@CookieValue
使用 @CookieValue 绑定请求中的 Cookie 值。
@RequestMapping("/testCookieValue")
public String testCookieValue(@CookieValue("JSESSIONID") String sessionId) {
System.out.println("testCookieValue: sessionId: " + sessionId);
##### return "success";
}
使用 POJO 作为参数
Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配,自动为该对象填充属性值。支持级联属性,如:dept.deptId、dept.address.tel 等。
public class User {
private Integer id ;
private String username;
private String password;
private String email;
private int age;
private Address address;
}
public class Address {
private String province;
private String city;
}
使用Servlet原生API作为参数
MVC 的 Handler 方法可以接受哪些 ServletAPI 类型的参数:HttpServletRequest、HttpServletResponse、HttpSession、java.security.Principal、Locale、InputStream、OutputStream、Reader 和 Writer。
@RequestMapping("/param")
public String registUser(User user, HttpServletRequest request) {
System.out.println(user);
System.out.println(request.getParameter("username"));
return "success";
}
AnnotationMethodHandlerAdapter$ServletHandlerMethodInvoker 部分源码:
进入 resolveHandlerArguments()
,找到其中 resolveCommonArgument()
并进入,进而再找到 resolveStandardArgument()
并进入。如此,便可知道 SpringMVC 本质上还是从原生 Servlet API 中获取相应的对象。
处理响应数据
Review:JavaWeb 处理响应数据
- request.setAttribute(xxx)
- request.getRequestDispatcher(地址).forward(req, resp);
如下途径输出模型数据
- ModelAndView:处理方法返回值类型为 ModelAndView 时, 方法体即可通过该对象添加模型数据
- Map 或 Model:若形参为
org.springframework.ui.Model
、org.springframework.ui.ModelMap
或java.uti.Map
类型,则当处理方法返回时,其中的数据会自动添加到模型中
ModelAndView
控制器处理方法的返回值如果为 ModelAndView,则其既包含视图信息,也包含模型数据信息。
@RequestMapping("/param")
public ModelAndView registUser(User user) {
ModelAndView mav = new ModelAndView();
mav.addObject("attr1", "value1"); // 往 request 作用域中放值
mav.setViewName("success"); // 设置视图名称,实现请求转发
return mav;
}
ModelAndView 源码
- 两个重要的成员变量
private Object view; // 视图信息 private ModelMap model; // 模型数据
- 添加模型数据
MoelAndView addObject(String attributeName, Object attributeValue) ModelAndView addAllObject(Map<String, ?> modelMap)
- 设置视图
void setView(View view) // 设置视图对象 void setViewName(String viewName) // 设置视图名字
- 获取模型数据
protected Map<String, Object> getModelInternal() public ModelMap getModelMap() public Map<String, Object> getModel()
过程源码
- 在 DispatcherServlet 中的 945 行, 开始调用请求处理器中的请求处理方法,方法执行完成后,返回一个 ModelAndView 对象
- 在 DispatcherServlet 的 959 行, 准备开始处理 ModelAndView
- 在 DispatcherServlet 的 1012 行,准备处理 ModelAndView 中的 视图信息 还有 模型数据
- 在 DispatcherServlet 的 1204 行,根据 ModelAndView 中的视图信息,通过 [视图解析器] 解析得到一个 View 视图对象
- 在 DispatcherServlet 的 1225 行, 开始处理模型数据(说明:
render()
是View<I>
中定义的,在 AbstractView 类中进行了实现,所有具体的视图类都使用 AbstractView 中实现的render()
) - 在 AbstractView 中的 266 行,整合输出模型数据(说明:
renderMergedOutputModel()
是 AbstractView 中定义的抽象方法, 在每个具体的视图实现类中进行实现) - 在 InternalResourceView 中的 180 行,将模型数据设置到 Request 域对象中
- 在 InternalResourceView 中的 189 行, 获取转发器
- 在 InternalResourceView 中的 209 行, 请求转发
Map & Model
Spring MVC 在内部使用了一个 org.springframework.ui.Model<I>
存储模型数据。
具体使用步骤:
- Spring MVC 在调用方法前会创建一个隐含的模型对象作为模型数据的存储容器
- 如果方法的形参为 Map 或 Model 类型,Spring MVC 会将隐含模型的引用传递给这个形参
- 在方法体内,开发者可以通过这个形参对象访问到模型中的所有数据,也可以向模型中添加新的属性数据
@RequestMapping(value="/param", method=RequestMethod.POST)
public String registUser(Map<String, Object> map) {
map.put("attr2", "attr2"); // 向作用域中放值
return "success"; // 返回视图名称
}
@RequestMapping(value="/param", method=RequestMethod.POST)
public String registUser(Model model) {
model.addAttribute("attr3", "value3");
return "success";
}
执行过程同 ### 7.2.2 相同。