整合SSM

什么是SSM?

Q:什么是SSM?

A:SSM是Spring + SpringMVC + Mybatis组合的一个企业级框架。

创建项目框架

这是本次实验的项目大体框架,由config、controller、dao、entity、service包组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
\---springmvc-05-ssm
\---src
+---main
| +---java
| | \---site
| | \---hikki
| | +---config
| | +---controller
| | +---dao
| | +---entity
| | \---service
| | \---impl
| +---resources
| \---webapp
| \---WEB-INF
\---test
\---site
\---hikki

管理pom.xml文件

添加依赖

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
72
73
74
75
76
77
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>

<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>

添加tomcat7插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<finalName>springmvc-05-ssm</finalName>
<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>

添加完tomcat7插件,选中右上角的编辑配置,再侧边框点击+添加一个Maven运行配置,选中工作目录为当前项目或模块的路径,然后在运行配置项下的输入框填入tomcat7:run。选中合适的名称即可。

连接数据库

创建数据库

设置数据库连接信息

resources资源文件夹创建jdbc.properties文件

1
2
3
4
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db
jdbc.username=root
jdbc.password=root

设置配置类

配置类的作用就是代替以前的spring-bean.xml管理,使用注解来减少xml文件配置

创建JdbcConfig配置类

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
package site.hikki.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;

public class JdbcConfig {

// 导入properties属性的值
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;

/**
* 添加数据库连接源
* @return
*/
@Bean
public DataSource dataSource(){
DruidDataSource druidDataSource = new DruidDataSource();

druidDataSource.setUrl(url);
druidDataSource.setDriverClassName(driver);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
return druidDataSource;
}
}

创建MybatisConfig配置类

对Mybatis配置进行配置管理,代替以前使用的mybatis-config.xml文件,减少一些繁琐的配置。

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
package site.hikki.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MybatisConfig {
/**
* mybatis配置 xml 文件
* @param dataSource
* @return
*/
@Bean
public SqlSessionFactoryBean sessionFactory(DataSource dataSource){
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
// 设置实体类的别名
sqlSessionFactoryBean.setTypeAliasesPackage("site.hikki.entity");
return sqlSessionFactoryBean;
}

@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
// 设置mybatis的 xml sql语句
configurer.setBasePackage("site.hikki.dao");
return configurer;
}
}

创建SpringConfig主配置类

该配置类是Spring的主配置类,Spring的Ioc容器是由他产生的

1
2
3
4
5
6
@Configuration
@ComponentScan({"com.itheima.service"})
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MyBatisConfig.class})
public class SpringConfig {
}

Spring整合SpringMVC

创建SpringMvcConfig

1
2
3
4
5
6
7
8
9
10
11
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration // 设置为spring的核心配置类
@ComponentScan("site.hikki.controller") // 扫描控制器的bean
@EnableWebMvc //开启web MVC
public class SpringMvcConfig {
}

创建ServletConfig配置类

加载 SpringMvcConfig 和 SpringConfig 配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package site.hikki.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}

// Servlet容器可访问Spring容器,但Spring容器不能访问MVC容器
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}

/**
* 拦截全部请求,都通过servlet的处理
* @return
*/
protected String[] getServletMappings() {
return new String[]{"/"};
}
}

功能模块开发

需求

使用mybatis在web网站上对spring_db下的tb_book表进行数据的增高删改查。

分析

  • controller对web请求进行路由控制
  • dao负责数据库的持久层操作
  • entity存放实体类
  • service负责业务处理

数据层开发

Book实体类

创建图书的实体类,用于对图书的基本数据进行封装存储。

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
package site.hikki.entity;

public class Book {
private Integer id;
private String type;
private String name;
private String description;
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
", type='" + type + '\'' +
'}';
}

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 getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}
}

BookDao接口

编写SQL语句,对数据进行增删改查操作。

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
package site.hikki.dao;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import site.hikki.entity.Book;

import java.util.List;

public interface BookDao {
@Select("select * from tb_book where id=#{id}")
Book findBookById(Integer id);

@Select("select * from tb_book")
List<Book> findAll();

@Insert("insert into tb_book (type,name,description) values(#{type},#{name},#{description})")
void addBook(Book book);

@Update("update tb_book set type=#{type},name=#{name},description=#{description} where id = #{id}")
void update(Book book);

@Delete("delete from tb_book where id = #{id}")
void delete(Integer id);
}

