SpringMVC-day02
1. 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 包
步骤 3:创建项目包结构

config 目录存放的是相关的配置类
controller 编写的是 Controller 类
dao 存放的是 Dao 接口,因为使用的是 Mapper 接口代理方式,所以没有实现类包
service 存的是 Service 接口,impl 存放的是 Service 实现类
resources:存入的是配置文件,如 Jdbc.properties
webapp:目录可以存放静态资源
test/java:存放的是测试类
步骤 4:创建 SpringConfig 配置类
步骤 5:创建 JdbcConfig 配置类
步骤 6:创建 MybatisConfig 配置类
*步骤 7:创建 jdbc.properties
在 resources 下提供 jdbc.properties,设置数据库连接四要素
步骤 8:创建 SpringMVC 配置类
步骤 9:创建 Web 项目入口配置类
至此 SSM 整合的环境就已经搭建好了。在这个环境上,我们如何进行功能模块的开发呢?
1.3 功能模块开发
需求:对表 tbl_book 进行新增、修改、删除、根据 ID 查询和查询所有
步骤 1:创建数据库及表
步骤 2:编写模型类
步骤 3:编写 Dao 接口
步骤 4:编写 Service 接口和实现类
说明:
bookDao 在 Service 中注入的会提示一个红线提示,为什么呢?
BookDao 是一个接口,没有实现类,接口是不能创建对象的,所以最终注入的应该是代理对象
代理对象是由 Spring 的 IOC 容器来创建管理的
IOC 容器又是在 Web 服务器启动的时候才会创建
IDEA 在检测依赖关系的时候,没有找到适合的类注入,所以会提示错误提示
但是程序运行的时候,代理对象就会被创建,框架会使用 DI 进行注入,所以程序运行无影响。
如何解决上述问题?
可以不用理会,因为运行是正常的
设置错误提示级别

步骤 5:编写 Contorller 类
对于图书模块的增删改查就已经完成了编写,我们可以从后往前写也可以从前往后写,最终只需要能把功能实现即可。
接下来我们就先把业务层的代码使用Spring整合Junit的知识点进行单元测试:
1.4 单元测试
步骤 1:新建测试类
步骤 2:注入 Service 类
步骤 3:编写测试方法
我们先来对查询进行单元测试。
根据 ID 查询,测试的结果为:

查询所有,测试的结果为:

1.5 PostMan 测试
新增
http://localhost/books

修改
http://localhost/books

删除
http://localhost/books/14

查询单个
http://localhost/books/1

查询所有
http://localhost/books

2. 统一结果封装
2.1 表现层与前端数据传输协议定义
SSM 整合以及功能模块开发完成后,接下来,我们在上述案例的基础上分析下有哪些问题需要我们去解决下。首先第一个问题是:
在 Controller 层增删改返回给前端的是 boolean 类型数据

1630653359533 在 Controller 层查询单个返回给前端的是对象

1630653385377 在 Controller 层查询所有返回给前端的是集合对象

1630653468887
目前我们就已经有三种数据类型返回给前端,如果随着业务的增长,我们需要返回的数据类型会越来越多。对于前端开发人员在解析数据的时候就比较凌乱了,所以对于前端来说,如果后台能够返回一个统一的数据结果,前端在解析的时候就可以按照一种方式进行解析。开发就会变得更加简单。
所以我们就想能不能将返回结果的数据进行统一,具体如何来做,大体的思路为:
为了封装返回的结果数据:创建结果模型类,封装数据到 data 属性中
为了封装返回的数据是何种操作及是否操作成功:封装操作结果到 code 属性中
操作失败后为了封装返回的错误信息:封装特殊消息到 message(msg)属性中

根据分析,我们可以设置统一数据返回结果类
注意: Result 类名及类中的字段并不是固定的,可以根据需要自行增减提供若干个构造方法,方便操作。
2.2 表现层与前端数据传输协议实现
前面我们已经分析了如何封装返回结果数据,具体在项目中该如何实现,我们通过个例子来操作一把
2.2.1 环境准备
创建一个 Web 的 Maven 项目
pom.xml 添加 SSM 整合所需 jar 包
创建对应的配置类
编写 Controller、Service 接口、Service 实现类、Dao 接口和模型类
resources 下提供 jdbc.properties 配置文件
因为这个项目环境的内容和 SSM 整合的内容是一致的,所以我们就不在把代码粘出来了,大家在练习的时候可以在前面整合的例子案例环境下,进行本节内容的开发。
最终创建好的项目结构如下:

