SpringBoot学习
三层架构
关于Maven
- 方便快捷的管理项目依赖的资源(jar包),避免版本冲突问题
只需要在maven项目的pom.xml
文件中,添加一段如下图所示的配置即可实现。
Maven项目的目录结构:
1 | maven-project01 |
POM配置
下面为pom.xml
的内容。POM指的是项目对象模型,用来描述当前的maven项目。
1 |
|
<project>
:pom文件的根标签,表示当前maven项目<modelVersion>
:声明项目描述遵循哪一个POM模型版本- 虽然模型本身的版本很少改变,但它仍然是必不可少的。目前POM模型版本是4.0.0
- 坐标 :
<groupId>
、<artifactId>
、<version>
- 定位项目在本地仓库中的位置,由以上三个标签组成一个坐标
<packaging>
:maven项目的打包方式,通常设置为jar或war(默认值:jar)
什么是坐标?
- Maven中的坐标是==资源的唯一标识== , 通过该坐标可以唯一定位资源位置
- 使用坐标来定义项目或引入项目中需要的依赖
Maven坐标主要组成
- groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.itheima)
- artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)
- version:定义当前项目版本号
依赖配置
引入依赖
除了描述这个项目,我们项目中还需要引入依赖,类似于前端的npm的包
例如:在当前工程中,我们需要用到logback来记录日志,此时就可以在maven工程的pom.xml文件中,引入logback的依赖。具体步骤如下:
- 在pom.xml中编写
<dependencies>
标签 - 在
<dependencies>
标签中使用<dependency>
引入坐标 - 定义坐标的 groupId、artifactId、version
1 | <dependencies> |
如果不知道依赖的坐标信息,可以到mvn的中央仓库(https://mvnrepository.com/)中搜索
依赖传递
依赖的传递性:若一个包依赖另外的包,也会把其他包一起导入
若不想使用间接依赖
,可以使用排除依赖
:指主动断开依赖的资源。(被排除的资源无需指定版本)
1 | <dependency> |
A依赖C,B依赖C,如果仅在B下面排除C,但是A仍然会引入A,这个做法可以用在两个依赖版本冲突时使用。
依赖范围
在项目中导入依赖的jar包后,默认情况下,可以在任何地方使用
如果希望限制依赖的使用范围,可以通过<scope>
标签设置其作用范围
scope值 | 主程序 | 测试程序(test) | 打包(运行) | 范例 |
---|---|---|---|---|
compile(默认) | Y | Y | Y | log4j |
test | - | Y | - | junit |
provided | Y | Y | - | servlet-api |
runtime | - | Y | Y | jdbc驱动 |
起步依赖
在SpringBoot的项目中,有很多的起步依赖,他们有一个共同的特征:就是以spring-boot-starter-
作为开头。在以后大家遇到spring-boot-starter-xxx这类的依赖,都为起步依赖。
起步依赖有什么特殊之处呢,这里我们以入门案例中引入的起步依赖做为讲解:
- spring-boot-starter-web:包含了web应用开发所需要的常见依赖
- spring-boot-starter-test:包含了单元测试所需要的常见依赖
在我们之前开发的SpringBoot入门案例中,我们通过maven引入的依赖,是没有指定具体的依赖版本号的。为什么没有指定<version>
版本号,可以正常使用呢?因为每一个SpringBoot工程,都有一个父工程。依赖的版本号,在父工程中统一管理。
1 |
|
生命周期
生命周期的顺序是:clean —> validate —> compile —> test —> package —> verify —> install —> site —> deploy
我们需要关注的就是:clean —> compile —> test —> package —> install
SpringBoot基础入门
第一步:创建工程
分层解耦
- 前端发起的请求,由Controller层接收(Controller响应数据给前端)
- Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
- Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
- Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)
分层解耦举个例子——
【分层】
(注:只实现了分层)
【解耦】
把业务类变为EmpServiceB时,需要修改controller层中的代码
为什么耦合了?
之前我们在编写代码时,需要什么对象,就直接new一个就可以了。所以,不能在EmpController中使用new对象
不能new,就意味着没有业务层对象(程序运行就报错),怎么办呢?我们的解决思路是:
- 提供一个容器,容器中存储一些对象(例:EmpService对象)
- controller程序从容器中获取EmpService类型的对象
IOC和DI基础
Spring中的两个核心概念:
控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。
这个容器称为:IOC容器或Spring容器依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
程序运行时需要某个资源,此时容器就为其提供这个资源。
例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象
IOC容器中创建、管理的对象,称之为:bean对象
【如何管理】
- Service层及Dao层的实现类,使用Spring提供的注解:
@Component
,交给IOC容器管理 - 为Controller及Service注入运行时依赖的对象,使用Spring提供的注解:
@Autowired
,就可以实现程序运行时IOC容器自动注入需要的依赖对象
【完整的三层代码】
Controller层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EmpController {
//运行时,从IOC容器中获取该类型对象,赋值给该变量
private EmpService empService ;
public Result list(){
//1. 调用service, 获取数据
List<Emp> empList = empService.listEmp();
//3. 响应数据
return Result.success(empList);
}
}Service层:
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//将当前对象交给IOC容器管理,成为IOC容器的bean
public class EmpServiceA implements EmpService {
//运行时,从IOC容器中获取该类型对象,赋值给该变量
private EmpDao empDao ;
public List<Emp> listEmp() {
//1. 调用dao, 获取数据
List<Emp> empList = empDao.listEmp();
//2. 对数据进行转换处理 - gender, job
empList.stream().forEach(emp -> {
//处理 gender 1: 男, 2: 女
String gender = emp.getGender();
if("1".equals(gender)){
emp.setGender("男");
}else if("2".equals(gender)){
emp.setGender("女");
}
//处理job - 1: 讲师, 2: 班主任 , 3: 就业指导
String job = emp.getJob();
if("1".equals(job)){
emp.setJob("讲师");
}else if("2".equals(job)){
emp.setJob("班主任");
}else if("3".equals(job)){
emp.setJob("就业指导");
}
});
return empList;
}
}
Dao层:
1 | //将当前对象交给IOC容器管理,成为IOC容器的bean |
IOC细节
pring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:
- @Controller (标注在控制层类上)
- 或
@RestController
- @RestController = @Controller + @ResponseBody
- 或
- @Service (标注在业务层类上)
- @Repository (标注在数据访问层dao类上)
声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
1 |
|
DI细节
我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。
入门程序举例:在EmpController运行的时候,就要到IOC容器当中去查找EmpService这个类型的对象,而我们的IOC容器中刚好有一个EmpService这个类型的对象,所以就找到了这个类型的对象完成注入操作。
那如果在IOC容器中,存在多个相同类型的bean对象,则会报错
Spring提供了以下几种解决方案:
- @Primary:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。
@Qualifier:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。
- @Qualifier注解不能单独使用,必须配合@Autowired使用
@Resource:使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。
与Mybatis结合(参考Mybatis文档)
实战
基础工作
准备
数据库准备
1 | -- 部门管理 |
生成pom.xml
文件
1 |
|
配置文件application.properties中引入mybatis的配置信息,准备对应的实体类
application.properties (直接把之前项目中的复制过来)
1
2
3
4
5
6
7
8
9
10
11#数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/tlias
spring.datasource.username=root
spring.datasource.password=1234
#开启mybatis的日志输出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#开启数据库表字段 到 实体类属性的驼峰映射
mybatis.configuration.map-underscore-to-camel-case=true实体类
1 | /*部门类*/ |
1 | /*员工类*/ |
第4步:准备对应的Mapper、Service(接口、实现类)、Controller基础结构
数据访问层:
- DeptMapper
1 | package com.itheima.mapper; |
- EmpMapper
1 | package com.itheima.mapper; |
业务层:
- DeptService
1 | package com.itheima.service; |
DeptServiceImpl
1
2
3
4
5
6
7
8
9package com.itheima.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
//部门业务实现类
public class DeptServiceImpl implements DeptService {
}EmpService
1
2
3
4
5package com.itheima.service;
//员工业务规则
public interface EmpService {
}EmpServiceImpl
1 | package com.itheima.service.impl; |
控制层:
- DeptController
1 | package com.itheima.controller; |
- EmpController
1 | package com.itheima.controller; |
项目工程结构:
编写接口
查询部门
DeptController1
2
3
4
5
6
7
8
9
10
11
12
13
14// 自动生成logger对象
public class DeptController {
private DeptService deptService; // 自动调用deptService
//@RequestMapping(value = "/depts" , method = RequestMethod.GET)
// get depts时
public Result list(){
log.info("查询所有部门数据");
List<Dept> deptList = deptService.list(); //调用deptService的list方法
return Result.success(deptList);
}
}
DeptService(业务接口)1
2
3
4
5
6
7public interface DeptService {
/**
* 查询所有的部门数据
* @return 存储Dept对象的集合
*/
List<Dept> list(); // list方法:方法将返回一个`List`集合,集合中包含`Dept`类型的对象
}
DeptServiceImpl(业务实现类)1
2
3
4
5
6
7
8
9
10
11
12
public class DeptServiceImpl implements DeptService {
private DeptMapper deptMapper;
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
return deptList;
}
}
DeptMapper1
2
3
4
5
6
public interface DeptMapper {
//查询所有部门数据
List<Dept> list();
}
删除部门
DeptController1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DeptController {
private DeptService deptService;
public Result delete( { Integer id)
//日志记录
log.info("根据id删除部门");
//调用service层功能
deptService.delete(id);
//响应
return Result.success();
}
//省略...
}
DeptService1
2
3
4
5
6
7
8
9
10public interface DeptService {
/**
* 根据id删除部门
* @param id 部门id
*/
void delete(Integer id);
//省略...
}
DeptServiceImpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DeptServiceImpl implements DeptService {
private DeptMapper deptMapper;
public void delete(Integer id) {
//调用持久层删除功能
deptMapper.deleteById(id);
}
//省略...
}
DeptMapper1
2
3
4
5
6
7
8
9
10
11
public interface DeptMapper {
/**
* 根据id删除部门信息
* @param id 部门id
*/
void deleteById(Integer id);
//省略...
}
新增部门
DeptController1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DeptController {
private DeptService deptService;
public Result add({ Dept dept)
// @RequestBody //把前端传递的json数据填充到实体类中
//记录日志
log.info("新增部门:{}",dept);
//调用service层添加功能
deptService.add(dept);
//响应
return Result.success();
}
//省略...
}
DeptService1
2
3
4
5
6
7
8
9
10public interface DeptService {
/**
* 新增部门
* @param dept 部门对象
*/
void add(Dept dept);
//省略...
}
DeptServiceImpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DeptServiceImpl implements DeptService {
private DeptMapper deptMapper;
public void add(Dept dept) {
//补全部门数据
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
//调用持久层增加功能
deptMapper.inser(dept);
}
//省略...
}
DeptMapper1
2
3
4
5
6
7
8
9
public interface DeptMapper {
void inser(Dept dept);
//省略...
}
分页查询
先定义分页类pageBean
1 | public class PageBean{ |
EmpController
1 | import com.itheima.pojo.PageBean; |
EmpService
1 | public interface EmpService { |
EmpServiceImpl1
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
import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Emp;
import com.itheima.pojo.PageBean;
import com.itheima.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
public class EmpServiceImpl implements EmpService {
private EmpMapper empMapper;
public PageBean page(Integer page, Integer pageSize) {
//1、获取总记录数
Long count = empMapper.count();
//2、获取分页查询结果列表
Integer start = (page - 1) * pageSize; //计算起始索引 , 公式: (页码-1)*页大小
List<Emp> empList = empMapper.list(start, pageSize);
//3、封装PageBean对象
PageBean pageBean = new PageBean(count , empList);
return pageBean;
}
}
EmpMapper
1 |
|
PageHelper
现成的分页插件完成。对于Mybatis来讲现在最主流的就是PageHelper。
pageHelper是Mybatis的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。
官网:https://pagehelper.github.io/
当使用了PageHelper分页插件进行分页,就无需再Mapper中进行手动分页了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中,调用Mapper的方法之前设置分页参数,在调用Mapper方法执行查询之后,解析分页结果,并将结果封装到PageBean对象中返回。
1、在pom.xml引入依赖1
2
3
4
5<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
2、EmpMapper1
2
3
4
5
6
public interface EmpMapper {
//获取当前页的结果列表
public List<Emp> page(Integer start, Integer pageSize);
}
3、EmpServiceImpl1
2
3
4
5
6
7
8
9
10
11
12
public PageBean page(Integer page, Integer pageSize) {
// 设置分页参数
PageHelper.startPage(page, pageSize);
// 执行分页查询
List<Emp> empList = empMapper.list(name,gender,begin,end);
// 获取分页结果
Page<Emp> p = (Page<Emp>) empList;
//封装PageBean
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
带条件分页
需求:
- 姓名:模糊匹配
- 性别:精确匹配
- 入职日期:范围匹配
例如:1
2
3
4
5
6
7select *
from emp
where
name like concat('%','张','%') -- 条件1:根据姓名模糊匹配
and gender = 1 -- 条件2:根据性别精确匹配
and entrydate = between '2000-01-01' and '2010-01-01' -- 条件3:根据入职日期范围匹配
order by update_time desc;
在原有分页查询的代码基础上进行改造:
EmpController1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class EmpController {
private EmpService empService;
//条件分页查询
public Result page( Integer page,
Integer pageSize,
String name, Short gender,
LocalDate begin,
{ LocalDate end)
//记录日志
log.info("分页查询,参数:{},{},{},{},{},{}", page, pageSize,name, gender, begin, end);
//调用业务层分页查询功能
PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end);
//响应
return Result.success(pageBean);
}
}
EmpService1
2
3
4
5
6
7
8
9
10
11
12
13public interface EmpService {
/**
* 条件分页查询
* @param page 页码
* @param pageSize 每页展示记录数
* @param name 姓名
* @param gender 性别
* @param begin 开始时间
* @param end 结束时间
* @return
*/
PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end);
}
EmpServiceImpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EmpServiceImpl implements EmpService {
private EmpMapper empMapper;
public PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {
//设置分页参数
PageHelper.startPage(page, pageSize);
//执行条件分页查询
List<Emp> empList = empMapper.list(name, gender, begin, end);
//获取查询结果
Page<Emp> p = (Page<Emp>) empList;
//封装PageBean
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
}
EmpMapper
1 |
|
EmpMapper.xml
1 |
|
文件上传
文件上传时在服务端会产生一个临时文件,请求响应完成之后,这个临时文件被自动删除,并没有进行保存。下面呢,我们就需要完成将上传的文件保存在服务器的本地磁盘上。
可以使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下
MultipartFile 常见方法:
- String getOriginalFilename(); //获取原始文件名
- void transferTo(File dest); //将接收的文件转存到磁盘文件中
- long getSize(); //获取文件的大小,单位:字节
- byte[] getBytes(); //获取文件内容的字节数组
- InputStream getInputStream(); //获取接收到的文件内容的输入流
1 |
|
UUID随机生成文件名
1 |
|
大文件
那么如果需要上传大文件,可以在application.properties进行如下配置:1
2
3
4
5#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
使用OSS服务
使用阿里云提供的OSS等服务,需要定义一些账户密码等信息,可以写入application中
可以将参数配置在配置文件中。如下:
1 | #自定义的阿里云OSS配置信息 |
在将阿里云OSS配置参数交给properties配置文件来管理之后,我们的AliOSSUtils工具类就变为以下形式:
1 |
|
在调用时,具体用法为: @Value(“${配置文件中的key}”)
1 |
|
登录认证
LoginController1
2
3
4
5
6
7
8
9
10
11
12
public class LoginController {
private EmpService empService;
public Result login({ Emp emp)
Emp e = empService.login(emp);
return e != null ? Result.success():Result.error("用户名或密码错误");
}
}
EmpService1
2
3
4
5
6
7
8
9
10
11public interface EmpService {
/**
* 用户登录
* @param emp
* @return
*/
public Emp login(Emp emp);
//省略其他代码...
}
EmpServiceImpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EmpServiceImpl implements EmpService {
private EmpMapper empMapper;
public Emp login(Emp emp) {
//调用dao层功能:登录
Emp loginEmp = empMapper.getByUsernameAndPassword(emp);
//返回查询结果给Controller
return loginEmp;
}
//省略其他代码...
}
EmpMapper1
2
3
4
5
6
7
8
9
10
public interface EmpMapper {
public Emp getByUsernameAndPassword(Emp emp);
//省略其他代码...
}
JWT与拦截器
JWT全称:JSON Web Token (官网:https://jwt.io/)
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
我们在登陆的时候,由服务器生成jwt字符串,由客户端进行存储。
在后续的请求当中,客户端都会在请求头中携带JWT令牌到服务端,而服务端需要统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。 那怎么样来统一拦截到所有的请求校验令牌的有效性呢?这里我们会学习两种解决方案:
- Filter过滤器
- Interceptor拦截器
下面对 JWT
\ Filter
\ Interceptor
作简单说明
JWT实现
引入JWT依赖
1 | <!-- JWT依赖--> |
编写JWT工具类函数util
1 | public class JwtUtils { |
登录成功时,在controller层进行返回
1 |
|
我们在发起一个查询部门数据的请求,此时我们可以看到在请求头中包含一个token(JWT令牌),后续的每一次请求当中,都会将这个令牌携带到服务端。
过滤器
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
- 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
- 使用方法
- 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
- 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
1 | //定义一个类,实现一个标准的Filter过滤器的接口 |
- init方法: 过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。
- doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。
- destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。
在定义完Filter之后,Filter其实并不会生效,还需要完成Filter的配置,Filter的配置非常简单,只需要在Filter类上添加一个注解:@WebFilter
,并指定属性urlPatterns
,通过这个属性指定过滤器要拦截哪些请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
//初始化方法, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
//拦截到请求之后调用, 调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Demo 拦截到了请求...放行前逻辑");
//放行
chain.doFilter(request,response);
}
//销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
拦截路径 | urlPatterns值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
当我们在Filter类上面加了@WebFilter
注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan
,通过这个@ServletComponentScan
注解来开启SpringBoot项目对于Servlet组件的支持。
1 |
|
可以定义多个filter,直接定义即可,无需其他配置。filter执行顺序按照“字典序”排序
最后,完整实现登录的filter
1 |
|
拦截器
一种动态拦截方法调用的机制,类似于过滤器。
自定义拦截器: 实现HandlerInterceptor接口,并重写其所有方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//自定义拦截器
public class LoginCheckInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //true表示放行
}
//目标资源方法执行后执行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ... ");
}
//视图渲染完毕后执行,最后执行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion .... ");
}
}
注意:
preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行
postHandle方法:目标资源方法执行后执行
afterCompletion方法:视图渲染完毕后执行,最后执行
注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法
1 |
|
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配 /depts/1 |
/** | 任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 |
/depts/* | /depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2,/depts |
/depts/** | /depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 |
过滤器和拦截器之间的区别,其实它们之间的区别主要是两点:
- 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
- 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
- 时间顺序不同:先执行filter,才会执行interceptor
登录拦截器的实现
1 | // ======================登陆配置拦截器=================== |
错误处理
- 方案一:在所有Controller的所有方法中进行try…catch处理
- 缺点:代码臃肿(不推荐)
- 方案二:全局异常处理器
- 好处:简单、优雅(推荐)
定义全局异常处理器:定义一个类,在类上加上一个注解@RestControllerAdvice
,加上这个注解就代表我们定义了一个全局异常处理器。
在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler
。通过@ExceptionHandler
注解当中的value属性来指定我们要捕获的是哪一类型的异常。
1 |
|
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端
事务管理
场景:删除部门表,删除需要同时删除部门下面的所有人员信息。当删除人员信息过程中发生错误,可能导致删除部门表成功但仍然留存一部分人员仍属于这个部门。
要想保证操作前后,数据的一致性,就需要让解散部门中涉及到的两个业务操作,要么全部成功,要么全部失败
在方法运行之前,开启事务,如果方法成功执行,就提交事务,如果方法执行的过程当中出现异常了,就回滚事务。
使用@Transactional
注解即可开启事务。
- @Transactional注解书写位置:
- 方法
- 当前方法交给spring进行事务管理
- 类
- 当前类中所有的方法都交由spring进行事务管理
- 接口
- 接口下所有的实现类当中所有的方法都交给spring 进行事务管理
- 方法
1 |
|
下面详细介绍@Transactional
注解的两个重要属性
rollbackFor
默认情况下,只有出现RuntimeException(运行时异常) 才会回滚事务。比如,把代码更改为
1 |
|
通过抛出异常将不会产生任何回滚效果。
假如我们想让所有的异常都回滚,需要来配置@Transactional
注解当中的rollbackFor
属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
1 |
|
propagation
这个属性是用来配置事务的传播行为的。
什么是事务的传播行为呢?就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
例如:A\B两个方法,都加了@Transactional
方法。A方法调用了B方法,那么在调用的时候,是新开启事务还是直接沿用同一个事物(因为在一个事务范围内,两个操作都会被回滚)?
我们在@Transactional注解的后面指定一个属性propagation
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
只需要关注前两个
场景:解散部门的操作需要非常完整的日志记录
我们采用REQUIRES_NEW
,这样即便解散失败,部门数据会回滚,但是日志记录仍然存在(如果日志操作事务成功)。若默认值,即便日志事务成功,一旦解散失败,日志也会回滚。
1 |
|
AOP:面向方法编程
面向切面(方法)编程
场景:有一个监控埋点,统计某个业务花费时间。针对不同业务那么都需要写相同的代码。实际上可以简化为:开始计时->调用对应方法->结束计时与分析
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
示例程序
1 | <dependency> |
1 |
|
- AOP典型应用场景:
- 记录系统的操作日志
- 权限控制
- 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务
核心概念
1. 连接点:JoinPoint
可以被AOP控制的方法(暗含方法执行时的相关信息)
例如:入门程序当中所有的业务方法都是可以被aop控制的方法。
- 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
- 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
2. 通知:Advice
指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
例如:上面示例程序的recordTime
就是一类通知Advice
- Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
1 |
|
程序发生异常的情况下:
- @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了(执行位置与AfterReturning相同)
- @Around环绕通知中原始方法调用时有异常,通知中的环绕后(around after) 的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)
3. 切入点:PointCut
匹配连接点的条件,通知仅会在切入点方法执行时被应用
即@Around括号内的内容,限制哪些方法受限于这个AOP方法。假如:切入点表达式改为DeptServiceImpl.list(),此时就代表仅仅只有list这一个方法是切入点。只有list()方法在运行的时候才会应用通知。
execution
若对同一切入点有多个方法调用,可以使用@PointCut
统一切入点,就不用写的重复。(专业名字叫抽取)
1 |
|
当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:全类名.方法名()
;比如@Before("com.itheima.aspect.MyAspect1.pt()")
annotation
若定义execution复杂,可以自行编写注解,然后为每个目标方法添加对应注解即可。
1 | // 自定义注解MyLog |
4.切面:Aspect
描述通知与切入点的对应关系(通知+切入点)
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
多个切面类同时存在,调用顺序为——默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
5. 目标对象:Target
通知所应用的对象
案例
- 自定义注解@Log
1 | /** |
- 修改业务实现类,在增删改业务方法上添加@Log注解
1 |
|
以同样的方式,修改EmpServiceImpl业务类
- 定义切面类,完成记录操作日志的逻辑
1 |
|
代码实现细节: 获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id。