业务层开发

BookService接口

编写BookService接口,对数据库的读写进行实现操作。

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
package site.hikki.service;

import site.hikki.entity.Book;

import java.util.List;
public interface BookService {

/**
* 根据ID查询全部书本
* @param id
* @return
*/
Book findBookById(Integer id);

/**
* 查询全部书本
* @return
*/
List<Book> findAll();

/**
* 添加书本
* @param book
* @return
*/
boolean addBook(Book book);

/**
* 更改书本信息
* @param book
* @return
*/
boolean update(Book book);

/**
* 删除书本
* @param id
* @return
*/
boolean delete(Integer id);
}

BookServiceImpl接口实现

对BookDao的方法进行调用,实现对数据库的增删改查操作。

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
package site.hikki.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.hikki.dao.BookDao;
import site.hikki.entity.Book;
import site.hikki.service.BookService;

import java.util.List;

@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao; // bookDao爆红不影响运行的

public Book findBookById(Integer id) {
return bookDao.findBookById(id);
}

public List<Book> findAll() {
return bookDao.findAll();
}

public boolean addBook(Book book) {
bookDao.addBook(book);
return true;
}

public boolean update(Book book) {
bookDao.update(book);
return true;
}

public boolean delete(Integer id) {
bookDao.delete(id);
return true;
}
}

表现层开发

也叫控制层,对Web项目的请求URL进行控制,也可以说是一个路由控制器。

在以往的Model2中,我们需要使用doGetdoPost来对数据的请求处理,但在SSM中,我们使用一个注解就可以省去这些麻烦的步骤了,比如使用get请求查询全部图书,使用@GetMapping注解就可以是实现查询全部图书了,非常方便。

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
package site.hikki.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import site.hikki.entity.Book;
import site.hikki.service.BookService;

import java.util.List;

@RestController
@RequestMapping("/books")
public class BookContrtoller {
@Autowired
private BookService bookService;

@GetMapping("/{id}")
public Book findBookById(@PathVariable Integer id) {
return bookService.findBookById(id);
}

@GetMapping
public List<Book> findAll() {
return bookService.findAll();
}

@PostMapping
public boolean addBook(@RequestBody Book book) {
return bookService.addBook(book);
}

@PutMapping
public boolean update(@RequestBody Book book) {
return bookService.update(book);
}

@DeleteMapping("/{id}")
public boolean delete(@PathVariable Integer id) {
return bookService.delete(id);
}
}

开启事务管理器

开启事务管理

@EnableTransactionManagement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
* @author lilbai518
*/
@Configuration
@ComponentScan(basePackages ={"site.hikki.service"})
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
@EnableTransactionManagement //开启事务管理器
public class SpringConfig {
}

SpringConfig配置类添加@EnableTransactionManagement注解开启事务管理

实现事务管理器接口

JdbcConfig配置类添加,或者在其他类添加也是可以的。记得添加bean,被扫描到就好。

1
2
3
4
5
6
7
8
9
10
11
/**
* 事务管理器
* @param dataSource
* @return
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager ds = new DataSourceTransactionManager();
ds.setDataSource(dataSource);
return ds;
}

DataSource自动装配

加事务

BookService添加@Transactional注解

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
package site.hikki.service;

import org.springframework.transaction.annotation.Transactional;
import site.hikki.entity.Book;

import java.util.List;
@Transactional
public interface BookService {

/**
* 根据ID查询全部书本
* @param id
* @return
*/
Book findBookById(Integer id);

/**
* 查询全部书本
* @return
*/
List<Book> findAll();

/**
* 添加书本
* @param book
* @return
*/
boolean addBook(Book book);

/**
* 更改书本信息
* @param book
* @return
*/
boolean update(Book book);

/**
* 删除书本
* @param id
* @return
*/
boolean delete(Integer id);
}

接口测试

Junit测试

