一、SSM整合
前面我们已经把
Mybatis
、
Spring
和
SpringMVC
三个框架进行了学习,今天主要的内容就是把这三个 框架整合在一起完成我们的业务功能开发,具体如何来整合,我们一步步来学习。
1.1 流程分析
(1)
创建工程
- 创建一个Maven的web工程
- pom.xml添加SSM需要的依赖jar包
- 编写Web项目的入口配置类,实现AbstractAnnotationConfigDispatcherServletInitializer
重写以下方法
getRootConfigClasses()
:返回
Spring
的配置类
->
需要
SpringConfig
配置类
getServletConfigClasses()
:返回
SpringMVC
的配置类
->
需要
SpringMvcConfig
配
置类
getServletMappings() :
设置
SpringMVC
请求拦截路径规则
getServletFilters()
:设置过滤器,解决
POST
请求中文乱码问题
(2)SSM
整合
[
重点是各个配置的编写
]
- SpringConfig
标识该类为配置类
@Configuration
扫描
Service
所在的包
@ComponentScan
在
Service
层要管理事务
@EnableTransactionManagement
读取外部的
properties
配置文件
@PropertySource
整合
Mybatis
需要引入
Mybatis
相关配置类
@Import
- 第三方数据源配置类 JdbcConfig
构建
DataSource
数据源,
DruidDataSouroce,
需要注入数据库连接四要素,
@Bean
@Value
构建平台事务管理器,
DataSourceTransactionManager,@Bean
- Mybatis配置类 MybatisConfig
构建
SqlSessionFactoryBean
并设置别名扫描与数据源,
@Bean
构建
MapperScannerConfigurer
并设置
DAO
层的包扫描
- SpringMvcConfig
标识该类为配置类
@Configuration
扫描
Controller
所在的包
@ComponentScan
开启
SpringMVC
注解支持
@EnableWebMvc
(3)
功能模块
[
与具体的业务模块有关
]
- 创建数据库表
- 根据数据库表创建对应的模型类
- 通过Dao层完成数据库表的增删改查(接口+自动代理)
- 编写Service层[Service接口+实现类]
@Service
@Transactional
整合
Junit
对业务层进行单元测试
@RunWith
@ContextConfiguration
@Test
- 编写Controller层
接收请求
@RequestMapping @GetMapping @PostMapping @PutMapping
@DeleteMapping
接收数据 简单、
POJO
、嵌套
POJO
、集合、数组、
JSON
数据类型
@RequestParam
@PathVariable
@RequestBody
转发业务层
@Autowired
响应结果
@ResponseBody
1.2 整合配置
掌握上述的知识点后,接下来,我们就可以按照上述的步骤一步步的来完成
SSM
的整合。
步骤
1
:创建
Maven
的
web
项目
可以使用
Maven
的骨架创建
步骤
2:
添加依赖
pom.xml
添加
SSM
所需要的依赖
jar
包
<?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.itheima
</groupId>
<artifactId>
springmvc_08_ssm
</artifactId>
9
<version>
1.0-SNAPSHOT
</version>
10
<packaging>
war
</packaging>
11
12
<dependencies>
13
<dependency>
14
<groupId>
org.springframework
</groupId>
15
<artifactId>
spring-webmvc
</artifactId>
16
<version>
5.2.10.RELEASE
</version>
17
</dependency>
18
19
<dependency>
20
<groupId>
org.springframework
</groupId>
21
<artifactId>
spring-jdbc
</artifactId>
22
<version>
5.2.10.RELEASE
</version>
23
</dependency>
24
25
<dependency>
26
<groupId>
org.springframework
</groupId>
27
<artifactId>
spring-test
</artifactId>
28
<version>
5.2.10.RELEASE
</version>
29
</dependency>
30
31
<dependency>
32
<groupId>
org.mybatis
</groupId>
33
<artifactId>
mybatis
</artifactId>
34
<version>
3.5.6
</version>
35
</dependency>
36
37
<dependency>
38
<groupId>
org.mybatis
</groupId>
39
<artifactId>
mybatis-spring
</artifactId>
40
<version>
1.3.0
</version>
41
</dependency>
42
43
<dependency>
44
<groupId>
mysql
</groupId>
45
<artifactId>
mysql-connector-java
</artifactId>
46
<version>
5.1.47
</version>
47
</dependency>
48
49
<dependency>
50
<groupId>
com.alibaba
</groupId>
51
<artifactId>
druid
</artifactId>
52
<version>
1.1.16
</version>
53
</dependency>
54
55
<dependency>
<groupId>
junit
</groupId>
<artifactId>
junit
</artifactId>
<version>
4.12
</version>
<scope>
test
</scope>
</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.9.0
</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>
org.apache.tomcat.maven
</groupId>
<artifactId>
tomcat7-maven-plugin
</artifactId>
<version>
2.1
</version>
<configuration>
<port>
80
</port>
<path>
/
</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
步骤
3:
创建项目包结构
- config目录存放的是相关的配置类
- controller编写的是Controller类
- dao存放的是Dao接口,因为使用的是Mapper接口代理方式,所以没有实现类包
- service存的是Service接口,impl存放的是Service实现类
- resources:存入的是配置文件,如Jdbc.properties
- webapp:目录可以存放静态资源
- test/java:存放的是测试类
步骤
4:
创建
SpringConfig
配置类
@Configuration
@ComponentScan
({
"com.itheima.service"
})
@PropertySource
(
"classpath:jdbc.properties"
)
@Import
({
JdbcConfig
.
class
,
MyBatisConfig
.
class
})
@EnableTransactionManagement
public class
SpringConfig
{
}
步骤
5:
创建
JdbcConfig
配置类
public class
JdbcConfig
{
@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 dataSource
(){
DruidDataSource dataSource
=
new
DruidDataSource
();
dataSource
.
setDriverClassName
(
driver
);
dataSource
.
setUrl
(
url
);
dataSource
.
setUsername
(
username
);
dataSource
.
setPassword
(
password
);
return
dataSource
;
}
@Bean
public
PlatformTransactionManager transactionManager
(
DataSource
dataSource
){
DataSourceTransactionManager ds
=
new
DataSourceTransactionManager
();
ds
.
setDataSource
(
dataSource
);
return
ds
;
}
}
步骤
6:
创建
MybatisConfig
配置类
public class
MyBatisConfig
{
@Bean
public
SqlSessionFactoryBean sqlSessionFactory
(
DataSource dataSource
){
SqlSessionFactoryBean factoryBean
=
new
SqlSessionFactoryBean
();
factoryBean
.
setDataSource
(
dataSource
);
factoryBean
.
setTypeAliasesPackage
(
"com.itheima.domain"
);
return
factoryBean
;
}
@Bean
public
MapperScannerConfigurer mapperScannerConfigurer
(){
MapperScannerConfigurer msc
=
new
MapperScannerConfigurer
();
msc
.
setBasePackage
(
"com.itheima.dao"
);
return
msc
;
}
}
步骤
7:
创建
jdbc.properties
在
resources
下提供
jdbc.properties,
设置数据库连接四要素
jdbc.driver
=
com.mysql.jdbc.Driver
jdbc.url
=
jdbc
:
mysql
:
//localhost
:
3306/ssm_db
jdbc.username
=
root
jdbc.password
=
root
步骤
8:
创建
SpringMVC
配置类
@Configuration
@ComponentScan
(
"com.itheima.controller"
)
@EnableWebMvc
public class
SpringMvcConfig
{
}
步骤
9:
创建
Web
项目入口配置类
public class
ServletConfig
extends
AbstractAnnotationConfigDispatcherServletInitializer
{
//
加载
Spring
配置类
protected
Class
<?>
[]
getRootConfigClasses
() {
return new
Class
[]{
SpringConfig
.
class
};
}
//
加载
SpringMVC
配置类
protected
Class
<?>
[]
getServletConfigClasses
() {
return new
Class
[]{
SpringMvcConfig
.
class
};
}
//
设置
SpringMVC
请求地址拦截规则
protected
String
[]
getServletMappings
() {
return new
String
[]{
"/"
};
}
//
设置
post
请求中文乱码过滤器
@Override
protected
Filter
[]
getServletFilters
() {
CharacterEncodingFilter filter
=
new
CharacterEncodingFilter
();
filter
.
setEncoding
(
"utf-8"
);
return new
Filter
[]{
filter
};
}
}
至此
SSM
整合的环境就已经搭建好了。在这个环境上,我们如何进行功能模块的开发呢
?
1.3
功能模块开发
需求
:
对表
tbl_book
进行新增、修改、删除、根据
ID
查询和查询所有
步骤
1:
创建数据库及表
create
database ssm_db character
set
utf8;
use ssm_db;
create table
tbl_book(
id
int
primary key auto_increment,
type
varchar
(
20
),
name
varchar
(
50
),
description
varchar
(
255
)
)
insert into
`tbl_book`(`id`,`type`,`name`,`description`)
values
(
1
,
'
计算机理
论
'
,
'Spring
实战 第五版
'
,
'Spring
入门经典教程,深入理解
Spring
原理技术内幕
'
),(
2
,
'
计算机理
论
'
,
'Spring 5
核心原理与
30
个类手写实践
'
,
'
十年沉淀之作,手写
Spring
精华思想
'
),(
3
,
'
计算机理
论
'
,
'Spring 5
设计模式
'
,
'
深入
Spring
源码刨析
Spring
源码中蕴含的
10
大设计模式
'
),(
4
,
'
计算机
理论
'
,
'Spring MVC+Mybatis
开发从入门到项目实战
'
,
'
全方位解析面向
Web
应用的轻量级框架,带你
成为
Spring MVC
开发高手
'
),(
5
,
'
计算机理论
'
,
'
轻量级
Java Web
企业应用实战
'
,
'
源码级刨析
Spring
框架,适合已掌握
Java
基础的读者
'
),(
6
,
'
计算机理论
'
,
'Java
核心技术 卷Ⅰ 基础知识
(
原书
第
11
版
)'
,
'Core Java
第
11
版,
Jolt
大奖获奖作品,针对
Java SE9
、
10
、
11
全面更新
'
),(
7
,
'
计算
机理论
'
,
'
深入理解
Java
虚拟机
'
,
'5
个纬度全面刨析
JVM,
大厂面试知识点全覆盖
'
),(
8
,
'
计算机理
论
'
,
'Java
编程思想
(
第
4
版
)'
,
'Java
学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉
'
),
(
9
,
'
计算机理论
'
,
'
零基础学
Java(
全彩版
)'
,
'
零基础自学编程的入门图书,由浅入深,详解
Java
语言
的编程思想和核心技术
'
),(
10
,
'
市场营销
'
,
'
直播就这么做
:
主播高效沟通实战指南
'
,
'
李子柒、李佳
奇、薇娅成长为网红的秘密都在书中
'
),(
11
,
'
市场营销
'
,
'
直播销讲实战一本通
'
,
'
和秋叶一起学系列网
络营销书籍
'
),(
12
,
'
市场营销
'
,
'
直播带货
:
淘宝、天猫直播从新手到高手
'
,
'
一本教你如何玩转直播的
书,
10
堂课轻松实现带货月入
3W+'
);
步骤
2:
编写模型类
public class
Book
{
private
Integer
id
;
private
String
type
;
private
String
name
;
private
String
description
;
//getter...setter...toString
省略
}
步骤
3:
编写
Dao
接口
public interface
BookDao
{
// @Insert("insert into tbl_book values(null,#{type},#{name},#
{description})")
@Insert
(
"insert into tbl_book (type,name,description) values(#{type},#
{name},#{description})"
)
public
void
save
(
Book book
);
@Update
(
"update tbl_book set type = #{type}, name = #{name}, description
= #{description} where id = #{id}"
)
public
void
update
(
Book book
);
@Delete
(
"delete from tbl_book where id = #{id}"
)
public
void
delete
(
Integer
id
);
@Select
(
"select * from tbl_book where id = #{id}"
)
public
Book getById
(
Integer
id
);
@Select
(
"select * from tbl_book"
)
public
List
<
Book
>
getAll
();
}
步骤
4:
编写
Service
接口和实现类
@Transactional
public interface
BookService
{
/**
*
保存
* @param book
* @return
*/
public
boolean
save
(
Book book
);
/**
*
修改
* @param book
* @return
*/
public
boolean
update
(
Book book
);
/**
*
按
id
删除
* @param id
* @return
*/
public
boolean
delete
(
Integer
id
);
/**
*
按
id
查询
* @param id
* @return
*/
public
Book getById
(
Integer
id
);
/**
*
查询全部
* @return
*/
35
public
List
<
Book
>
getAll
();
36
}
1
@Service
2
public class
BookServiceImpl
implements
BookService
{
3
@Autowired
4
private
BookDao bookDao
;
5
6
public
boolean
save
(
Book book
) {
7
bookDao
.
save
(
book
);
8
return
true
;
9
}
10
11
public
boolean
update
(
Book book
) {
12
bookDao
.
update
(
book
);
13
return
true
;
14
}
15
16
public
boolean
delete
(
Integer
id
) {
17
bookDao
.
delete
(
id
);
18
return
true
;
19
}
20
21
public
Book getById
(
Integer
id
) {
22
return
bookDao
.
getById
(
id
);
23
}
24
25
public
List
<
Book
>
getAll
() {
26
return
bookDao
.
getAll
();
27
}
28
}
说明
:
- bookDao在Service中注入的会提示一个红线提示,为什么呢?
BookDao
是一个接口,没有实现类,接口是不能创建对象的,所以最终注入的应该是代理对象
代理对象是由
Spring
的
IOC
容器来创建管理的
IOC
容器又是在
Web
服务器启动的时候才会创建
IDEA
在检测依赖关系的时候,没有找到适合的类注入,所以会提示错误提示
但是程序运行的时候,代理对象就会被创建,框架会使用
DI
进行注入,所以程序运行无影响。
- 如何解决上述问题?
可以不用理会,因为运行是正常的
设置错误提示级别
步骤
5:
编写
Contorller
类
@RestController
@RequestMapping
(
"/books"
)
public class
BookController
{
@Autowired
private
BookService bookService
;
@PostMapping
public
boolean
save
(
@RequestBody
Book book
) {
return
bookService
.
save
(
book
);
}
@PutMapping
public
boolean
update
(
@RequestBody
Book book
) {
return
bookService
.
update
(
book
);
}
@DeleteMapping
(
"/{id}"
)
public
boolean
delete
(
@PathVariable
Integer
id
) {
return
bookService
.
delete
(
id
);
}
@GetMapping
(
"/{id}"
)
public
Book getById
(
@PathVariable
Integer
id
) {
return
bookService
.
getById
(
id
);
}
@GetMapping
public
List
<
Book
>
getAll
() {
return
bookService
.
getAll
();
}
}
对于图书模块的增删改查就已经完成了编写,我们可以从后往前写也可以从前往后写,最终只需要能 把功能实现即可。
接下来我们就先把业务层的代码使用
Spring
整合
Junit
的知识点进行单元测试
:
1.4 单元测试
步骤1:新建测试类
@RunWith
(
SpringJUnit4ClassRunner
.
class
)
@ContextConfiguration
(
classes
=
SpringConfig
.
class
)
public class
BookServiceTest
{
}
步骤2:注入Service类
@RunWith
(
SpringJUnit4ClassRunner
.
class
)
@ContextConfiguration
(
classes
=
SpringConfig
.
class
)
public class
BookServiceTest
{
@Autowired
private
BookService bookService
;
}
步骤3:编写测试方法
我们先来对查询进行单元测试。
@RunWith
(
SpringJUnit4ClassRunner
.
class
)
@ContextConfiguration
(
classes
=
SpringConfig
.
class
)
public class
BookServiceTest
{
@Autowired
private
BookService bookService
;
@Test
public
void
testGetById
(){
Book book
=
bookService
.
getById
(
1
);
System
.
out
.
println
(
book
);
}
@Test
public
void
testGetAll
(){
List
<
Book
>
all
=
bookService
.
getAll
();
System
.
out
.
println
(
all
);
}
}
根据
ID
查询,测试的结果为
:
查询所有,测试的结果为
:
1.5 PostMan测试
新增
http://localhost/books
{
"type"
:
"
类别测试数据
"
,
"name"
:
"
书名测试数据
"
,
"description"
:
"
描述测试数据
"
}
修改
http://localhost/books
{
"id"
:
13
,
"type"
:
"
类别测试数据
"
,
"name"
:
"
书名测试数据
"
,
"description"
:
"
描述测试数据
"
}
删除
http://localhost/books/14
查询单个
http://localhost/books/1
查询所有
http://localhost/books
二、统一结果封装
2.1 表现层与前端数据传输协议定义
SSM
整合以及功能模块开发完成后,接下来,我们在上述案例的基础上分析下有哪些问题需要我们去解 决下。首先第一个问题是:
- 在Controller层增删改返回给前端的是boolean类型数据
- 在Controller层查询单个返回给前端的是对象
在
Controller
层查询所有返回给前端的是集合对象
目前我们就已经有三种数据类型返回给前端,如果随着业务的增长,我们需要返回的数据类型会越来 越多。对于前端开发人员在解析数据的时候就比较凌乱了,所以对于前端来说,如果后台能够返回一 个统一的数据结果,前端在解析的时候就可以按照一种方式进行解析。开发就会变得更加简单。
所以我们就想能不能将返回结果的数据进行统一,具体如何来做,大体的思路为
:
- 为了封装返回的结果数据:创建结果模型类,封装数据到data属性中
- 为了封装返回的数据是何种操作及是否操作成功:封装操作结果到code属性中
- 操作失败后为了封装返回的错误信息:封装特殊消息到message(msg)属性中
根据分析,我们可以设置统一数据返回结果类
public class
Result
{
private
Object
data
;
private
Integer
code
;
private
String
msg
;
}
注意
:
Result
类名及类中的字段并不是固定的,可以根据需要自行增减提供若干个构造方法,方便操 作。
2.2 表现层与前端数据传输协议实现
前面我们已经分析了如何封装返回结果数据,具体在项目中该如何实现,我们通过个例子来操作一把
1 .环境准备
- 创建一个Web的Maven项目
- pom.xml添加SSM整合所需jar包
- 创建对应的配置类
- 编写Controller、Service接口、Service实现类、Dao接口和模型类
- resources下提供jdbc.properties配置文件
因为这个项目环境的内容和
SSM
整合的内容是一致的,所以我们就不在把代码粘出来了,大家在练习的 时候可以在前面整合的例子案例环境下,进行本节内容的开发。
最终创建好的项目结构如下
:
2. 结果封装
对于结果封装,我们应该是在表现层进行处理,所以我们把结果类放在
controller
包下,当然你也
可以放在
domain
包,这个都是可以的,具体如何实现结果封装,具体的步骤为
:
步骤
1:
创建
Result
类
public class
Result
{
//
描述统一格式中的数据
private
Object
data
;
//
描述统一格式中的编码,用于区分操作,可以简化配置
0
或
1
表示成功失败
private
Integer
code
;
//
描述统一格式中的消息,可选属性
private
String
msg
;
public
Result
() {
}
//
构造方法是方便对象的创建
public
Result
(
Integer
code
,
Object
data
) {
this
.
data
=
data
;
this
.
code
=
code
;
}
//
构造方法是方便对象的创建
public
Result
(
Integer
code
,
Object
data
,
String
msg
) {
this
.
data
=
data
;
this
.
code
=
code
;
this
.
msg
=
msg
;
}
//setter...getter...
省略
}
步骤
2:
定义返回码
Code
类
//
状态码
public class
Code
{
public static final
Integer
SAVE_OK
=
20011
;
public static final
Integer
DELETE_OK
=
20021
;
public static final
Integer
UPDATE_OK
=
20031
;
public static final
Integer
GET_OK
=
20041
;
public static final
Integer
SAVE_ERR
=
20010
;
public static final
Integer
DELETE_ERR
=
20020
;
public static final
Integer
UPDATE_ERR
=
20030
;
public static final
Integer
GET_ERR
=
20040
;
}
注意
:
code
类中的常量设计也不是固定的,可以根据需要自行增减,例如将查询再进行细分为
GET_OK,GET_ALL_OK,GET_PAGE_OK
等。
骤
3:
修改
Controller
类的返回值
//
统一每一个控制器方法返回值
@RestController
@RequestMapping
(
"/books"
)
public class
BookController
{
@Autowired
private
BookService bookService
;
@PostMapping
public
Result save
(
@RequestBody
Book book
) {
boolean
flag
=
bookService
.
save
(
book
);
return new
Result
(
flag
?
Code
.
SAVE_OK
:
Code
.
SAVE_ERR
,
flag
);
}
@PutMapping
public
Result update
(
@RequestBody
Book book
) {
boolean
flag
=
bookService
.
update
(
book
);
return new
Result
(
flag
?
Code
.
UPDATE_OK
:
Code
.
UPDATE_ERR
,
flag
);
}
@DeleteMapping
(
"/{id}"
)
public
Result delete
(
@PathVariable
Integer
id
) {
boolean
flag
=
bookService
.
delete
(
id
);
return new
Result
(
flag
?
Code
.
DELETE_OK
:
Code
.
DELETE_ERR
,
flag
);
}
@GetMapping
(
"/{id}"
)
public
Result getById
(
@PathVariable
Integer
id
) {
Book book
=
bookService
.
getById
(
id
);
Integer
code
=
book
!=
null
?
Code
.
GET_OK
:
Code
.
GET_ERR
;
String
msg
=
book
!=
null
?
""
:
"
数据查询失败,请重试!
"
;
return new
Result
(
code
,
book
,
msg
);
}
@GetMapping
public
Result getAll
() {
List
<
Book
>
bookList
=
bookService
.
getAll
();
Integer
code
=
bookList
!=
null
?
Code
.
GET_OK
:
Code
.
GET_ERR
;
String
msg
=
bookList
!=
null
?
""
:
"
数据查询失败,请重试!
"
;
return new
Result
(
code
,
bookList
,
msg
);
}
}
步骤
4:
启动服务测试
至此,我们的返回结果就已经能以一种统一的格式返回给前端。前端根据返回的结果,先从中获取
code
,
根据
code
判断,如果成功则取
data
属性的值,如果失败,则取
msg
中的值做提示。
三、统一异常处理
3.1 问题描述
在讲解这一部分知识点之前,我们先来演示个效果,修改
BookController
类的
getById
方法
1
@GetMapping
(
"/{id}"
)
2
public
Result
getById
(
@PathVariable
Integer
id
) {
3
//
手动添加一个错误信息
4
if
(
id
==
1
){
5
int
i
=
1
/
0
;
6
}
7
Book book
=
bookService
.
getById
(
id
);
8
Integer
code
=
book
!=
null
?
Code
.
GET_OK
:
Code
.
GET_ERR
;
9
String
msg
=
book
!=
null
?
""
:
"
数据查询失败,请重试!
"
;
10
return new
Result
(
code
,
book
,
msg
);
11
}
重新启动运行项目,使用
PostMan
发送请求,当传入的
id
为
1
,则会出现如下效果:
前端接收到这个信息后和之前我们约定的格式不一致,这个问题该如何解决
?
在解决问题之前,我们先来看下异常的种类及出现异常的原因
:
- 框架内部抛出的异常:因使用不合规导致
- 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
- 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
- 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
- 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)
看完上面这些出现异常的位置,你会发现,在我们开发的任何一个位置都有可能出现异常,而且这些 异常是不能避免的。所以我们就得将异常进行处理。
思考
1.
各个层级均出现异常,异常处理代码书写在哪一层
?
所有的异常均抛出到表现层进行处理
2.
异常的种类很多,表现层如何将所有的异常都处理到呢
?
异常分类
3.
表现层处理异常,每个方法中单独书写,代码书写量巨大且意义不强,如何解决
?
AOP
对于上面这些问题及解决方案,
SpringMVC
已经为我们提供了一套解决方案
:
- 异常处理器:
集中的、统一的处理项目中出现的异常。
3.2 异常处理器的使用
1. 环境准备
- 创建一个Web的Maven项目
- pom.xml添加SSM整合所需jar包
- 创建对应的配置类
- 编写Controller、Service接口、Service实现类、Dao接口和模型类
- resources下提供jdbc.properties配置文件
内容参考前面的项目或者直接使用前面的项目进行本节内容的学习。
最终创建好的项目结构如下
:
2. 使用步骤
步骤1:创建异常处理器类
//@RestControllerAdvice
用于标识当前类为
REST
风格对应的异常处理器
@RestControllerAdvice
public class
ProjectExceptionAdvice
{
//
除了自定义的异常处理器,保留对
Exception
类型的异常处理,用于处理非预期的异常
@ExceptionHandler
(
Exception
.
class
)
public
void
doException
(
Exception ex
){
System
.
out
.
println
(
"
嘿嘿
,
异常你哪里跑!
"
)
}
}
确保
SpringMvcConfig
能够扫描到异常处理器类
步骤
2:
让程序抛出异常
修改
BookController
的
getById
方法,添加
int i = 1/0
.
@GetMapping
(
"/{id}"
)
public
Result
getById
(
@PathVariable
Integer
id
) {
int
i
=
1
/
0
;
Book book
=
bookService
.
getById
(
id
);
Integer
code
=
book
!=
null
?
Code
.
GET_OK
:
Code
.
GET_ERR
;
String
msg
=
book
!=
null
?
""
:
"
数据查询失败,请重试!
"
;
return new
Result
(
code
,
book
,
msg
);
}
步骤
3:
运行程序,测试
说明异常已经被拦截并执行了
doException
方法。
异常处理器类返回结果给前端
//@RestControllerAdvice
用于标识当前类为
REST
风格对应的异常处理器
@RestControllerAdvice
public class
ProjectExceptionAdvice
{
//
除了自定义的异常处理器,保留对
Exception
类型的异常处理,用于处理非预期的异常
@ExceptionHandler
(
Exception
.
class
)
public
Result doException
(
Exception ex
){
System
.
out
.
println
(
"
嘿嘿
,
异常你哪里跑!
"
)
return new
Result
(
666
,
null
,
"
嘿嘿
,
异常你哪里跑!
"
);
}
}
启动运行程序,测试
至此,就算后台执行的过程中抛出异常,最终也能按照我们和前端约定好的格式返回给前端。
知识点
1
:
@RestControllerAdvice
说明
:
此注解自带
@ResponseBody
注解与
@Component
注解,具备对应的功能
知识点
2
:
@ExceptionHandler
说明:
此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常
3.3 项目异常处理方案
1. 异常分类
异常处理器我们已经能够使用了,那么在咱们的项目中该如何来处理异常呢
?
因为异常的种类有很多,如果每一个异常都对应一个
@ExceptionHandler
,那得写多少个方法来处
理各自的异常,所以我们在处理异常之前,需要对异常进行一个分类
:
- 业务异常(BusinessException)
规范的用户行为产生的异常
用户在页面输入内容的时候未按照指定格式进行数据填写,如在年龄框输入的是字符串
不规范的用户行为操作产生的异常
如用户故意传递错误数据
系统异常(
SystemException
)
项目运行过程中可预计但无法避免的异常
比如数据库或服务器宕机
其他异常(
Exception
)
编程人员未预期到的异常,如
:
用到的文件不存在
将异常分类以后,针对不同类型的异常,要提供具体的解决方案
:
2 . 异常解决方案
- 业务异常(BusinessException)
发送对应消息传递给用户,提醒规范操作
大家常见的就是提示用户名已存在或密码格式不正确等
- 系统异常(SystemException)
发送固定消息传递给用户,安抚用户
系统繁忙,请稍后再试
系统正在维护升级,请稍后再试
系统出问题,请联系系统管理员等
发送特定消息给运维人员,提醒维护
可以发送短信、邮箱或者是公司内部通信软件
记录日志
发消息和记录日志对用户来说是不可见的,属于后台程序
- 其他异常(Exception)
发送固定消息传递给用户,安抚用户
发送特定消息给编程人员,提醒维护(纳入预期范围内)
一般是程序没有考虑全,比如未做非空校验等
记录日志
3. 异常解决方案的具体实现
思路
:
1.
先通过自定义异常,完成
BusinessException
和
SystemException
的定义
2.
将其他异常包装成自定义异常类型
3.
在异常处理器类中对不同的异常进行处理
步骤
1:
自定义异常类
//
自定义异常处理器,用于封装异常信息,对异常进行分类
public class
SystemException
extends
RuntimeException
{
private
Integer
code
;
public
Integer
getCode
() {
return
code
;
}
public
void
setCode
(
Integer
code
) {
this
.
code
=
code
;
}
public
SystemException
(
Integer
code
,
String
message
) {
super
(
message
);
this
.
code
=
code
;
}
public
SystemException
(
Integer
code
,
String
message
,
Throwable cause
) {
super
(
message
,
cause
);
this
.
code
=
code
;
}
}
//
自定义异常处理器,用于封装异常信息,对异常进行分类
public class
BusinessException
extends
RuntimeException
{
private
Integer
code
;
public
Integer
getCode
() {
return
code
;
}
public
void
setCode
(
Integer
code
) {
this
.
code
=
code
;
}
public
BusinessException
(
Integer
code
,
String
message
) {
super
(
message
);
this
.
code
=
code
;
}
public
BusinessException
(
Integer
code
,
String
message
,
Throwable cause
) {
super
(
message
,
cause
);
this
.
code
=
code
;
}
}
说明
:
- 让自定义异常类继承RuntimeException的好处是,后期在抛出这两个异常的时候,就不用在
try...catch...
或
throws
了
- 自定义异常类中添加code属性的原因是为了更好的区分异常是来自哪个业务的
步骤
2:
将其他异常包成自定义异常
假如在
BookServiceImpl
的
getById
方法抛异常了,该如何来包装呢
?
public
Book
getById
(
Integer
id
) {
//
模拟业务异常,包装成自定义异常
if
(
id
==
1
){
throw new
BusinessException
(
Code
.
BUSINESS_ERR
,
"
请不要使用你的技术挑战我的
耐性
!"
);
}
//
模拟系统异常,将可能出现的异常进行包装,转换成自定义异常
try
{
int
i
=
1
/
0
;
}
catch
(
Exception e
){
throw new
SystemException
(
Code
.
SYSTEM_TIMEOUT_ERR
,
"
服务器访问超时,请重
试
!"
,
e
);
}
return
bookDao
.
getById
(
id
);
}
具体的包装方式有:
- 方式一: try{}catch(){}在catch中重新throw我们自定义异常即可。
- 方式二:直接throw自定义异常即可
上面为了使
code
看着更专业些,我们在
Code
类中再新增需要的属性
//
状态码
public class
Code
{
public static final
Integer
SAVE_OK
=
20011
;
public static final
Integer
DELETE_OK
=
20021
;
public static final
Integer
UPDATE_OK
=
20031
;
public static final
Integer
GET_OK
=
20041
;
public static final
Integer
SAVE_ERR
=
20010
;
public static final
Integer
DELETE_ERR
=
20020
;
public static final
Integer
UPDATE_ERR
=
20030
;
public static final
Integer
GET_ERR
=
20040
;
public static final
Integer
SYSTEM_ERR
=
50001
;
public static final
Integer
SYSTEM_TIMEOUT_ERR
=
50002
;
public static final
Integer
SYSTEM_UNKNOW_ERR
=
59999
;
public static final
Integer
BUSINESS_ERR
=
60002
;
}
步骤
3:
处理器类中处理自定义异常
//@RestControllerAdvice
用于标识当前类为
REST
风格对应的异常处理器
@RestControllerAdvice
public class
ProjectExceptionAdvice
{
//@ExceptionHandler
用于设置当前处理器类对应的异常类型
@ExceptionHandler
(
SystemException
.
class
)
public
Result doSystemException
(
SystemException ex
){
//
记录日志
//
发送消息给运维
//
发送邮件给开发人员
,ex
对象发送给开发人员
return new
Result
(
ex
.
getCode
(),
null
,
ex
.
getMessage
());
}
@ExceptionHandler
(
BusinessException
.
class
)
public
Result doBusinessException
(
BusinessException ex
){
return new
Result
(
ex
.
getCode
(),
null
,
ex
.
getMessage
());
}
//
除了自定义的异常处理器,保留对
Exception
类型的异常处理,用于处理非预期的异常
@ExceptionHandler
(
Exception
.
class
)
public
Result doOtherException
(
Exception ex
){
//
记录日志
//
发送消息给运维
//
发送邮件给开发人员
,ex
对象发送给开发人员
return new
Result
(
Code
.
SYSTEM_UNKNOW_ERR
,
null
,
"
系统繁忙,请稍后再试!
"
);
}
}
步骤
4:
运行程序
根据
ID
查询,
如果传入的参数为
1
,会报
BusinessException
如果传入的是其他参数,会报
SystemException
对于异常我们就已经处理完成了,不管后台哪一层抛出异常,都会以我们与前端约定好的方式进行返 回,前端只需要把信息获取到,根据返回的正确与否来展示不同的内容即可。
小结
以后项目中的异常处理方式为
:
四、前后台协议联调
4.1 环境准备
- 创建一个Web的Maven项目
- pom.xml添加SSM整合所需jar包
- 创建对应的配置类
- 编写Controller、Service接口、Service实现类、Dao接口和模型类
- resources下提供jdbc.properties配置文件
内容参考前面的项目或者直接使用前面的项目进行本节内容的学习。
最终创建好的项目结构如下
:
1.
将
资料
\SSM
功能页面
下面的静态资源拷贝到
webapp
下。
2.
因为添加了静态资源,
SpringMVC
会拦截,所有需要在
SpringConfig
的配置类中将静态资源进
行放行。
- 新建SpringMvcSupport
@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
(
"/plugins/**"
).
addResourceLocations
(
"/plugins
/"
);
}
}
- 在SpringMvcConfig中扫描SpringMvcSupport
@Configuration
@ComponentScan
({
"com.itheima.controller"
,
"com.itheima.config"
})
@EnableWebMvc
public class
SpringMvcConfig
{
}
接下来我们就需要将所有的列表查询、新增、修改、删除等功能一个个来实现下。
4.2 列表功能
需求
:
页面加载完后发送异步请求到后台获取列表数据进行展示。
1.
找到页面的钩子函数,
created()
2.
created()
方法中调用了
this.getAll()
方法
3.
在
getAll()
方法中使用
axios
发送异步请求从后台获取数据
4.
访问的路径为
http://localhost/books
5.
返回数据
返回数据
res.data
的内容如下
:
{
"data"
: [
{
"id"
:
1
,
"type"
:
"
计算机理论
"
,
"name"
:
"Spring
实战 第五版
"
,
"description"
:
"Spring
入门经典教程,深入理解
Spring
原理技术内幕
"
},
{
"id"
:
2
,
"type"
:
"
计算机理论
"
,
"name"
:
"Spring 5
核心原理与
30
个类手写实践
"
,
"description"
:
"
十年沉淀之作,手写
Spring
精华思想
"
},
...
],
"code"
:
20041
,
"msg"
:
""
}
发送方式
:
getAll
() {
//
发送
ajax
请求
axios
.
get
(
"/books"
).
then
((
res
)
=>
{
this
.
dataList
=
res
.
data
.
data
;
});
}
4.3 添加功能
需求
:
完成图片的新增功能模块
1.
找到页面上的
新建
按钮,按钮上绑定了
@click="handleCreate()"
方法
2.
在
method
中找到
handleCreate
方法,方法中打开新增面板
3.
新增面板中找到
确定
按钮
,
按钮上绑定了
@click="handleAdd()"
方法
4.
在
method
中找到
handleAdd
方法
5.
在方法中发送请求和数据,响应成功后将新增面板关闭并重新查询数据
handleCreate
打开新增面板
handleCreate
() {
this
.
dialogFormVisible
=
true
;
},
handleAdd
方法发送异步请求并携带数据
handleAdd
() {
//
发送
ajax
请求
//this.formData
是表单中的数据,最后是一个
json
数据
axios
.
post
(
"/books"
,
this
.
formData
).
then
((
res
)
=>
{
this
.
dialogFormVisible
=
false
;
this
.
getAll
();
});
}
4.4 添加功能状态处理
基础的新增功能已经完成,但是还有一些问题需要解决下
:
需求
:
新增成功是关闭面板,重新查询数据,那么新增失败以后该如何处理
?
1.
在
handlerAdd
方法中根据后台返回的数据来进行不同的处理
2.
如果后台返回的是成功,则提示成功信息,并关闭面板
3.
如果后台返回的是失败,则提示错误信息
(1)
修改前端页面
1
handleAdd
() {
2
//
发送
ajax
请求
3
axios
.
post
(
"/books"
,
this
.
formData
).
then
((
res
)
=>
{
4
//
如果操作成功,关闭弹层,显示数据
5
if
(
res
.
data
.
code
==
20011
){
6
this
.
dialogFormVisible
=
false
;
7
this
.
$message
.
success
(
"
添加成功
"
);
8
}
else if
(
res
.
data
.
code
==
20010
){
9
this
.
$message
.
error
(
"
添加失败
"
);
10
}
else
{
11
this
.
$message
.
error
(
res
.
data
.
msg
);
12
}
13
}).
finally
(()
=>
{
14
this
.
getAll
();
15
});
16
}
(2)
后台返回操作结果,将
Dao
层的增删改方法返回值从
void
改成
int
1
public interface
BookDao
{
2
3
// @Insert("insert into tbl_book values(null,#{type},#{name},#
{description})")
4
@Insert
(
"insert into tbl_book (type,name,description) values(#{type},#
{name},#{description})"
)
5
public
int
save
(
Book book
);
6
7
@Update
(
"update tbl_book set type = #{type}, name = #{name}, description
= #{description} where id = #{id}"
)
8
public
int
update
(
Book book
);
9
10
@Delete
(
"delete from tbl_book where id = #{id}"
)
11
public
int
delete
(
Integer
id
);
12
13
@Select
(
"select * from tbl_book where id = #{id}"
)
14
public
Book getById
(
Integer
id
);
15
16
@Select
(
"select * from tbl_book"
)
17
public
List
<
Book
>
getAll
();
18
}
(3)
在
BookServiceImpl
中,增删改方法根据
DAO
的返回值来决定返回
true/false
1
@Service
2
public class
BookServiceImpl
implements
BookService
{
3
@Autowired
4
private
BookDao bookDao
;
5
6
public
boolean
save
(
Book book
) {
7
return
bookDao
.
save
(
book
)
>
0
;
8
}
9
10
public
boolean
update
(
Book book
) {
11
return
bookDao
.
update
(
book
)
>
0
;
12
}
13
14
public
boolean
delete
(
Integer
id
) {
15
return
bookDao
.
delete
(
id
)
>
0
;
16
}
17
18
public
Book getById
(
Integer
id
) {
19
if
(
id
==
1
){
20
throw new
BusinessException
(
Code
.
BUSINESS_ERR
,
"
请不要使用你的技术挑战
我的耐性
!"
);
21
}
22
// //
将可能出现的异常进行包装,转换成自定义异常
23
// try{
24
// int i = 1/0;
25
// }catch (Exception e){
26
// throw new SystemException(Code.SYSTEM_TIMEOUT_ERR,"
服务器访问超
时,请重试
!",e);
27
// }
28
return
bookDao
.
getById
(
id
);
29
}
30
31
public
List
<
Book
>
getAll
() {
32
return
bookDao
.
getAll
();
33
}
34
}
35
(4)
测试错误情况,将图书类别长度设置超出范围即可
处理完新增后,会发现新增还存在一个问题,
新增成功后,再次点击
新增
按钮会发现之前的数据还存在,这个时候就需要在新增的时候将表单内容 清空。
resetForm
(){
this
.
formData
=
{};
}
handleCreate
() {
this
.
dialogFormVisible
=
true
;
this
.
resetForm
();
}
4.5 修改功能
需求
:
完成图书信息的修改功能
1.
找到页面中的
编辑
按钮,该按钮绑定了
@click="handleUpdate(scope.row)"
2.
在
method
的
handleUpdate
方法中发送异步请求根据
ID
查询图书信息
3.
根据后台返回的结果,判断是否查询成功
如果查询成功打开修改面板回显数据,如果失败提示错误信息
4.
修改完成后找到修改面板的
确定
按钮,该按钮绑定了
@click="handleEdit()"
5.
在
method
的
handleEdit
方法中发送异步请求提交修改数据
6.
根据后台返回的结果,判断是否修改成功
如果成功提示错误信息,关闭修改面板,重新查询数据,如果失败提示错误信息
scope.row
代表的是当前行的行数据,也就是说
,scope.row
就是选中行对应的
json
数据,如下
:
1
{
2
"id"
:
1
,
3
"type"
:
"
计算机理论
"
,
4
"name"
:
"Spring
实战 第五版
"
,
5
"description"
:
"Spring
入门经典教程,深入理解
Spring
原理技术内幕
"
6
}
修改
handleUpdate
方法
1
//
弹出编辑窗口
2
handleUpdate
(
row
) {
3
// console.log(row); //row.id
查询条件
4
//
查询数据,根据
id
查询
5
axios
.
get
(
"/books/"
+
row
.
id
).
then
((
res
)
=>
{
6
if
(
res
.
data
.
code
==
20041
){
7
//
展示弹层,加载数据
8
this
.
formData
=
res
.
data
.
data
;
9
this
.
dialogFormVisible4Edit
=
true
;
10
}
else
{
11
this
.
$message
.
error
(
res
.
data
.
msg
);
12
}
13
});
14
}
修改
handleEdit
方法
1
handleEdit
() {
2
//
发送
ajax
请求
3
axios
.
put
(
"/books"
,
this
.
formData
).
then
((
res
)
=>
{
4
//
如果操作成功,关闭弹层,显示数据
5
if
(
res
.
data
.
code
==
20031
){
6
this
.
dialogFormVisible4Edit
=
false
;
7
this
.
$message
.
success
(
"
修改成功
"
);
8
}
else if
(
res
.
data
.
code
==
20030
){
9
this
.
$message
.
error
(
"
修改失败
"
);
10
}
else
{
11
this
.
$message
.
error
(
res
.
data
.
msg
);
12
}
13
}).
finally
(()
=>
{
14
this
.
getAll
();
15
});
至此修改功能就已经完成。
4.6 删除功能
需求
:
完成页面的删除功能。
1.
找到页面的删除按钮,按钮上绑定了
@click="handleDelete(scope.row)"
2.method
的
handleDelete
方法弹出提示框
3.
用户点击取消
,
提示操作已经被取消。
4.
用户点击确定,发送异步请求并携带需要删除数据的主键
ID
5.
根据后台返回结果做不同的操作
如果返回成功,提示成功信息,并重新查询数据
如果返回失败,提示错误信息,并重新查询数据
修改
handleDelete
方法
handleDelete
(
row
) {
//1.
弹出提示框
this
.
$confirm
(
"
此操作永久删除当前数据,是否继续?
"
,
"
提示
"
,{
type
:
'info'
}).
then
(()
=>
{
//2.
做删除业务
axios
.
delete
(
"/books/"
+
row
.
id
).
then
((
res
)
=>
{
if
(
res
.
data
.
code
==
20021
){
this
.
$message
.
success
(
"
删除成功
"
);
}
else
{
this
.
$message
.
error
(
"
删除失败
"
);
}
}).
finally
(()
=>
{
this
.
getAll
();
});
}).
catch
(()
=>
{
//3.
取消删除
this
.
$message
.
info
(
"
取消删除操作
"
);
});
}
接下来,下面是一个完整页面
1
<!DOCTYPE html>
2
3
<html>
4
5
<head>
6
7
<!--
页面
meta -->
8
9
<meta
charset
=
"utf-8"
>
10
11
<meta
http-equiv
=
"X-UA-Compatible"
content
=
"IE=edge"
>
12
13
<title>
SpringMVC
案例
</title>
14
15
<meta
content
=
"width=device-width,initial-scale=1,maximum
scale=1,user-scalable=no"
name
=
"viewport"
>
16
17
<!--
引入样式
-->
18
19
<link
rel
=
"stylesheet"
href
=
"../plugins/elementui/index.css"
>
20
21
<link
rel
=
"stylesheet"
href
=
"../plugins/font-awesome/css/font
awesome.min.css"
>
22
23
<link
rel
=
"stylesheet"
href
=
"../css/style.css"
>
24
25
</head>
26
27
<body
class
=
"hold-transition"
>
28
29
<div
id
=
"app"
>
30
31
<div
class
=
"content-header"
>
32
33
<h1>
图书管理
</h1>
34
35
</div>
36
37
<div
class
=
"app-container"
>
38
39
<div
class
=
"box"
>
40
41
<div
class
=
"filter-container"
>
42
43
<el-input
placeholder
=
"
图书名称
"
v
model
=
"pagination.queryString"
style
=
"width: 200px;"
class
=
"filter-item"
>
</el-input>
44
45
<el-button
@click
=
"getAll()"
class
=
"dalfBut"
>
查询
</el-button>
46
47
<el-button
type
=
"primary"
class
=
"butT"
@click
=
"handleCreate()"
>
新建
</el-button>
48
49
</div>
50
51
<el-table
size
=
"small"
current-row-key
=
"id"
:data
=
"dataList"
stripe highlight-current-row
>
52
53
<el-table-column
type
=
"index"
align
=
"center"
label
=
"
序号
"
></el-table-column>
54
55
<el-table-column
prop
=
"type"
label
=
"
图书类别
"
align
=
"center"
></el-table-column>
56
57
<el-table-column
prop
=
"name"
label
=
"
图书名称
"
align
=
"center"
></el-table-column>
58
59
<el-table-column
prop
=
"description"
label
=
"
描述
"
align
=
"center"
></el-table-column>
60
61
<el-table-column
label
=
"
操作
"
align
=
"center"
>
62
63
<template
slot-scope
=
"scope"
>
64
65
<el-button
type
=
"primary"
size
=
"mini"
@click
=
"handleUpdate(scope.row)"
>
编辑
</el-button>
66
67
<el-button
type
=
"danger"
size
=
"mini"
@click
=
"handleDelete(scope.row)"
>
删除
</el-button>
68
69
</template>
70
71
</el-table-column>
72
73
</el-table>
74
75
<!--
新增标签弹层
-->
76
77
<div
class
=
"add-form"
>
78
79
<el-dialog
title
=
"
新增图书
"
:visible.sync
=
"dialogFormVisible"
>
80
81
<el-form
ref
=
"dataAddForm"
:model
=
"formData"
:rules
=
"rules"
label-position
=
"right"
label-width
=
"100px"
>
82
83
<el-row>
84
85
<el-col
:span
=
"12"
>
86
87
<el-form-item
label
=
"
图书类别
"
prop
=
"type"
>
88
89
<el-input
v
model
=
"formData.type"
/>
90
91
</el-form-item>
92
93
</el-col>
94
95
<el-col
:span
=
"12"
>
96
97
<el-form-item
label
=
"
图书名称
"
prop
=
"name"
>
98
99
<el-input
v
model
=
"formData.name"
/>
100
101
</el-form-item>
102
103
</el-col>
104
105
</el-row>
106
107
108
<el-row>
109
110
<el-col
:span
=
"24"
>
111
112
<el-form-item
label
=
"
描述
"
>
113
114
<el-input
v
model
=
"formData.description"
type
=
"textarea"
></el-input>
115
116
</el-form-item>
117
118
</el-col>
119
120
</el-row>
121
122
</el-form>
123
124
<div
slot
=
"footer"
class
=
"dialog-footer"
>
125
126
<el-button
@click
=
"dialogFormVisible =
false"
>
取消
</el-button>
127
128
<el-button
type
=
"primary"
@click
=
"handleAdd()"
>
确定
</el-button>
129
130
</div>
131
132
</el-dialog>
133
134
</div>
135
136
<!--
编辑标签弹层
-->
137
138
<div
class
=
"add-form"
>
139
140
<el-dialog
title
=
"
编辑检查项
"
:visible.sync
=
"dialogFormVisible4Edit"
>
141
142
<el-form
ref
=
"dataEditForm"
:model
=
"formData"
:rules
=
"rules"
label-position
=
"right"
label-width
=
"100px"
>
143
144
<el-row>
145
146
<el-col
:span
=
"12"
>
147
148
<el-form-item
label
=
"
图书类别
"
prop
=
"type"
>
149
150
<el-input
v
model
=
"formData.type"
/>
151
152
</el-form-item>
153
154
</el-col>
155
156
<el-col
:span
=
"12"
>
157
158
<el-form-item
label
=
"
图书名称
"
prop
=
"name"
>
159
160
<el-input
v
model
=
"formData.name"
/>
161
162
</el-form-item>
163
164
</el-col>
165
166
</el-row>
167
168
<el-row>
169
170
<el-col
:span
=
"24"
>
171
172
<el-form-item
label
=
"
描述
"
>
173
174
<el-input
v
model
=
"formData.description"
type
=
"textarea"
></el-input>
175
176
</el-form-item>
177
178
</el-col>
179
180
</el-row>
181
182
</el-form>
183
184
<div
slot
=
"footer"
class
=
"dialog-footer"
>
185
186
<el-button
@click
=
"dialogFormVisible4Edit =
false"
>
取消
</el-button>
187
188
<el-button
type
=
"primary"
@click
=
"handleEdit()"
>
确定
</el-button>
189
190
</div>
191
192
</el-dialog>
193
194
</div>
195
196
</div>
197
198
</div>
199
200
</div>
201
202
</body>
203
204
<!--
引入组件库
-->
205
206
<script
src
=
"../js/vue.js"
></script>
207
208
<script
src
=
"../plugins/elementui/index.js"
></script>
209
210
<script
type
=
"text/javascript"
src
=
"../js/jquery.min.js"
></script>
211
212
<script
src
=
"../js/axios-0.18.0.js"
></script>
213
214
<script>
215
var
vue
=
new
Vue
({
216
217
el
:
'#app'
,
218
data
:{
219
pagination
: {},
220
dataList
: [],
//
当前页要展示的列表数据
221
formData
: {},
//
表单数据
222
dialogFormVisible
:
false
,
//
控制表单是否可见
223
dialogFormVisible4Edit
:
false
,
//
编辑表单是否可见
224
rules
: {
//
校验规则
225
type
: [{
required
:
true
,
message
:
'
图书类别为必填项
'
,
trigger
:
'blur'
}],
226
name
: [{
required
:
true
,
message
:
'
图书名称为必填项
'
,
trigger
:
'blur'
}]
227
}
228
},
229
230
//
钩子函数,
VUE
对象初始化完成后自动执行
231
created
() {
232
this
.
getAll
();
233
},
234
235
methods
: {
236
//
列表
237
getAll
() {
238
//
发送
ajax
请求
239
axios
.
get
(
"/books"
).
then
((
res
)
=>
{
240
this
.
dataList
=
res
.
data
.
data
;
241
});
242
},
243
244
//
弹出添加窗口
245
handleCreate
() {
246
this
.
dialogFormVisible
=
true
;
247
this
.
resetForm
();
248
},
249
250
//
重置表单
251
resetForm
() {
252
this
.
formData
=
{};
253
},
254
255
//
添加
256
handleAdd
() {
257
//
发送
ajax
请求
258
axios
.
post
(
"/books"
,
this
.
formData
).
then
((
res
)
=>
{
259
console
.
log
(
res
.
data
);
260
//
如果操作成功,关闭弹层,显示数据
261
if
(
res
.
data
.
code
==
20011
){
262
this
.
dialogFormVisible
=
false
;
263
this
.
$message
.
success
(
"
添加成功
"
);
264
}
else if
(
res
.
data
.
code
==
20010
){
265
this
.
$message
.
error
(
"
添加失败
"
);
266
}
else
{
267
this
.
$message
.
error
(
res
.
data
.
msg
);
268
}
269
}).
finally
(()
=>
{
270
this
.
getAll
();
271
});
272
},
273
274
//
弹出编辑窗口
275
handleUpdate
(
row
) {
276
// console.log(row); //row.id
查询条件
277
//
查询数据,根据
id
查询
278
axios
.
get
(
"/books/"
+
row
.
id
).
then
((
res
)
=>
{
279
// console.log(res.data.data);
280
if
(
res
.
data
.
code
==
20041
){
281
//
展示弹层,加载数据
282
this
.
formData
=
res
.
data
.
data
;
283
this
.
dialogFormVisible4Edit
=
true
;
284
}
else
{
285
this
.
$message
.
error
(
res
.
data
.
msg
);
286
}
287
});
288
},
289
290
//
编辑
291
handleEdit
() {
292
//
发送
ajax
请求
293
axios
.
put
(
"/books"
,
this
.
formData
).
then
((
res
)
=>
{
//
如果操作成功,关闭弹层,显示数据
if
(
res
.
data
.
code
==
20031
){
this
.
dialogFormVisible4Edit
=
false
;
this
.
$message
.
success
(
"
修改成功
"
);
}
else if
(
res
.
data
.
code
==
20030
){
this
.
$message
.
error
(
"
修改失败
"
);
}
else
{
this
.
$message
.
error
(
res
.
data
.
msg
);
}
}).
finally
(()
=>
{
this
.
getAll
();
});
},
//
删除
handleDelete
(
row
) {
//1.
弹出提示框
this
.
$confirm
(
"
此操作永久删除当前数据,是否继续?
"
,
"
提示
"
,{
type
:
'info'
}).
then
(()
=>
{
//2.
做删除业务
axios
.
delete
(
"/books/"
+
row
.
id
).
then
((
res
)
=>
{
if
(
res
.
data
.
code
==
20021
){
this
.
$message
.
success
(
"
删除成功
"
);
}
else
{
this
.
$message
.
error
(
"
删除失败
"
);
}
}).
finally
(()
=>
{
this
.
getAll
();
});
}).
catch
(()
=>
{
//3.
取消删除
this
.
$message
.
info
(
"
取消删除操作
"
);
});
}
}
})
</script>
</html>
五、拦截器
对于拦截器这节的知识,我们需要学习如下内容
:
- 拦截器概念
- 入门案例
- 拦截器参数
- 拦截器工作流程分析
5.1 拦截器概念
讲解拦截器的概念之前,我们先看一张图
:
(1)
浏览器发送一个请求会先到
Tomcat
的
web
服务器
(2)Tomcat
服务器接收到请求以后,会去判断请求的是静态资源还是动态资源
(3)
如果是静态资源,会直接到
Tomcat
的项目部署目录下去直接访问
(4)
如果是动态资源,就需要交给项目的后台代码进行处理
(5)
在找到具体的方法之前,我们可以去配置过滤器
(
可以配置多个
)
,按照顺序进行执行
(6)
然后进入到到中央处理器
(SpringMVC
中的内容
)
,
SpringMVC
会根据配置的规则进行拦截
(7)
如果满足规则,则进行处理,找到其对应的
controller
类中的方法进行执行
,
完成后返回结果
(8)
如果不满足规则,则不进行处理
(9)
这个时候,如果我们需要在每个
Controller
方法执行的前后添加业务,具体该如何来实现
?
这个就是拦截器要做的事。
- 拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法 的执行
- 作用:
在指定的方法调用前后执行预先设定的代码
阻止原始方法的执行
总结:拦截器就是用来做增强
看完以后,大家会发现
- 拦截器和过滤器在作用和执行顺序上也很相似
所以这个时候,就有一个问题需要思考
:
拦截器和过滤器之间的区别是什么
?
- 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术
- 拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强
5.2 拦截器入门案例
1.环境准备
- 创建一个Web的Maven项目
- pom.xml添加SSM整合所需jar包
<?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.itheima
</groupId>
<artifactId>
springmvc_12_interceptor
</artifactId>
<version>
1.0-SNAPSHOT
</version>
<packaging>
war
</packaging>
<dependencies>
<dependency>
<groupId>
javax.servlet
</groupId>
<artifactId>
javax.servlet-api
</artifactId>
<version>
3.1.0
</version>
<scope>
provided
</scope>
</dependency>
<dependency>
<groupId>
org.springframework
</groupId>
<artifactId>
spring-webmvc
</artifactId>
22
<version>
5.2.10.RELEASE
</version>
23
</dependency>
24
<dependency>
25
<groupId>
com.fasterxml.jackson.core
</groupId>
26
<artifactId>
jackson-databind
</artifactId>
27
<version>
2.9.0
</version>
28
</dependency>
29
</dependencies>
30
31
<build>
32
<plugins>
33
<plugin>
34
<groupId>
org.apache.tomcat.maven
</groupId>
35
<artifactId>
tomcat7-maven-plugin
</artifactId>
36
<version>
2.1
</version>
37
<configuration>
38
<port>
80
</port>
39
<path>
/
</path>
40
</configuration>
41
</plugin>
42
<plugin>
43
<groupId>
org.apache.maven.plugins
</groupId>
44
<artifactId>
maven-compiler-plugin
</artifactId>
45
<configuration>
46
<source>
8
</source>
47
<target>
8
</target>
48
</configuration>
49
</plugin>
50
</plugins>
51
</build>
52
</project>
53
- 创建对应的配置类
1
public class
ServletContainersInitConfig
extends
AbstractAnnotationConfigDispatcherServletInitializer
{
2
protected
Class
<?>
[]
getRootConfigClasses
() {
3
return new
Class
[
0
];
4
}
5
6
protected
Class
<?>
[]
getServletConfigClasses
() {
7
return new
Class
[]{
SpringMvcConfig
.
class
};
8
}
9
10
protected
String
[]
getServletMappings
() {
11
return new
String
[]{
"/"
};
12
}
13
14
//
乱码处理
15
@Override
16
protected
Filter
[]
getServletFilters
() {
17
CharacterEncodingFilter filter
=
new
CharacterEncodingFilter
();
18
filter
.
setEncoding
(
"UTF-8"
);
19
return new
Filter
[]{
filter
};
20
}
21
}
22
23
@Configuration
24
@ComponentScan
({
"com.itheima.controller"
})
25
@EnableWebMvc
26
public class
SpringMvcConfig
{
27
28
}
- 创建模型类Book
1
public class
Book
{
2
private
String
name
;
3
private
double
price
;
4
5
public
String
getName
() {
6
return
name
;
7
}
8
9
public
void
setName
(
String
name
) {
10
this
.
name
=
name
;
11
}
12
13
public
double
getPrice
() {
14
return
price
;
15
}
16
17
public
void
setPrice
(
double
price
) {
18
this
.
price
=
price
;
19
}
20
21
@Override
22
public
String
toString
() {
23
return
"Book{"
+
24
"
书名
='"
+
name
+
'\''
+
25
",
价格
="
+
price
+
26
'}'
;
27
}
28
}
- 编写Controller
1
@RestController
2
@RequestMapping
(
"/books"
)
3
public class
BookController
{
4
5
@PostMapping
6
public
String
save
(
@RequestBody
Book book
){
7
System
.
out
.
println
(
"book save..."
+
book
);
8
return
"{'module':'book save'}"
;
9
}
10
11
@DeleteMapping
(
"/{id}"
)
12
public
String
delete
(
@PathVariable
Integer
id
){
13
System
.
out
.
println
(
"book delete..."
+
id
);
14
return
"{'module':'book delete'}"
;
15
}
16
17
@PutMapping
18
public
String
update
(
@RequestBody
Book book
){
19
System
.
out
.
println
(
"book update..."
+
book
);
20
return
"{'module':'book update'}"
;
21
}
22
23
@GetMapping
(
"/{id}"
)
24
public
String
getById
(
@PathVariable
Integer
id
){
25
System
.
out
.
println
(
"book getById..."
+
id
);
26
return
"{'module':'book getById'}"
;
27
}
28
29
@GetMapping
30
public
String
getAll
(){
31
System
.
out
.
println
(
"book getAll..."
);
32
return
"{'module':'book getAll'}"
;
33
}
34
}
最终创建好的项目结构如下
:
2. 拦截器开发
步骤
1:
创建拦截器类
让类实现
HandlerInterceptor
接口,重写接口中的三个方法。
@Component
//
定义拦截器类,实现
HandlerInterceptor
接口
//
注意当前类必须受
Spring
容器控制
public class
ProjectInterceptor
implements
HandlerInterceptor
{
@Override
//
原始方法调用前执行的内容
public
boolean
preHandle
(
HttpServletRequest request
,
HttpServletResponse
response
,
Object
handler
)
throws
Exception
{
System
.
out
.
println
(
"preHandle..."
);
return
true
;
}
@Override
//
原始方法调用后执行的内容
public
void
postHandle
(
HttpServletRequest request
,
HttpServletResponse
response
,
Object
handler
,
ModelAndView modelAndView
)
throws
Exception
{
System
.
out
.
println
(
"postHandle..."
);
}
@Override
//
原始方法调用完成后执行的内容
public
void
afterCompletion
(
HttpServletRequest request
,
HttpServletResponse response
,
Object
handler
,
Exception ex
)
throws
Exception
{
System
.
out
.
println
(
"afterCompletion..."
);
}
}
注意
:
拦截器类要被
SpringMVC
容器扫描到。
步骤
2:
配置拦截器类
@Configuration
public class
SpringMvcSupport
extends
WebMvcConfigurationSupport
{
@Autowired
private
ProjectInterceptor projectInterceptor
;
@Override
protected
void
addResourceHandlers
(
ResourceHandlerRegistry registry
) {
registry
.
addResourceHandler
(
"/pages/**"
).
addResourceLocations
(
"/pages/"
);
}
@Override
protected
void
addInterceptors
(
InterceptorRegistry registry
) {
//
配置拦截器
registry
.
addInterceptor
(
projectInterceptor
).
addPathPatterns
(
"/books"
);
}
}
步骤
3:SpringMVC
添加
SpringMvcSupport
包扫描
@Configuration
@ComponentScan
({
"com.itheima.controller"
,
"com.itheima.config"
})
@EnableWebMvc
public class
SpringMvcConfig
{
}
步骤
4:
运行程序测试
使用
PostMan
发送
http://localhost/books
如果发送
http://localhost/books/100
会发现拦截器没有被执行,原因是拦截器的
addPathPatterns
方法配置的拦截路径是
/books
,
我们现在发送的是
/books/100
,所以没有匹配
上,因此没有拦截,拦截器就不会执行。
步骤
5:
修改拦截器拦截规则
@Configuration
public class
SpringMvcSupport
extends
WebMvcConfigurationSupport
{
@Autowired
private
ProjectInterceptor projectInterceptor
;
@Override
protected
void
addResourceHandlers
(
ResourceHandlerRegistry registry
) {
registry
.
addResourceHandler
(
"/pages/**"
).
addResourceLocations
(
"/pages/"
);
}
@Override
protected
void
addInterceptors
(
InterceptorRegistry registry
) {
//
配置拦截器
registry
.
addInterceptor
(
projectInterceptor
).
addPathPatterns
(
"/books"
,
"/books
/*"
);
}
}
这个时候,如果再次访问
http://localhost/books/100
,拦截器就会被执行。
最后说一件事,就是拦截器中的
preHandler
方法,如果返回
true,
则代表放行,会执行原始
Controller
类中要请求的方法,如果返回
false
,则代表拦截,后面的就不会再执行了。
步骤
6:
简化
SpringMvcSupport
的编写
@Configuration
@ComponentScan
({
"com.itheima.controller"
})
@EnableWebMvc
//
实现
WebMvcConfigurer
接口可以简化开发,但具有一定的侵入性
public class
SpringMvcConfig
implements
WebMvcConfigurer
{
@Autowired
private
ProjectInterceptor projectInterceptor
;
@Override
public
void
addInterceptors
(
InterceptorRegistry registry
) {
//
配置多拦截器
registry
.
addInterceptor
(
projectInterceptor
).
addPathPatterns
(
"/books"
,
"/books
/*"
);
}
}
此后咱们就不用再写
SpringMvcSupport
类了。
最后我们来看下拦截器的执行流程
:
当有拦截器后,请求会先进入
preHandle
方法,
如果方法返回
true
,则放行继续执行后面的
handle[controller
的方法
]
和后面的方法
如果返回
false
,则直接跳过后面方法的执行。
5.3 拦截器参数
1. 前置处理方法
原始方法之前运行
preHandle
public
boolean
preHandle
(
HttpServletRequest request
,
HttpServletResponse response
,
Object
handler
)
throws
Exception
{
System
.
out
.
println
(
"preHandle"
);
return
true
;
}
- request:请求对象
- response:响应对象
- handler:被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装
使用
request
对象可以获取请求数据中的内容,如获取请求头的
Content
-
Type
public
boolean
preHandle
(
HttpServletRequest request
,
HttpServletResponse
response
,
Object
handler
)
throws
Exception
{
String
contentType
=
request
.
getHeader
(
"Content-Type"
);
System
.
out
.
println
(
"preHandle..."
+
contentType
);
return
true
;
}
使用
handler
参数,可以获取方法的相关信息
public
boolean
preHandle
(
HttpServletRequest request
,
HttpServletResponse
response
,
Object
handler
)
throws
Exception
{
HandlerMethod hm
=
(
HandlerMethod
)
handler
;
String
methodName
=
hm
.
getMethod
().
getName
();
//
可以获取方法的名称
System
.
out
.
println
(
"preHandle..."
+
methodName
);
return
true
;
}
2. 后置处理方法
原始方法运行后运行,如果原始方法被拦截,则不执行
public
void
postHandle
(
HttpServletRequest request
,
HttpServletResponse response
,
Object
handler
,
ModelAndView modelAndView
)
throws
Exception
{
System
.
out
.
println
(
"postHandle"
);
}
前三个参数和上面的是一致的。
modelAndView:
如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整
因为咱们现在都是返回
json
数据,所以该参数的使用率不高。
3. 完成处理方法
拦截器最后执行的方法,无论原始方法是否执行
public
void
afterCompletion
(
HttpServletRequest request
,
HttpServletResponse response
,
Object
handler
,
Exception ex
)
throws
Exception
{
System
.
out
.
println
(
"afterCompletion"
);
}
前三个参数与上面的是一致的。
ex:
如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理
因为我们现在已经有全局异常处理器类,所以该参数的使用率也不高。
这三个方法中,最常用的是
preHandle
,
在这个方法中可以通过返回值来决定是否要进行放行,我们 可以把业务逻辑放在该方法中,如果满足业务则返回true
放行,不满足则返回
false
拦截。
5.4 拦截器链配置
目前,我们在项目中只添加了一个拦截器,如果有多个,该如何配置
?
配置多个后,执行顺序是什么
?
1. 配置多个拦截器
步骤
1:
创建拦截器类
实现接口,并重写接口中的方法
步骤
2:
配置拦截器类
@Component
public class
ProjectInterceptor2
implements
HandlerInterceptor
{
@Override
public
boolean
preHandle
(
HttpServletRequest request
,
HttpServletResponse
response
,
Object
handler
)
throws
Exception
{
System
.
out
.
println
(
"preHandle...222"
);
return
false
;
}
@Override
public
void
postHandle
(
HttpServletRequest request
,
HttpServletResponse
response
,
Object
handler
,
ModelAndView modelAndView
)
throws
Exception
{
System
.
out
.
println
(
"postHandle...222"
);
}
@Override
public
void
afterCompletion
(
HttpServletRequest request
,
HttpServletResponse response
,
Object
handler
,
Exception ex
)
throws
Exception
{
System
.
out
.
println
(
"afterCompletion...222"
);
}
}
步骤
2:
配置拦截器类
@Configuration
@ComponentScan
({
"com.itheima.controller"
})
@EnableWebMvc
//
实现
WebMvcConfigurer
接口可以简化开发,但具有一定的侵入性
public class
SpringMvcConfig
implements
WebMvcConfigurer
{
@Autowired
private
ProjectInterceptor projectInterceptor
;
@Autowired
private
ProjectInterceptor2 projectInterceptor2
;
@Override
public
void
addInterceptors
(
InterceptorRegistry registry
) {
//
配置多拦截器
registry
.
addInterceptor
(
projectInterceptor
).
addPathPatterns
(
"/books"
,
"/books
/*"
);
registry
.
addInterceptor
(
projectInterceptor2
).
addPathPatterns
(
"/books"
,
"/book
s/*"
);
}
}
步骤
3:
运行程序,观察顺序
拦截器执行的顺序是和配置顺序有关。就和前面所提到的运维人员进入机房的案例,先进后出。
- 当配置多个拦截器时,形成拦截器链
- 拦截器链的运行顺序参照拦截器添加顺序为准
- 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
- 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作
preHandle
:与配置顺序相同,必定运行
postHandle:
与配置顺序相反,可能不运行
afterCompletion:
与配置顺序相反,可能不运行。
这个顺序不太好记,最终只需要把握住一个原则即可
:
以最终的运行结果为准