@Otokaze
2018-12-16T18:13:57.000000Z
字数 135920
阅读 821
Java
Spring MVC 全称 Spring Web MVC,是 Struts2 的对标产品。Struts2 曾经是最流行的 MVC 框架,但由于存在过多安全漏洞,现在越来越多的开发团队选择了 Spring MVC,因为 Spring MVC 是 Spring Framework 家族中的一个,相比较 Struts2,Spring MVC 更容易与 Spring 框架相契合,上手难度也低于 Struts2。自从 Spring MVC 2.5 版本引入注解驱动功能后,Controller 已经不再需要继承任何接口,无需在 XML 配置文件中定义请求和 Controller 的映射关系,仅仅使用注解就可以让一个 POJO 具有 Controller 的全部功能,使得 Spring MVC 框架的易用性又得到了进一步的增强。在框架灵活性、易用性和扩展性上,Spring MVC 已经全面超越了其它的 MVC 框架,伴随着 Spring 一路高唱猛进,可以预见 Spring MVC 在 MVC 市场上的吸引力将越来越不可抗拒。
SSM 三大框架是目前主流的 Java Web 开发框架,即 Spring、SpringMVC、MyBatis。其中 Spring 我们前面已经学习过了,现在我们紧接着开始学习 Spring MVC,学完 Spring MVC 后就学习 MyBatis 持久化框架。最后将这三大框架进行整合,变成 SSM 框架组合,然后就是项目练手了。
Spring MVC 和 Spring 框架是紧密结合的,先学习 Spring 的 IoC 容器,对 SpringMVC 的学习非常有帮助,在 Spring MVC 中,我们依旧会使用 Spring 的 IoC 容器,而且是必不可少的,Spring MVC 是建立在 Spring 的 IoC 容器之上的(Spring 的 IoC 容器是 Spring 框架的核心概念,必须熟练掌握)。
关于 MVC,我们再来熟悉一下相关的处理流程。首先客户端请求到达 Controller 控制器,Controller 负责处理用户发来的请求,然后将处理结果包装为 Model(POJO 对象),最后将 Model 对象传递给用来渲染页面的 View;View 一般就是 JSP,在 JSP 中,可以通过 EL 表达式来获取 Model 对象中的数据,然后渲染页面,最后返回给请求用户。
可以发现,Controller 的职责就是用来处理用户请求的,处理完一般都会有一个结果,为了方便传递,我们一般都会将处理完得到的数据封装到一个对象中(Bean/POJO),然后将这个结果对象通过 setAttribute() 方法存放到 request 对象中,然后将 request 交给 view 处理。
view 通常就是 JSP 文件,所以我们可以通过 EL 表达式来获取 request 对象中的 model 数据,在需要的时候还可以通过 jstl 来进行简单的逻辑判断,循环处理等,最后将渲染得到的页面返回给请求客户端,这样,一个 request 就算完成了它的使命了。
view 中的逻辑一般都很轻松,就是从 request 中读取 model 数据,然后渲染页面。而 controller 是处理请求的地方,为了避免 controller 过于庞大,一般我们的应用程序都不会直接在 controller 处理业务逻辑,而是将业务逻辑放到一个叫做 service 的地方,在大多数简单的情况下,controller 内部仅仅是调用对应的 service 类而已。所以 service 是我们写业务逻辑的地方;而又因为 service 通常又会与数据库进行交互,为了解耦,我们又抽象出了一个 dao 层,dao 就是 data access object,封装常用的数据库操作,方便 service 层调用。即 controller -> service -> dao -> 数据库系统。
这样我们的 web 应用又分为了 3 层(由外到内排列):web 层、业务层、持久层:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9</groupId>
<artifactId>SpringMVC_Learn</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>4.3.20.RELEASE</spring.version>
<mysql.version>8.0.13</mysql.version>
<servlet.version>3.1.0</servlet.version>
<jstl.version>1.2</jstl.version>
<jackson.version>2.9.7</jackson.version>
<fileupload.version>1.3.3</fileupload.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${fileupload.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
Spring MVC 测试环境:
创建项目目录
mvn -B archetype:generate -DarchetypeArtifactId=maven-archetype-webapp -DgroupId=com.zfl9 -DartifactId=springmvc-learn
编辑 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9</groupId>
<artifactId>springmvc-learn</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<maven.test.skip>true</maven.test.skip>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.20.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<warName>ROOT</warName>
<failOnMissingWebXml>false</failOnMissingWebXml>
<outputDirectory>/usr/local/tomcat/apps</outputDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>
简单解释一下:
maven.test.skip
表示跳过测试步骤(HelloWorld 不需要什么测试,多此一举)。maven.compiler.source
和 maven.compiler.target
用来指定 JDK 版本为 1.8。org.springframework:spring-webmvc:4.3.20.RELEASE
引入 spring mvc 4.x 依赖。上面的 pom.xml 是单纯使用 Maven 管理的,其实如果使用 Idea 等 IDE 工具的话,完全不需要配置什么 warName、outputDirectory,因为直接 Shift + F10 就能运行了(Idea 配合 IdeaVim 插件简直无敌)。spring-webmvc 模块会依赖 spring-context、spring-web,所以我们并不需要在 pom.xml 中配置它们。
项目目录结构
$ tree
.
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── zfl9
│ └── controller
│ └── HelloWorldController.java
└── webapp
├── index.jsp
└── WEB-INF
├── mvc.xml
├── views
│ └── helloworld.jsp
└── web.xml
mvc.xml 其实就是我们前面学习 Spring 时候的 beans.xml、spring.xml,即 Spring 配置文件。注意 view 文件存放的路径,我们并没有放到 webapp 目录下,因为这样客户端就能直接访问我们的 jsp 了,而因为这些 view 里面的 jsp 一般都需要读取经 controller 处理得到的 model 数据,所以直接访问它们通常都会出现错误,甚至可能被黑客利用,所以我们一般都会把 view 放到 WEB-INF 目录下,WEB-INF 是说所谓的“安全目录”,这样外部就不能访问这些 jsp 了,只能访问我们暴露出去的 url,这些 url 通常都是映射到某个 controller,只有经过 controller 处理后,才访问这些 jsp(forward 过去),这样就安全多了。
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
/
,即所有 url 都映射到 springmvc。/WEB-INF/${servlet-name}-servlet.xml
。mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zfl9"/>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
通过前面的 web.xml 可以得知,Tomcat 收到的任何 HTTP 请求都将被路由给 springmvc,因为 springmvc 对应的 url-pattern 为 /。因为 springmvc 的 load-on-startup
值为 1,所以 springmvc 会在容器启动时进行初始化。springmvc 初始化过程中,会读取 mvc.xml 配置文件,初始化 IoC 容器,可以得知要扫描的 package 为 com.zfl9
(包括所有子包)、对应的 View 视图路径为 /WEB-INF/views
、视图后缀名为 .jsp
。HTTP 请求到达 springmvc 后,springmvc 首先查询 Handler Mapping
,获取 url 对应的 controller,然后将该 request 交给 controller 处理,controller 处理完后返回一个 ModelAndView 对象(携带 Model 数据的 View 对象),然后通过查询 View Resolver
,获取 ModelAndView 对象对应的视图页面(通常为 JSP),接着将 model 数据传递给对应的 view 页面,在 view 页面渲染完成后,springmvc 将最终的响应结果传回给客户端。如下图所示:
简单的说就是这几个步骤:
HelloWorldController
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloWorldController {
@RequestMapping("/helloworld")
public ModelAndView helloworld(@RequestParam(name = "name", required = false, defaultValue = "World") String name) {
ModelAndView mav = new ModelAndView("helloworld");
mav.addObject("name", name);
return mav;
}
}
@Controller
注解表示对应的类是一个 Controller。@RequestMapping
注解用来指定方法对应的 URL 路径。@RequestParam
注解表示 name 参数对应的 url 查询参数。new ModelAndView("helloworld")
的 helloworld 为 view 名。mav.addObject("name", name)
方法用来添加 name-value 名值对。很容易知道,Controller 中被 @RequestMapping 标注的方法都是用来处理用户请求的,每个请求处理方法都有两个必要元素,一个是 @RequestMapping 里面指定的 uri,发送给该 uri 的请求都将被该方法处理;另一个就是请求处理方法中返回的 view 名/对象。在 ModelAndView 中,构造函数的参数就是 view 名,View Resolver 会结合 prefix 和 suffix 来确定 view 的绝对路径,然后将 request 转发给 view 处理。
@Controller 是 @Component 的子接口,还记得吗?@Component 有 3 个子接口:
这里我们提到了 控制层、业务层、持久层,注意不要和 MVC 模型搞混了,虽然它们都是所谓的“三层结构”。所谓控制层就是我们常说的 controller/action 层,而业务层就是 service 层,持久层就是 dao 层。此外我们还有一个 model 层(准确来说应该是包)。controller 层又被称为 web 层,web 层位于最外边,是与 web 服务器(tomcat)直接交互的一个层面,web 层除了 controller 外,还有 view。在 controller 中,不建议编写任何复杂的逻辑,基本上就是调用对应的 service 层,service 层才是我们编写具体业务代码的地方,通常我们的业务逻辑都需要与数据库进行交互,但我们并不会直接与数据库进行交互,而是通过 dao 层,dao 即 data access object(数据访问对象),所谓 dao 就是对数据库访问逻辑的封装。
调用层次为:controller -> service -> dao;controller 返回 model 给 view,最后返回浏览器。
再次强调,controller 不建议编写任何与业务相关的逻辑,大多数简单的情况下,controller 内部就是调用对应的 service 类,仅此而已。service 层才是编写业务逻辑的地方,而 dao 层就是编写数据库访问逻辑的地方,一定要记住它们的职责,不要搞混了。一般情况下,为了解耦,我们的 service 层会分为 service 接口和 service impl 实现,对应的,dao 层也会分为 dao 接口和 dao imple 实现。
典型的 web 应用程序结构为:
$ tree springmvc
springmvc
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── zfl9
│ ├── controller
│ │ └── EmployeeController.java
│ ├── dao
│ │ ├── EmployeeDao.java
│ │ └── impl
│ │ └── EmployeeDaoImpl.java
│ ├── model
│ │ └── EmployeeBean.java
│ └── service
│ ├── EmployeeService.java
│ └── impl
│ └── EmployeeServiceImpl.java
└── webapp
├── index.jsp
└── WEB-INF
├── mvc.xml
├── views
│ └── employee.jsp
└── web.xml
helloworld.jsp
<%@page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HelloWorld</title>
</head>
<body>
<h1>Hello, ${name}!</h1>
</body>
</html>
${name}
是一个 EL 表达式,用来从 request 中获取 addObject() 传递的对象。index.jsp
<%@page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>index.jsp</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
编译、运行
# root @ arch in ~/maven-workspace/springmvc-learn [10:36:39]
$ mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.zfl9:springmvc-learn >----------------------
[INFO] Building springmvc-learn 1.0.0
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ springmvc-learn ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ springmvc-learn ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /root/maven-workspace/springmvc-learn/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ springmvc-learn ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /root/maven-workspace/springmvc-learn/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ springmvc-learn ---
[INFO] Not copying test resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ springmvc-learn ---
[INFO] Not compiling test sources
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ springmvc-learn ---
[INFO] Tests are skipped.
[INFO]
[INFO] --- maven-war-plugin:3.2.2:war (default-war) @ springmvc-learn ---
[INFO] Packaging webapp
[INFO] Assembling webapp [springmvc-learn] in [/root/maven-workspace/springmvc-learn/target/springmvc-learn-1.0.0]
[INFO] Processing war project
[INFO] Copying webapp resources [/root/maven-workspace/springmvc-learn/src/main/webapp]
[INFO] Webapp assembled in [39 msecs]
[INFO] Building war: /usr/local/tomcat/apps/ROOT.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.683 s
[INFO] Finished at: 2018-11-25T10:36:48+08:00
[INFO] ------------------------------------------------------------------------
# root @ arch in ~/maven-workspace/springmvc-learn [10:36:48]
$ tomcat start
Using CATALINA_BASE: /usr/local/tomcat
Using CATALINA_HOME: /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME: /usr/local/jdk/jdk1.8
Using CLASSPATH: /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
Tomcat started.
# root @ arch in ~/maven-workspace/springmvc-learn [10:36:52]
$ curl 'http://127.0.0.1/'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>index.jsp</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
# root @ arch in ~/maven-workspace/springmvc-learn [10:37:03]
$ curl 'http://127.0.0.1/helloworld'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HelloWorld</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
# root @ arch in ~/maven-workspace/springmvc-learn [10:37:08]
$ curl 'http://127.0.0.1/helloworld?name=Otokaze'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HelloWorld</title>
</head>
<body>
<h1>Hello, Otokaze!</h1>
</body>
</html>
# root @ arch in ~/maven-workspace/springmvc-learn [10:37:21]
$ curl 'http://127.0.0.1/helloworld' -d 'name=中国'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HelloWorld</title>
</head>
<body>
<h1>Hello, 中国!</h1>
</body>
</html>
在有些 Spring MVC 教程中,会出现这两个配置文件,它们贴出来的 web.xml 像这样:
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
对比 HelloWorld 里面的 web.xml,可以发现这里的 web.xml 多了个 ContextLoaderListener 上下文加载监听器,还有就是多了个 context-param 上下文初始化参数。ContextLoaderListener 会从 contexxt-param 中读取 Spring 的配置文件路径(如果省略 context-param,那么默认路径就是 /WEB-INF/applicationContext.xml),ContextLoaderListener 会初始化一个 IoC 容器。
那么问题来了,ContextLoaderListener 和 DispatcherServlet 都会初始化对应的 IoC 容器,也就意味这 web 应用中有两个 Spring IoC 容器,一个是由 ContextLoaderListener 管理的,一个是由 DispatcherServlet 管理的。这两个 IoC 容器有什么区别呢?来看官方的解答:
Spring 允许您在父子层次结构中定义多个 ApplicationContext 上下文。applicationContext.xml
是 web 应用的 root 上下文(根上下文);${servlet-name}-servlet.xml
是 DispatcherServlet 中的上下文,在同一个 web 应用中运行有多个 DispatcherServlet 实例,每个 DispatcherServlet 都有属于自己的上下文。有关 MVC 的配置需要放到对应的 DispatcherServlet 上下文配置文件中,而 ApplicationContext 中的 bean 可以被所有 DispatcherServlet 上下文所共享,但是 ApplicationContext 不能获取 DispatcherServlet 上下文中的 bean(这很好理解)。
在大多数简单的情况下,applicationContext.xml 上下文是不必要的。除非你需要在多个 servlet 上下文中共享相同的 bean 实例,否则你完全不需要配置 ContextLoaderListener 和 applicationContext.xml。在绝大多数情况下,我们只需要配置一个 DispatcherServlet 和对应的 mvc.xml 就行了。我们的 bean 可以配置在 mvc.xml 中,在程序中可以通过 context 来获取这些 bean。
我们来回顾一下 HelloWorld 中的 Controller 代码:
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloWorldController {
@RequestMapping("/helloworld")
public ModelAndView helloworld(@RequestParam(name = "name", required = false, defaultValue = "World") String name) {
ModelAndView mav = new ModelAndView("helloworld");
mav.addObject("name", name);
return mav;
}
}
@Controller 是 @Component 的子注解,表示对应的 POJO 类是一个 Controller 控制器。
@RequestMapping 注解的作用则是用来告诉 Handler Mapping,当前方法对应的 uri 路径。
当 dispatcher 收到 uri 为 /helloworld
的请求时,将交给 helloworld() 方法去处理。
helloworld() 是自定义的 POJO 方法,返回值为 ModelAndView,接收一个 http 请求参数。
name 参数上使用了 @RequestParam 注解,SpringMVC 会自动注入对应的请求参数到此参数上。
@RequestParam 的 name 为请求参数名,required 为是否为请求的,defaultValue 为默认值。
ModelAndView 是 Model 模型和 View 页面的合体类型,传入的构造参数是对应的 view 页面名。
mav 对象和 request 对象一样,可以存入 name-value 对,它等同于 request.setAttribute()。
本质上,ModelAndView 中的 addObject() 方法内部就是调用 request.setAttribute() 方法而已。
也因为如此,我们可以在 view 页面中,如 JSP,使用 EL 表达式来获取在 ModelAndView 中设置的值。
在 HelloWorld 例子中,我们使用的是 ModelAndView 传递 model 数据,其实有三种常用方式来传递 model 数据,分别是 Model、ModelMap、ModelAndView,据说前两者在内部都会转换为 ModelAndView 形式:
model.addAttribute(name, value)
modelMap.addAttribute(name, value)
modelAndView.addObject(name, value)
Model 是一个接口,接口声明如下:
public interface Model {
Model addAttribute(String name, Object value); // 添加 name-value 对
Model addAttribute(Object value); // name 为类名(首字母小写,非全限定类名)
Model addAllAttributes(Collection<?> values); // name 为类名,转换规则同上
Model addAllAttributes(Map<String, ?> attributes); // 合并 Map 上的键值对
Model mergeAttributes(Map<String, ?> attributes); // 合并 Map 上的键值对
Map<String, Object> asMap(); // 返回 java.util.Map 对象(Map 视图)
boolean containsAttribute(String name); // 是否存在名为 name 的键值对
}
ModelMap 是 LinkedHashMap<String, Object>
的子类,方法与 Model 相似,如下:
public class ModelMap extends LinkedHashMap<String, Object> {
/* 构造方法 */
public ModelMap();
public ModelMap(String name, Object value);
public ModelMap(Object value); // 使用类名作为 name,首字母小写
Model addAttribute(String name, Object value); // 添加 name-value 对
Model addAttribute(Object value); // name 为类名(首字母小写,非全限定类名)
Model addAllAttributes(Collection<?> values); // name 为类名,转换规则同上
Model addAllAttributes(Map<String, ?> attributes); // 合并 Map 上的键值对
Model mergeAttributes(Map<String, ?> attributes); // 合并 Map 上的键值对
boolean containsAttribute(String name); // 是否存在名为 name 的键值对
}
ModelAndView 是最原始的传值方式,但也是最强大的,因为 ModelAndView 不仅仅是传值,还有其它操作。
public class ModelAndView {
/* 构造函数 */
public ModelAndView(); // for bean
public ModelAndView(View view);
public ModelAndView(String viewName);
public ModelAndView(String viewName, HttpStatus status); // 4.3+
/* model 即传递的数据 */
public ModelAndView(View view, Map<String, ?> model);
public ModelAndView(String viewName, Map<String, ?> model);
public ModelAndView(String viewName, Map<String, ?> model, HttpStatus status); // 4.3+
public ModelAndView(View view, String name, Object value);
public ModelAndView(String viewName, String name, Object value);
/* viewName */
public void setViewName(String viewName);
public String getViewName();
/* view */
public void setView(View view);
public View getView()
/* view 相关 */
public boolean hasView(); // 是否设置了 view
public boolean isReference(); // 是否为 view 引用
/* model/modelMap */
public ModelMap getModelMap();
public Map<String, Object> getModel();
/* status v4.3+ */
public void setStatus(HttpStatus status);
public HttpStatus getStatus();
/* addObject */
public ModelAndView addObject(String name, Object value);
public ModelAndView addObject(Object value);
public ModelAndView addAllObjects(Map<String, ?> modelMap);
/* clear */
public void clear(); // 清空 modelAndView 对象
public boolean isEmpty();
public boolean wasCleared();
/* toString */
public String toString();
}
其中,Model 和 ModelMap 对象可以放在方法参数中,Spring MVC 会自动注入对应的实例,而 ModelAndView 需要自己 new 出来,我们改写一下前面的 HelloWorld 控制器,如下:
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloWorldController {
@RequestMapping("/helloA")
public String helloA(@RequestParam(name = "name", required = false, defaultValue = "WorldA") String name, Model model) {
model.addAttribute("name", name);
return "helloworld";
}
@RequestMapping("/helloB")
public String helloB(@RequestParam(name = "name", required = false, defaultValue = "WorldB") String name, ModelMap modelMap) {
modelMap.addAttribute("name", name);
return "helloworld";
}
@RequestMapping("/helloC")
public ModelAndView helloC(@RequestParam(name = "name", required = false, defaultValue = "WorldC") String name) {
ModelAndView mav = new ModelAndView("helloworld");
mav.addObject("name", name);
return mav;
}
}
注意,helloA()、helloB()、helloC() 方法返回的 view 页面都为 helloworld,结合 mvc.xml 中的 prefix 和 suffix,Spring MVC 就可以知道 view 页面的绝对路径为 /WEB-INF/views/helloworld.jsp:
# Otokaze @ Otokaze-Win10 in ~ [19:50:44]
$ curl localhost/helloA
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>helloworld</title>
</head>
<body>
<h1>Hello, WorldA!</h1>
</body>
</html>
# Otokaze @ Otokaze-Win10 in ~ [19:50:55]
$ curl localhost/helloB
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>helloworld</title>
</head>
<body>
<h1>Hello, WorldB!</h1>
</body>
</html>
# Otokaze @ Otokaze-Win10 in ~ [19:50:57]
$ curl localhost/helloC
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>helloworld</title>
</head>
<body>
<h1>Hello, WorldC!</h1>
</body>
</html>
测试没问题。分别返回 Hello, WorldA
、Hello, WorldB
、Hello, WorldC
。
更正,ModelAndView 对象其实也是可以让 Spring MVC 生成的,例子:
HelloController.java
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloController {
@GetMapping("/hello")
public ModelAndView hello(ModelAndView mav) {
mav.setViewName("hello");
mav.addObject("message", "hello, world!");
return mav;
}
}
hello.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<h1>${message}</h1>
</body>
</html>
测试结果:
$ curl localhost/hello
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<h1>hello, world!</h1>
</body>
</html>
注意,我们前面说了,是通常情况下,有上面这三种返回模型数据的方法,但其实我们还有其它几个方法:
1、返回 java.util.Map,view 名为 @RequestMapping 中的名称(/WEB-INF/views/helloD.jsp
):
@RequestMapping("/helloD")
public Map<String, Object> helloD(@RequestParam(name = "name", required = false, defaultValue = "WorldD") String name) {
Map<String, Object> map = new HashMap<>();
map.put("name", name);
return map;
}
2、返回 void,view 名为 @RequestMapping 中的名称(/WEB-INF/views/helloD.jsp
):
@RequestMapping("/helloD")
public void helloD() {}
3、返回 String,其实就是前面 Model、ModelMap 的简化版,返回的 String 就是对应的 view 名:
@RequestMapping("/helloD")
public String helloD() {
return "helloD";
}
4、返回 String 作为响应体(不需要 view,直接返回字符串作为响应体),使用 @ResponseBody 注解:
@RequestMapping("/helloD")
@ResponseBody
public String helloD() {
return "helloD";
}
5、返回 null,会发生什么结果呢?答案是得到 404 Not Found 响应!!!面试题哦!!!
@RequestMapping 可以用在 Controller 上或者 Controller 中的方法上,如果用在 Controller 上,则相当于给所有方法上的 @RequestMapping 加上了父路径,比如在类上的 uri 为 /home,方法上的 uri 为 /list,则该方法实际映射到的 uri 为 /home/list,例子,访问地址为 /wmyskxz/hello
:
@Controller
@RequestMapping("/wmyskxz")
public class HelloController {
@RequestMapping("/hello")
public ModelAndView handleRequest(....) throws Exception {
....
}
}
基本上我们可以认为,放在 Controller 上面的 @RequestMapping 要么是像上面那样提供一个父路径,要么就是给 Controller 中的方法提供默认值,这个默认值可以被方法中的 @RequestMapping 的注解所覆盖。
String name
:@RequestMapping 的名称,不常用String[] value
:映射的 uri,支持 ant 风格模式String[] path
:映射的 uri,支持 ant 风格模式,v4.2+RequestMethod[] method
:限定请求方法,如 GET、POSTString[] params
:限定请求的查询参数,见后面的表达式语法String[] headers
:限定请求的请求头部,见后面的表达式语法String[] consumes
:限定请求的ContentType,见后面的表达式语法String[] produces
:限定请求的 Accept 类型,见后面的表达式语法所谓 ant-style pattern 就是这三个通配符:
?
:匹配任意单个字符*
:匹配任意长度字符**
:匹配任意深度目录method 属性则是用来限定对应方法可以处理的 HTTP 请求方法的,默认是全部都可以处理,如果需要我们可以将限定为只处理 GET 请求。从 Spring 4.3 版本开始,提供了 4 个方便的注解,分别用来指定 GET、POST、PUT、DELETE 请求方法的 @RequestMapping
(实际就是子注解,属性相同,除了没有 method),分别为:
@PutMapping
:@ReqestMapping 的子注解,HTTP PUT 方法@GetMapping
:@RequestMapping 的子注解,HTTP GET 方法@PostMapping
:@RequestMapping 的子注解,HTTP POST 方法@DeleteMapping
:@RequestMapping 的子注解,HTTP DELETE 方法params 属性表示只有符合指定条件的请求才会被对应方法处理,后面的 headers、consumes、produces 都是差不多的作用,用来进行条件限定的。如 param=value
表示请求必须带有 param
参数且值为 value
,否则不会被当前方法处理;param!=value
则与它相反;param
表示请求必须带有 parm 参数,不限定它的值;而 !param
则与它相反。
headers 属性的作用和 params 属性的作用相似,只不过这是用来限定请求头字段的,语法为 header=value
、header!=value
、header
、!header
,对于 MIME 类型,支持 *
通配符。
consumes 属性和 params/headers 属性差不多,但这是用来限定请求的 Context-Type 类型的,比如 text/html
表示只处理请求 MIME 为 text/html 的请求,其它类型的请求不进行处理,MIME 中可以出现通配符,如 text/*
,也可以出现 !
取反符号,如 !text/plain
表示出了 text/plain
外的都会处理。produces 属性和 consumes 属性相似,但 produces 是用来限定请求头中的 Accept 字段的。
例一:(实现效果同下,不推荐)
@Controller
public class EmployeeController
{
@RequestMapping("/employee-management/employees")
public String getAllEmployees(Model model)
{
//application code
return "employeesList";
}
@RequestMapping("/employee-management/employees/add")
public String addEmployee(EmployeeVO employee)
{
//application code
return "employeesDetail";
}
@RequestMapping("/employee-management/employees/update")
public String updateEmployee(EmployeeVO employee)
{
//application code
return "employeesDetail";
}
@RequestMapping(value={"/employee-management/employees/remove","/employee-management/employees/delete"})
public String removeEmployee(@RequestParam("id") String employeeId)
{
//application code
return "employeesList";
}
}
例二:(实现效果同上,推荐)
@Controller
@RequestMapping("/employee-management/employees")
public class EmployeeController
{
@RequestMapping
public String getAllEmployees(Model model)
{
//application code
return "employeesList";
}
@RequestMapping("/add")
public String addEmployee(EmployeeVO employee)
{
//application code
return "employeesDetail";
}
@RequestMapping("/update")
public String updateEmployee(EmployeeVO employee)
{
//application code
return "employeesDetail";
}
@RequestMapping(value={"/remove","/delete"})
public String removeEmployee(@RequestParam("id") String employeeId)
{
//application code
return "employeesList";
}
}
例三:RESTFul API 风格:
@Controller
@RequestMapping("/employee-management/employees")
public class EmployeeController
{
@RequestMapping (method = RequestMethod.GET)
public String getAllEmployees(Model model)
{
//application code
return "employeesList";
}
@RequestMapping (method = RequestMethod.POST)
public String addEmployee(EmployeeVO employee)
{
//application code
return "employeesDetail";
}
@RequestMapping (method = RequestMethod.PUT)
public String updateEmployee(EmployeeVO employee)
{
//application code
return "employeesDetail";
}
@RequestMapping (method = RequestMethod.DELETE)
public String removeEmployee(@RequestParam("id") String employeeId)
{
//application code
return "employeesList";
}
}
用在方法参数上,表示该参数将接受对应的 HTTP 请求参数(查询参数、表单数据、文件上传等),属性有:
String value
:name 属性的别名,默认同参数名;String name
:要绑定的请求参数名称,默认同参数名;boolean required
:请求参数是否是必选的,默认 true;String defaultValue
:当请求参数未提供时,设置该默认值。例子,我们可以将上面的 HelloWorld 改为这样(效果一样):
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloWorldController {
@RequestMapping("/helloA")
public String helloA(@RequestParam(defaultValue = "WorldA") String name, Model model) {
model.addAttribute("name", name);
return "helloworld";
}
@RequestMapping("/helloB")
public String helloB(@RequestParam(defaultValue = "WorldB") String name, ModelMap modelMap) {
modelMap.addAttribute("name", name);
return "helloworld";
}
@RequestMapping("/helloC")
public ModelAndView helloC(@RequestParam(defaultValue = "WorldC") String name) {
ModelAndView mav = new ModelAndView("helloworld");
mav.addObject("name", name);
return mav;
}
}
扩展:如果处理方法的参数与查询参数同名,那么即使没有 @RequestParam 注解也会被注入对应的值:
@RequestMapping("/helloD")
@ResponseBody
public String helloD(String name) {
if (name == null) {
return "<h1>Hello, WorldD!</h1>";
} else {
return "<h1>Hello, " + name + "!</h1>";
}
}
测试一下,没带参数就是打印 Hello, WorldD!
,带参数就是打印 Hello, ${name}!
:
# Otokaze @ Otokaze-Win10 in ~ [13:56:36]
$ curl 'localhost/helloD'
<h1>Hello, WorldD!</h1>
# Otokaze @ Otokaze-Win10 in ~ [13:56:39]
$ curl 'localhost/helloD?name=Otokaze'
<h1>Hello, Otokaze!</h1>
虽然可行,但最好还是使用 @RequestParam 注解标明一下,就如同少了 @Override
注解一样能工作一样。
上一节提到了 @RequestParam 注解,它的作用是将请求参数注入到方法参数中;而 @ModelAttribute 注解的作用就如同它的名字一样,将请求参数直接注入到 Model 对象中(addAttribute 方法),什么意思呢?
前面我们说了,ModelAndView 是最古老的传值方式,而 Model 是较新的传值方式(ModelMap 和 Model 很相似,暂时忽略,一般用的最多的就是 Model 和 ModelAndView 两种),ModelAndView 需要自己手动在方法中 new 出来,而 Model 对象则是 Spring 自动注入的,不需要我们 new 出来。
现在假设这么一个情形,/queryUser 接收两个参数,username 和 password,分别表示用户的用户名和密码,为了方便,我们使用一个 User 类来表示一个用户,这个 User 类有两个私有成员,username 和 password,且这两个私有成员都有自己对应的公共 getter/setter 方法,如下:
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
用我们上面学的知识,我们可以这样写一个控制器方法:
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class HelloWorldController {
@RequestMapping("/queryUser")
public String queryUser(Model model, @RequestParam String username, @RequestParam String password) {
User user = new User();
user.setUsername(username);
user.setPassword(password);
model.addAttribute("user", user);
return "queryUser";
}
}
queryUser.jsp 内容:
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>queryUser</title>
</head>
<body>
<h1>username: ${user.username}</h1>
<h1>password: ${user.password}</h1>
</body>
</html>
执行结果,没问题:
$ curl 'http://localhost/queryUser?username=Otokaze&password=123456'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>queryUser</title>
</head>
<body>
<h1>username: Otokaze</h1>
<h1>password: 123456</h1>
</body>
</html>
但我们其实可以让 Spring 自动将与 User 类的属性同名的查询参数注入到 User 类的 setter 方法,并且将 User 类的实例注入到 Model 对象中(调用 addAttribute() 方法),如下(@ModelAttribute 注解):
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloWorldController {
@RequestMapping("/queryUser")
public String queryUser(@ModelAttribute User user) {
return "queryUser";
}
}
注意到我们的 @ModelAttribute 注解,它的作用就是自动将 User 实例注入到 Model 对象中,name 即使 User 类的首字母小写格式,value 就是 user 实例,User 实例会被 Spring 自动创建(无参构造函数),然后会根据同名查询参数,将参数值存放到对应的 setter 方法。与我们前面的代码是一样的。测试结果相同。
当然,你会发现去掉 @ModelAttribute 注解也能正常工作,如下,但是这不建议,可读性不如上面的这种:
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloWorldController {
@RequestMapping("/queryUser")
public String queryUser(User user) {
return "queryUser";
}
}
当然,我们还可以改写一下这个例子,将 User 实例的注入放到一个单独的方法中:
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloWorldController {
@ModelAttribute
public void receiveUser(@ModelAttribute User user) {}
@RequestMapping("/queryUser")
public String queryUser() {
return "queryUser";
}
}
需要注意的是,被 @ModelAttribute 注解的方法会在每个控制器方法前执行,要慎用。
其实就是一个特殊的 view name 而已,即 redirect:/path/to/target/url
:
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloWorldController {
@RequestMapping("/www.zfl9.com")
public String redirectToZfl9() {
return "redirect:https://www.zfl9.com";
}
@RequestMapping("/www.baidu.com")
public String redirectToBaidu() {
return "redirect:https://www.baidu.com";
}
@RequestMapping("/www.google.com")
public String redirectToGoogle() {
return "redirect:https://www.google.com";
}
}
注意我们前面的 web.xml 配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
我们将所有 url 都映射到了 springmvc 这个 servlet。springmvc 基本上是这样工作的,当 springmvc 收到一个请求时,都会向 Handler Mapping 查询对应的 Controller 处理器(所谓请求处理器就是 Controller 里面的处理方法),如果找到了对应的请求处理器,就将请求交给它处理,如果没找到那就返回 404 错误。
但是如果我们要访问静态资源,就会出现问题了,比如我们在根目录下下有一个 /images/site.png 图片资源,通常我们想直接通过 http://localhost/$contextPath/images/site.png
来访问它,而不是先定义一个 Controller 处理器来访问,怎么办呢?如果不进行任何配置,当你访问这个 url 的时候就会得到一个 404 Not Found 错误。
这时候你就会感到奇怪了,为什么我们的 Hello World 例子中的根目录下的 index.jsp 能直接访问呢?是因为 jsp 有什么特殊待遇么?是的,经过我测试,Spring MVC 不认为 JSP 是静态资源,所以能直接访问(当然是除了 WEB-INF 目录下的文件)。
那么对于其他静态资源,如果想要直接访问,该怎么处理呢?别慌,Spring MVC 提供两个解决办法:
<mvc:default-servlet-handler/>
:最简单的方式,将静态资源交给 default Servlet 处理。<mvc:resources mapping="${uri}" location="${path}"/>
:手动进行 resources 资源映射。第一种方式最简单,即将静态资源交给 default 这个 Servlet 去处理,动态资源(包括 JSP)则交给 SpringMVC 这个 Servlet 去处理,配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zfl9"/>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
第二种方式也很好理解,mapping 表示映射出去的 uri 路径,而 location 表示实际对应的目录:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zfl9"/>
<mvc:annotation-driven/>
<!-- 将 Web 根路径 "/" 及类路径下 /META-INF/publicResources/ 的目录映射为 /resources 路径(uri)。假设 Web 根路径下拥有 images、js 这两个资源目录,在 images 下面有 bg.gif 图片,在 js 下面有 test.js 文件,则可以通过 /resources/images/bg.gif 和 /resources/js/test.js 访问这二个静态资源。-->
<mvc:resources location="/,classpath:/META-INF/publicResources/" mapping="/resources/**"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
注意,如果你喜欢使用绝对路径,你可以在 jsp 页面中这样做:
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<core:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<div align="center">
<h3><a href="${ctx}/">ctx</a></h3>
<h3><a href="${ctx}/test">test</a></h3>
<h3><a href="${ctx}/dir/test">dir/test</a></h3>
<h3><a href="${ctx}/dir/dir/test">dir/dir/test</a></h3>
</div>
</body>
</html>
其中 ctx 变量就是我们的上下文路径,注意即使是根路径,也要记得加上 /,否则会变成空字符串!!!
目录结构:
├─java
│ └─com
│ └─zfl9
│ ├─controller
│ │ EmployeeController.java
│ │
│ ├─dao
│ │ EmployeeDao.java
│ │ EmployeeDaoImpl.java
│ │
│ ├─model
│ │ Employee.java
│ │
│ └─service
│ EmployeeService.java
│ EmployeeServiceImpl.java
│
└─webapp
│ index.html
│
└─WEB-INF
│ mvc.xml
│ web.xml
│
└─views
Employee_ListAll.jsp
一个员工管理系统,目前只有一个功能,那就是列出所有员工的信息。
Employee.java
package com.zfl9.model;
public class Employee {
private int id;
private String firstName;
private String lastName;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("[ id = %d, firstName: %s, lastName: %s ]", id, firstName, lastName);
}
}
EmployeeDao.java
package com.zfl9.dao;
import java.util.List;
import com.zfl9.model.Employee;
public interface EmployeeDao {
List<Employee> getAllEmployee();
}
EmployeeDaoImpl.java
package com.zfl9.dao;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.zfl9.model.Employee;
@Repository
public class EmployeeDaoImpl implements EmployeeDao {
@Override
public List<Employee> getAllEmployee() {
List<Employee> employees = new ArrayList<>(3);
Employee employeeA = new Employee();
employeeA.setId(1);
employeeA.setFirstName("张");
employeeA.setLastName("小明");
employees.add(employeeA);
Employee employeeB = new Employee();
employeeB.setId(2);
employeeB.setFirstName("王");
employeeB.setLastName("老五");
employees.add(employeeB);
Employee employeeC = new Employee();
employeeC.setId(3);
employeeC.setFirstName("李");
employeeC.setLastName("小胖");
employees.add(employeeC);
return employees;
}
}
EmployeeService.java
package com.zfl9.service;
import java.util.List;
import com.zfl9.model.Employee;
public interface EmployeeService {
List<Employee> listAllEmployee();
}
EmployeeServiceImpl.java
package com.zfl9.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.zfl9.dao.EmployeeDao;
import com.zfl9.model.Employee;
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeDao employeeDao;
@Override
public List<Employee> listAllEmployee() {
return employeeDao.getAllEmployee();
}
}
EmployeeController.java
package com.zfl9.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.zfl9.service.EmployeeService;
@Controller
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/listAll")
public String listAllEmployee(Model model) {
model.addAttribute("employees", employeeService.listAllEmployee());
return "Employee_ListAll";
}
}
Employee_ListAll.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>listAll</title>
</head>
<body>
<table border="1">
<tr>
<th>ID</td>
<th>First Name</td>
<th>Last Name</td>
</tr>
<core:forEach items="${employees}" var="employee">
<tr>
<td>${employee.id}</td>
<td>${employee.firstName}</td>
<td>${employee.lastName}</td>
</tr>
</core:forEach>
</table>
</body>
</html>
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>index.html</title>
</head>
<body>
<h1><a href="employee/listAll">listAllEmployee</a></h1>
</body>
</html>
测试结果:
简单解析:我们采用了文章开头的 web层、业务层、控制层这样的三层结构,其中 Service 和 Dao 层都采用了面向接口编程的方式,注意几个特殊的注解,我们在 Controller 类上使用了 @Controller 注解,在 ServiceImpl 类上使用了 @Service 注解,在 DaoImpl 类上使用了 @Repository 注解,然后我们再 ServiceImpl 类中使用 @Autowired 来自动装配 DaoImpl 实现类,同理,我们在 Controller 类中使用 @Autowired 来自动装配 ServiceImpl 实现类,所以我们的控制处理器方法就是直接调用 serviceImpl 的 getAllEmployee 方法,然后将得到的 list 存放到 request 域中。在 jsp 文件中,我们使用 jstl 的 forEach 标签来遍历 List 中的元素,打印一个表格,最终效果如上。
一个忠告:在 WEBAPP 中,尽量使用相对路径,不要使用绝对路径,不要使用绝对路径,不要使用绝对路径,就如同上面的 HTML 文件中的 employee/listAll 路径一样,相对路径是兼容性最好的,如果使用绝对路径,那么当你将 webapp 放到非 ROOT 目录时就会出现 404 错误,因为它不会自动加上 context 的路径前缀!!!
在这之前,我们先来学习几个常用的 数据绑定 相关的注解。
根据处理的 Request 的不同部分,将它们分为四类(常用的):
@PathVariable
;@RequestHeader
、@CookieValue
;@RequestParam
、@RequestBody
、@RequestPart
;@ModelAttribute
、@SessionAttribute
、@SessionAttributes
注意,html 表单的编码方式为 application/x-www-form-urlencoded
,可以使用 GET 和 POST 两种提交方式,编码方式是一样的,只不过 GET 方式是将 param 附着在 uri 的查询参数上,而 POST 则是作为请求体发送到服务端;但是无论如何,Servlet-API 解析 application/x-www-form-urlencoded
数据的方式是一致的,与 GET 还是 POST 无关。所以上面的分类其实还可以细分为 5 类,即处理 queryString 的注解为 @RequestParam,不过其实 @RequestParam 可以解析 quertString、get/post 提交的表单数据、multipart 文件上传等类型的请求数据,大家理解就行。
说到 @PathVariable 注解,就不得不先提一下 @RequestMapping(衍生的 @GetMapping、@PostMapping、@PutMapping、@PatchMapping、@DeleteMapping 同理,这个就不用多说了吧),@RequestMapping 里面有一个 path 属性,指定对应的处理器映射到的 uri 路径,这个 uri 有一个特殊模式。@RequestMapping 是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。@RequestMapping 有 6 个属性,我们先来详细了解 path/value 属性,分为 3 种:
/employees
:普通字符串,对应的 uri 为 /employees
;/employees/{id}
:包含变量定义的字符串,变量名为 id,匹配 uri 上对应的字符串;/employees/{id:\d{1,5}}
:包含变量定义的正则字符串,变量名为 id,匹配对应正则匹配的字符串。@PathVariable 注解关心的就是后两种 uri 类型,说是说两种,其实就是一种,包含变量的 path 而已,只不过我们可以接一个 :
然后写上 regex 匹配模式而已。id
就是匹配到的字符串的变量名,待会有用。
那么 @PathVariable 怎么使用呢?当然是用在方法参数上了,接收匹配到的字符串啦,注解有一个常用属性 value,表示对应参数要绑定到的 path variable 变量名,当参数名和变量名相同时可以省略,建议这么做。
@GetMapping("/employees/{id}")
@ResponseBody
public String testForPathVar(@PathVariable String id) {
return "request employee id is " + id;
}
@GetMapping("/students/{page:\\d++\\.\\w++}")
@ResponseBody
public String testForPathRegex(@PathVariable String page) {
return "request students page is " + page;
}
@RequestHeader 注解用来从 http request 中绑定对应的 header 头部值。@CookieValue 注解用来绑定 http request 中对应的 cookie 键值对,使用例子:
@GetMapping("/testForHeader")
@ResponseBody
public String testForRequestHeaderAndCookie(@RequestHeader("User-Agent") String userAgent, @CookieValue("SessionId") String sessionId) {
return "User-Agent: " + userAgent + "\nSessionId: " + sessionId + "\n";
}
$ curl localhost/testForHeader -H 'Cookie: SessionId=www.zfl9.com'
User-Agent: curl/7.41.0
SessionId: www.zfl9.com
@RequestParam 注解可以用来解析 query parameters(get), form data(post), and parts in multipart requests. 简单的说就是用来解析表单数据和文件上传相关的(解析文件上传需要一些额外的工作),我们暂且不关心 multipart 请求(即文件上传),@RequestParam 通常用来绑定简单类型的数据(或者说标量数据),即从 String 转换为目标类型,如 String、整数、小数、日期等。所以对于 MIME 为 application/json
、application/xml
的请求时(通常是 RESTful 请求),那么就不能使用 @RequestParam 来解析,而是要用 @RequestBody 来解析(很好理解,其实)。
@PostMapping("/uploadJson")
@ResponseBody
public String uploadJson(@RequestBody String json) {
return json;
}
$ curl -X POST -H 'Content-Type: application/json' -d $'{ "id": 0, "username": "root", "password": "www.zfl9.com" }\n' localhost/uploadJson
{ "id": 0, "username": "root", "password": "www.zfl9.com" }
@RequestParam 还有一个用法没有提到,如果对应的参数是 Map<String, String>
或 MultiValueMap<String, String>
,并且注解属性 value 没有指定,那么 Spring 会将所有 parameters 放到 map 中;如果指定了 value 属性,则只存放指定的那个 param,例子:
@ResponseBody
@PostMapping("/params")
public String params(@RequestParam Map<String, String> map) {
return map.toString();
}
$ curl -d 'name=Otoakze' -d 'site=www.zfl9.com' localhost/params
{name=Otoakze, site=www.zfl9.com}
@RequestPart 和 @RequestParam 都可以处理 multipart 请求(即文件上传),但是 @RequestPart 是专门为了处理 multipart 请求而生的,而 @RequestParam 可以处理表单数据、查询字符串、multipart,基本上两者没差别,可以互换,一般情况下,使用 @RequestParam
就行了,所以 @RequestPart
基本可忽略。
@ModelAttribute 可以注解控制器方法,也可以注解控制器方法的参数。
如果用在方法上,则这个方法会在所有 @RequestMapping 方法前执行,这个方法可以拥有与 @RequestMapping 相同的方法参数(自动注入),该方法如果返回一个对象,则这个对象会存储到 Model 作用域中(其实就是 request 作用域),name 默认是类名的首字母小写形式,当然也可以在注解上指定 name。
如果用在方法参数上,则 @ModelAttribute 会先解析 quertString、form-data 中的 params 参数,然后 new 一个参数对象,然后与 setter 方法同名的 param 将会被自动注入到该对象中,最后将这个对象存储到 model/request 作用域中。所以我们可以直接在 jsp 中使用 ${name.attr}
来访问被 @ModelAttribute 注解的参数对象。默认 name 的规则同上,也可手动指定。
@SessionAttributes 注解用来注释 Controller 类,用来将 model/request 作用域中的对象存储到 session 作用域中,可以指定多个 name,这些 name 对应的对象都会被存储到 session 作用域中。
@SessionAttribute 注解用来注释控制器方法的参数,用来将 session 作用域中的对象绑定到参数对象上。注意,@ModelAttribute 和 @SessionAttribute 都可以注解方法参数,而且都是将 request/session 作用域中的对象绑定到参数对象上,但是它们有一个区别,@ModelAttribute 是先从请求中读取参数,然后 new 出参数对象,然后使用 setter 注入 params,最后才是将对象存放到 request 作用域,但是 @SessionAttribute 仅仅是从 session 作用域中读取对应 name 的对象而已。不要搞错了。
@ModelAttribute 注解就不用多说了,已经演示过很多次,我们只要了解 @SessionAttritebute 和 @SessionAttributes 两个注解(虽然就差一个字符,但是区别还是很大的),@SessionAttributes 注解用在 Type 上,@SessionAttribute 注解用在 Parameter 上!前者是将 request 中的指定对象存储到 session 作用域,后者是从 session 作用域读取指定对象然后绑定到参数上。
不过我测试的时候 @SessionAttributes 注解貌似不能处理方法参数中的 @ModelAttribute 注解,只能识别方法级别上的 @ModelAttribute,不过官方 javadoc 貌似建议 HttpSession 对象好一点,暂时就这样吧。
再次说明:ModelAttribute 用来绑定 request 作用域上的对象,SessionAttribute 用来绑定 session 作用域上的对象,请牢记在 Spring MVC 中,Model 和 Request 基本上是同义词(这个词混淆度太高了)。
在不给定注解的情况下,参数是怎样绑定的?
@RequestParam
来处理的。 @ModelAttribute
来处理的。这里的简单类型指 ConversionService 可以直接 String 转换成目标对象的类型。如 int、String、Date。虽然可以省略注解,但是强烈建议加上注解,这样可读性强,也更不容易出错!!在此说明一下,Spring 很聪明,我们可以在处理方法的参数中放入很多与 Servlet 相关的参数,比如 HttpServletRequest、HttpServletResponse、HttpSession,Spring MVC 会自动注入合适的对象!
更新,在 Controller 方法中,可以使用 @Autowired 来自动装配 ServletContext 等组件,其实原理很简单,就是从 IoC 容器中注入而已。不过要注意,因为 Controller 是单例模式(默认就是这样,IoC 容器),所以成员变量中设置的 Autowired 属性应该是符合单例模式条件的,比如 ServletContext,这是安全的。
@RestController
注解等价于 @ResponseBody + @Controller
注解一起使用,将当前 Controller 作为 RESTful 服务时很有用,这样就不需要在每个 RESTful 方法中打上 @ResponseBody 注解了。在 Spring MVC 4.0 的时候引入的,方便用于 RESTful 的控制器:
@Controller
public class HelloController {
@ResponseBody
@GetMapping("/employees/{id}")
public Employee getEmployee(...) {
...
}
@ResponseBody
@DeleteMapping("/employees/{id}")
public Employee deleteEmployee(...) {
...
}
}
等价于:
@Controller
@ResponseBody
public class HelloController {
@GetMapping("/employees/{id}")
public Employee getEmployee(...) {
...
}
@DeleteMapping("/employees/{id}")
public Employee deleteEmployee(...) {
...
}
}
等价于:
@RestController
public class HelloController {
@GetMapping("/employees/{id}")
public Employee getEmployee(...) {
...
}
@DeleteMapping("/employees/{id}")
public Employee deleteEmployee(...) {
...
}
}
我们再来写一个简单程序,但是要让它实用一点,用到 MySQL 数据库,之前我们什么都没用到。所谓员工管理系统就是四个操作:CRUD,创建,读取,更新,删除,我们使用 Spring MVC 来实现它,巩固前面学的知识。
项目结构
├─java
│ └─com
│ └─zfl9
│ ├─controller
│ │ EmployeeController.java
│ │
│ ├─dao
│ │ EmployeeDao.java
│ │ EmployeeDaoImpl.java
│ │
│ ├─model
│ │ Employee.java
│ │
│ └─service
│ EmployeeService.java
│ EmployeeServiceImpl.java
│
└─webapp
└─WEB-INF
│ mvc.xml
│ web.xml
│
└─views
employee-edit.jsp
employee-list.jsp
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9</groupId>
<artifactId>SpringMVC_Learn</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>4.3.20.RELEASE</spring.version>
<mysql.version>8.0.13</mysql.version>
<servlet.version>3.1.0</servlet.version>
<jstl.version>1.2</jstl.version>
<jackson.version>2.9.7</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<!-- Object2Json 的库 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
</project>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zfl9"/>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/test?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
Employee
package com.zfl9.model;
public class Employee implements java.io.Serializable {
private static final long serialVersionUID = 6782234751084760161L;
private Integer id;
private String name;
private String email;
private String address;
private String telephone;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
@Override
public String toString() {
return String.format("Employee { id = %d, name = %s, email = %s, address = %s, telephone = %s }", id, name, email, address, telephone);
}
}
EmployeeDao
package com.zfl9.dao;
import java.util.List;
import com.zfl9.model.Employee;
public interface EmployeeDao {
Employee getEmployee(int id);
List<Employee> getAllEmployee();
void addEmployee(Employee employee);
void updateEmployee(Employee employee);
void deleteEmployee(int id);
void deleteAllEmployee();
}
EmployeeDaoImpl
package com.zfl9.dao;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import com.zfl9.model.Employee;
@Repository
public class EmployeeDaoImpl implements EmployeeDao {
@Autowired
private JdbcTemplate jdbcTemplate;
private RowMapper<Employee> rowMapper = (resultSet, rowNum) -> {
Employee employee = new Employee();
employee.setId(resultSet.getInt("id"));
employee.setName(resultSet.getString("name"));
employee.setEmail(resultSet.getString("email"));
employee.setAddress(resultSet.getString("address"));
employee.setTelephone(resultSet.getString("telephone"));
return employee;
};
@Override
public Employee getEmployee(int id) {
String sql = "select * from employee where id = ?";
return jdbcTemplate.queryForObject(sql, rowMapper, id);
}
@Override
public List<Employee> getAllEmployee() {
String sql = "select * from employee";
return jdbcTemplate.query(sql, rowMapper);
}
@Override
public void addEmployee(Employee employee) {
String sql = "insert into employee (name, email, address, telephone) values (?, ?, ?, ?)";
jdbcTemplate.update(sql, employee.getName(), employee.getEmail(), employee.getAddress(), employee.getTelephone());
}
@Override
public void updateEmployee(Employee employee) {
String sql = "update employee set name = ?, email = ?, address = ?, telephone = ? where id = ?";
jdbcTemplate.update(sql, employee.getName(), employee.getEmail(), employee.getAddress(), employee.getTelephone(), employee.getId());
}
@Override
public void deleteEmployee(int id) {
String sql = "delete from employee where id = ?";
jdbcTemplate.update(sql, id);
}
@Override
public void deleteAllEmployee() {
String sql = "truncate table employee";
jdbcTemplate.update(sql);
}
}
EmployeeService
package com.zfl9.service;
import java.util.List;
import com.zfl9.model.Employee;
public interface EmployeeService {
Employee getEmployee(int id);
List<Employee> getAllEmployee();
void addEmployee(Employee employee);
void updateEmployee(Employee employee);
void deleteEmployee(int id);
void deleteAllEmployee();
}
EmployeeServiceImpl
package com.zfl9.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.zfl9.dao.EmployeeDao;
import com.zfl9.model.Employee;
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeDao employeeDao;
@Override
public Employee getEmployee(int id) {
return employeeDao.getEmployee(id);
}
@Override
public List<Employee> getAllEmployee() {
return employeeDao.getAllEmployee();
}
@Override
public void addEmployee(Employee employee) {
employeeDao.addEmployee(employee);
}
@Override
public void updateEmployee(Employee employee) {
employeeDao.updateEmployee(employee);
}
@Override
public void deleteEmployee(int id) {
employeeDao.deleteEmployee(id);
}
@Override
public void deleteAllEmployee() {
employeeDao.deleteAllEmployee();
}
}
EmployeeController
package com.zfl9.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.zfl9.model.Employee;
import com.zfl9.service.EmployeeService;
@Controller
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/")
public String index() {
return "redirect:/employees";
}
@GetMapping("/employees")
public String listEmployee(Model model) {
model.addAttribute("employees", employeeService.getAllEmployee());
return "employee-list";
}
@GetMapping("/employees/new")
public String newEmployee(Model model) {
model.addAttribute("title", "Create Employee");
return "employee-edit";
}
@GetMapping("/employees/edit/{id}")
public String editEmployee(Model model, @PathVariable int id) {
model.addAttribute("title", "Update Employee");
model.addAttribute("employee", employeeService.getEmployee(id));
return "employee-edit";
}
@PostMapping("/employees/save")
public String saveEmployee(@ModelAttribute Employee employee) {
if (employee.getId() == null) {
employeeService.addEmployee(employee);
} else {
employeeService.updateEmployee(employee);
}
return "redirect:/employees";
}
@GetMapping("/employees/delete/{id}")
public String deleteEmployee(@PathVariable String id) {
if ("all".equals(id)) {
employeeService.deleteAllEmployee();
} else {
employeeService.deleteEmployee(Integer.valueOf(id));
}
return "redirect:/employees";
}
}
employee-list.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Employee Management System</title>
</head>
<body>
<div align="center">
<h1>Employee List</h1>
<table border="1">
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Address</th>
<th>Telephone</th>
<th>Action</th>
</tr>
<core:forEach items="${employees}" var="employee">
<tr>
<td>${employee.id}</td>
<td>${employee.name}</td>
<td>${employee.email}</td>
<td>${employee.address}</td>
<td>${employee.telephone}</td>
<td>
<a href="employees/edit/${employee.id}">Edit</a>
<a href="employees/delete/${employee.id}">Delete</a>
</td>
</tr>
</core:forEach>
</table>
<h3><a href="employees/new">Create Employee</a></h3>
<h3><a href="employees/delete/all">Delete All Employee</a></h3>
</div>
</body>
</html>
employee-edit.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${title}</title>
</head>
<body>
<div align="center">
<h1>${title}</h1>
<core:url var="save" value="/employees/save"/>
<form action="${save}" method="post">
<input type="hidden" name="id" value="${employee.id}">
<table>
<tr>
<td>Name:</td>
<td><input type="text" name="name" value="${employee.name}"></td>
</tr>
<tr>
<td>Email:</td>
<td><input type="text" name="email" value="${employee.email}"></td>
</tr>
<tr>
<td>Address:</td>
<td><input type="text" name="address" value="${employee.address}"></td>
</tr>
<tr>
<td>Telephone:</td>
<td><input type="text" name="telephone" value="${employee.telephone}"></td>
</tr>
</table>
<input type="submit" value="Save">
</form>
</div>
</body>
</html>
有两个值得注意的地方:
1、Employee 这个 Value Object 对象中,id 字段我用的是 Integer 类型而不是 int 类型,这是有原因的,主要问题在于 employee-edit.jsp 视图上面,里面有一个隐藏字段,即 id,设置的 value 是 ${employee.id}
,这个视图会有两个 Controller forword 过来,一个是 /employees/new,一个是 /employees/edit/id,其中只有 edit 有 employee 对象,new 是没有这个对象的,所以这个 value 会变成 ""
空字符串值,而当 save 这个处理器解析时,无法将空字符串转换为 int 类型(报错),而将它改为 Integer 类型就没问题了,空串默认转换为 null 空指针。
2、employee-edit.jsp 视图里面的 <core:url var="save" value="/employees/save"/>
元素,这个标签唯一的作用就是会根据上下文路径的不同,转换出正确的 /employees/save 路径,因为 new 和 edit 两个视图的 url 是不一样的,所以不能直接通过“相对路径”来完成,只能这样做,当然也可以使用 jstl 的 if 来做判断,但还是这种方法最方便。var 属性就是对应的变量名,value 就是 url 路径。
其实 Spring MVC 和 Struts2 一样,提供了自己的 taglib 库,方便 jsp 视图的开发(如自动转换为正确的 url 路径,加上 context-path 路径),待会我们会学习它,别急。
Spring MVC 提供两套标签库,一套是 spring,一套是 form,因为 spring 这套标签库不太常用也不太实用,所以本文重点讲述 spring 提供的表单 taglib 库。jsp 文件头声明如下,前缀一般设为 form:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
input 标签
例一:注意我们使用 path 变量来自动绑定 model 里面的数据(当然可以没有 command
这个 model)
<form:form action="formTag/form.do" method="post">
<table>
<tr>
<td>Name:</td><td><form:input path="name"/></td>
</tr>
<tr>
<td>Age:</td><td><form:input path="age"/></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="提交"/></td>
</tr>
</table>
</form:form>
如果 Model 中存在一个属性名称为 command
的 javaBean,而且该 javaBean 拥有属性 name 和 age 的时候,在渲染上面的代码时就会取 command 的对应属性值赋给对应标签的值。假设 Model 中存在一个属性名称为 command 的 javaBean,且它的 name 和 age 属性分别为 Zhangsan
和 36
时,生成的代码如下:
<form id="command" action="formTag/form.do" method="post">
<table>
<tr>
<td>Name:</td><td><input id="name" name="name" type="text" value="ZhangSan"/></td>
</tr>
<tr>
<td>Age:</td><td><input id="age" name="age" type="text" value="36"/></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="提交"/></td>
</tr>
</table>
</form>
form 标签会自动绑定 Model(其实就是 request)中的 command
属性(getAttribute("command")
),那么如果我们要绑定的对象不是 command 怎么办呢?对于这种情况,Spring 给我们提供了一个 commandName
属性,我们可以通过该属性来指定要绑定的对象名称,除了 commandName 属性外,modelAttribute
属性也可以达到相同的效果(这两个属性是等价的)。
<form:form action="formTag/form.do" method="post" commandName="user">
<table>
<tr>
<td>Name:</td><td><form:input path="name"/></td>
</tr>
<tr>
<td>Age:</td><td><form:input path="age"/></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="提交"/></td>
</tr>
</table>
</form:form>
除 input 标签外,支持所有 html form 里面的标签,举几个常用的,password
、hidden
、textarea
。
支持 get/post/put/patch/delete 方法(RESTful)
<form:form action="formTag/form.do" method="delete" modelAttribute="user">
<table>
<tr>
<td>Name:</td><td><form:input path="name"/></td>
</tr>
<tr>
<td>Age:</td><td><form:input path="age"/></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="提交"/></td>
</tr>
</table>
</form:form>
但是我们知道,html 的 form 明确指定了,只支持 get 和 post 两种请求方法,难道 spring form 还有什么黑科技?我们来看看生成的 html 是什么样子的:
<form id="user" action="formTag/form.do" method="post">
<input type="hidden" name="_method" value="delete"/>
<table>
<tr>
<td>Name:</td><td><input id="name" name="name" type="text" value="ZhangSan"/></td>
</tr>
<tr>
<td>Age:</td><td><input id="age" name="age" type="text" value="36"/></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="提交"/></td>
</tr>
</table>
</form>
原来只是多了一个 hidden
标签,name 为 _method
,value 为 delete
(请求方法名),另外 form 的 method 为 post,这不就是增加了一个请求属性吗,这有什么作用,又和其他 restful 方法有什么关联呢?
是不是按照上面这样做就能直接使用 PUT、PATCH、DELETE 方法呢?当然不是的,因为实际请求方法为 POST,所以 Spring 提供了一个 Filter,这个 Filter 会处理带有 _method
隐藏字段的请求,怎么处理呢?转换 HTTP 请求报文吗?并不是,Spring 采用了一个巧方法,使用一个 RequestWrapper 对象替换了原来的 Request 对象,并且在 RequestWrapper 对象的 getMethod() 方法中,重写了它,返回 _method
指定的方法名。所以在处理 Controller 处理器的时候,获取到的就是 PUT、PATCH、DELETE 这些方法了。
所以我们还需要配置 web.xml,添加一个 filter,用来处理这种情况:
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<servlet-name>springmvc</servlet-name>
</filter-mapping>
另外需要注意的是在有 Multipart 请求处理的时候,HiddenHttpMethodFilter 需要在 Multipart 处理之后执行,因为在处理 Multipart 时需要从 POST 请求体中获取参数。所以我们通常会在 HiddenHttpMethodFilter 之前设立一个 MultipartFilter。MultipartFilter 默认会去寻找一个名称为 filterMultipartResolver 的 MultipartResolver bean 对象来对当前的请求进行封装。所以当你定义的MultipartResolver的名称不为filterMultipartResolver 的时候就需要在定义 MultipartFilter 的时候通过参数 multipartResolverBeanName 来指定。
<filter>
<filter-name>multipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
<init-param>
<param-name>multipartResolverBeanName</param-name>
<param-value>multipartResolver</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>multipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
<init-param>
<param-name>methodParam</param-name>
<param-value>requestMethod</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
现在我们来使用 spring-form 标签改写 CRUD 例子中的 jsp 视图,如下:
EmployeeController.java
package com.zfl9.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.zfl9.model.Employee;
import com.zfl9.service.EmployeeService;
@Controller
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/")
public String index() {
return "redirect:/employees";
}
@GetMapping("/employees")
public String listEmployee(Model model) {
model.addAttribute("employees", employeeService.getAllEmployee());
return "employee-list";
}
@GetMapping("/employees/new")
public String newEmployee(Model model) {
model.addAttribute("title", "Create Employee");
model.addAttribute("employee", new Employee());
return "employee-edit";
}
@GetMapping("/employees/edit/{id}")
public String editEmployee(Model model, @PathVariable int id) {
model.addAttribute("title", "Update Employee");
model.addAttribute("employee", employeeService.getEmployee(id));
return "employee-edit";
}
@PostMapping("/employees/save")
public String saveEmployee(@ModelAttribute Employee employee) {
if (employee.getId() == null) {
employeeService.addEmployee(employee);
} else {
employeeService.updateEmployee(employee);
}
return "redirect:/employees";
}
@GetMapping("/employees/delete/{id}")
public String deleteEmployee(@PathVariable String id) {
if ("all".equals(id)) {
employeeService.deleteAllEmployee();
} else {
employeeService.deleteEmployee(Integer.valueOf(id));
}
return "redirect:/employees";
}
}
employee-edit.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${title}</title>
</head>
<body>
<div align="center">
<h1>${title}</h1>
<core:url var="saveAction" value="/employees/save"/>
<form:form action="${saveAction}" method="post" modelAttribute="employee">
<form:hidden path="id"/>
<table>
<tr>
<td>Name:</td><td><form:input path="name"/></td>
</tr>
<tr>
<td>Email:</td><td><form:input path="email"/></td>
</tr>
<tr>
<td>Address:</td><td><form:input path="address"/></td>
</tr>
<tr>
<td>Telephone:</td><td><form:input path="telephone"/></td>
</tr>
</table>
<input type="submit" value="Save">
</form:form>
</div>
</body>
</html>
不同于 EL,Spring 的 form 标签中的 model 属性必须存在,否则报 500 错误,而 EL 表达式则不会。
注意,其实我们可以不用 jstl 的 url 标签,也能输出 contextPath 的路径,那就是使用 EL 表达式:
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${title}</title>
</head>
<body>
<div align="center">
<h1>${title}</h1>
<form:form action="${pageContext.request.contextPath}/employees/save" method="post" modelAttribute="employee">
<form:hidden path="id"/>
<table>
<tr>
<td>Name:</td><td><form:input path="name"/></td>
</tr>
<tr>
<td>Email:</td><td><form:input path="email"/></td>
</tr>
<tr>
<td>Address:</td><td><form:input path="address"/></td>
</tr>
<tr>
<td>Telephone:</td><td><form:input path="telephone"/></td>
</tr>
</table>
<input type="submit" value="Save">
</form:form>
</div>
</body>
</html>
第一种方式是使用 JSON 工具将对象序列化成 json 字符串,常用工具有 Jackson,fastjson,gson。
第二种方式,在 mvc.xml 中配置 <mvc:annotation-driven/>
,添加 jackson-databind.jar 依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
借助 @ResponseBody,我们可以直接返回一个 Bean/Pojo 对象,Spring 会自动序列化为 json 字符串:
@ResponseBody
@GetMapping("/getJson")
public Employee getJson() {
Employee employee = new Employee();
employee.setId(1);
employee.setName("Otokaze");
employee.setEmail("zfl9.com@gmail.com");
employee.setAddress("xxx xxx xxx xxx");
employee.setTelephone("1234567890");
return employee;
}
$ curl localhost/getJson
{"id":1,"name":"Otokaze","email":"zfl9.com@gmail.com","address":"xxx xxx xxx xxx","telephone":"1234567890"}
Spring MVC 支持文件上传(multipart 请求),需要在 pom.xml 中引入 commons-fileupload
依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${fileupload.version}</version>
</dependency>
然后配置 mvc.xml,注册 MultiPart 请求的解析处理器,web.xml 配置如下:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- postMaxSize: 30M -->
<property name="maxUploadSize" value="31457280"/>
<!-- fileMaxSize: 10M -->
<property name="maxUploadSizePerFile" value="10485760"/>
</bean>
然后编写我们的上传表单,以及上传成功的消息页面:
fileUpload.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>fileUpload</title>
</head>
<body>
<div align="center">
<form action="fileUpload" method="post" enctype="multipart/form-data">
<input type="file" name="file" multiple>
<input type="submit" value="Upload">
</form>
</div>
</body>
</html>
uploadSuccess.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>uploadSuccess</title>
</head>
<body>
<div align="center">
<table border="1">
<tr>
<th>FileName</th>
<th>FileSize</th>
</tr>
<core:forEach items="${files}" var="file">
<tr>
<td>${file.originalFilename}</td>
<td>${file.size}</td>
</tr>
</core:forEach>
</table>
</div>
</body>
</html>
FileUploadController.java
package com.zfl9.controller;
import java.io.File;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
@Controller
public class FileUploadController {
@GetMapping("/fileUpload")
public String fileUploadForm() {
return "fileUpload";
}
@PostMapping("/fileUpload")
public String fileUploadSave(@RequestParam("file") MultipartFile[] files, HttpServletRequest request) throws IOException {
File saveDir = new File(request.getServletContext().getRealPath("/WEB-INF/files"));
if (!saveDir.exists()) {
saveDir.mkdirs();
}
for (MultipartFile file : files) {
String filename = file.getOriginalFilename();
if (filename == null || filename.isEmpty()) {
continue;
}
file.transferTo(new File(saveDir, filename));
}
request.setAttribute("files", files);
return "uploadSuccess";
}
}
在处理器方法中,我们可以返回 "redirect:/path/to/redirect"
来发送 302 临时重定向,其实还有一个特殊用法,那就是 "forward:/path/to/forward"
来触发 Servlet 的 forward 机制(注意它和直接返回 view 名的不同,forward 会触发 uri 对应的 controller 方法)。
forward 会保留 request 里面的对象,因为这只是发生在 servlet context 内部,对外部是透明的,所以浏览器地址栏也不会有变化。而 redirect 是发送 HTTP 协议定义的重定向响应,浏览器会发起新的请求。
但是,在实际应用中,我们常常有这样一个需求,我需要发送 redirect 重定向给浏览器,但是我又希望能够传递数据给重定向后的处理器方法。该怎么做呢?其实 Spring MVC 提供了相应的解决办法。不过在这之前,我们先自己思考一下,如果要我们自己来实现,该如何做?
因为 redirect 是 HTTP 协议层面的事情,浏览器收到 redirect 响应后,会发起一个全新的 HTTP 请求到目标服务器,所以,如果要携带数据,只能将数据作为 queryString 加到 url 中来传递。但是又因为 queryString 会直接暴露在浏览器地址栏,不安全,所以这种方式不太建议。
虽然不建议使用,不过难免会有用到的时候,我们来看下如何实现这种方式的 redirect 数据携带:
RedirectAttributes 接口提供两个常用的传值方式:
addAttribute(name, value)
addFlashAttribute(name, value)
其中,addAttribute 是利用普通的 url 传参方式进行传递,而 addFlashAttribute 不同于前者,它是将 value 存放到 session 中,然后我们可以在目标处理器方法上,使用 @ModelAttribute
来读取这个 value,从而完成优雅的数据传递。注意,之所以称为 flash attribute,是因为这些 value 在使用 @ModelAttribute 接收后就会被删除,即闪存的本意。
纠错:addFlashAttribute 方法会将属性暂存到 session 中,我们可以在目标方法上通过 Model、ModelMap、@ModelAttribute 等方式来接收,本质都是从 request 作用域 中读取 flash 属性,注意,这些属性只在 redirect 后的第一次请求中有效,只要你刷新一下页面,属性就没了,这就是易失属性的特性,也是其名字的由来。
测试例子:
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class ForTestController {
@GetMapping("/test01")
public String test01(RedirectAttributes redirectAttributes) {
redirectAttributes.addAttribute("name1", "value1");
redirectAttributes.addAttribute("name2", "value2");
redirectAttributes.addAttribute("name3", "value3");
return "redirect:https://www.zfl9.com";
}
@GetMapping("/test02")
public String test02(RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("name1", "value1");
redirectAttributes.addFlashAttribute("name2", "value2");
redirectAttributes.addFlashAttribute("name3", "value3");
return "redirect:/test03";
}
@ResponseBody
@GetMapping("/test03")
public String test03(Model model) {
return model.toString();
}
}
测试结果:
# Otokaze @ Otokaze-Win10 in ~ [14:54:45]
$ \curl -i localhost/test01
HTTP/1.1 302
Location: https://www.zfl9.com?name1=value1&name2=value2&name3=value3
Content-Language: zh-CN
Content-Length: 0
Date: Wed, 12 Dec 2018 07:01:39 GMT
# Otokaze @ Otokaze-Win10 in ~ [15:01:39]
$ \curl -i localhost/test02
HTTP/1.1 302
Set-Cookie: SessionID=D9A909AB34202893DD182A602E14F16C; Path=/; HttpOnly
Location: /test03;SessionID=D9A909AB34202893DD182A602E14F16C
Content-Language: zh-CN
Content-Length: 0
Date: Wed, 12 Dec 2018 07:01:44 GMT
# Otokaze @ Otokaze-Win10 in ~ [15:01:44]
$ \curl -iL localhost/test02
HTTP/1.1 302
Set-Cookie: SessionID=1C1616E83C1A34209AA5FA93BBB39CAE; Path=/; HttpOnly
Location: /test03;SessionID=1C1616E83C1A34209AA5FA93BBB39CAE
Content-Language: zh-CN
Content-Length: 0
Date: Wed, 12 Dec 2018 07:01:49 GMT
HTTP/1.1 200
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 42
Date: Wed, 12 Dec 2018 07:01:49 GMT
{name3=value3, name2=value2, name1=value1}
注意,如果使用浏览器测试,是没有 ;SessionID
的,因为浏览器支持 Cookie。另外,如果你被重定向到 test03 页面后,刷新一下浏览器,会发现数据是空的,说明确实是 flash attribute。另外,通过这种方式传递过来的数据只能通过 Model、ModelMap、@ModelAttribute 来读取!
Spring MVC 和 Struts2 一样,支持“拦截器”概念,拦截器的英文是:Interceptor。Interceptor 和 Filter 很相似,都可以用来定义 预处理、后处理 操作,不同的是,Filter 的实现原理是 函数调用链,而 Interceptor 的实现原理是 JDK 动态代理,虽然实现原理不同,但是它们之间却有很多相似的地方,甚至我们可以说,拦截器就是 SpringMVC/Struts2 提供的“过滤器”在框架中的实现!Filter 和 Interceptor 都可以有多个,它们之间的执行顺序由 web.xml、mvc.xml 的出现顺序定义,并且先定义的 Filter/Interceptor 的预处理方法先执行,而后处理方法则与定义顺序相反,先定义的后执行,后定义的先执行。可以说,除了实现原理不同,其他的特征都是一样的。
在 Spring MVC 中,如果要定义拦截器,有两种常见方式:
preHandle()
、postHandle()
和 afterCompletion()
,分别表示:在 Controller 方法之前处理、在 Controller 方法之后处理,在 View 视图返回后处理(请求完成后)。我们先来看看 HandlerInterceptor 接口的三个方法定义:
/* 预处理方法,在 Controller 方法调用前执行 */
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
/* 后处理方法,在 Controller 方法返回后执行 */
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
/* 最终处理方法,在 View 视图渲染完成后执行 */
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
注意 preHandle 方法,它返回的是一个 boolean 值,其他方法没有返回值。这个 boolean 值的意义是这样的,如果返回 true,则继续调用下一个拦截器的 preHandle 方法,或者调用 Controller 的处理方法(如果是最后一个拦截器的话),如果返回 false,则表示当前请求就到此为止了,preHandle 方法会返回一个响应结果给请求客户端(一般是检测到异常或者权限不足或者是其他请求时,会这么做)。
来实现一个简单的日志记录拦截器,分别在 preHandle、postHandle、afterCompletion 位置记录日志:
package com.zfl9.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class LogInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("[pre handle] handler: " + handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("[post handle] handler: " + handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("[after completion] handler: " + handler);
}
}
这个 handler 对象其实是 org.springframework.web.method.HandlerMethod 的一个实例,打印出来的结果就是 Controller 方法的签名而已,一般情况下没什么很大的作用。接下来我们来看看如何配置拦截器。很显然,因为拦截器的执行顺序与定义顺序有关系,所以只能使用 mvc.xml 配置文件来配置(不过好像也可以使用 Java-Based 形式来配置),拦截器配置在 <mvc:interceptors>
元素中,如下:
<mvc:interceptors>
<bean class="com.zfl9.interceptor.LogInterceptor"/>
</mvc:interceptors>
放在 interceptors 元素下的拦截器,会匹配所有请求。如果只想拦截指定路径下的请求,可以这么做:
<mvc:interceptors>
<!-- 拦截所有请求 -->
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
<!-- 拦截 /secure/ 的请求(不包括子目录) -->
<mvc:interceptor>
<mvc:mapping path="/secure/*"/>
<bean class="org.example.SecurityInterceptor"/>
</mvc:interceptor>
<!-- 拦截除 /admin/ 下的所有请求(包括子目录) -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/admin/**"/>
<bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
</mvc:interceptor>
<mvc:interceptors>
拦截器的执行顺序与他们在 xml 中定义的先后顺序相同,先定义的先执行,这一点和 Filter 是一样的。
输出结果如下:
[pre handle] handler: public java.lang.String com.zfl9.controller.EmployeeController.index()
[post handle] handler: public java.lang.String com.zfl9.controller.EmployeeController.index()
[after completion] handler: public java.lang.String com.zfl9.controller.EmployeeController.index()
[pre handle] handler: public java.lang.String com.zfl9.controller.EmployeeController.listEmployee(org.springframework.ui.Model)
[post handle] handler: public java.lang.String com.zfl9.controller.EmployeeController.listEmployee(org.springframework.ui.Model)
[after completion] handler: public java.lang.String com.zfl9.controller.EmployeeController.listEmployee(org.springframework.ui.Model)
所谓数据校验就是对客户端提供的数据进行有效性校验,有两个作用,一是防止无效数据或非法数据存储到我们的系统中,二是避免攻击者利用非法数据攻击我们的系统,所以数据校验的重要性不言而喻。通常我们的 Web 应用会进行两重校验,首先是浏览器端的 JavaScript 校验,然后是后台系统(controller)的数据校验。你可能会问,服务端的校验是不是多余的呢?其实不是的,虽然 JavaScript 校验能够避免大部分无效数据传入后台系统,但是浏览器的 JS 是可以被禁用的,而且非法分子可能直接利用 curl 等客户端来绕开 JS 校验,所以服务端上的校验也是必不可少的。
Spring MVC 提供了一个简单实用的机制来帮助我们校验提交过来的数据,Spring MVC Framework 默认支持 JSR-303 规范,我们只需要在 Spring MVC 应用程序中添加 JSR-303 规范接口及其 JSR-303 实现类依赖项就行(JSR-303 只是定义了一些规范,提供对应的注解,但是并没有提供对应的实现,hibernate validation 提供了 JSR-303 的实现,并且还提供了一些额外的校验注解)。Spring MVC 还提供了 @Validator
注解和 BindingResult
类,通过 BindingResult,我们可以在请求处理方法中获取 Validator 实现引发的错误。
对于任何一个应用而言在客户端做的数据有效性验证都不是安全有效的,这时候就要求我们在开发的时候在服务端也对数据的有效性进行验证。Spring MVC 自身对数据在服务端的校验有一个比较好的支持,它能将我们提交到服务端的数据按照我们事先的约定进行数据有效性验证,对于不合格的数据信息 Spring MVC 会把它保存在错误对象中,这些错误信息我们也可以通过 Spring MVC 提供的标签在前端 JSP 页面上进行展示。
Spring MVC 提供两种数据校验方式,一种是基于 Validator 接口,另一种是使用 JSR-303 注解。Validator 接口需要我们自己去实现,而 JSR-303 注解是可以开箱即用的(添加对应依赖即可)。
基于 Validator 接口进行数据验证
Validator 接口是 Spring 提供的,方便我们定义验证类来对实体类进行数据验证。假设我们存在这样一个 User 类,我们需要对其中的 username 和 password 字段进行验证,避免非法数据进入我们的系统:
package com.zfl9.model;
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
那么当我们需要使用 Spring MVC 提供的 Validator 接口来对该实体类进行校验的时候该如何做呢?这个时候我们应该提供一个 Validator 实现类,实现 Validator 接口的 supports() 方法和 validate() 方法。supports() 方法用于判断当前的 Validator 实现类是否支持校验对应的实体类,只有当 supports() 方法的返回结果为 true 的时候,该 Validator 接口实现类的 validate() 方法才会被调用,来对当前需要校验的实体类进行校验。
首先创建一个 package,存储我们的 Validator 验证器,然后编写 User 类对应的 UserValidator 类:
package com.zfl9.validator;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.zfl9.model.User;
@Component
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> aClass) {
// 如果目标类为 User 类或其子类则返回 true
return User.class.isAssignableFrom(aClass);
}
@Override
public void validate(Object o, Errors errors) {
// 验证 username 字段 (使用 ValidationUtils 实用类)
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", null, "username is empty");
// 验证 password 字段 (手动进行校验,使用 errors 对象)
User user = (User) o;
if (user.getPassword() == null || user.getPassword().matches("^\\s*+$")) {
errors.rejectValue("password", null, "password is empty");
}
}
}
注意我在 validate 方法中使用了两种不同的方式来进行字段的验证,这样做的目的仅仅是为了举例子而已。
我们已经定义了一个对 User 类进行校验的 UserValidator 了,但是这个时候 UserValidator 还不能对 User 对象进行校验,因为我们还没有告诉 Spring 应该使用 UserValidator 来校验 User 对象。在 SpringMVC 中我们可以使用 DataBinder 来设定当前 Controller 需要使用的 Validator。对应的方法需要使用 @InitBinder
注解进行标注,如下:
package com.zfl9.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.zfl9.model.User;
@Controller
public class UserController {
@Autowired
@Qualifier("userValidator")
private Validator userValidator;
@InitBinder
public void initBinder(DataBinder databinder) {
databinder.setValidator(userValidator);
}
@GetMapping("/login")
public String loginForm(Model model) {
if (!model.containsAttribute("user")) {
model.addAttribute("user", new User());
}
return "login-form";
}
@PostMapping("/login")
public String loginProc(@Validated @ModelAttribute User user, BindingResult result, RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
redirectAttributes.addFlashAttribute("user", user);
redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.user", result);
return "redirect:/login";
}
return "login-success";
}
}
@Validated
注解的对象会被 Spring MVC 执行数据校验,@Validated
注解和 BindingResult
是成对出现的,如果有多个对象需要被校验,请成对放在 Controller 方法的参数列表中。BindingResult 是对应对象的校验结果,我们可以通过它的 hasErrors()
方法来判断是否校验成功,如果出现校验不正确的情况,则返回 true,因为我们是使用 redirect 来重定向到 login 登录表单,所以需要使用 RedirectAttribtues 的 flashAttribute 来添加易失属性,然后 login 表单才会正确显示 model 对象和 error 对象(使用 spring 的 form 标签库),如果不是 redirect,那么是不需要添加什么 flashAttribute 的,直接 return "login-failure"
就行了。注意错误对象的 name,最后一个字符串 user
是对应的 model 对象的名称(BindingResult 是 Errors 的子类,所以可以使用 result 对象替代 errors 对象)。
注意,你可能会从别的教程中看到,使用 @Valid
注解替代 @Validated
注解的情况,不要惊讶,@Valid
注解是 JSR-303 中定义的,而 @Validated
注解是 Spring 定义的,后者对前者进行了扩展,后者支持分组校验的特性,而前者不支持,它们基本上是可以互换的。之所以我没有使用 @Valid
注解,是因为我没有添加 JSR-303 的依赖项,所以就使用 @validated
注解了。
我们知道在 Controller类中通过 @InitBinder
标记的方法只有在请求当前 Controller 的时候才会被执行,所以其中定义的 Validator 也只能在当前 Controller 中使用,如果我们希望一个 Validator 对所有的 Controller 都起作用的话,我们可以在 SpringMVC 的配置文件中通过 mvc:annotation-driven
的 validator 属性指定全局的 Validator。代码如下所示:
<mvc:annotation-driven validator="userValidator"/>
<bean id="userValidator" class="com.xxx.xxx.UserValidator"/>
那么我们该如何在 JSP 页面中展示验证错误的信息呢?很简单,Spring 提供了一个 form 标签库,里面有一个 errors 标签,该标签会读取我们上面指定的 errors/result 对象,该标签有一个 path 属性,属性值如果为 *
则用来显示所有的错误信息,如果是对应的字段名,则显示对应的字段的错误信息。
login-form.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<core:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>login-form</title>
</head>
<body>
<div align="center">
<form:form action="${ctx}/login" methpd="post" modelAttribute="user">
<table>
<tr>
<td>Username:</td>
<td><form:input path="username"/></td>
<td><form:errors path="username" cssStyle="color:red"/></td>
</tr>
<tr>
<td>Password:</td>
<td><form:password path="password"/></td>
<td><form:errors path="password" cssStyle="color:red"/></td>
</tr>
</table>
<input type="submit" value="Login">
</form:form>
</div>
</body>
</html>
login-success.jsp
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<core:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>login-success</title>
</head>
<body>
<div align="center">
<h1>Welcome: ${user.username}</h1>
<table>
<tr>
<td>Username: </td>
<td>${user.username}</td>
</tr>
<tr>
<td>Password: </td>
<td>${user.password}</td>
</tr>
</table>
</div>
</body>
</html>
基于 JSR-303 Validation 的数据验证
JSR-303 是一个数据验证的规范,JSR-303 只是一个规范,而 Spring 也没有对这一规范进行实现,那么当我们在 SpringMVC 中需要使用到 JSR-303 的时候就需要我们提供一个对 JSR-303 规范的实现,Hibernate Validator 是实现了这一规范的,这里将它作为 JSR-303 的实现来讲解 SpringMVC 对 JSR-303 的支持。
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${validatorapi.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${validatorimpl.version}</version>
</dependency>
JSR-303 的校验是基于注解的,它内部已经定义好了一系列的验证注解,我们只需要把这些注解标记在需要验证的实体类的属性上或是其对应的 getter 方法上。看下需要验证的实体类 User 的代码(一般用在属性上):
package com.zfl9.model;
import javax.validation.constraints.NotBlank;
public class User {
@NotBlank(message = "username is empty")
private String username;
@NotBlank(message = "password is empty")
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
其中 message 是错误提示,然后去掉 UserController.java 里面的 @InitBinder 方法,结果是一样的:
package com.zfl9.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.zfl9.model.User;
@Controller
public class UserController {
@GetMapping("/login")
public String loginForm(Model model) {
if (!model.containsAttribute("user")) {
model.addAttribute("user", new User());
}
return "login-form";
}
@PostMapping("/login")
public String loginProc(@Validated @ModelAttribute User user, BindingResult result, RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
redirectAttributes.addFlashAttribute("user", user);
redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.user", result);
return "redirect:/login";
}
return "login-success";
}
}
常用注解有这些:
@Null
:必须为 null@NotNull
:必须不为 null@AssertTrue
:必须为 true@AssertFalse
:必须为 false@Min
:最小值为 value(数值、数值字符串)@Max
:最大值为 value(数值、数值字符串)@Range
:数值大小必须在指定范围内(数值、数值字符串)@Size
:字段长度必须在指定范围内(字符串、数组、集合)@Past
:被注释的元素必须是一个过去的日期@Future
:被注释的元素必须是一个将来的日期@Email
:被注释的字符串必须是有效的电子邮箱地址 @NotEmpty
:字段的的长度不能为零(字符串、数组、集合)@NotBlank
:字符串的长度不能为零(trim()
后的字符串)@Length
:字符串的长度必须在指定范围@Pattern
:字符串必须被正则表达式匹配自定义数据验证的注解
除了 JSR-303 原生支持的验证注解外,我们也可以定义自己的验证注解(并且用法完全一致)。定义自己的验证注解有两个步骤,第一步是定义一个注解,第二步是定义一个 ConstraintValidator 的实现类。注解和注解处理类,它们是一对的,单单定义一个注解是不行的,因为注解仅仅是存放了元数据,我们必须定义一个注解处理程序,而数据验证注解的处理程序就是一个实现了 javax.validation.ConstraintValidator
接口的类。
定义一个数据验证注解,@Username
,规定 username 的正确格式:
package com.zfl9.constraint;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
@Constraint(validatedBy = UsernameValidator.class)
public @interface Username {
String message() default "invalid username";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
定义一个数据验证注解,@Password
,规定 password 的正确格式:
package com.zfl9.constraint;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
@Constraint(validatedBy = PasswordValidator.class)
public @interface Password {
String message() default "invalid password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
注意 @Constraint(validatedBy = UsernameValidator.class)
元注解,它用来指定处理当前验证注解的实现类,然后就是 message 属性,我们为它设置一个默认的错误提示信息,其它两个属性我们可以暂时不管。注意,无论何时,Constraint 注解的元素必须有上面三个,即 message、groups、payload。除此之外我们还可以定义其他属性,比如 value、min、max、pattern 等等。如果我们在属性上设置了默认值,而又想在实现类上引用它,直接在 initialize 方法中使用 annotationObj.value()
方法获取就行(其他的同理)。
UsernameValidator.java
package com.zfl9.constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class UsernameValidator implements ConstraintValidator<Username, String> {
@Override
public void initialize(Username constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
return value != null && value.matches("^\\w++$");
}
}
PasswordValidator.java
package com.zfl9.constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class PasswordValidator implements ConstraintValidator<Password, String> {
@Override
public void initialize(Password constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
return value != null && value.matches("^[0-9a-zA-Z._-]++$");
}
}
Controller 和 View 都不用动,我们重载一下应用程序,测试 username 和 password 的验证是否正常。
Spring 的 @Validated 注解的分组验证
所谓分组验证就是,有些时候,我们需要对同一个实体类进行多种验证,比如 id 字段,创建时是不需要验证的,默认为 null,而更新时则是需要验证的,不能为 null。那么我们该怎么办呢?别慌,我们前面说了,JSR 自带的 @Valid
不支持分组验证功能,但是 Spring 提供的 @Validated
注解扩展了 JSR 的注解,支持分组验证功能。所以一般情况下,我们使用 @Validated
注解会比使用 @Valid
注解更好一些。
分组验证听起来很复杂,其实不然,很简单,只是定义两个接口而已,它们都是空接口(标记接口),我们会利用它们的 Class 对象来进行分组,一般情况下,我们会把这些注解放到实体类内部,即作为静态内部接口。
Student.java
package com.zfl9.model;
import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
public class Student {
public interface Create {}
public interface Update {}
@NotNull(message = "invalid id", groups = Update.class)
@Min(value = 1, message = "invalid id", groups = Update.class)
private Integer id;
@NotNull(message = "invalid name", groups = {Create.class, Update.class})
@Pattern(regexp = "^\\w++$", message = "invalid name", groups = {Create.class, Update.class})
private String name;
@NotNull(message = "invalid email", groups = {Create.class, Update.class})
@Email(message = "invalid email", groups = {Create.class, Update.class})
private String email;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
StudentController.java
package com.zfl9.controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.zfl9.model.Student;
@RestController
public class StudentController {
@PostMapping("/students")
public String create(@Validated(Student.Create.class) @RequestBody Student student, BindingResult result) {
if (result.hasErrors()) return "failure";
return "success";
}
@PutMapping("/students")
public String update(@Validated(Student.Update.class) @RequestBody Student student, BindingResult result) {
if (result.hasErrors()) return "failure";
return "success";
}
}
测试结果
# Otokaze @ Otokaze-Win10 in ~ [19:58:55]
$ curl localhost/students -X POST -H 'Content-Type: application/json' -d '{ "id": 0, "name": "Otokaze", "email": "root@zfl9.com" }'
success
# Otokaze @ Otokaze-Win10 in ~ [20:00:04]
$ curl localhost/students -X PUT -H 'Content-Type: application/json' -d '{ "id": 0, "name": "Otokaze", "email": "root@zfl9.com" }'
failure
# Otokaze @ Otokaze-Win10 in ~ [20:00:09]
$ curl localhost/students -X PUT -H 'Content-Type: application/json' -d '{ "id": 11, "name": "Otokaze", "email": "root@zfl9.com" }'
success
web 应用的三层结构
被这些注解标注的类会被 Spring 的 IoC 容器实例化,放到 bean 容器中进行管理。我们可以使用 @Autowired
、@Resource
注解来自动装配这些 bean。目的是为了“控制反转”,降低类与类之间的依赖度。
JSP 页面中的绝对 url
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<core:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<div align="center">
<h3><a href="${ctx}/">ctx</a></h3>
<h3><a href="${ctx}/test">test</a></h3>
<h3><a href="${ctx}/dir/test">dir/test</a></h3>
<h3><a href="${ctx}/dir/dir/test">dir/dir/test</a></h3>
</div>
</body>
</html>
建议将开头三行代码放入 jsp 的文件模版中,这样就不用每次编写 jsp 文件都重复编写这些内容了。
Model、ModelMap、ModelAndView
虽然它们都叫做 Model*,但其实我们可以认为它们都是 request 对象的封装类,ModelAndView 是最原始的传值方式(这里说的传值就是传统意义上的 request.setAttribute()),基本上现在已经可以不用 ModelAndView 对象了。通常,我们都是使用 Model 或者 ModelMap 对象,这三个对象都可以用来向 view 页面传递 request 属性(注意,Model* 对象不同于 HttpServletRequest 对象!!!)。
Model 是一个接口,定义了一些 addAttribute() 方法,而 ModelMap 则是 LinkedHashMap 的子类,ModelMap 暴露的方法和 Model 是一样的,基本上没区别,可以根据自己的喜好,使用任意一个对象来进行 request 传值,我个人的话,比较喜欢使用 Model 对象。
注意,虽然 ModelAndView 是最原始的传值方式,但是在 Spring MVC 实现层面,Model 和 ModelMap 依旧会被封装成 ModelAndView 对象来进行处理,我们可以认为 Model/ModelMap 是 ModelAndView 的封装。
@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@PatchMapping、@DeleteMapping
这些注解都是用来将 Controller 方法映射到指定 url 的,最开始只有 @RequestMapping,不过后来为了方便开发 RESTful 风格的服务,Spring 又提供了 @RequestMapping 的 GET、POST、PUT、PATCH、DELETE 方法的特定注解,这些注解除了没有 method 属性,其他的特征与 @RequestMapping 注解是完全一样的(当然还有一点区别就是,@RequestMapping 可以用在 Controller 类上,而 @GetMapping、@PostMapping、@PutMapping、@PatchMapping、@DeleteMapping 这些只能用来标注 Controller 方法)。
@RequestMapping 如果用在 Controller 类上,则该 Controller 中的所有方法映射到的 uri 都是以类上的 uri 为上下文的(即父路径),这在做 RESTful API 服务的时候很有用。如果不指定 value/path 属性,则默认为 ""
空字符串。比如 Controller 上使用 @RequestMapping("/employees")
标注,而 Get 方法上使用 @GetMapping
标注,则表示该 Get 方法的 url 为 /employees
,没有 /
分隔符哦。
有必要强调一下,@RequestMapping 可以用在 Controller 类上,此时表示,该 Controller 类中的所有处理方法都将继承该 @RequestMapping 上的属性值(所有属性都是这样,方法上的注解会继承这些属性值)。
@RequestMapping 的 6 个属性:
请求路径、请求方法
?
、*
、**
等 Ant 通配符。支持多个 uri。请求参数、请求头部
name=value
、name!=value
、name
、!name
字符串模式header=value
、header!=value
、header
、!header
字符串模式提交的 MIME 类型、期望的 MIME 类型:
text/html
、!text/plain
、!application/*
字符串模式text/html
、!text/plain
、!application/*
字符串模式注意 produces 属性,该属性还有一个副作用,那就是它会将匹配到的 MIME 类型写入到 response 头中,组合起来,该属性的作用就是:只会响应与 request 的 Accept 头部相匹配的 MIME 请求,并且还会修改 response 中的 Content-Type 头部,将其设为当前生效的 Content-Type。什么意思呢?举个栗子:
@ResponseBody
@GetMapping(path = "/test", produces = {"text/html; charset=UTF-8", "application/json; charset=UTF-8"})
public String test() {
return "hello, world\n世界,你好\n";
}
PS C:\Windows\system32> curl.exe localhost/test -H 'Accept: text/*' -i
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 29
Date: Fri, 14 Dec 2018 11:40:15 GMT
hello, world
世界,你好
PS C:\Windows\system32> curl.exe localhost/test -H 'Accept: application/*' -i
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 29
Date: Fri, 14 Dec 2018 11:40:18 GMT
hello, world
世界,你好
如果不指定 charset=UTF-8 编码,那么默认会变为 Latin-1 编码,虽然我们设置了 CharsetFilter!
@RequestParam、@RequestBody、@RequestPart,解析请求数据
application/x-www-form-urlencoded
请求,表单数据(含 url 参数)application/json
、application/xml
等请求,在 RESTful API 中常用multipart/form-data
请求(文件上传),@RequestParam
也支持这种请求@RequestParam 注解的参数应该为基本类型、基本类型的包装类、String、Date 等简单类型,或者叫原语。而 @RequestPart 注解的参数应该为一个 bean/pojo 对象,spring 会自动根据 bean/pojo 对象的 setter 方法和 json 的同名字段来进行 json 到 object 之间的转换(object 到 json 的转换原理也是类似的)。
虽然 @RequestParam 也支持 multipart/form-data 请求,但尽量使用 @RequestPart 来替代,符合语意。对于 @RequestParam 注解,如果对应的方法参数为 Map 类型,则所有的请求参数都会存入到这个 Map 中。@RequestPart 和 @RequestParam 如果是用来处理 multipart 请求,则参数类型一般为 MultipartFile。
@PathVariable、@RequestHeader、@CookieValue,解析请求元数据
{id}
、{id:\w+}
变量@ModelAttribute、@RequestAttribute、@SessionAttribute、@SessionAttributes
@RequestAttribute 和 @SessionAttribute 很好理解,就是字面意思,将 request 和 session 作用域中的指定 attribute 绑定到被注解的方法参数中,如果没有找到对应的则会报错,当然可以将它们的 required 属性设为 false 来避免这种情况。
@SessionAttributes 注解是用在 Controller 类上,用来同步 Model、ModelMap、ModelAndView 中设置的 model 属性,同步到 session 作用域,所以我们可以在 jsp 页面中,通过 request 和 session 都能访问这些 attribute 对象。
@ModelAttribute 的工作原理是这样的,首先它会对参数对象执行 new 操作,创建一个对象出来,然后查找 request params(@RequestParam)中的与 setter 方法同名的 param,然后将其注入到 setter 方法,最后,@ModelAttribute 注解还会将这个 bean/pojo 保存到 Model* 中,而 Spring MVC 会自动将 Model* 中的数据同步到 httpServletRequest 对象中,所以可以在 jsp 中通过 request 作用域访问这些对象。
@ModelAttribute 用在方法上时(实际是用在方法返回值上),这个方法会在所有 @RequestMapping 方法之前执行(包括 Get/Post/Put/Patch/Delete 子注解),并且这个方法可以有 @RequestMapping 中的所有参数类型(Spring 会自动注入),作用是将方法返回值存入到 Model* 作用域中,因为会在所有请求处理方法之前执行,所以在这里处理一些所有方法共享的 Model 对象是一个最佳实践。
@ModelAttribute、@RequestBody 注解的 bean/pojo 类,里面如果有 short/int/long 等基本类型,请改为对应的 Short/Integer/Long 包装类,否则,如果 request params 中的 param 的 value 为空字符串,那么 Spring MVC 会报告转换错误,因为空字符串无法转换为 short/int/long 等类型,而将它们改为对应的包装类后,传递空字符串的 value 和不传递这个 param 的效果是一样的,即对应的字段会被设为 null 值。但是要注意,如果字段类型为 String,那么传递空字符串就是空字符串,不传递的时候才会被设为 null 值。
不管是在 dao 层、service 层、controller 层,都有可能抛出异常,默认情况下,Spring MVC 会给客户端返回一个 500 响应,并且附带一个错误页面(通常是异常对象的堆栈跟踪信息),在开发环境中,这种默认处理方式或许还能够接受,因为堆栈跟踪信息中通常有很多有用的信息,帮助我们排错。但是在实际生产环境中,如果用户收到这样一个丑陋难懂的错误页,大概都会觉得系统很 low,而且这些堆栈跟踪信息还可能被攻击者研究,然后入侵我们的系统。
所以我们很有必要学习一下 Spring MVC 中的异常处理机制,Spring 提供三种异常处理方式,分别是:
Class<? extends Throwable>[]
,表示当前异常处理方法只会处理里面列出的异常类型,如果留空,那么该处理方法会处理方法参数中出现的异常类型。同时,我们还可以使用 @ResponseStatus
来注释异常处理方法,此时异常处理方法的方法体应该为空,因为此时表示对应的异常将会被转换为指定的 status 和 reason,所以方法体没有意义,应该留空。异常处理方法的参数签名中可以有异常对象、HttpServletRequest/HttpServletResponse 对象、HttpSession 对象、Model 对象;异常处理方法的返回值可以是:void、String、Model、ModelAndView、HttpEntity、ResponseEntity;注意,参数中的 Model 没有任何预填充的属性,它的作用仅仅是用来传递属性给异常处理页面。而 String、ModelAndView 返回值表示该异常处理方法会返回一个错误处理页面。不过,因为在 Controller 中的被 @ExceptionHandler 注解的方法只能处理当前 Controller 方法中抛出的异常,如果我们像统一处理所有 Controller 中抛出的异常该怎么办呢?很简单,Spring 提供了两个注解,@ControllerAdvice
和 @RestControllerAdvice
,它们的区别和 Controller 和 RestController 的区别是一样的,方便我们省略 @ResponseBody 注解而已,没有其他特别的。被这两个注解标注的类会被作为所有 Controller/RestController 类的增强类,我们可以在这个类里面编写 @ExceptionHandler 注解的异常处理方法,他将处理系统中所有的 Controller/RestController 方法中抛出的异常。虽然有 3 大方法可以用来处理异常,不过第二种方法貌似有点过时了,所以我们一般情况下,只要合理利用 @ResponseStatus
+ 自定义异常类
、@ExceptionHandler
+ @ControllerAdvice
两种方式就行了?你可能会想了,这两种方式有没有冲突呢?比如一个异常已经被 @ResponseStatus 标注了,我们在 Controller 方法中抛出了这个异常,那么它究竟会被转换为对应的 status + reason 还是被 @ExceptionHandler 异常处理方法给处理呢?经过测试,如果定义了 @ExceptionHandler 异常处理方法,并且与指定异常相匹配,那么会被该异常处理方法给处理,而不会转换为对应的响应状态码。所以推荐用 @ControllerAdvice 和 @ExceptionHandler 方式来统一处理系统抛出的所有异常。
不过,虽然建议使用最后一种方式来统一处理异常,但是前两种异常处理方法我们还是要接触一下的。
@ResponseStatus + 自定义异常
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Resource Not Found")
public static class ResourceNotFoundException extends RuntimeException {
}
@GetMapping("/test")
public String test() {
throw new ResourceNotFoundException();
}
当我们访问 /test 时,将会得到 404 响应,错误提示 message 为 Resource Not Found,很简单。
SimpleMappingExceptionResolver 简单异常处理器
前面说了,只要在 bean 容器中注册了 HandlerExceptionResolver 接口的实现类的实例,那么 Spring MVC 就会将这个异常处理器作为全局异常处理器,现在我们来配置一下 SimpleMappingExceptionResolver:
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="defaultErrorView" value="error-system"/>
<property name="exceptionMappings">
<props>
<prop key="com.zfl9.exception.ClientException">error-client</prop>
<prop key="com.zfl9.exception.ServerException">error-server</prop>
</props>
</property>
</bean>
defaultErrorView 是默认错误页面,这里我设为了 error-system(会结合 view 的 prefix 和 suffix)
exceptionMappings 里面可以设置多个自定义的异常错误页面,对应异常将会被 forward 到指定的错误页面
ExceptionController.java
package com.zfl9.controller;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.zfl9.exception.ClientException;
import com.zfl9.exception.ServerException;
@Controller
@RequestMapping("/exception")
public class ExceptionController {
@GetMapping("/system")
public String systemException(HttpServletRequest request) {
request.setAttribute("url", request.getRequestURI());
throw new RuntimeException("system exception");
}
@GetMapping("/client")
public String clientException(HttpServletRequest request) {
request.setAttribute("url", request.getRequestURI());
throw new ClientException("client exception");
}
@GetMapping("/server")
public String ServerException(HttpServletRequest request) {
request.setAttribute("url", request.getRequestURI());
throw new ServerException("server exception");
}
}
error-system.jsp 错误页,其他的两个差不多,没什么新意:
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<core:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>System Exception</title>
</head>
<body>
<h3>URL: ${url}</h3>
<h3>Exception: ${exception.message}</h3>
<pre>
<core:forEach items="${exception.stackTrace}" var="ste">${ste}
</core:forEach>
</pre>
</body>
</html>
自定义异常处理器,太无聊,就照搬别人的吧
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(
HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex)
{
ex.printStackTrace();
CustomException customException = null;
if(ex instanceof CustomException) {
customException = (CustomException) ex;
} else {
customException = new CustomException("系统未知错误");
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("message", customException.getMessage());
modelAndView.setViewName("error");
return modelAndView;
}
}
使用 @ExceptionHandler 异常处理方法
@Controller
public class ExceptionHandlingController {
// Convert a predefined exception to an HTTP Status code
@ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation")
@ExceptionHandler(DataIntegrityViolationException.class)
public void conflict() {}
// Specify the name of a specific view that will be used to display the error:
@ExceptionHandler({SQLException.class,DataAccessException.class})
public String databaseError() {
return "databaseError";
}
// Total control - setup a model and return the view name yourself. Or consider
// subclassing ExceptionHandlerExceptionResolver (see below).
@ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletRequest req, Exception exception) {
logger.error("Request: " + req.getRequestURL() + " raised " + exception);
ModelAndView mav = new ModelAndView();
mav.addObject("exception", exception);
mav.addObject("url", req.getRequestURL());
mav.setViewName("error");
return mav;
}
}
如果要处理全部异常,可以将这些异常处理方法放到 @ControllerAdvice 注解的类中:
@ControllerAdvice
class GlobalDefaultExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
// 如果异常类被 @ResponseStatus 注解,则直接抛出这个异常,不用管
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
throw e;
// Otherwise setup and send the user to a default error-view.
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
Spring 在异常处理方面提供了一如既往的强大特性和支持,那么在应用开发中我们应该如何使用这些方法呢?以下提供一些经验性的准则:
@ExceptionHandler
异常处理方法国际化又叫做 I18N,本地化又叫做 L10N,所谓 18 和 10 都是对应的英文单词的长度而已。Spring MVC 对国际化提供了很好的支持,通过几个简单的配置就能直接使用。Spring MVC 中有 3 种实现国际化的方式:
Accept-Language
头部来确定 locale 区域。如果要改变 locale,只能改变 Accept-Language 头部来改变,不太灵活。Spring MVC 的国际化是建立在 Java 的国际化的基础上的,我们来回顾一下 Java 国际化的配置步骤:
<basename>_zh_CN.properties
(中文)、<basename>_en_US.properties
(英文)、<basename>.properties
(默认)等资源文件(根据优先级和匹配度选择具体的资源文件,每个资源文件都代表一个不同的 Locale,basename 是资源文件的名称,如果找不到匹配的资源文件,则使用 <basename>.properties
默认资源文件),资源文件的格式很简单,key = value
:键值对,key 区分大小写,value 的前导空格将被忽略,value 中可使用 \t
、\n
等转移序列;key 和 value 都可以有中文,但是必须使用 unicode 字符,JDK 提供了 native2ascii 工具用于 unicode 的转换。value 支持位置参数,如 hello, {0}! goodbye {1}!
中的 {N}
,N 为索引值,从 0 开始,spring 的 message 标签可以传递参数值,稍后会解释。首先定义 properties 资源文件
messages_en.properties
message.name = Name
message.email = Email
message.address = Address
message.telephone = Telephone
message.showMessage = name: {0} \t age: {1} \t sex: {2}
messages_zh.properties
message.name = 名字
message.email = 电子邮件
message.address = 家庭住址
message.telephone = 手机号码
message.showMessage = 名字:{0},\t 年龄:{1},\t 性别:{2}
然后配置 mvc.xml,添加资源束:
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="message"/>
<property name="defaultEncoding" value="UTF-8"/>
<property name="useCodeAsDefaultMessage" value="true"/>
</bean>
因为默认 Locale 实现方式为 Accept-Language 头部,所以不需要其他额外的设置了,直接编写 jsp:
<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
<core:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>i18n-test</title>
</head>
<body>
<pre>
<spring:message code="message.name"/>: Otokaze
<spring:message code="message.email"/>: root@zfl9.com
<spring:message code="message.address"/>: unknown area
<spring:message code="message.telephone"/>: 1234567890
<spring:message code="message.showMessage" arguments="Otokaze,20,男性"/>
</pre>
</body>
</html>
使用 spring 提供的 message 标签,code 就是 key,arguments 为参数,默认分隔符为 ,
即英文分号。
cookie 实现方式
配置 mvc.xml,具体如下:
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="message"/>
<property name="defaultEncoding" value="UTF-8"/>
<property name="useCodeAsDefaultMessage" value="true"/>
</bean>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="defaultLocale" value="en"/>
<property name="cookieName" value="language"/>
<property name="cookieMaxAge" value="2592000"/>
</bean>
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="language"/>
</bean>
</mvc:interceptors>
cookie 和 session 形式需要配置 localeChangeInterceptor 拦截器,该拦截器可以实现通过 queryString 参数改变 locale 设置,默认参数名为 locale,这里我们将它改为了 language。
第一次访问时因为没有设置 language 这个 cookie,所以使用默认的 locale,即 en,当我们传递 ?language=zh
后,发现 locale 已经转换为了 zh,然后去掉这个参数,刷新页面依旧是 zh 简体中文,然后传递 ?language=en
可以将其转换为 en 英文,刷新后依旧是英文,使用调试工具可以看到 Spring MVC 设置了一个名为 language 的 cookie,value 就是 zh 或 en,这也是为什么可以记忆 locale 的原因了。
session 实现方式
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="message"/>
<property name="defaultEncoding" value="UTF-8"/>
<property name="useCodeAsDefaultMessage" value="true"/>
</bean>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<property name="defaultLocale" value="en"/>
<property name="localeAttributeName" value="language"/>
</bean>
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="language"/>
</bean>
</mvc:interceptors>
总体上和 cookie 实现方式没多大区别,只不过是将 language 属性放到了服务器上,而不是 cookie 上。
读取 Cookie
读取 Cookie 很简单,使用 @CookieValue 注解来标注我们的 Controller 方法参数就可以了,Spring 会自动将对应的 Cookie 值绑定到参数上,@CookieValue 注解支持的属性有这么几个:
可以看到,@CookieValue 注解的属性和 @RequestParam 注解的属性时完全一模一样的。测试:
package com.zfl9.controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/cookie/test")
public String cookieTest(@CookieValue(defaultValue = "Otokaze") String name, @CookieValue(defaultValue = "root@zfl9.com") String email) {
return String.format("name: %s, email: %s\n", name, email);
}
}
# Otokaze @ Otokaze-Win10 in ~ [19:31:24]
$ curl localhost/cookie/test --cookie 'name=Otokaze; email=zfl9.com@gmail.com'
name: Otokaze, email: zfl9.com@gmail.com
# Otokaze @ Otokaze-Win10 in ~ [19:31:31]
$ curl localhost/cookie/test
name: Otokaze, email: root@zfl9.com
设置 Cookie
Spring MVC 没有提供什么神奇的设置 Cookie 的注解或方法,因为 Servlet-API 中的 response.addCookie 已经很好用了,我们知道,在 Servlet 编程中,使用一个 javax.servlet.http.Cookie
对象表示一个 cookie,cookie 的两个基本属性就是 name 和 value,分别表示 cookie 的名称和 cookie 的值,注意,为了符合 cookie name 和 value 的字符规范,建议对 name 和 value 做 base64 或 url 编码处理。一个好的方法是,name 使用正常的英文字母,这样就不需要编码处理,而 value 则进行编码处理,比如 base64 编码或 url 编码。
package com.zfl9.controller;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(produces = "text/plain; charset=UTF-8")
public class TestController {
@GetMapping("/cookie/get")
public String getCookie(@CookieValue String name, @CookieValue String email) throws UnsupportedEncodingException {
name = URLDecoder.decode(name, "UTF-8");
email = URLDecoder.decode(email, "UTF-8");
return String.format("name: %s, email: %s\n", name, email);
}
@GetMapping("/cookie/add")
public String setCookie(@RequestParam String name, @RequestParam String email, HttpServletResponse response) throws UnsupportedEncodingException {
Cookie cookieForName = new Cookie("name", URLEncoder.encode(name, "UTF-8"));
Cookie cookieForEmail = new Cookie("email", URLEncoder.encode(email, "UTF-8"));
response.addCookie(cookieForName);
response.addCookie(cookieForEmail);
return "cookie is add";
}
}
删除 Cookie
Cookie 的删除很简单,发送一个 maxAge 为 0 的同名 cookie 给浏览器就行了,value 可以设为 null:
package com.zfl9.controller;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(path = "/cookie", produces = "text/plain; charset=UTF-8")
public class TestController {
@GetMapping("/get")
public String getCookie(@CookieValue String name, @CookieValue String email) throws UnsupportedEncodingException {
name = URLDecoder.decode(name, "UTF-8");
email = URLDecoder.decode(email, "UTF-8");
return String.format("name: %s, email: %s\n", name, email);
}
@GetMapping("/add")
public String setCookie(@RequestParam String name, @RequestParam String email, HttpServletResponse response) throws UnsupportedEncodingException {
Cookie cookieForName = new Cookie("name", URLEncoder.encode(name, "UTF-8"));
Cookie cookieForEmail = new Cookie("email", URLEncoder.encode(email, "UTF-8"));
response.addCookie(cookieForName);
response.addCookie(cookieForEmail);
return "cookie is add";
}
@GetMapping("/del")
public String delCookie(HttpServletResponse response) {
Cookie cookieForName = new Cookie("name", null);
cookieForName.setMaxAge(0);
Cookie cookieForEmail = new Cookie("email", null);
cookieForEmail.setMaxAge(0);
response.addCookie(cookieForName);
response.addCookie(cookieForEmail);
return "cookie is del";
}
}
读取/添加/删除 session
读取 session 也很简单,使用 @SessionAttribute 注解标注方法参数,该参数就会自动绑定到对应的 session attribute 了。@SessionAttribute 注解有两个属性,即 name/value,表示 session 属性的名称,而 required 属性表示该属性是否是请求的,默认为 true,即如果没有对应的 session 属性,Spring 会抛出异常。可以设置为 false,这样,当该属性不存在时,参数将指向 null。
package com.zfl9.controller;
import javax.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttribute;
@RestController
@RequestMapping(path = "/session", produces = "text/plain; charset=UTF-8")
public class SessionController {
@GetMapping("/get")
public String getSession(@SessionAttribute String name, @SessionAttribute String email) {
return String.format("name: %s, email: %s\n", name, email);
}
@GetMapping("/add")
public String addSession(@RequestParam String name, @RequestParam String email, HttpSession session) {
session.setAttribute("name", name);
session.setAttribute("email", email);
return "session is add";
}
@GetMapping("/del")
public String delSession(HttpSession session) {
session.removeAttribute("name");
session.removeAttribute("email");
return "session is del";
}
}
@SessionAttributes 注解的作用以及用法
根据前面的学习,SessionAttributes 注解是用来同步 Model 中的 attribute 到 Session 中的,@SessionAttributes 注解用在 Controller 类上,有两个属性,names/value,字符串数组,同名的 model 属性会被自动存储到 session 中,而 types 属性时 Class 数组,对应类型的 model 属性也会被自动存储到 session 中,两个参数可以同时指定,它们是一个并集关系。官方 javadoc 文档是这样说的,这个注解是用来临时存储 model 数据到 session 中用的,一旦 Controller 方法指定 session 会话完成(调用 SessionStatus 的 setComplete 方法可将会话标记为已完成,这时候这些 session 属性就会被清除),Spring 将会自动删除这些属性,所以对于持久性的 session,请使用 HttpSession.setAttribute 方法。
package com.zfl9.controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
@RestController
@SessionAttributes({"name", "email", "profile"})
@RequestMapping(path = "/session", produces = "text/plain; charset=UTF-8")
public class SessionController {
@GetMapping("/add")
public String addSession(@RequestParam String name, @RequestParam String email, @RequestParam String profile, Model model) {
model.addAttribute("name", name);
model.addAttribute("email", email);
model.addAttribute("profile", profile);
return "session is add";
}
@GetMapping("/get")
public String getSession(@SessionAttribute String name, @SessionAttribute String email, @SessionAttribute String profile) {
return String.format("name: %s, email: %s, profile: %s\n", name, email, profile);
}
@GetMapping("/del")
public String delSession(SessionStatus sessionStatus) {
sessionStatus.setComplete();
return "session is del";
}
}
所谓 RESTful API 就是:使用 URL 定位资源,使用 Method 描述操作。典型的 CRUD 操作:
POST /employees # 创建一个员工
GET /employees # 获取所有员工
GET /employees/${id} # 获取指定员工
PUT /employees/${id} # 更新员工信息(完整更新)
PATCH /employees/${id} # 更新员工信息(部分更新)
DELETE /employees/${id} # 删除指定员工
DELETE /employees # 删除所有员工
/
结尾。如果想要了解有关 RESTful API 的更多信息,可参考之前的 RESTful API 感想 一文。
Spring MVC 对 RESTful API 提供了非常良好的支持,不仅有编写 RESTful API 服务端的类库,还提供了一个 RestTemplate 客户端帮助类,便于我们编写 Java 代码,测试我们编写的 RESTful API 是否工作正常。
不过,本文暂时不介绍 RestTemplate 类的使用,再说 RestTemplate 的使用其实很简单,没什么可讲的,直接看几个官方的 case 就行了,就如同 JdbcTemplate 模板类一样,简单易用,容易上手。这里我就使用 Postman 来进行 RESTful API 的 CRUD 测试,当然也可以使用 curl 命令行工具进行测试。
前面说了,RESTful API 通常情况下,都是使用 JSON 作为数据交互格式,因为 JSON 和 Java 对象之间的转换非常简单,兼容性非常强,而且 JSON 的两大数据类型:数组和对象,和 Java 中的 Bean/POJO、集合对象基本上都可以进行很好的互相转换操作,我们来回顾一下 JSON 是什么,以及 JSON 的数据类型:
JSON(JavaScript Object Notation,JS 对象表示法),是一种由 道格拉斯·克罗克福特 构想设计、轻量级的数据交换格式,以文本为基础,且易于让人阅读。尽管 JSON 是 Javascript 的一个子集,但 JSON 是独立于语言的文本格式,并且采用了类似于 C 语言家族的一些习惯。
JSON 数据格式与语言无关,脱胎于 JavaScript,但目前很多编程语言都支持 JSON 格式数据的生成和解析。JSON 的官方 MIME 类型是 application/json
,文件扩展名是 .json
。
JSON 建构于两种结构:
这两种结构分别对应 JavaScript 中的 对象 和 数组。注意,JSON 只是一个字符串!是一个纯文本!
{k1: v1, k2: v2, ..., kN: vN}
,key 必须显式得加上双引号[e1, e2, e3, ..., eN]
,JS 数组其实就是对象,其 key 是隐式的值(即对象中的 value、数组中的 element)可以是以下类型:
null
:空指针true/false
:布尔值number
:数值(十进制)string
:字符串(双引号)array
:数组object
:对象number
只支持十进制的整数、浮点数。其中浮点数支持科学记数法,即 1.3E4
表示 13000(E 大小写不敏感)。
string
必须使用双引号包围,包括 object 中的 key,这是为了适应 C/C++、Java 中的"单引号为字符,双引号为字符串"语法。此外,还支持一些转义序列:
\"
:双引号\\
:反斜杠\/
:正斜杠\b
:退格符\t
:制表符\r
:回车符\n
:换行符\f
:换页符\uhhhh
:UTF-16 code-unit编写 RESTful API 的一个关键点就是,Controller 方法接收 json 数据,同时,Controller 方法返回的也是 json 数据,我们知道 json 其实就是一个字符串,那么在 Spring 中,我们如何接收 json 请求体,并且又如何返回 json 响应呢?别慌,Spring MVC 提供了一系列机制,来方便我们编写 RESTful API 应用。
回顾前面的 JSON 支持一节,我们只需要在 pom.xml 中添加 jackson-databind 依赖,然后配置 spring mvc 的 annotation-driven 注解驱动元素,Spring MVC 就会自动检测到 jackson-databind 的存在,然后使用 jackson 来进行 bean/pojo/集合对象/数组对象 到 json 字符串之间的转换(称之为序列化),当然 jackson 也可以将 json 字符串反序列化为 bean/pojo/集合对象/数组对象,总之就是无缝的转换。
我们已经知道 JSON 的两大底层数据类型,数组和对象,而 Java 中常见的数据类型就是:Array/List、HashMap 两种,List 和 Array 基本上可以看作一种类型,即 JSON 口中的数组,而 HashMap 就是对象,因为 JSON 中的对象其实就是键值对,也就是 Java 中的 Map;那么 Bean/Pojo 呢?bean 和 pojo 也都可以映射到 JSON 的对象,即键值对,key 就是对象的数据成员名称,value 就是对象的数据成员值,比如一个 Student 类,有 name 和 age 两个 private 属性,同时我们定义了它们的 getter、setter 方法,那么该 student 对象就可以序列化为 { "name": "Otokaze", "age": 20 }
,怎么样,是不是很形象。
Spring MVC 中接收 JSON 请求
在前面的注解复习一节中,我们接触了 @RequestParam
、@RequestBody
、@RequestPart
三个与请求数据绑定的注解,@RequestParam 是用来绑定表单数据的,@RequestBody 是用来绑定 json/xml 数据的,@RequestPart 是用来绑定文件上传的。所以很显然,在 RESTful 中,如果要绑定 json 数据(自动序列化为 Java 对象),那么就要使用 @RequestBody 注解,被注解的参数类型应该是一个 bean/pojo、list/map,这样 jackson-databind 才能将 json 字符串正确的序列化为 java 对象。
Spring MVC 中返回 JSON 数据
现在我们已经知道如何接收 JSON 请求数据了,接下来我们来看看如何响应 JSON 数据给客户端,因为之前的 Spring MVC example 中,我们返回的都是一个 String 或 ModelAndView,表示这个请求将被 forward 给对应的 view 视图进行进一步处理,处理完之后,http 响应才会被发往客户端,请求结束。不过在 RESTful 中,根本不需要什么 view 视图,我们需要的是直接在 Controller 方法中返回响应结果给客户端,而不经过 forward to view 这个步骤,这时候我们就需要使用 @ResponseBody
注解标注我们的 Controller 方法,这个注解的意思非常明了,意思就是说这个方法的返回值就是响应的结果。在 Spring MVC 4.0 之后,我们可以直接在 Controller 类上使用这个注解,此时表示 Controller 里面的所有方法都是 REST 方法,相当于给每个处理方法都标上了 @ResponseBody 注解,不过,Spring MVC 之后又提供了一个 @RestController 注解,它和 @Controller + @ResponseBody 注解一起使用的效果是等价的,可看作 @Controller 的子注解。
此外,我们也可以不使用任何 @ResponseBody、@RestController 注解,而是依旧使用原先的 @Controller 注解,然后我们的控制器方法返回值改为 ResponseEntity<T>
类,ResponseEntity 中文意思就是“响应实体”,它就是一个完整的 HTTP response 的抽象表示,由 method、url、header、body 四个部分表示,所以我们不需要 @ResponseBody 标注这些方法或控制器类,因为这个返回值就表示一个完整的 HTTP 响应。
大家可以自由的选择使用 @RestController注解 + 返回Object、@Controller注解 + 返回ResponseEntity 两种形式,它们都可以用来编写 RESTful API 服务,不论哪种方式,Spring 都会使用 jackson-databind 对 Object/ResponseEntity 里面的 Object 进行序列化操作,转换为 JSON 字符串。一般情况下,使用前者就可以了,不过如果你需要设置 HTTP 响应头,那么使用 ResponseEntity 可能会方便一点。虽然 ResponseEntity 很灵活和很强大,但是不应该过度使用 ResponseEntity,而是应该更简单的传统方式,这样可读性更强。当然也不是说不能使用 ResponseEntity,如果有足够的理由使用,那就大胆的使用吧。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9</groupId>
<artifactId>SpringMVC_Learn</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>4.3.20.RELEASE</spring.version>
<mysql.version>8.0.13</mysql.version>
<servlet.version>3.1.0</servlet.version>
<jstl.version>1.2</jstl.version>
<jackson.version>2.9.7</jackson.version>
<fileupload.version>1.3.3</fileupload.version>
<validatorapi.version>2.0.1.Final</validatorapi.version>
<validatorimpl.version>6.0.13.Final</validatorimpl.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${fileupload.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${validatorapi.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${validatorimpl.version}</version>
</dependency>
</dependencies>
</project>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<servlet-name>springmvc</servlet-name>
</filter-mapping>
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<servlet-name>springmvc</servlet-name>
</filter-mapping>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zfl9"/>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="31457280"/> <!-- postMaxSize: 30M -->
<property name="maxUploadSizePerFile" value="10485760"/> <!-- fileMaxSize: 10M -->
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/test?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
@RestController 方式
EmployeeRestController.java
package com.zfl9.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zfl9.model.Employee;
import com.zfl9.service.EmployeeService;
@RestController
@RequestMapping("/api/employees")
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping
public List<Employee> getAllEmployees() {
return employeeService.getAllEmployee();
}
}
使用 Postman 进行 API 接口测试,得到以下结果:
[
{
"id": 2,
"name": "Justine ",
"email": "Nikita_Koch93@yahoo.com",
"address": "9838 Treutel Loaf",
"telephone": "1-077-812-5934 x9878"
},
{
"id": 3,
"name": "Eldridge ",
"email": "Eldridge.Flatley13@gmail.com",
"address": "4810 Bosco Views",
"telephone": "1-344-711-8794"
},
{
"id": 4,
"name": "Kirk ",
"email": "Kirk_Franecki13@hotmail.com",
"address": "183 Buckridge Street",
"telephone": "349.586.6572"
},
{
"id": 5,
"name": "Donny ",
"email": "Donny15@gmail.com",
"address": "49378 Nathanial Mountain",
"telephone": "1-244-325-0932 x750"
},
{
"id": 6,
"name": "Roslyn ",
"email": "Roslyn.Heidenreich@gmail.com",
"address": "83699 Cummerata Orchard",
"telephone": "530-781-8206"
},
{
"id": 7,
"name": "美咲 佐々木",
"email": "Gerard_Crona1@hotmail.com",
"address": "91855 愛菜 Village",
"telephone": "608.588.5885 x57195"
},
{
"id": 8,
"name": "梁 鹏涛",
"email": "liang.peng.tao@hotmail.com",
"address": "富裕省贫穷市土豪村",
"telephone": "85449355157"
},
{
"id": 9,
"name": "Kennedi ",
"email": "Nigel61@hotmail.com",
"address": "837 LeQuesne Station St",
"telephone": "0468 699 235"
},
{
"id": 10,
"name": "Johannes ",
"email": "Johannes_Breuer82@yahoo.com",
"address": "7510 Janne Shores",
"telephone": "08641 5000"
},
{
"id": 11,
"name": "Tyshawn ",
"email": "Tyshawn3@yahoo.com",
"address": "824 Marie Path",
"telephone": "(930) 300-1584 x0236"
},
{
"id": 12,
"name": "شیرزاد لنکرانی",
"email": "Nadia30@gmail.com",
"address": "96290 فریاد Isle",
"telephone": "553-204-7203 x7653"
},
{
"id": 13,
"name": "Hohnheiser",
"email": "Jessy_Hohnheiser14@gmail.com",
"address": "26051 Romeo Lodge",
"telephone": "(0235) 816647440"
},
{
"id": 14,
"name": "Nathan ",
"email": "Nathan_Allgeyer@yahoo.com",
"address": "98684 Otte Ramp",
"telephone": "(0963) 130796580"
},
{
"id": 15,
"name": "Maxime ",
"email": "Maxime72@gmail.com",
"address": "270 Sterling Corner",
"telephone": "014995 26191"
},
{
"id": 16,
"name": "陽翔 佐々木",
"email": "Novella76@hotmail.com",
"address": "452 陽菜 Lock",
"telephone": "+96 63 0510448"
},
{
"id": 17,
"name": "結愛 木村",
"email": "Otha_Dare@hotmail.com",
"address": "214 高橋 Mills",
"telephone": "(246)726-1911 x221"
},
{
"id": 18,
"name": "蒼空 斎藤",
"email": "Giordano.Farin@yahoo.com",
"address": "16996 小林 Springs",
"telephone": "1-140-787-4492"
},
{
"id": 21,
"name": "Kiara ",
"email": "Kiara.Clarke@gmail.com",
"address": "5294 Lara Mews",
"telephone": "05 2185 0888"
},
{
"id": 23,
"name": "Otokaze",
"email": "zfl9.com@gmail.com",
"address": "jiangxi.ganzhou",
"telephone": "15307973676"
}
]
可以看到,List 被转换为了 JSON 数组,Employee 实体类被转换为了 JSON 对象。
ResponseEntity 方式
EmployeeRestController.java
package com.zfl9.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.zfl9.model.Employee;
import com.zfl9.service.EmployeeService;
@Controller
@RequestMapping("/api/employees")
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping
public ResponseEntity<List<Employee>> getAllEmployees() {
return ResponseEntity.ok(employeeService.getAllEmployee());
}
}
使用 Postman 测试,结果是一样的,就不贴出来了。一般还是建议使用 @RestController + Pojo 形式。
题外话,因为提到了 @ResponseBody,那就再提一下 @ResponseStatus 注解吧,该注解有两个属性:
@ResponseStatus 可以用来标记三个东西,它们的意思分别是:
ResponseEntity 使用详解
虽然不建议使用 ResponseEntity,不过有时候使用 ResponseEntity 是真的方便,可以完全脱离 Servlet-API,比如设置 HTTP 响应状态码,虽然可以使用 @ResponseStatus 注解处理器方法,但是我们不能在方法内部动态的设置 Status Code,可能你会说可以通过 @ResponseStatus + 自定义异常类来完成这个需求,但是我并不想通过这种别扭的方式来返回指定响应状态,你可能又会说,可以使用 HttpServletResponse 来设置啊,暂时不争论这个,我们来学习一下 ResponseEntity 的常见用法吧。
ResponseEntity 的类签名
public class ResponseEntity<T> extends HttpEntity<T> {
...
}
可以看到,这是一个泛型类,其中 T 是响应体的类型,比如 String、Employee、List<Employee>
。
我们来看一下 ResponseEntity 的构造方法:
public ResponseEntity(HttpStatus status)
public ResponseEntity(T body, HttpStatus status)
public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus status)
public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus status)
关心一下 MultiValueMap<String, String>
类型,根据 javadoc 描述,这是一个可以存储多个 value 的 key-values 键值对数据结构,它是 java.util.Map<K, List<V>>
的子接口,我们来看一下它的签名:
public interface MultiValueMap<K, V> extends Map<K, List<V>>
MultiValueMap 的常用实现类就是 HttpHeaders,它的 K 和 V 都是字符串类型,表示 HTTP 头部。
ResponseEntity 的静态方法 since v4.1+
// 设置指定 body,status 为 200 OK
public static <T> ResponseEntity<T> ok(T body)
// 设置 404/204 状态码,因为没有 body,所以返回 header builder
public static ResponseEntity.HeadersBuilder<?> notFound()
public static ResponseEntity.HeadersBuilder<?> noContent()
// 设置对应的 status 状态码,然后返回 body builder
public static ResponseEntity.BodyBuilder ok()
public static ResponseEntity.BodyBuilder accepted()
public static ResponseEntity.BodyBuilder badRequest()
public static ResponseEntity.BodyBuilder created(java.net.URI location)
public static ResponseEntity.BodyBuilder status(int status)
public static ResponseEntity.BodyBuilder status(HttpStatus status)
我们来看看 HeadersBuilder 内部接口(返回的是 HeadersBuilder):
B headers(HttpHeaders headers) // 包含指定 headers
B header(String headerName, String... headerValues) // 设置指定 header
B location(java.net.URI location) // 设置 Location 重定向响应头部
<T> ResponseEntity<T> build() // 完成构造,返回 ResponseEntity
来看看 BodyBuilder 内部接口(返回的是 BodyBuilder,BodyBuilder 是 HeadersBuilder 的子接口):
ResponseEntity.BodyBuilder contentLength(long contentLength) // Content-Length 头
ResponseEntity.BodyBuilder contentType(MediaType contentType) // Content-Type 头
<T> ResponseEntity<T> body(body) // 设置 body,响应体,返回 ResponseEntity
好了,我们来看几个 ResponseEntity 的用法,熟悉一下怎么用:
@GetMapping("/hello")
ResponseEntity<String> hello() {
return new ResponseEntity<>("Hello World!", HttpStatus.OK);
}
@GetMapping("/age")
ResponseEntity<String> age(
@RequestParam("yearOfBirth") int yearOfBirth) {
if (isInFuture(yearOfBirth)) {
return new ResponseEntity<>(
"Year of birth cannot be in the future",
HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(
"Your age is " + calculateAge(yearOfBirth),
HttpStatus.OK);
}
@GetMapping("/customHeader")
ResponseEntity<String> customHeader() {
HttpHeaders headers = new HttpHeaders();
headers.add("Custom-Header", "foo");
return new ResponseEntity<>(
"Custom header set", headers, HttpStatus.OK);
}
@GetMapping("/hello")
ResponseEntity<String> hello() {
return ResponseEntity.ok("Hello World!");
}
@GetMapping("/age")
ResponseEntity<String> age(@RequestParam("yearOfBirth") int yearOfBirth) {
if (isInFuture(yearOfBirth)) {
return ResponseEntity.badRequest()
.body("Year of birth cannot be in the future");
}
return ResponseEntity.status(HttpStatus.OK)
.body("Your age is " + calculateAge(yearOfBirth));
}
@GetMapping("/customHeader")
ResponseEntity<String> customHeader() {
return ResponseEntity.ok()
.header("Custom-Header", "foo")
.body("Custom header set");
}
Employee RESTful API CRUD 例子
Employee.java
package com.zfl9.model;
import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
public class Employee implements java.io.Serializable {
private static final long serialVersionUID = 6782234751084760161L;
public interface Create {}
public interface Update {}
@NotNull(message = "id is null", groups = Update.class)
@Min(value = 1, message = "id is invalid", groups = Update.class)
private Integer id;
@NotNull(message = "name is null", groups = {Create.class, Update.class})
@NotBlank(message = "name is invalid", groups = {Create.class, Update.class})
private String name;
@NotNull(message = "email is null", groups = {Create.class, Update.class})
@Email(message = "email is invalid", groups = {Create.class, Update.class})
private String email;
@NotNull(message = "address is null", groups = {Create.class, Update.class})
@NotBlank(message = "address is invalid", groups = {Create.class, Update.class})
private String address;
@NotNull(message = "telephone is null", groups = {Create.class, Update.class})
@Pattern(regexp = "^[0-9 -]++$", message ="telephone is invalid", groups = {Create.class, Update.class})
private String telephone;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
@Override
public String toString() {
return String.format("Employee { id = %d, name = %s, email = %s, address = %s, telephone = %s }", id, name, email, address, telephone);
}
}
EmployeeDao.java
package com.zfl9.dao;
import java.util.List;
import com.zfl9.model.Employee;
public interface EmployeeDao {
Employee getEmployee(int id);
List<Employee> getAllEmployee();
void addEmployee(Employee employee);
void updateEmployee(Employee employee);
void deleteEmployee(int id);
void deleteAllEmployee();
}
EmployeeDaoImpl.java
package com.zfl9.dao;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import com.zfl9.model.Employee;
@Repository
public class EmployeeDaoImpl implements EmployeeDao {
@Autowired
private JdbcTemplate jdbcTemplate;
private RowMapper<Employee> rowMapper = (resultSet, rowNum) -> {
Employee employee = new Employee();
employee.setId(resultSet.getInt("id"));
employee.setName(resultSet.getString("name"));
employee.setEmail(resultSet.getString("email"));
employee.setAddress(resultSet.getString("address"));
employee.setTelephone(resultSet.getString("telephone"));
return employee;
};
@Override
public Employee getEmployee(int id) {
String sql = "select * from employee where id = ?";
return jdbcTemplate.queryForObject(sql, rowMapper, id);
}
@Override
public List<Employee> getAllEmployee() {
String sql = "select * from employee";
return jdbcTemplate.query(sql, rowMapper);
}
@Override
public void addEmployee(Employee employee) {
String sql = "insert into employee (name, email, address, telephone) values (?, ?, ?, ?)";
jdbcTemplate.update(sql, employee.getName(), employee.getEmail(), employee.getAddress(), employee.getTelephone());
}
@Override
public void updateEmployee(Employee employee) {
String sql = "update employee set name = ?, email = ?, address = ?, telephone = ? where id = ?";
jdbcTemplate.update(sql, employee.getName(), employee.getEmail(), employee.getAddress(), employee.getTelephone(), employee.getId());
}
@Override
public void deleteEmployee(int id) {
String sql = "delete from employee where id = ?";
jdbcTemplate.update(sql, id);
}
@Override
public void deleteAllEmployee() {
String sql = "truncate table employee";
jdbcTemplate.update(sql);
}
}
EmployeeService.java
package com.zfl9.service;
import java.util.List;
import com.zfl9.model.Employee;
public interface EmployeeService {
Employee getEmployee(int id);
List<Employee> getAllEmployee();
void addEmployee(Employee employee);
void updateEmployee(Employee employee);
void deleteEmployee(int id);
void deleteAllEmployee();
}
EmployeeServiceImpl.java
package com.zfl9.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.zfl9.dao.EmployeeDao;
import com.zfl9.model.Employee;
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeDao employeeDao;
@Override
public Employee getEmployee(int id) {
return employeeDao.getEmployee(id);
}
@Override
public List<Employee> getAllEmployee() {
return employeeDao.getAllEmployee();
}
@Override
public void addEmployee(Employee employee) {
employeeDao.addEmployee(employee);
}
@Override
public void updateEmployee(Employee employee) {
employeeDao.updateEmployee(employee);
}
@Override
public void deleteEmployee(int id) {
employeeDao.deleteEmployee(id);
}
@Override
public void deleteAllEmployee() {
employeeDao.deleteAllEmployee();
}
}
EmployeeRestController.java
package com.zfl9.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import com.zfl9.model.Employee;
import com.zfl9.service.EmployeeService;
@Controller
@RequestMapping(path = "/api/employees", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping
public ResponseEntity getAllEmployee() {
List<Employee> employees = employeeService.getAllEmployee();
if (employees.isEmpty())
return ResponseEntity.noContent().build();
return ResponseEntity.ok(employees);
}
@GetMapping("/{id}")
public ResponseEntity getEmployee(@PathVariable int id) {
return ResponseEntity.ok(employeeService.getEmployee(id));
}
@PostMapping
public ResponseEntity createEmployee(@Validated(Employee.Create.class) @RequestBody Employee employee, BindingResult result) {
if (result.hasErrors())
return ResponseEntity.badRequest().body(result.getFieldErrors());
employeeService.addEmployee(employee);
return ResponseEntity.noContent().build();
}
@PutMapping("/{id}")
public ResponseEntity updateEmployee(@PathVariable int id, @Validated(Employee.Update.class) @RequestBody Employee employee, BindingResult result) {
if (result.hasErrors())
return ResponseEntity.badRequest().body(result.getFieldErrors());
employeeService.updateEmployee(employee);
return ResponseEntity.ok(employee);
}
@DeleteMapping("/{id}")
public ResponseEntity deleteEmployee(@PathVariable int id) {
employeeService.deleteEmployee(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
public ResponseEntity deleteAllEmployee() {
employeeService.deleteAllEmployee();
return ResponseEntity.noContent().build();
}
}
然后使用 Postman 或 curl 测试吧。