2.2.2 结果封装
对于结果封装,我们应该是在表现层进行处理,所以我们把结果类放在 controller 包下,当然你也可以放在 domain 包,这个都是可以的,具体如何实现结果封装,具体的步骤为:
步骤 1:创建 Result 类
步骤 2:定义返回码 Code 类
注意: code 类中的常量设计也不是固定的,可以根据需要自行增减,例如将查询再进行细分为 GET_OK,GET_ALL_OK,GET_PAGE_OK 等。
步骤 3:修改 Controller 类的返回值
步骤 4:启动服务测试

至此,我们的返回结果就已经能以一种统一的格式返回给前端。前端根据返回的结果,先从中获取code,根据 code 判断,如果成功则取data属性的值,如果失败,则取msg中的值做提示。
3. 统一异常处理
3.1 问题描述
在讲解这一部分知识点之前,我们先来演示个效果,修改 BookController 类的getById方法
重新启动运行项目,使用 PostMan 发送请求,当传入的 id 为 1,则会出现如下效果:

前端接收到这个信息后和之前我们约定的格式不一致,这个问题该如何解决?
在解决问题之前,我们先来看下异常的种类及出现异常的原因:
框架内部抛出的异常:因使用不合规导致
数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)
看完上面这些出现异常的位置,你会发现,在我们开发的任何一个位置都有可能出现异常,而且这些异常是不能避免的。所以我们就得将异常进行处理。
思考
各个层级均出现异常,异常处理代码书写在哪一层?
所有的异常均抛出到表现层进行处理
异常的种类很多,表现层如何将所有的异常都处理到呢?
异常分类
表现层处理异常,每个方法中单独书写,代码书写量巨大且意义不强,如何解决?
AOP
对于上面这些问题及解决方案,SpringMVC 已经为我们提供了一套解决方案:
异常处理器:
集中的、统一的处理项目中出现的异常。

1630657791653
3.2 异常处理器的使用
3.2.1 环境准备
创建一个 Web 的 Maven 项目
pom.xml 添加 SSM 整合所需 jar 包
创建对应的配置类
编写 Controller、Service 接口、Service 实现类、Dao 接口和模型类
resources 下提供 jdbc.properties 配置文件
内容参考前面的项目或者直接使用前面的项目进行本节内容的学习。
最终创建好的项目结构如下:

3.2.2 使用步骤
步骤 1:创建异常处理器类
确保 SpringMvcConfig 能够扫描到异常处理器类
步骤 2:让程序抛出异常
修改BookController的 getById 方法,添加int i = 1/0.
步骤 3:运行程序,测试

说明异常已经被拦截并执行了doException方法。
异常处理器类返回结果给前端
启动运行程序,测试

至此,就算后台执行的过程中抛出异常,最终也能按照我们和前端约定好的格式返回给前端。
知识点 1:@RestControllerAdvice
类型
类注解
位置
Rest 风格开发的控制器增强类定义上方
作用
为 Rest 风格开发的控制器类做增强
说明: 此注解自带@ResponseBody 注解与@Component 注解,具备对应的功能

知识点 2:@ExceptionHandler
类型
方法注解
位置
专用于异常处理的控制器方法上方
作用
设置指定异常的处理方案,功能等同于控制器方法, 出现异常后终止原始控制器执行,并转入当前方法执行
说明: 此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常
3.3 项目异常处理方案
3.3.1 异常分类
异常处理器我们已经能够使用了,那么在咱们的项目中该如何来处理异常呢?
因为异常的种类有很多,如果每一个异常都对应一个@ExceptionHandler,那得写多少个方法来处理各自的异常,所以我们在处理异常之前,需要对异常进行一个分类:
业务异常(BusinessException)
规范的用户行为产生的异常
用户在页面输入内容的时候未按照指定格式进行数据填写,如在年龄框输入的是字符串

1630659599983
不规范的用户行为操作产生的异常
如用户故意传递错误数据

1630659622958
系统异常(SystemException)
项目运行过程中可预计但无法避免的异常
比如数据库或服务器宕机
其他异常(Exception)
编程人员未预期到的异常,如:用到的文件不存在