我们在实现完业务逻辑代码时,我们得先使用Junit测试方法是否正常运行,然后再使用web测试方法。

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
package site.hikki;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import site.hikki.config.SpringConfig;
import site.hikki.entity.Book;
import site.hikki.service.BookService;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class BookServiceTest {
@Autowired
private BookService bookService;

/**
* 根据ID查询图书测试
*/
@Test
public void testGetById(){
Book bookById = bookService.findBookById(1);
System.out.println(bookById);
}
/**
* 查询全部图书测试
*/
@Test
public void findALl(){
List<Book> all = bookService.findAll();
for (Book b :all) {
System.out.println(b);
}
}
}

Web测试表现层接口

我这里使用Apifox测试接口

测试查询全部图书

01-apifox测试-20230409-288

有需要测试的文件接口,可以下载导入apifox,如果你是postman,或许可以使用,博主未测试,若你导入成功,还望告知一声。

蓝奏云接口测试下载:https://rookie1679.lanzoum.com/iKs9w0sjj8rc

本次实验项目下载:https://rookie1679.lanzoum.com/iRfdG0sjk65e

表现层数据封装

为什么需要封装数据?

我们在web前端对后端发起请求时,如果发起的请求URL不正确或者发起请求成功了,但返回没有数据(比如返回null),前端要怎么判定该请求是否有效呢?

我们一般是打开开发者(F12)查看请求URL状态是不是200,如果是200,则说明请求的URL成功了,但返回的内容为空,这个要怎么判断呢?

我要怎么知道是我的请求方式不对,还是请求携带的内容不对,还是后端处理逻辑有问题呢? 还是数据库出现了问题呢?

于是我们想,如果后端在返回数据的时候,返回状态码或者返回消息说明该请求是哪里出错了就好了,这样可以通过F12判断请求状态码是否对后端请求成功了,然后通过请求成功后返回的消息中的状态码来判断请求是哪里出错了,是缺少了什么请求头还是缺少了什么东西,这样可以更好的解决前端对请求的URL不清晰的描述。

返回对象

我们可以封装一个带有返回状态码数据消息的一个实体类,然后返回时,使用指定的状态码来表示不同的结果。(如果你还有需要返回需求,你可以根据自己的需求来作修改)

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
package site.hikki.controller;

public class Result {
private Integer code;
private Object data;
private String msg;

// 无参构造方法
public Result() {
}

// 不带消息的对象
public Result(Integer code, Object data) {
this.code = code;
this.data = data;
}

// 完整的返回对象
public Result(Integer code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}
}

我们为什么需要使用三个构造方法,其实不一定,你也可以不添加构造方法,直接使用默认添加的无参构造方法也行,只是添加了其他两个,在使用时方便一点,不需要每次使用都new一个对象出来,然后再对该对象进行set方法设置值。

自定义返回状态码

我们的状态码由5位数字组成,前三位200表示该请求是有效的,第四位表示各种请求类型,第五位表示该请求的后台逻辑代码是否有误0表示失败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
package site.hikki.controller;

public class Code {
// 1 表示成功
// 0 表示失败

// 添加成功
public static final Integer SAVE_OK =20011;
// 更新成功
public static final Integer UPDATE_OK =20021;
// 删除成功
public static final Integer DELETE_OK =20031;
// 查询成功
public static final Integer GET_OK =20041;

// 添加失败
public static final Integer SAVE_ERR =20010;
// 更新失败
public static final Integer UPDATE_ERR =20020;
// 删除失败
public static final Integer DELETE_ERR =20030;
// 查询失败
public static final Integer GET_ERR =20040;
}

这里必须设置public static,因为后面在使用该变量时,是在别的类调用的,访问权限需要public,同时,需要该变量同jvm一起初始化,这样使用时不会存在找不到该变量的情况。

修改返回状态

我们之前在BookContrtoller下设置了URL的请求路由,并且作出了返回结果,我们上面定义了返回对象,我们准备在该控制器设置返回对象。

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
package site.hikki.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import site.hikki.entity.Book;
import site.hikki.service.BookService;

import java.util.List;

