一、SpringMVC 介绍
0 什么是 MVC
知识回顾:Appli Web课程笔记 - MVC
1 什么是 SpringMVC
在 Spring 中我们重点介绍了数据访问层 dao
与业务逻辑层 service
通过 Spring 框架的实现。SpringMVC 是 Spring 的一个子项目,是表述层 Controller
的一整套完备解决方案。
在如上的设计中:
controller
负责请求和数据的接收,接收后将其转发给 service
进行业务处理
service
根据需要会调用 dao
对数据进行增删改查
dao
把数据处理完后,将结果交给 service
,service
再交给 controller
controller
根据需求将 Model
和 View
组合起来生成页面,转发给前端浏览器
这样做的好处就是 controller
可以处理多个请求,并对请求进行分发,执行不同的业务操作。
随着互联网的发展,上面的模式因为是同步调用,性能慢慢的跟不需求,所以异步调用 成为了如今流行的一种处理方式,如下图所示
因为是异步调用,所以后端不需要返回 View
视图,将其去除
前端如果通过异步调用的方式进行交互,后端就需要将返回的数据转换成 JSON 格式进行返回
特点
Spring 家族原生产品,与 IOC 容器无缝对接 。
基于原生 Servlet ,通过了功能强大的前端控制器 DispatcherServlet
来进行请求的统一管理 。即,将 servlet
封装为 DispatcherServlet
。
代码清新简洁;内部组件化程度高 、可插拔式组件即插即用,想要什么功能配置相应组件即可。
性能高,适合大型互联网项目需求。
2 SpringMVC 入门案例
2.1 准备工作
构建 maven
项目
2.2 配置 Spring 核心设置
.xml
配置文件
SpringMVC
的配置文件默认的位置和名称:
位置:${project_path}/src/main/webapp/WEB-INF/
目录下
名称:<servlet-name>-servlet.xml
,在本例子中即为 SpringMVC-servlet.xml
1 2 3 4 <context:component-scan base-package ="fr.gdai.springmvc.controller" />
注解方案
1 2 3 4 5 6 package fr.gdai.springmvc.config;@Configuration @ComponentScan("fr.gdai.springmvc.controller") public class SpringMvcConfig {}
2.3 tomcat
服务器加载配置
我们之前说过,SpringMVC 的本质是 servlet
,通过将 servlet
封装成 DispatcherServlet
,从而对浏览器的请求统一管理 。
web.xml
配置文件
我们需要在 web.xml
中配置 DispatcherServlet
。如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <servlet > <servlet-name > SpringMVC</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > SpringMVC</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping >
在配置 SpringMVC 的前端控制器 DispatcherServlet
时,<url-pattern>
中 /
与 /*
的区别:
注解方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package fr.gdai.springmvc.config;public class DispatcherServletUtil extends AbstractDispatcherServletInitializer { @Override protected WebApplicationContext createServletApplicationContext () { AnnotationConfigWebApplicationContext webCtx = new AnnotationConfigWebApplicationContext (); webCtx.register(SpringMvcConfig.class); return webCtx; } @Override protected String[] getServletMappings() { return new String []{"/" }; } @Override protected WebApplicationContext createRootApplicationContext () { return null ; } }
根据以上的配置,我们给出一种更加简洁的配置方式。其实际上是继承 AbstractDispatcherServletInitializer
的子类 AbstractAnnotationConfigDispatcherServletInitializer
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package fr.gdai.springmvc.config;public class DispatcherServletUtil extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class [0 ]; } @Override protected Class<?>[] getServletConfigClasses() { return new Class []{SpringMvcConfig.class}; } @Override protected String[] getServletMappings() { return new String []{"/" }; } }
2.4 完成 controller
及测试
我们继续编写 controller
包下的代码以完成测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package fr.gdai.springmvc.controller;@Controller public class UserController { @RequestMapping("/save") @ResponseBody public String save () { System.out.println("user saved!" ); return "{'module':'springmvc:save()'}" ; } @RequestMapping("/delect") @ResponseBody public String delect () { System.out.println("user delected!" ); return "{'module':'springmvc:delect()'}" ; } }
我们可以启动 tomcat
服务器来测试这个save()
方法;也可以在 maven
中设置 tomcat
的插件:
在 maven
的配置文 pom.xml
中设置 tomcat
的插件
1 2 3 4 5 6 7 8 9 10 11 12 13 <build > <plugins > <plugin > <groupId > org.apache.tomcat.maven</groupId > <artifactId > tomcat7-maven-plugin</artifactId > <version > 2.1</version > <configuration > <port > 8081</port > <path > /springmvc</path > </configuration > </plugin > </plugins > </build >
然后在 IDEA 中新建一个 Run/Debug Configuration
1 2 Name : xxx Run : tomcat7:run
启动服务器。在浏览器中输入
1 http://localhost:8081/springmvc/save
可以观察到页面显示 {'module':'springmvc:save'}
且控制台输出 user saved!
3 入门案例工作流程
3.1 启动服务器初始化过程
服务器启动,执行 DispatcherServletUtil
类,初始化 web 容器
执行createServletApplicationContext()
方法,创建了 webApplicationContext
对象
加载 SpringMvcConfig.class
执行 @ComponentScan
加载对应的 Bean
加载 UserController
,每个 @RequestMapping("/xxx")
的名称对应一个具体的方法 ,由 SpringMVC
统一管理,并不是由每个 Bean
单独管理。
执行 getServletMappings()
方法,定义所有的请求都通过 SpringMVC
3.2 单次请求过程
发送请求 http://localhost:8081/springmvc/save
web 容器发现所有请求都经过 SpringMVC
,将请求交给 SpringMVC` 处理
解析请求路径 /save
由 /save
匹配执行对应的方法 save()
执行 save()
检测到有 @ResponseBody
直接将 save()
方法的返回值作为响应体返回给请求方
4 指定包扫描范围
在一个完整的项目中,我们考虑如下的项目结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |-- fr.gdai |-- config |-- SpringConfig.java |-- JDBCDataSource.java |-- MyBatisConfig.java |-- SpringMvcConfig.java |-- DispatcherServletUtil.java |-- domain |-- User.java |-- Book.java |-- dao |-- UserDao.java |-- BookDao.java |-- service |-- UserService.java |-- BookService.java |-- controller |-- UserController.java |-- BookController.java
我们可以很容易的发现,在我们的项目中有几种不同种类的 Bean
:
Spring
维护的 Bean
对象:
业务 Bean
:service
等
功能 Bean
:JDBCDataSource
等
SpringMVC
维护的 Bean
对象:
我们在包扫描 @ComponentScan()
时需要区分不同的作用域:
SpringConfig.java
1 2 3 4 5 6 7 8 9 @Configuration @ComponentScan({"fr.gdai.springmvc.dao", "fr.gdai.springmvc.service"}) @ComponentScan(value = "fr.gdai.springmvc", excludeFilters = @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = Controller.class)) public class SpringConfig {}
SpringMvcConfig.java
1 2 3 4 @Configuration @ComponentScan("fr.gdai.springmvc.controller") public class SpringMvcConfig {}
5 静态资源的“放行”
在之前的 SpringMVC
中 DispatcherServlet
的设置中,我们定义了需要 SpringMVC
处理的请求路径:
1 2 3 4 5 @Override protected String[] getServletMappings() { return new String []{"/" }; }
那么在这个路径下的所有请求都会被 SpringMVC
所拦截。当用户想访问一个静态资源,如 html
页面、图片等,该请求也会被 SpringMVC
拦截,并尝试将其处理,而此时就会出现问题。
例如,我们使用 GET
方式请求静态资源 index.html
1 http://localhost:8081/pages/index.html
SpringMVC
会拦截该请求,并匹配 DispatcherServlet
所管理的 Controller
中是否有满足 /pages/index.html
的 GET
请求的处理。从而与我们的要求不符,出现错误。此时就需要 SpringMVC
“放行”该请求,使其交给 tomcat
服务器直接提供该静态资源。
SpringMVC
框架提供了一个 WebMvcConfigurationSupport
的类来解决该问题,我们需要创建一个类来继承它,通过重写 addResourceHandlers()
方法来实现:
1 2 3 4 5 6 7 8 9 10 11 12 package fr.gdai.springmvc.config;@Configuration public class SpringMvcSupport extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/pages/**" ).addResourceLocations("/pages/" ); registry.addResourceHandler("/css/**" ).addResourceLocations("/css/" ); registry.addResourceHandler("/js/**" ).addResourceLocations("/js/" ); registry.addResourceHandler("/images/**" ).addResourceLocations("/images/" ); } }
然后将这个 @Configuration
添加到 SpringMvcConfig.java
的扫描范围内
1 2 3 4 5 6 7 package fr.gdai.springmvc.config;@Configuration @ComponentScan({"fr.gdai.springmvc.controller", "fr.gdai.springmvc.config"}) @EnableWebMvc public class SpringMvcConfig {}
二、请求与响应
1 请求
请求的映射路径
我们考虑如下的项目结构:
1 2 3 4 5 |-- fr.gdai |-- ... |-- controller |-- UserController.java |-- BookController.java
UserController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 @Controller public class UserController { @Autowired private UserServiceImpl userService; @RequestMapping("/save") @ResponseBody public String userSave () { System.out.println("user saved!" ); return "{'module':'springmvc:userSave()'}" ; } }
BookController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 @Controller public class BookController { @Autowired private BookServiceImpl bookService; @RequestMapping("/save") @ResponseBody public String bookSave () { System.out.println("book saved!" ); return "{'module':'springmvc:bookSave()'}" ; } }
在两个 @Controller
中,我们设置了两个相同的请求路径映射 @RequestMapping("/save")
。这种情况是无法成功初始化 servlet
的,错误信息:
1 Initialization of bean failed; nested exception is java.lang.IllegalStateException: Cannot map handler 'userController' to URL path [/save]: There is already handler of type [class fr.gdai.springmvc.controller.BookController] mapped.
我们可以完善请求路径映射来解决这个问题:
1 2 3 4 5 6 7 8 9 10 @Controller public class UserController { @RequestMapping("/user/save") @ResponseBody public String userSave () { System.out.println("user saved!" ); return "{'module':'springmvc:userSave()'}" ; } }
1 2 3 4 5 6 7 8 9 10 @Controller public class BookController { @RequestMapping("/book/save") @ResponseBody public String bookSave () { System.out.println("book saved!" ); return "{'module':'springmvc:bookSave()'}" ; } }
或者使用请求路径的前缀:
1 2 3 4 5 6 7 8 9 10 11 @Controller @RequestMapping("/user") public class UserController { @RequestMapping("/save") @ResponseBody public String userSave () { System.out.println("user saved!" ); return "{'module':'springmvc:userSave()'}" ; } }
请求映射路径
名称:@RequestMapping(value = "PATH/PREFIX")
即可用于 Controller
的 方法的注解 ,设置请求的访问路径
也可用于 Controller
类的注解 ,为这个类设置统一的请求访问路径前缀
请求方式
Get
请求
我们知道,使用 Get
请求的参数 param1
和 param2
会在 url
中体现出来。如下所示
1 http://localhost:8081/xxx/xxx?param1=value1¶m2=value2
所以普通基本类型 的参数 param1
和 param2
都可以按照如下形式,被服务器端的方法接收:
1 2 3 @RequestMapping("/xxx/xxx") @ResponseBody public void xxx (Type param1, Type param2) { }
Post
请求
对于 Post
请求,我们需要注意:与 servlet
的 doGet()
和 doPost()
方法不同,SpringMVC
的不区分这两种方法 ,即对于程序员来讲后端代码是没有区别的 。
在 Post
请求中,所有的请求参数都在请求体 (RequestBody )中,并不会表现在 url
中。在 PostMan
中可以通过如下方式模拟发送 Post
请求:
服务器端的方法依然是
1 2 3 @RequestMapping("/xxx/xxx") @ResponseBody public void xxx (Type param1, Type param2) { }
关于中文乱码问题
p.s.:根据网友反馈,Tomcat ver.8
以上的版本中,Get
请求不存在中文乱码问题,Post
请求中的中文乱码。
对于 Get
请求的中文乱码问题 :因为 Get
请求参数是通过 url
传递的,所以我们需要设置 tomcat
服务器的 urlEncoding
属性为 utf-8
。在 Maven
集成的 tomcat
插件中,我们需要在 pom.xml
文件中关于 tomcat
的设置中添加如下内容:
1 2 3 <configuration > <uriEncoding > utf-8</uriEncoding > </configuration >
对于 Post
请求的中文乱码问题 :我们可以在 servlet
中的 Filter
过滤器中设置编码类型。在 SpringMVC
中,我们依然可以在 DispatcherServlet
的配置中通过设置 Filter
的方法 解决:
1 2 3 4 5 6 7 8 9 @Override protected Filter[] getServletFilters() { CharacterEncodingFilter filter = new CharacterEncodingFilter (); filter.setEncoding("utf-8" ); return new Filter []{filter}; }
请求的参数
基本数据参数
url
地址传递参数,url
地址参数名与形参变量名相同,定义形参即可接收参数。
1 2 3 4 5 6 7 @RequestMapping("/addUser") @ResponseBody public String addUser (String name, int age) { System.out.println("普通参数传递 : addUser ---> name = " + name); System.out.println("普通参数传递 : addUser ---> age = " + age); return "{'module':'springmvc:addUser()'}" ; }
1 2 普通参数传递 : addUser ---> name = gdai 普通参数传递 : addUser ---> age = 24
url
地址传递参数。若 url
地址参数名与形参变量名不相同 ,可使用 @RequestParam()
注解定义形参即可接收参数。
1 2 3 4 5 6 7 8 @RequestMapping("/addUserDiffParamName") @ResponseBody public String addUserDiffParamName (@RequestParam("name") String userName, @RequestParam("age") int userAge) { System.out.println("普通参数(不同参数名)传递 : addUser ---> name = " + userName); System.out.println("普通参数(不同参数名)传递 : addUser ---> age = " + userAge); return "{'module':'springmvc:addUser()'}" ; }
1 2 普通参数(不同参数名)传递 : addUser ---> name = gdai 普通参数(不同参数名)传递 : addUser ---> age = 24
@RequestParam
作为形参注解 ,用于绑定请求参数与方法内形参间的关系 。
其中,required
参数:是否为必传参数
defaultValue
参数:参数默认值
日期类型参数
在日常编程中,不同的系统中的日期类型数据也不尽相同:
1998-01-31
1998/01/31
31/01/1998
,等
在 SpringMVC
中,默认的日期格式为 yyyy/MM/dd
,可以自动的将字符串 yyyy/MM/dd
转化成 Date
类型的数据。如下所示
1 2 3 4 5 6 7 @RequestMapping("/dateParam") @ResponseBody public String dateParam (Date date) { System.out.println("日期类型集合参数传递 ---> " + date); return "{'module':'springmvc:dateParam()'}" ; } }
1 日期类型集合参数传递 ---> Sat Jan 31 00:00:00 CET 1998
我们也可以更改为其他的格式,则需要在 Date
形参前使用 @DateTimeFormat(pattern="")
注解指定日期格式。如下所示:
1 2 3 4 5 6 @RequestMapping("/dateParam") @ResponseBody public String dateParam (@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date date) { System.out.println("日期类型集合参数传递 ---> " + date); return "{'module':'springmvc:dateParam()'}" ; }
1 日期类型集合参数传递 ---> Sat Jan 31 01:59:59 CET 1998
实体类参数类型
更多情况下,我们使用实体类的参数作为形参传入方法中。在 SpringMVC
框架中,可以自动地将请求内容中的类的属性创建成一个实体类。如下所示:
1 2 3 4 5 6 @RequestMapping("/addBookEntity") @ResponseBody public String addBookEntity (Book book) { System.out.println("实体类型参数传递 ---> " + book); return "{'module':'springmvc:addBookEntity()'}" ; }
1 实体类型参数传递 ---> Book{bookId=null, bookName='test', price=100, stock=100}
如果在实体类中引用了其他类,在请求参数中可以使用 Xxx.xxx
对这个引用类进行创建并赋值,如下
1 实体类型参数传递 ---> Book{bookId=null, bookName='test', author=Person{name='gdai', birthday='1998-01-01'}}
数组类型参数
对于数组类型的接收,我们可以使用相同的参数名 来实现。如下所示
1 2 3 4 5 6 @RequestMapping("/arrayParam") @ResponseBody public String arrayParam (String[] args) { System.out.println("数组类型参数传递 ---> " + Arrays.toString(args)); return "{'module':'springmvc:arrayParam()'}" ; }
1 数组类型参数传递 ---> [val1, val2, val3]
集合类型参数
在集合类型(Set
、List
、Map
等类型)中,不能直接像数组类型那样直接使用相同的参数名来实现。因为我们如果不使用 @RequestParam
指定参数,SpringMVC
会将 List
视为一个没有实现类的接口,从而报错 Failed to instantiate [java.util.List]: Specified class is an interface
。
所以在使用集合类型参数时,我们需要使用 @RequestParam
指定参数 :
1 2 3 4 5 6 @RequestMapping("/listParam") @ResponseBody public String listParam (@RequestParam List<String> args) { System.out.println("集合类型参数传递 ---> " + args); return "{'module':'springmvc:listParam()'}" ; }
1 集合类型参数传递 ---> [val1, val2, val3]
JSON
集合类型参数
JSON
是一种轻量级的数据交换格式 ,易于人阅读和编写,可以在多种语言之间进行数据交换。JSON
遵循一定的语法词法结构来格式化数据,在数据的接收段也应该有对应的解释器来将这种格式化的数据转化成程序内的属性。所以
(1)我们首先需要在 Maven
中导入一个反序列化 JSON
的工具 jackson-databind
1 2 3 4 5 6 <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > 2.12.2</version > </dependency >
(2)在 SpringMVC
中开启自动地将 JSON
转化成 Java
的类属性:在 SpringMVCConfig.java
中添加 @EnableWebMvc
注解 :
注意 ⚠️:@EnableWebMvc
的功能有许多,将在后面的内容中介绍。
1 2 3 4 5 6 7 package fr.gdai.springmvc.config;@Configuration @ComponentScan("fr.gdai.springmvc.controller") @EnableWebMvc public class SpringMvcConfig {}
(3)在后端相关代码的形参前添加 @RequestBody
注解 来表明请求的参数是由请求体携带的
1 2 3 4 5 6 @RequestMapping("/listParamForJson") @ResponseBody public String listParamForJson (@RequestBody List<String> args) { System.out.println("JSON类型参数传递 ---> " + args); return "{'module':'listParamForJson()'}" ; }
(4)在 PostMan
中使用 JSON
格式传输请求数据:POST -> Body -> raw -> JSON
(5)测试结果
1 JSON类型参数传递 ---> [test1, test2, test3]
JSON
实体类型参数
对于 JSON
的实体类型参数,后端的相关代码与表单的形式类似:
1 2 3 4 5 6 @RequestMapping("/entityParamForJson") @ResponseBody public String entityParamForJson (@RequestBody Book book) { System.out.println("JSON实体类型参数传递 ---> " + book); return "{'module':'springmvc:entityParamForJson()'}" ; }
1 JSON实体类型参数传递 ---> Book{bookId=null, bookName='testForJSON', price=200, stock=200}
相同的,如果在实体类中引用了其他类,也可以在 JSON
格式中使用嵌套的形式赋值:
1 JSON实体类型参数传递 ---> Book{bookId=null, bookName='test', author=Person{name='gdai', birthday='1998-01-01'}}
JSON
实体集合类型参数
对于 JSON
的实体集合类型参数(List<Book> books
),后端的相关代码与表单的形式类似:
1 2 3 4 5 6 @RequestMapping("/entityListParamForJson") @ResponseBody public String entityListParamForJson (@RequestBody List<Book> books) { System.out.println("JSON实体类型集合参数传递 ---> " + books); return "{'module':'springmvc:entityListParamForJson()'}" ; }
1 2 3 4 JSON实体类型集合参数传递 ---> [Book{bookId=null, bookName='test01', price=100, stock=100}, Book{bookId=null, bookName='test02', price=200, stock=200}, Book{bookId=null, bookName='test03', price=300, stock=300}]
2 响应
所谓“响应”,就是将用户的请求经过处理后的结构反馈给用户。响应大题分为两种类型:响应页面 与响应数据 。
响应页面
我们在之前的请求部分中使用了 @ResponseBody
来将函数返回的内容作为响应体给用户浏览器。当我们想响应某个页面时,我们不需要使用该注解,而是将响应页面的相对 webapp
的地址作为字符串返回 。如下所示:
文件系统结构
1 2 3 4 5 6 7 8 9 10 |-- ... |-- java |-- fr.gdai |-- ... |-- controller |-- UserController.java |-- BookController.java |-- webapp |-- WEB-INF |-- index.jsp
1 2 3 4 5 6 7 8 9 10 @Controller @RequestMapping("/book") public class BookController { @RequestMapping("/jumpToIndex") public String jumpToIndex () { System.out.println("跳转到index" ); return "/index.jsp" ; } }
此时我们在浏览器中输入
1 http://localhost:8081/springmvc/book/jumpToIndex
即可跳转到 index.jsp
页面
响应数据
响应基本数据
对于响应数据 来讲,我们需要将返回的数据最为响应题反馈给用户浏览器,所以我们需要 @ResponseBody
注解。下面以字符串为例,其他基本数据类型同理:
1 2 3 4 5 6 7 8 9 10 11 @Controller @RequestMapping("/book") public class BookController { @RequestMapping("/toText") @ResponseBody public String toText () { System.out.println("返回纯文本信息" ); return "response text" ; } }
此时我们在浏览器中输入
1 http://localhost:8081/springmvc/book/toText
即可得到响应的数据:
响应数组/集合类型
1 2 3 4 5 6 7 8 9 10 11 @Controller @RequestMapping("/book") public class BookController { @RequestMapping("/toArray") @ResponseBody public int [] toText() { System.out.println("返回数组信息" ); return new int []{1 ,2 ,3 }; } }
此时我们在浏览器中输入
1 http://localhost:8081/springmvc/book/toArray
即可得到响应的数据:
响应实体类型数据
如果我们想响应一个实体类对象,只需要返回该实体类对象,并将其作为响应体 @ResponseBody
即可。SpringMVC
框架与 jackson-databind
可以自动将实体类对象的属性数据转化为 JSON
格式 ,如下所示:
1 2 3 4 5 6 7 8 9 10 11 @Controller @RequestMapping("/book") public class BookController { @RequestMapping("/toEntity") @ResponseBody public Book toEntity () { System.out.println("返回实体类信息" ); return new Book (null , "responseJSON" , 100 , 100 ); } }
此时我们在 PostMan
的 Get
请求中输入
1 http://localhost:8081/springmvc/book/toEntity
即可得到响应的数据:
1 2 3 4 5 6 { "bookId" : null , "bookName" : "responseJSON" , "price" : 100 , "stock" : 100 }
响应实体组数/集合类型数据
该例子我们结合了 MySQL
、MyBatis
、Spring
和 SpringMVC
框架对数据库进行查询,并响应查询结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Controller @RequestMapping("/book") public class BookController { @Autowired private BookServiceImpl bookService; @RequestMapping("/toEntityList") @ResponseBody public List<Book> showAllBook () { List<Book> books = bookService.showAllBooks(); System.out.println("返回实体集合信息" ); return books; } }
此时我们在 PostMan
的 Get
请求中输入
1 http://localhost:8081/springmvc/book/toEntityList
即可得到响应的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [ { "bookId" : 1 , "bookName" : "L_etranger" , "price" : 100 , "stock" : 100 } , { "bookId" : 2 , "bookName" : "Nausea" , "price" : 100 , "stock" : 200 } ]
3 数据的封装
通过前面的学习,我们可以看到:在响应请求时,响应数据分为很多类型:基本数据类型、数组、JSON
等,前端人员在拿到这些信息时需要对于这些信息的类型再做处理,显然这样又增加了前后端的耦合度。此时我们希望通过封装响应体中的响应数据 ,使得数据结构具有通用性。
我们创建一个结果类 Result
,其中声明若干属性:(该类的属性并不是固定的,根据需要增减)
<T> data
:用来存放请求处理后的结果
Integer code
:用来描述操作的种类
String msg
:用来存放返回到前端的消息
1 2 3 4 5 6 7 8 9 package fr.gdai.springmvc.controller;public class Result <T> { private Integer code; private T data; private String msg; }
1 2 3 4 5 6 7 8 9 10 11 12 13 package fr.gdai.springmvc.controller;public class ResultCode { public static final Integer INSERT_SUCC = 20011 ; public static final Integer DELETE_SUCC = 20021 ; public static final Integer UPDATE_SUCC = 20031 ; public static final Integer SELECT_SUCC = 20041 ; public static final Integer INSERT_ERR = 20010 ; public static final Integer DELETE_ERR = 20020 ; public static final Integer UPDATE_ERR = 20030 ; public static final Integer SELECT_ERR = 20040 ; }
最后,将我们所有的响应数据都通过 Result
类反馈给前端浏览器。我们以一个“结合了 MySQL
、 MyBatis
、Spring
和 SpringMVC
框架对数据库进行查询,并响应查询结果”的例子作为演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Controller @RequestMapping("/book") public class BookController { @Autowired private BookServiceImpl bookService; @RequestMapping("/toEntityList") @ResponseBody public Result showAllBook () { Integer code; String message; List<Book> books = bookService.showAllBooks(); if (books == null ){ code = ResultCode.SELECT_ERR; message = "查询结果为空,请重试" ; } else { code = ResultCode.SELECT_SUCC; message = "查询成功" ; } System.out.println("返回实体集合信息" ); return new Result (code, books, message); } }
此时我们在 PostMan
的 Get
请求中输入
1 http://localhost:8081/springmvc/book/toEntityList
即可得到响应的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "code" : 20041 , "data" : [ { "bookId" : 1 , "bookName" : "L_etranger" , "price" : 100 , "stock" : 100 } , { "bookId" : 2 , "bookName" : "Nausea" , "price" : 100 , "stock" : 200 } ] , "msg" : "查询成功" }
三、REST
1 REST
简介
REST
(Representation State Transfer),表现形式状态转换。通俗来讲就是:资源 在网络中以某种表现形式进行状态转移。分解开来:
Resource:资源,即数据(前面说过网络的核心)。比如 newsfeed,friends等;
Representational:某种表现形式,比如用 JSON,XML,JPEG等;
State Transfer:状态变化。通过HTTP动词实现。
传统风格的资源描述形式:
1 2 http://localhost:8081/user/getById?id=1 http://localhost:8081/user/saveUser
REST
风格的资源描述形式(RESTful ):
1 2 http://localhost:8081/user/1 http://localhost:8081/user
优点:
隐藏资源的访问行为 ,无法通过地址得知对资源是何种操作
书写简化
我们通过 REST
风格来描述资源,使得无法通过地址得知对资源是何种操作,那么我们又该如何确定对于资源的操作 呢?
一般来说,几乎所有的主流网络协议都有两个部分,一个是协议头 ,一个是协议体 。协议头中是协议自己要用的数据,协议体才是用户的数据。所以,协议头主要是用于协议的控制逻辑,而协议体则是业务逻辑。HTTP 的动词 (或是 Method)是在协议头中,所以,其主要用于控制逻辑。
下面是 HTTP 的动词 规范:常用的为 GET
、PUT
、DELETE
和 POST
。
方法
描述
幂等
GET
用于查询操作,对应于数据库的 select
操作
✔︎
PUT
用于所有的信息更新,对应于数据库的 update
操作
✔︎︎
DELETE
用于更新操作,对应于数据库的 delete
操作
✔︎︎
POST
用于新增操作,对应于数据库的 insert
操作
✘
HEAD
用于返回一个资源对象的“元数据”,或是用于探测 API 是否健康
✔︎
PATCH
用于局部信息的更新,对应于数据库的 update
操作
✘
OPTIONS
获取 API 的相关的信息。
✔︎
注意 ⚠️:
描述模块的名称通常使用复数,也就是使用复数的形式表示此类资源,而非单个资源。例如 users
、books
等
2 RESTful
风格的改写
我们按照之前介绍的传统风格的资源描述形式重新写一个 Controller
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package fr.gdai.springmvc.controller;@Controller public class TestController { @RequestMapping("/test/select") @ResponseBody public String select (int id) { System.out.println("test:select id=" + id); return "{'module':'test_select()'}" ; } @RequestMapping("/test/update") @ResponseBody public String update (@RequestBody User user) { System.out.println("test:update " + user); return "{'module':'test_update()'}" ; } @RequestMapping("/test/delete") @ResponseBody public String delete (int id) { System.out.println("test:delete id=" + id); return "{'module':'test_delete()'}" ; } @RequestMapping("/test/insert") @ResponseBody public String insert (@RequestBody User user) { System.out.println("test:insert " + user); return "{'module':'test_insert()'}" ; } }
现在我们就开始着手在我们已经掌握的传统风格的基础上,将其改写成 REST
风格:
关于 select(int id)
方法的改写
首先我们将 @RequestMapping()
内的 value
值 改为 "/users"
;
然后需要指定 @RequestMapping()
内的 method
值 ,从而设置它的请求行为(HTTP 动作)
对于请求体内传递的参数 ,与之前一样,我们任然使用 @RequestBody
来指定相关的参数
对于非请求体内传递的参数 ,如 select()
方法中的参数 id
,与之前不同的是,参数的传递使用的是路径的方式,即
所以,我们需要在方法的形参前使用 @PathVariable
注解 ,然后在请求路径中使用 {id}
与接收数据的形参
1 2 3 4 5 6 @RequestMapping(value="/tests", method=RequestMethod.POST) @ResponseBody public String select (int id) { System.out.println("test:select id=" + id); return "{'module':'test_select()'}" ; }
将其余方法都按照上述步骤更改为其对应的请求行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package fr.gdai.springmvc.controller;@Controller public class TestController { @RequestMapping(value = "/tests/{id}", method = RequestMethod.GET) @ResponseBody public String select (@PathVariable int id) { System.out.println("test:select id=" + id); return "{'module':'test_select()'}" ; } @RequestMapping(value = "/tests", method = RequestMethod.PUT) @ResponseBody public String update (@RequestBody User user) { System.out.println("test:update " + user); return "{'module':'test_update()'}" ; } @RequestMapping(value = "/tests/{id}", method = RequestMethod.DELETE) @ResponseBody public String delete (@PathVariable int id) { System.out.println("test:delete id=" + id); return "{'module':'test_delete()'}" ; } @RequestMapping(value = "/tests", method = RequestMethod.POST) @ResponseBody public String insert (@RequestBody User user) { System.out.println("test:insert " + user); return "{'module':'test_insert()'}" ; } }
@RequestBody
、@RequestParam
与 @PathVariable
区别
@RequestBody
用于接收请求体内的数据 ,通常是 JSON
数据;
@RequestParam
用于接收 url
地址传参 或表单传参 ;
@PathVariable
用于接收路径参数 ,使用 {参数名称}
描述路径参数 。
应用
后期开发中,发送请求参数超过一个 时,以 JSON
格式为主,@RequestBody
应用较广;
如果发送非 JSON
格式数据 ,选用 @RequestParam
接收请求参数;
采用 RESTful
进行开发,当发送请求参数较少 时(例如一个),可以采用 @PathVariable
接收请求路径参数,常用于传递 id
值。当发送请求参数超过一个 时,以 JSON
格式为主。
3 简化 RESTful
风格
我们之前已经成功的将传统风格的请求方式改写成了 RESTful
风格,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package fr.gdai.springmvc.controller;@Controller public class TestController { @RequestMapping(value = "/tests/{id}", method = RequestMethod.GET) @ResponseBody public String select (@PathVariable int id) { System.out.println("test:select id=" + id); return "{'module':'test_select()'}" ; } @RequestMapping(value = "/tests", method = RequestMethod.PUT) @ResponseBody public String update (@RequestBody User user) { System.out.println("test:update " + user); return "{'module':'test_update()'}" ; } @RequestMapping(value = "/tests/{id}", method = RequestMethod.DELETE) @ResponseBody public String delete (@PathVariable int id) { System.out.println("test:delete id=" + id); return "{'module':'test_delete()'}" ; } @RequestMapping(value = "/tests", method = RequestMethod.POST) @ResponseBody public String insert (@RequestBody User user) { System.out.println("test:insert " + user); return "{'module':'test_insert()'}" ; } }
但是我们发现,
对于每个方法,都需要分别配置 @RequestMapping
的属性,且 value
的前缀都是 /tests
,甚是麻烦。所以我们可以将其简化,在整个 Controller
类前,使用 @RequestMapping("/tests")
简化。
对于每个方法,都使用了响应体 @ResponseBody
来反馈用户浏览器,所以我们也将其置于整个 Controller
类前。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package fr.gdai.springmvc.controller;@Controller @RequestMapping("/tests") @ResponseBody public class TestController { @RequestMapping(value = "/{id}", method = RequestMethod.GET) public String select (@PathVariable int id) {...} @RequestMapping(method = RequestMethod.PUT) public String update (@RequestBody User user) {...} @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public String delete (@PathVariable int id) {...} @RequestMapping(method = RequestMethod.POST) public String insert (@RequestBody User user) {...} }
可也以使用 @RestController
注解进一步简化 @Controller + @ResponseBody
至此,我们的业务代码已经简化到只剩 @RequestMapping(..., method)
注解了。我们任然可以将 @RequestMapping(..., method)
简化成 @MethodMapping
,即 @GetMapping
、@PutMapping
、 @DeleteMapping
和 @PostMapping
等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package fr.gdai.springmvc.controller;@RestController @RequestMapping("/tests") public class TestController { @GetMapping("/{id}") public String select (@PathVariable int id) {...} @PutMapping public String update (@RequestBody User user) {...} @DeleteMapping("/{id}") public String delete (@PathVariable int id) {...} @PostMapping public String insert (@RequestBody User user) {...} }
四、拦截器
TODO
五、SSM
整合
1 文件体系
在本章节,我们不以实际的项目代码作为展示,只是将配置过程显示出来。首先我们先给出一个项目的基本文件体系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |-- pom.xml >-- target v-- src >-- test v-- main v-- java v-- fr.gdai.PORJECT_NAME v-- config |-- SpringConfig.java |-- SpringJdbcConfig.java |-- SpringMvcConfig.java |-- SpringMvcSupport.java |-- ServletConfig.java |-- MyBatisConfig.java |-- DataSourceConfig.java >-- domain >-- dao (接口, 关联MyBatis的Mapping文件) v-- service |-- XxxService.java >-- implement >-- controller >-- interceptor v-- resources |-- jdbc.properties v-- fr/gdai/springmvc/dao |-- XxxDao.xml (MyBatis的Mapping文件) v-- webapp >-- WEB-INF >-- pages >-- images >-- css >-- js |-- index.jsp
2 Maven
构建项目
我们使用 Maven
构建项目,在 pom.xml
文件中将打包方式更改为 war
:
1 <packaging > war</packaging >
添加依赖坐标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 4.3.5.RELEASE</version > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 3.1.0</version > <scope > provided</scope > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > 2.12.2</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.7</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis-spring</artifactId > <version > 1.3.1</version > </dependency > <dependency > <groupId > com.mchange</groupId > <artifactId > c3p0</artifactId > <version > 0.9.5.2</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 4.3.5.RELEASE</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 4.3.5.RELEASE</version > </dependency > </dependencies >
我们既可以使用单独的 tomcat
服务器,也可以使用 Maven
整合的 tomcat
服务器插件,配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <build > <plugins > <plugin > <groupId > org.apache.tomcat.maven</groupId > <artifactId > tomcat7-maven-plugin</artifactId > <version > 2.1</version > <configuration > <port > 端口号</port > <path > /项目根目录路径</path > <uriEncoding > utf-8</uriEncoding > </configuration > </plugin > </plugins > </build >
3 MyBatis
与数据库连接
因为本章节不考虑实际项目的数据,所以在此我们不提供数据库建表语句。但是我在下方给出一个数据库快速建表的在线工具,作者:程序员鱼皮
关于 MyBatis
的相关知识,参考以下链接:
下面我们开始设置数据库的连接:
jdbc.properties
我们使用 .properties
文件来配置 JDBC
连接信息:
${PROJECT_ROOT}/src/main/resources/jdbc.properties
1 2 3 4 jdbc.driver =com.mysql.jdbc.Driver jdbc.url =jdbc:mysql://localhost:3306/数据库名 jdbc.username =root jdbc.password =root
DataSourceConfig.java
fr.gdai.PROJECT_NAME.config.DataSourceConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package fr.gdai.ssm.config;@PropertySource("classpath:jdbc.properties") public class DataSourceConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String userName; @Value("${jdbc.password}") private String password; @Bean public DataSource getDataSource () throws PropertyVetoException { ComboPooledDataSource dataSource = new ComboPooledDataSource (); dataSource.setDriverClass(driver); dataSource.setJdbcUrl(url); dataSource.setUser(userName); dataSource.setPassword(password); return dataSource; } }
MyBatisConfig.java
fr.gdai.PROJECT_NAME.config.MyBatisConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package fr.gdai.ssm.config;public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactoryBean (DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setTypeAliasesPackage("fr.gdai.springmvc.domain" ); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; } @Bean public MapperScannerConfigurer mapperScannerConfigurer () { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer (); mapperScannerConfigurer.setBasePackage("fr.gdai.springmvc.dao" ); return mapperScannerConfigurer; } }
XxxDao.java
``fr.gdai.PROJECT_NAME.dao.XxxDao.java`
1 2 3 4 package fr.gdai.springmvc.dao;@Repository public interface XxxDao {...}
XxxDao.xml
注意 ⚠️:
要与 XxxDao.java
接口处于同一包下(即 XxxDao
接口的全类名和映射文件的命名空间 namespace
保持一致)
${PROJECT_ROOT}/src/main/resources/fr/gdai/PORJECT_NAME/dao/XxxDao.xml
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="fr.gdai.PORJECT_NAME.dao.XxxDao" > <select id ="methodOfInterface" resultType ="TYPE" > selece * from xxx </select > ... </mapper >
4 Spring
与 SpringMVC
配置
SpringConfig.java
我们统一将所有的配置都放在 config
包下:
fr.gdai.PORJECT_NAME.config.SpringConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 package fr.gdai.ssm.config;@Configuration @Import({DataSourceConfig.class, SpringJdbcConfig.class, MyBatisConfig.class}) @ComponentScan(value = "fr.gdai.ssm", excludeFilters = @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = Controller.class)) public class SpringConfig {}
SpringJdbcConfig.java
fr.gdai.PORJECT_NAME.config.SpringJdbcConfig.java
1 2 3 4 5 6 7 8 9 10 import org.springframework.jdbc.core.JdbcTemplate;public class SpringJdbcConfig { @Bean public static JdbcTemplate getJdbcTemplate (DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate (); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } }
SpringMvcConfig.java
fr.gdai.PORJECT_NAME.config.SpringMvcConfig.java
1 2 3 4 5 6 7 8 package fr.gdai.ssm.config;@Configuration @ComponentScan({"fr.gdai.springmvc.controller", "fr.gdai.springmvc.config"}) @EnableWebMvc public class SpringMvcConfig {}
SpringMvcSupport.java
fr.gdai.PORJECT_NAME.config.SpringMvcSupport.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package fr.gdai.ssm.config;@Configuration public class SpringMvcSupport extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/pages/**" ).addResourceLocations("/pages/" ); registry.addResourceHandler("/css/**" ).addResourceLocations("/css/" ); registry.addResourceHandler("/js/**" ).addResourceLocations("/js/" ); registry.addResourceHandler("/images/**" ).addResourceLocations("/images/" ); } }
ServletConfig.java
fr.gdai.PORJECT_NAME.config.ServletConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package fr.gdai.ssm.config;public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class []{SpringConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class []{SpringMvcConfig.class}; } @Override protected String[] getServletMappings() { return new String []{"/" }; } @Override protected Filter[] getServletFilters() { CharacterEncodingFilter filter = new CharacterEncodingFilter (); filter.setEncoding("utf-8" ); return new Filter []{filter}; } }