1630659690341
将异常分类以后,针对不同类型的异常,要提供具体的解决方案:
3.3.2 异常解决方案
业务异常(BusinessException)
发送对应消息传递给用户,提醒规范操作
大家常见的就是提示用户名已存在或密码格式不正确等
系统异常(SystemException)
发送固定消息传递给用户,安抚用户
系统繁忙,请稍后再试
系统正在维护升级,请稍后再试
系统出问题,请联系系统管理员等
发送特定消息给运维人员,提醒维护
可以发送短信、邮箱或者是公司内部通信软件
记录日志
发消息和记录日志对用户来说是不可见的,属于后台程序
其他异常(Exception)
发送固定消息传递给用户,安抚用户
发送特定消息给编程人员,提醒维护(纳入预期范围内)
一般是程序没有考虑全,比如未做非空校验等
记录日志
3.3.3 异常解决方案的具体实现
思路:
1.先通过自定义异常,完成 BusinessException 和 SystemException 的定义
2.将其他异常包装成自定义异常类型
3.在异常处理器类中对不同的异常进行处理
步骤 1:自定义异常类
说明:
让自定义异常类继承
RuntimeException的好处是,后期在抛出这两个异常的时候,就不用在 try...catch...或 throws 了自定义异常类中添加
code属性的原因是为了更好的区分异常是来自哪个业务的
步骤 2:将其他异常包成自定义异常
假如在 BookServiceImpl 的 getById 方法抛异常了,该如何来包装呢?
具体的包装方式有:
方式一:
try{}catch(){}在 catch 中重新 throw 我们自定义异常即可。方式二:直接 throw 自定义异常即可
上面为了使code看着更专业些,我们在 Code 类中再新增需要的属性
步骤 3:处理器类中处理自定义异常
步骤 4:运行程序
根据 ID 查询,
如果传入的参数为 1,会报BusinessException

如果传入的是其他参数,会报SystemException

对于异常我们就已经处理完成了,不管后台哪一层抛出异常,都会以我们与前端约定好的方式进行返回,前端只需要把信息获取到,根据返回的正确与否来展示不同的内容即可。
小结
以后项目中的异常处理方式为:

4. 前后台协议联调
4.1 环境准备
创建一个 Web 的 Maven 项目
pom.xml 添加 SSM 整合所需 jar 包
创建对应的配置类
编写 Controller、Service 接口、Service 实现类、Dao 接口和模型类
resources 下提供 jdbc.properties 配置文件
内容参考前面的项目或者直接使用前面的项目进行本节内容的学习。
最终创建好的项目结构如下:

将
资料\SSM功能页面下面的静态资源拷贝到 webapp 下。

因为添加了静态资源,SpringMVC 会拦截,所有需要在 SpringConfig 的配置类中将静态资源进行放行。
新建 SpringMvcSupport
在 SpringMvcConfig 中扫描 SpringMvcSupport
接下来我们就需要将所有的列表查询、新增、修改、删除等功能一个个来实现下。
4.2 列表功能

需求:页面加载完后发送异步请求到后台获取列表数据进行展示。
1.找到页面的钩子函数,
created()2.
created()方法中调用了this.getAll()方法3.在 getAll()方法中使用 axios 发送异步请求从后台获取数据
4.访问的路径为
http://localhost/books5.返回数据
返回数据 res.data 的内容如下:
发送方式:

4.3 添加功能

需求:完成图片的新增功能模块
1.找到页面上的
新建按钮,按钮上绑定了@click="handleCreate()"方法2.在 method 中找到
handleCreate方法,方法中打开新增面板3.新增面板中找到
确定按钮,按钮上绑定了@click="handleAdd()"方法4.在 method 中找到
handleAdd方法5.在方法中发送请求和数据,响应成功后将新增面板关闭并重新查询数据
handleCreate打开新增面板
handleAdd方法发送异步请求并携带数据
4.4 添加功能状态处理
基础的新增功能已经完成,但是还有一些问题需要解决下:
需求:新增成功是关闭面板,重新查询数据,那么新增失败以后该如何处理?
1.在 handlerAdd 方法中根据后台返回的数据来进行不同的处理
2.如果后台返回的是成功,则提示成功信息,并关闭面板
3.如果后台返回的是失败,则提示错误信息
(1)修改前端页面
(2)后台返回操作结果,将 Dao 层的增删改方法返回值从void改成int
(3)在 BookServiceImpl 中,增删改方法根据 DAO 的返回值来决定返回 true/false
(4)测试错误情况,将图书类别长度设置超出范围即可

处理完新增后,会发现新增还存在一个问题,
新增成功后,再次点击新增按钮会发现之前的数据还存在,这个时候就需要在新增的时候将表单内容清空。
4.5 修改功能

需求:完成图书信息的修改功能
1.找到页面中的
编辑按钮,该按钮绑定了@click="handleUpdate(scope.row)"2.在 method 的
handleUpdate方法中发送异步请求根据 ID 查询图书信息3.根据后台返回的结果,判断是否查询成功
如果查询成功打开修改面板回显数据,如果失败提示错误信息
4.修改完成后找到修改面板的
确定按钮,该按钮绑定了@click="handleEdit()"5.在 method 的
handleEdit方法中发送异步请求提交修改数据6.根据后台返回的结果,判断是否修改成功
如果成功提示错误信息,关闭修改面板,重新查询数据,如果失败提示错误信息
scope.row 代表的是当前行的行数据,也就是说,scope.row 就是选中行对应的 json 数据,如下:
修改handleUpdate方法
修改handleEdit方法
至此修改功能就已经完成。
4.6 删除功能