@RestController
@RequestMapping("/books")
public class BookContrtoller {
@Autowired
private BookService bookService;

@GetMapping("/{id}")
public Result findBookById(@PathVariable Integer id) {
Book book = bookService.findBookById(id);
Integer code = book != null ? Code.GET_OK : Code.GET_ERR;
String msg = book!=null?"数据查询成功":"数据查询失败,请重试";
return new Result(code,book,msg);
}

@GetMapping
public Result findAll() {
List<Book> all = bookService.findAll();
Integer code = all != null ? Code.GET_OK : Code.GET_ERR;
String msg = all != null? "数据查询成功":"数据查询失败,请重试";
return new Result(code,all,"查询成功");
}

@PostMapping
public Result addBook(@RequestBody Book book) {
boolean b = bookService.addBook(book);
return new Result(b?Code.SAVE_OK:Code.SAVE_ERR,b);
}

@PutMapping
public Result update(@RequestBody Book book) {
boolean b = bookService.update(book);
return new Result(b?Code.UPDATE_OK:Code.UPDATE_ERR,b);
}

@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
boolean b = bookService.delete(id);
return new Result(b?Code.DELETE_OK:Code.DELETE_ERR,b);
}
}

这里的返回结果,我们使用三目运算符,提高开发效率。

测试结果

使用Apifox测试根据ID查询结果如下:

02-返回对象测试-20230410-920

在返回数据中,多了状态码、数据和消息,便于前端开发者对该数据进行处理。

异常处理

异常介绍

异常是开发中不可避免的情况,数据中心起火、服务器掉线、开发者代码不规范等等行为都会导致异常出现,但我们不能给用户看到这种异常。

03-错误样例-20230410-299

出现异常现象的常见位置与常见诱因如下:

  1. 框架内部抛出的异常:因使用不合规导致
  2. 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
  3. 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
  4. 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
  5. 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)

异常处理器

编写异常

由于异常处理是属于表现层的,我们将放在controller下,新建一个ProjectExceptionAdvice类位于controller包下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package site.hikki.controller;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


@RestControllerAdvice
public class ProjectExceptionAdvice {

/**
* 处理其他全部异常
* @param e 异常信息
* @return
*/
@ExceptionHandler(Exception.class)
public Result doException(Exception e){
return new Result(666,null);
}
}

返回错误结果:

04-restcontroller-20230410-002

@RestControllerAdvice注解介绍

  • 名称:@RestControllerAdvice

  • 类型:类注解

  • 位置:Rest风格开发的控制器增强类定义上方

  • 作用:为Rest风格开发的控制器类做增强

  • 说明:此注解自带@ResponseBody注解与@Component注解,具备对应的功能

@ExceptionHandler注解介绍

  • 名称:@ExceptionHandler
  • 类型:方法注解
  • 位置:专用于异常处理的控制器方法上方
  • 作用:设置指定异常的处理方案,功能等同于控制器方法,出现异常后终止原始控制器执行,并转入当前方法执行
  • 说明:此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常

项目异常处理介绍

项目异常分类

  • 业务异常(BusinessException)
    • 规范的用户行为产生的异常
    • 不规范的用户行为操作产生的异常
  • 系统异常(SystemException)
    • 项目运行过程中可预计且无法避免的异常
  • 其他异常(Exception)
    • 编程人员未预期到的异常

项目异常处理方案

  • 业务异常(BusinessException)
    • 发送对应消息传递给用户,提醒规范操作
  • 系统异常(SystemException)
    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给运维人员,提醒维护
    • 记录日志
  • 其他异常(Exception)
    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给编程人员,提醒维护(纳入预期范围内)
    • 记录日志

项目异常处理代码实现

因为异常种类有多种,我们一般将它区分开,使用一个实体类将该异常种类区分开,比如业务异常、系统异常、其他异常,在上面我们已经创建了一个其他异常的类了。

系统级异常处理

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
package site.hikki.controller;

/**
* 自定义系统级异常处理器,用于封装异常信息,对异常进行分类
*/
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;
}
}

业务级异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package site.hikki.controller;

/**
* 自定义业务级异常处理器,用于封装异常信息,对异常进行分类
*/
public class BusinessException extends RuntimeException{
private Integer 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;
}
}

自定义异常状态码

我们在封装表现层的数据时,我们也定义过一部分的状态码,在给前端页面返回数据时,带上状态码,让前端开发人员更清楚的了解到返回的是什么东西。

我们在发生异常时,会抛出异常,我们需要告诉前端人员,后端遇到了异常,暂时无法正常运行,我们也需要让前端开发人员安抚用户,让用户等一下或者让用户稍后再试。

那我们遇到异常时,也定义一个状态码,让前端开发人员根据状态码来给用户提示响应的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package site.hikki.controller;