需求:完成页面的删除功能。
1.找到页面的删除按钮,按钮上绑定了
@click="handleDelete(scope.row)"2.method 的
handleDelete方法弹出提示框3.用户点击取消,提示操作已经被取消。
4.用户点击确定,发送异步请求并携带需要删除数据的主键 ID
5.根据后台返回结果做不同的操作
如果返回成功,提示成功信息,并重新查询数据
如果返回失败,提示错误信息,并重新查询数据
修改handleDelete方法
接下来,下面是一个完整页面
5. 拦截器
对于拦截器这节的知识,我们需要学习如下内容:
拦截器概念
入门案例
拦截器参数
拦截器工作流程分析
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 拦截器入门案例
5.2.1 环境准备
创建一个 Web 的 Maven 项目
pom.xml 添加 SSM 整合所需 jar 包
创建对应的配置类
创建模型类 Book
编写 Controller
最终创建好的项目结构如下:

5.2.2 拦截器开发
步骤 1:创建拦截器类
让类实现 HandlerInterceptor 接口,重写接口中的三个方法。
注意: 拦截器类要被 SpringMVC 容器扫描到。
步骤 2:配置拦截器类
步骤 3:SpringMVC 添加 SpringMvcSupport 包扫描
步骤 4:运行程序测试
使用 PostMan 发送http://localhost/books

如果发送http://localhost/books/100会发现拦截器没有被执行,原因是拦截器的addPathPatterns方法配置的拦截路径是/books,我们现在发送的是/books/100,所以没有匹配上,因此没有拦截,拦截器就不会执行。
步骤 5:修改拦截器拦截规则
这个时候,如果再次访问http://localhost/books/100,拦截器就会被执行。
最后说一件事,就是拦截器中的preHandler方法,如果返回 true,则代表放行,会执行原始 Controller 类中要请求的方法,如果返回 false,则代表拦截,后面的就不会再执行了。
步骤 6:简化 SpringMvcSupport 的编写
此后咱们就不用再写SpringMvcSupport类了。
最后我们来看下拦截器的执行流程:

当有拦截器后,请求会先进入 preHandle 方法,
如果方法返回 true,则放行继续执行后面的 handle[controller 的方法]和后面的方法
如果返回 false,则直接跳过后面方法的执行。
5.3 拦截器参数
5.3.1 前置处理方法
原始方法之前运行 preHandle
request:请求对象
response:响应对象
handler:被调用的处理器对象,本质上是一个方法对象,对反射中的 Method 对象进行了再包装
使用 request 对象可以获取请求数据中的内容,如获取请求头的Content-Type
使用 handler 参数,可以获取方法的相关信息
5.3.2 后置处理方法
原始方法运行后运行,如果原始方法被拦截,则不执行
前三个参数和上面的是一致的。
modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整
因为咱们现在都是返回 json 数据,所以该参数的使用率不高。
5.3.3 完成处理方法
拦截器最后执行的方法,无论原始方法是否执行
前三个参数与上面的是一致的。
ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理
因为我们现在已经有全局异常处理器类,所以该参数的使用率也不高。
这三个方法中,最常用的是 preHandle,在这个方法中可以通过返回值来决定是否要进行放行,我们可以把业务逻辑放在该方法中,如果满足业务则返回 true 放行,不满足则返回 false 拦截。
5.4 拦截器链配置
目前,我们在项目中只添加了一个拦截器,如果有多个,该如何配置?配置多个后,执行顺序是什么?
5.4.1 配置多个拦截器
步骤 1:创建拦截器类
实现接口,并重写接口中的方法
步骤 2:配置拦截器类
步骤 3:运行程序,观察顺序

拦截器执行的顺序是和配置顺序有关。就和前面所提到的运维人员进入机房的案例,先进后出。
当配置多个拦截器时,形成拦截器链
拦截器链的运行顺序参照拦截器添加顺序为准
当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
当拦截器运行中断,仅运行配置在前面的拦截器的 afterCompletion 操作

preHandle:与配置顺序相同,必定运行
postHandle:与配置顺序相反,可能不运行
afterCompletion:与配置顺序相反,可能不运行。
这个顺序不太好记,最终只需要把握住一个原则即可:以最终的运行结果为准
Last updated
Was this helpful?