public class Code {

// 之前其他状态码省略没写,以下是新补充的状态码,可以根据需要自己补充

// 系统错误
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;
}

触发异常案例

模拟异常抛出

site.hikki.service.impl.BookServiceImpl类中的findBookById方法增加如下内容,模拟异常测试。

1
2
3
4
5
6
7
8
9
10
11
12
public Book findBookById(Integer id) {
//模拟业务异常,包装成自定义异常
if (id ==1){
throw new BusinessException(666,"服务器断电了");
}
try {
int i =5/2;
}catch (Exception e){
throw new SystemException(666,"服务器连接超时",e);
}
return bookDao.findBookById(id);
}

处理异常

在异常通知类中拦截并处理异常

我们原本已经在ProjectExceptionAdvice类中已经添加过一个方法doException方法了,该方法是处理所有异常错误,我们新添加两个方法,用于处理系统级业务级的异常信息。

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
package site.hikki.controller;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


@RestControllerAdvice
public class ProjectExceptionAdvice {

@ExceptionHandler(SystemException.class)
public Result doSystemException(SystemException e){
//TODO:记录日志,发送消息给运维人员,发送邮件给开发人员,内容应该包括错误信息e
return new Result(e.getCode(),null,e.getMessage());
}

@ExceptionHandler(BusinessException.class)
public Result doBusinessException(BusinessException e){
//TODO:记录日志,发送消息给运维人员,发送邮件给开发人员,内容应该包括错误信息e
return new Result(e.getCode(),null,e.getMessage());
}

/**
* 处理其他全部异常
* @param e 异常信息
* @return
*/
@ExceptionHandler(Exception.class)
public Result doException(Exception e){
//TODO:记录日志,发送消息给运维人员,发送邮件给开发人员,内容应该包括错误信息e
return new Result(Code.SYSTEM_UNKNOW_ERR,"系统繁忙,请稍后再试");
}
}

测试异常

测试:在apifox中发送请求访问getById方法,传递参数1,得到以下结果:

05-error测试-20230410-040

SSM整合页面开发

该实验整合web页面使用了Vue。

设置静态资源过滤

新建SpringMvcSupport类

为了确保静态资源能够被访问到,需要设置静态资源过滤

site.hikki.config包下新建SpringMvcSupport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package site.hikki.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@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配置类中导扫描site.hikki.config

1
2
3
4
5
6
7
8
9
10
11
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration // 设置为spring的核心配置类
@ComponentScan({"site.hikki.controller","site.hikki.config"}) // 扫描控制器的bean
@EnableWebMvc //开启web MVC
public class SpringMvcConfig {
}

列表查询功能

webapp/pages/books.html下添加了如下代码,表示对后台/books发起get请求。

1
2
3
4
5
6
getAll() {
//发送ajax请求
axios.get("/books").then((res) => {
this.dataList = res.data.data;
});
},

添加功能

前端代码

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
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
},

//重置表单
resetForm() {
this.formData = {};
},

//添加
handleAdd() {
//发送ajax请求
axios.post("/books", this.formData).then((res) => {
console.log(res.data);
//如果操作成功,关闭弹层,显示数据
if (res.data.code == 20011) {
this.dialogFormVisible = false;
this.$message.success("添加成功");
} else if (res.data.code == 20010) {
this.$message.error("添加失败");
} else {
this.$message.error(res.data.msg);
}
}).finally(() => {
this.getAll();
});
},

后端代码

site.hikki.dao.BookDao接口的增删改的返回类型由void改为int

然后再将site.hikki.service.impl.BookServiceImpl实现类的返回稍微修改一下,若影响条数大于0,则返回true,否则返回false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//增删改的方法判断了影响的行数是否大于0,而不是固定返回true
public boolean addBook(Book book) {
return bookDao.addBook(book) >0 ;
}

//增删改的方法判断了影响的行数是否大于0,而不是固定返回true
public boolean update(Book book) {
return bookDao.update(book) >0 ;
}

//增删改的方法判断了影响的行数是否大于0,而不是固定返回true
public boolean delete(Integer id) {
return bookDao.delete(id) >0 ;
}

修改功能

在site.hikki.service.impl.BookServiceImpl.findBookById方法中模拟了异常抛出,当点击ID为1的图书修改按钮时,会返回异常。

1
2
3
4
5
6
7
public Book findBookById(Integer id) {
//模拟业务异常,包装成自定义异常
if (id ==1){
throw new BusinessException(666,"服务器断电了");
}
return bookDao.findBookById(id);
}

此案例为了模拟前端返回错误演示。

显示弹出框查询图书信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//弹出编辑窗口
handleUpdate(row) {
// console.log(row); //row.id 查询条件
//查询数据,根据id查询
axios.get("/books/" + row.id).then((res) => {
// console.log(res.data.data);
if (res.data.code == 20041) {
//展示弹层,加载数据
this.formData = res.data.data;
this.dialogFormVisible4Edit = true;
} else {
this.$message.error(res.data.msg);
}
});
},

保存修改后的图书信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//编辑
handleEdit() {
//发送ajax请求
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();
});
},

删除功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 删除
handleDelete(row) {
//1.弹出提示框
this.$confirm("此操作永久删除当前数据,是否继续?", "提示", {
type: 'info'
}).then(() => {
//2.做删除业务
axios.delete("/books/" + row.id).then((res) => {
if (res.data.code == 20031) {
this.$message.success("删除成功");
} else {
this.$message.error("删除失败");
}
}).finally(() => {
this.getAll();
});
}).catch(() => {
//3.取消删除
this.$message.info("取消删除操作");
});
}

拦截器

拦截器的概念

  • 拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行
  • 作用:
    1. 在指定的方法调用前后执行预先设定的代码
    2. 阻止原始方法的执行
    3. 总结:增强
  • 核心原理:AOP思想

拦截器和过滤器的区别

image-20230410193310411

归属不同

  • Filter(过滤器)属于Servlet技术

  • Interceptor(拦截器)属于SpringMVC技术

拦截内容不同

  • Filter(过滤器)对所有访问进行增强

  • Interceptor(拦截器)仅针对SpringMVC的访问进行增强

生命周期

  • Interceptor(拦截器)是由框架管理的对象,其生命周期由框架控制

  • Filter(过滤器)是由Servlet容器管理的对象,其生命周期与Servlet容器相同

使用场景

  • Interceptor(拦截器)通常用于在请求到达控制器之前或之后执行某些操作,例如身份验证、日志记录等

  • Filter(过滤器)用于在请求到达Servlet之前或响应发送回客户端之前执行某些操作,例如字符编码转换、安全性检查等。

作用对象

  • Interceptor(拦截器)可以对请求、响应、Action以及Action中的方法进行拦截

  • Filter(过滤器)只能对请求和响应进行过滤。

执行顺序

  • Interceptor(拦截器)可以定义多个,并按照预定顺序依次执行

  • Filter(过滤器)也可以定义多个,但执行顺序取决于它们在web.xml文件中的配置顺序。

image-20230410193317521

入门案例

初始化项目

项目结构

1
2
3
4
5
6
7
8
9
10
11
\---src
+---main
| +---java
| | \---site
| | \---hikki
| | +---config
| | \---controller
| +---resources
| \---webapp
| \---WEB-INF
\---test

SpringConfig

Spring项目核心配置

1
2
3
4
5
6
7
package site.hikki.config;

import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
}

SpringMvcConfig

SpringMVC项目核心配置

1
2
3
4
5
6
7
8
9
10
11
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@ComponentScan({"site.hikki.controller","site.hikki.config"})
@Configuration
@EnableWebMvc
public class SpringMvcConfig {
}

ServletConfig

管理SpringMVC容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package site.hikki.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}

// Servlet容器可访问Spring容器,但Spring容器不能访问MVC容器
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}

/**
* 拦截全部请求,都通过servlet的处理
* @return
*/
protected String[] getServletMappings() {
return new String[]{"/"};
}
}

BookController

Book控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package site.hikki.controller;

import org.springframework.web.bind.annotation.*;

/**
* @author lilbai518
*/
@RestController
@RequestMapping("/books")
public class BookController {

@GetMapping("/{id}")
public String getBook(@PathVariable Integer id){
return "{'name':'AI','desc':'ChatGPT'}";
}

@GetMapping
public String getAll(){
return "[{'name':'Java web','desc':'this is Java'},{'name':'blog','desc':'blog.hikki.site'}]";
}

}

一个简单的web项目已经创建好了,测试如下:

springboot-interceptor-20230410-766

定义拦截器

在在site.hikki.controller.interceptor包下创建一个类,实现HandlerInterceptor接口,并且重写preHandlepostHandleafterCompletion方法即可。他们的分别是拦截前实现拦截后实现拦截结束后实现

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
package site.hikki.controller.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class ProjectInterceptor implements HandlerInterceptor {

//原始方法调用前执行的内容
//返回值类型可以拦截控制的执行,true放行,false终止
@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....");
}
}

配置加载拦截器

site.hikki.config包下创建一个名为SpringMvcSupport类,并重写addInterceptors方法,然后注册一个addInterceptor接口,然后设置拦截路径addPathPatterns

我们之前也使用过这个接口,之前使用的是addResourceHandlers接口,之前是设置了不拦截静态资源,设置了相关的不拦截路径,具体使用如下:

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
package site.hikki.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import site.hikki.controller.interceptor.ProjectInterceptor;

@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {

@Autowired
private ProjectInterceptor interceptor;

/**
* 加载拦截器
* @param registry
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/books","/books/*");
}

/**
* 对于前端请求静态资源不拦截
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/pages/").addResourceLocations("/pages/");
registry.addResourceHandler("/js/").addResourceLocations("/js/");
}
}

在给接口设置拦截路径时,addPathPatterns方法是可以设置多个参数的,比如上面就设置了两个参数/books/books/*

/books/books/*,设置的效果不一样的,如果只是设置/books/*,你请求的路径如果是http://localhost/books的话,是不会被拦截到的。也就是说这个路径是精确匹配的。

测试拦截效果

07-intercept-20230410-879

拦截器链设置

拦截器流程

image-20230410233804320

定义第二个拦截器

site.hikki.controller.interceptor包下新建ProjectInterceptor2类,可以直接复制ProjectInterceptor的文件修改文件名即可。

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
package site.hikki.controller.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class ProjectInterceptor2 implements HandlerInterceptor {

//原始方法调用前执行的内容
//返回值类型可以拦截控制的执行,true放行,false终止
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle....222");
return true;
}
//原始方法调用后执行的内容
@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");
}
}

配置拦截器

还是在之前配置拦截器的类中,site.hikki.config.SpringMvcSupport注册一个拦截器,并添加拦截路径。

  1. 自动注入ProjectInterceptor2
  2. 注册拦截器并设置拦截路径
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
package site.hikki.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import site.hikki.controller.interceptor.ProjectInterceptor;
import site.hikki.controller.interceptor.ProjectInterceptor2;

@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {

@Autowired
private ProjectInterceptor interceptor;

@Autowired
private ProjectInterceptor2 interceptor2;
/**
* 加载拦截器
* @param registry
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/books","/books/*");
registry.addInterceptor(interceptor2).addPathPatterns("/books","/books/*");
}

/**
* 对于前端请求静态资源不拦截
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/pages/").addResourceLocations("/pages/");
registry.addResourceHandler("/js/").addResourceLocations("/js/");
}
}

测试拦截器链

由结果可以看到,拦截器的执行顺序,首先是preHandle....111-->preHandle....222-->postHandle....222-->postHandle....111-->afterCompletion....222-->afterCompletion....111,这个方式,有点点像栈,先进后出。

08-拦截器链-20230410-707

多个拦截器工作流程分析

在拦截器中,我们可以通过改变定义拦截器中的preHandle方法返回的boolean的值来改变后续执行的拦截器流程。

比如我下面改变ProjectInterceptor定义拦截器的preHandle方法的返回值为false,我们测试一下结果是怎么样的。

09-拦截器返回false-20230410-664

可以看到返回的结果只有执行了preHandle方法,当前拦截器的其他方法也不执行了,连其他拦截器的全部方法都不执行了。

总结

  • 当配置多个拦截器时,形成拦截器链
  • 拦截器链的运行顺序参照拦截器添加顺序为准
  • 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
  • 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作

下图是采用三个拦截器来描述拦截器的工作流程。

分别当拦截器的preHandle返回false时后续的拦截器的执行方法顺序。

image-20230411000029208

到目前为止全部SpringMVC实验下载地址:https://rookie1679.lanzoum.com/ixdJh0sn016j

本文章来源于我的博客:https://blog.hikki.site