Terminal 4.

SSM

2024/11/10
loading

归来!

Spring Framwork系统架构

MAVEN

下载

添加环境配置
创建仓库
配置IntelliJ IDEA中maven配置
module选择maven

依赖管理

依赖配置

maven仓库依赖:https://mvnrepository.com
找到dependency(坐标) 加入pom.xml

依赖传递

间接依赖也可以传递到

排除依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<!--排除依赖-->
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>

依赖范围

通过 <scope>...</scope> 设置作用范围

生命周期

  • clean:清理工作
  • default:核心工作(编译、测试、打包、安装、部署等)
    • COMPILE TEST PACKAGE INSTALL···
  • site:生成报告、发布站点等。

同一套声明周期中,当运行后面阶段时,前面的阶段也会被运行
执行:直接双击或在终端中mvn command

HTTP请求响应

创建springboot

新建项目选择springboot,勾选spring web这个依赖
创建请求处理类HelloController,添加请求处理方法hello,运行启动类

1
2
3
4
5
6
7
8
9
//请求处理类
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("hello?");
return "hello!";
}
}

浏览器:

  • 输入网址:http://192.168.100.11:8080/hello
    • 通过IP地址192.168.100.11定位到网络上的一台计算机
    • 通过端口号8080找到计算机上运行的程序

      localhost:8080 , 意思是在本地计算机中找到正在运行的8080端口的程序

    • /hello是请求资源位置
      • 资源:对计算机而言资源就是数据
        • web资源:通过网络可以访问到的资源(通常是指存放在服务器上的数据)

          localhost:8080/hello ,意思是向本地计算机中的8080端口程序,获取资源位置是/hello的数据

          • 8080端口程序,在服务器找/hello位置的资源数据,发给浏览器

服务器:(可以理解为ServerSocket)

  • 接收到浏览器发送的信息(如:/hello)
  • 在服务器上找到/hello的资源
  • 把资源发送给浏览器

Tomcat

=servlet容器

Postman

  • Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。

    Postman原是Chrome浏览器的插件,可以模拟浏览器向后端服务器发起任何形式(如:get、post)的HTTP请求
    使用Postman还可以在发起请求时,携带一些请求参数、请求头等信息

  • 作用:常用于进行接口测试

请求响应

请求

原始方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class RequestController {
//原始方式
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){
// http://localhost:8080/simpleParam?name=Tom&age=10
// 请求参数: name=Tom&age=10 (有2个请求参数)
// 第1个请求参数: name=Tom 参数名:name,参数值:Tom
// 第2个请求参数: age=10 参数名:age , 参数值:10

String name = request.getParameter("name");//name就是请求参数名
String ageStr = request.getParameter("age");//age就是请求参数名

int age = Integer.parseInt(ageStr);//需要手动进行类型转换
System.out.println(name+" : "+age);
return "OK";
}
}

SpringBoot方式
在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致
System.out.println(name+" : "+age);
return "OK";
}
}

参数名不一致

1
2
3
4
5
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){
System.out.println(username+ ":" + age);
return "OK";
}

注意事项:

@RequestParam中的required属性默认为true(默认值也是true),代表该请求参数必须传递,如果不传递将报错

实体参数

要想完成数据封装,需要遵守如下规则:请求参数名与实体类的属性名相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping("/simplePojo")
public String simplePojo(User user){
System.out.println(user);
return "OK";
}
//数组集合参数
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby){
System.out.println(Arrays.toString(hobby));
return "OK";
}
//默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用@RequestParam绑定参数关系
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobby){
System.out.println(hobby);
return "OK";
}
//日期时间参数
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
System.out.println(updateTime);
return "OK";
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.example.springbootquickstart.pojo;

public class User {
private String name;
private int age;

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

get和set方法必须设置,如果不设置拿不到对应参数输出结果为:
1
User{name='null', age=0}

数组集合Postman测试:
在前端请求时,有两种传递形式:
方式一: xxxxxxxxxx?hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java

aside: IDEA中ALT+INSERT调出函数生成器快速写getset,tostring方法
ALT+ENTER自动导包

复杂实体

复杂实体对象的封装,需要遵守如下规则:

  • 请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。

JSON参数

  • @RequestBody注解:将JSON数据映射到形参的实体类对象中(JSON中的key和实体类中的属性名保持一致)

实体类:Address

1
2
3
4
5
6
public class Address {
private String province;
private String city;

//省略GET , SET 方法
}

实体类:User

1
2
3
4
5
6
7
public class User {
private String name;
private Integer age;
private Address address;

//省略GET , SET 方法
}

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//JSON参数
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
System.out.println(user);
return "OK";
}
}

postman接口测试:

路径参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class RequestController {
//路径参数
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id){
System.out.println(id);
return "OK";
//传递多个路径参数:
@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id, @PathVariable String name){
System.out.println(id+ " : " +name);
return "OK";
}
}

响应

统一响应结果

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
@RestController
public class ResponseController {
@RequestMapping("/hello")
public Result hello(){
System.out.println("Hello World ~");
//return new Result(1,"success","Hello World ~");
return Result.success("Hello World ~");
}

@RequestMapping("/getAddr")
public Result getAddr(){
Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");
return Result.success(addr);
}

@RequestMapping("/listAddr")
public Result listAddr(){
List<Address> list = new ArrayList<>();

Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");

Address addr2 = new Address();
addr2.setProvince("陕西");
addr2.setCity("西安");

list.add(addr);
list.add(addr2);
return Result.success(list);
}
}
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
package org.example.springbootquickstart.pojo;

/**
* 统一响应结果封装类
*/
public class Result {
private Integer code ;//1 成功 , 0 失败
private String msg; //提示信息
private Object data; //数据 date

public Result() {
}
public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}

public static Result success(Object data){
return new Result(1, "success", data);
}
public static Result success(){
return new Result(1, "success", null);
}
public static Result error(String msg){
return new Result(0, msg, null);
}

@Override
public String toString() {
return "Result{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}

分层解耦

三层架构

  • Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
  • Service:业务逻辑层。处理具体的业务逻辑。
  • Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。

充分解耦

使用对象时,在程序中不要主动使用new产生对象,转换为中外部提供对象

  • IoC(Inversion of Control)控制反转
    • 对象的创建控制权由程序转移到外部 (比如JButton)
    • Spring提供了一个IoC容器,用来充当IoC思想中的“外部”
    • IoC容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在IoC容器中统称为Bean
  • DI(Dependency Injection)依赖注入
    • 容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
  • 效果:使用对象时不仅能直接从IoC容器中获取,并且获取到的bean已经绑定了所有的依赖关系
  • Controller层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class EmpController {

@Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量
private EmpService empService ;

@RequestMapping("/listEmp")
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
@Component //将当前对象交给IOC容器管理,成为IOC容器的bean
public class EmpServiceA implements EmpService {

@Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量
private EmpDao empDao ;

@Override
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
2
3
4
5
6
7
8
9
10
11
@Component //将当前对象交给IOC容器管理,成为IOC容器的bean
public class EmpDaoA implements EmpDao {
@Override
public List<Emp> listEmp() {
//1. 加载并解析emp.xml
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
System.out.println(file);
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
return empList;
}
}

IoC案例

@Component的衍生注解

注解 位置
@Controller 标注在控制器类上(RestController已包含)
@Service 标注在业务类上
@Repository 标注在数据访问类上(由于与mybatis整合,用得少)

@Repository(value = "daoA") 指定bean的名字

此四个注解想要生效,需要被扫描注解@ComponentScan扫描,虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplicatiion中,默认扫描范围是启动类所在包及其子包。

手动设置: @ComponentScan(("dao","com.arg")) ←包名

*设置这个注解之后会把原来的覆盖掉,很麻烦,所以一般按照项目规范放在启动类那个包下面。

DI案例

如果bean有多个:

注解 作用
@Primary 使得注解下面的bean优先注入
@Aurowired + @Qualifier(“bean value”) 注入指定名称的bean
@Resource(name =”bean value”) 注入指定名称的bean

@q和@a是spring提供的 @r是jdk提供的
@a默认按照类型注入 @r默认按照名称注入

Springboot案例

开发规范

  • REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。

传统URL风格如下:

1
2
3
4
http://localhost:8080/user/getById?id=1     GET:查询id为1的用户
http://localhost:8080/user/saveUser POST:新增用户
http://localhost:8080/user/updateUser POST:修改用户
http://localhost:8080/user/deleteUser?id=1 GET:删除id为1的用户

我们看到,原始的传统URL呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。

基于REST风格URL如下:

1
2
3
4
http://localhost:8080/users/1  GET:查询id为1的用户
http://localhost:8080/users POST:新增用户
http://localhost:8080/users PUT:修改用户
http://localhost:8080/users/1 DELETE:删除id为1的用户

其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。

统一响应结果

前后端工程在进行交互时,使用统一响应结果 Result。

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
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;//响应码,1 代表成功; 0 代表失败
private String msg; //响应信息 描述字符串
private Object data; //返回的数据

//增删改 成功响应
public static Result success(){
return new Result(1,"success",null);
}
//查询 成功响应
public static Result success(Object data){
return new Result(1,"success",data);
}
//失败响应
public static Result error(String msg){
return new Result(0,msg,null);
}
}

开发流程

  1. 查看页面原型明确需求

    • 根据页面原型和需求,进行表结构设计、编写接口文档(已提供)
  2. 阅读接口文档

  3. 思路分析
  4. 功能接口开发
    • 就是开发后台的业务功能,一个业务功能,我们称为一个接口
  5. 功能接口测试
    • 功能开发完毕后,先通过Postman进行功能接口测试,测试通过后,再和前端进行联调测试
  6. 前后端联调测试
    • 和前端开发人员开发好的前端工程一起测试

请求路径优化

在Spring当中为了简化请求路径的定义,可以把公共的请求路径,直接抽取到类上,在类上加一个注解@RequestMapping,并指定请求路径”/depts”。

注意事项:一个完整的请求路径,应该是类上@RequestMapping的value属性 + 方法上的 @RequestMapping的value属性

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
@Slf4j
@RestController
@RequestMapping("/depts")
public class DeptController {
@Autowired
private DeptService deptService;

@GetMapping
public Result list(){
log.info("鏌ヨ鎵€鏈夐儴闂ㄦ暟鎹?);
List<Dept> deptList = deptService.list();
return Result.success(deptList);
}

@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
//鏃ュ織璁板綍
log.info("鏍规嵁id鍒犻櫎閮ㄩ棬");
//璋冪敤service灞傚姛鑳?
deptService.delete(id);
//鍝嶅簲
return Result.success();
}

@PostMapping
public Result add(@RequestBody Dept dept){
//璁板綍鏃ュ織
log.info("鏂板閮ㄩ棬锛歿}",dept);
//璋冪敤service灞傛坊鍔犲姛鑳?
deptService.add(dept);
//鍝嶅簲
return Result.success();
}
}

查询优化

原始方式的分页查询,存在着”步骤固定”、”代码频繁”的问题

解决方案:可以使用一些现成的分页插件完成。对于Mybatis来讲现在最主流的就是PageHelper。

EmpMapper

1
2
3
4
5
6
@Mapper
public interface EmpMapper {
//获取当前页的结果列表
@Select("select * from emp")
public List<Emp> list();
}

EmpServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public PageBean page(Integer page, Integer pageSize) {
// 设置分页参数
PageHelper.startPage(page, pageSize);
// 执行分页查询
List<Emp> empList = empMapper.list();
// 获取分页结果
Page<Emp> p = (Page<Emp>) empList;
//封装PageBean
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}

文件上传

保证每次上传文件时文件名都唯一的(使用UUID获取随机文件名)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@RestController
public class UploadController {

@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);

//获取原始文件名
String originalFilename = image.getOriginalFilename();

//构建新的文件名
String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名
String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名

//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+newFileName));

return Result.success();
}

}

那么如果需要上传大文件,可以在application.properties进行如下配置:

1
2
3
4
5
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB

#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB

如果直接存储在服务器的磁盘目录中,存在以下缺点:

  • 不安全:磁盘如果损坏,所有的文件就会丢失
  • 容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)
  • 无法直接访问

为了解决上述问题呢,通常有两种解决方案:

  • 自己搭建存储服务器,如:fastDFS 、MinIO
  • 使用现成的云服务,如:阿里云,腾讯云,华为云

流式上传与分片上传的原理与实现

文件与文件流

阿里云OSS

参数配置化

因为application.properties是springboot项目默认的配置文件,所以springboot程序在启动时会默认读取application.properties配置文件,而我们可以使用一个现成的注解:@Value,获取配置文件中的数据。

@Value 注解通常用于外部配置的属性注入,具体用法为: @Value(“${配置文件中的key}”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class AliOSSUtils {

@Value("${aliyun.oss.endpoint}")
private String endpoint;

@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;

@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;

@Value("${aliyun.oss.bucketName}")
private String bucketName;

//省略其他代码...
}

yml配置文件

在springboot项目当中是支持多种配置方式的,除了支持properties配置文件以外,还支持yml格式的配置文件。

基本语法:

  • 大小写敏感
  • 数值前边必须有空格,作为分隔符
  • 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • #表示注释,从这个字符一直到行尾,都会被解析器忽略

常见的数据格式:

对象/Map集合

1
2
3
4
user:
name: zhangsan
age: 18
password: 123456

数组/List/Set集合

1
2
3
4
hobby: 
- java
- game
- sport

@ConfigurationProperties

登录校验

怎么来实现登录校验的操作呢?具体的实现思路可以分为两部分:

  1. 在员工登录成功后,需要将用户登录成功的信息存起来,记录用户已经登录成功的标记。
  2. 在浏览器发起请求时,需要在服务端进行统一拦截,拦截后进行登录校验。

会话技术

web开发当中,会话指的就是浏览器与服务器之间的一次连接,我们就称为一次会话

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。

统一拦截技术会话跟踪技术有两种:

  1. Cookie(客户端会话跟踪技术)
    • 数据存储在客户端浏览器当中
  2. Session(服务端会话跟踪技术)
    • 数据存储在储在服务端
  3. 令牌技术

JWT令牌

JWT全称:JSON Web Token (官网:https://jwt.io/)

  • 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
    JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)

  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{“alg”:”HS256”,”type”:”JWT”}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:”1”,”username”:”Tom”}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

要想使用JWT令牌,需要先引入JWT的依赖:

1
2
3
4
5
6
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void genJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","Tom");

String jwt = Jwts.builder()
.setClaims(claims) //自定义内容(载荷)
.signWith(SignatureAlgorithm.HS256, "shining") //签名算法
.setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期
.compact();

System.out.println(jwt);
}

打开JWT的官网,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来。

校验:

1
2
3
4
5
6
7
8
9
@Test
public void parseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("shining")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk")
.getBody();

System.out.println(claims);
}

Servlet规范中的Filter过滤器

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
@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
//前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

//1.获取请求url
String url = request.getRequestURL().toString();
log.info("请求路径:{}", url); //请求路径:http://localhost:8080/login


//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("/login")){
chain.doFilter(request, response);//放行请求
return;//结束当前方法的执行
}


//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
log.info("从请求头中获取的令牌:{}",token);


//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(token)){
log.info("Token不存在");

Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return;
}

//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("令牌解析失败!");

Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return;
}


//6.放行
chain.doFilter(request, response);

}
}

*需导入fastjson依赖

Spring提供的interceptor拦截器

拦截器:

  • 是一种动态拦截方法调用的机制,类似于过滤器。
  • 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。

拦截器的作用:

  • 拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//自定义拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");

return true; //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 .... ");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration  
public class WebConfig implements WebMvcConfigurer {

//自定义的拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;


@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
}
}

拦截路径

addPathPatterns("要拦截路径")方法,指定要拦截哪些资源。

excludePathPatterns("不拦截路径")方法,指定哪些资源不需要拦截。

1
2
3
4
5
6
7
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
.excludePathPatterns("/login");//设置不拦截的请求路径
}

过滤器和拦截器之间的区别:

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

异常处理

当我们没有做任何的异常处理时,我们三层架构处理异常的方案:

  • Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。
  • service 中也存在异常了,会抛给controller。
  • 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。

全局异常处理器

  • 在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
  • 定义一个方法来捕获异常,加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
1
2
3
4
5
6
7
8
9
10
11
12
@RestControllerAdvice
public class GlobalExceptionHandler {

//处理异常
@ExceptionHandler(Exception.class) //指定能够处理的异常类型
public Result ex(Exception e){
e.printStackTrace();//打印堆栈中的异常信息

//捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
}
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

处理异常的方法返回值会转换为json后再响应给前端

事务&AOP

Spring事务管理

在方法运行之前,开启事务,如果方法成功执行,就提交事务,如果方法执行的过程当中出现异常了,就回滚事务。

Transactional注解

作用:开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。

@Transactional注解书写位置:

  • 方法 -> 当前方法交给spring进行事务管理
  • 类 -> 当前类中所有的方法都交由spring进行事务管理
  • 接口 -> 接口下所有的实现类当中所有的方法都交给spring 进行事务管理

般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。

rollbackFor

默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。

假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。

1
@Transactional(rollbackFor=Exception.class)

propagation

事务的传播行为:

  • 当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

常见的事务传播行为:

属性值 含义
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须没事务,否则抛异常

AOP

Aspect Oriented Programming(面向切面编程、面向方面编程)

AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)

动态代理技术

AOP的优势:

  1. 减少重复代码
  2. 提高开发效率
  3. 维护方便

常见的应用场景如下:

  • 记录系统的操作日志
  • 权限控制
  • 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务

AOP依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {

@Around("execution(* com.example.demo.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();

//执行原始方法
Object result = pjp.proceed();

//记录方法执行结束时间
long end = System.currentTimeMillis();

//计算方法执行耗时
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);

return result;
}
}

JoinPoint

连接点指的是可以被aop控制的方法。

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型

  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//获取目标类名
String name = pjp.getTarget().getClass().getName();
log.info("目标类名:{}",name);

//目标方法名
String methodName = pjp.getSignature().getName();
log.info("目标方法名:{}",methodName);

//获取方法执行时需要的参数
Object[] args = pjp.getArgs();
log.info("目标方法参数:{}", Arrays.toString(args));

//执行原始方法
Object returnValue = pjp.proceed();

return returnValue;
}

SpringBoot 原理

Advice

指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

上面recordTime方法即是

PointCut

匹配连接点的条件,通知仅会在切入点方法执行时被应用

在aop的开发当中,我们通常会通过一个切入点表达式来描述切入点

execution(* com.example.demo.service.*.*(..))

execution

语法为:

1
execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

?:可以省略的部分

事例:

1
@Before("execution(void com.example.demo.impl.DeptServiceImpl.delete(java.lang.Integer))")

可以使用通配符描述切入点

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

使用..省略包名, 使用*代替类名

1
execution(* com..*.delete(java.lang.Integer))   

根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。

1
execution(* com.example.demo.service.DeptService.list(..)) || execution(* com.example.demo.service.DeptService.delete(..))
1
2
//匹配DeptServiceImpl类中以find开头的方法
execution(* com.itheima.service.impl.DeptServiceImpl.find*(..))

在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包

@annotation

实现步骤:

  1. 编写自定义注解

  2. 在业务类要做为连接点的方法上添加自定义注解

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
1
2
3
4
5
6
7
8
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
//int num = 10/0;
return deptList;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知

//前置通知
@Before("@annotation(com.example.demo.aop.MyLog)")
public void before(){
log.info("MyAspect6 -> before ...");
}

//后置通知
@After("@annotation(com.example.demo.aop.MyLog)")
public void after(){
log.info("MyAspect6 -> after ...");
}
}
  • execution切入点表达式

    根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式

    如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐

  • annotation 切入点表达式

    基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

Aspect

当通知和切入点结合在一起,就形成了一个切面

切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)

Target

目标对象指的就是通知所应用的对象,我们就称之为目标对象。

如com.example.demo.service中的所有方法

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

通知类型

Spring中AOP的通知类型:

注解 效果
@Around 环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before 前置通知,此注解标注的通知方法在目标方法前被执行
@After 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing 异常后通知,此注解标注的通知方法发生异常后执行

执行顺序: around before -> before -> afterReturning /afterthrowing(若有异常)-> after -> around after

在不同切面类中,默认按照切面类的类名字母排序:

  • 方法前:字母排名靠前的先执行
  • 方法后:字母排名靠前的后执行

※@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行

@PointCut

Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@Component
@Aspect
public class MyAspect1 {

//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.example.demo.service.*.*(..))")
private void pt(){

}

//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");

}
}

※当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public

@Order

使用@Order注解,控制通知的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
// 通知...
}

@Slf4j
@Component
@Aspect
@Order(3) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect3 {
//通知...
}

CATALOG
  1. 1. Spring Framwork系统架构
  2. 2. MAVEN
    1. 2.1. 下载
    2. 2.2. 依赖管理
      1. 2.2.1. 依赖配置
      2. 2.2.2. 依赖传递
      3. 2.2.3. 依赖范围
      4. 2.2.4. 生命周期
  3. 3. HTTP请求响应
    1. 3.1. 创建springboot
  4. 4. Tomcat
  5. 5. Postman
  6. 6. 请求响应
    1. 6.1. 请求
      1. 6.1.1. 参数名不一致
      2. 6.1.2. 实体参数
      3. 6.1.3. 复杂实体
      4. 6.1.4. JSON参数
      5. 6.1.5. 路径参数
    2. 6.2. 响应
      1. 6.2.1. 统一响应结果
  7. 7. 分层解耦
    1. 7.1. 三层架构
    2. 7.2. 充分解耦
      1. 7.2.1. IoC案例
        1. 7.2.1.1. @Component的衍生注解
      2. 7.2.2. DI案例
  8. 8. Springboot案例
    1. 8.1. 开发规范
    2. 8.2. 统一响应结果
    3. 8.3. 开发流程
    4. 8.4. 请求路径优化
    5. 8.5. 查询优化
    6. 8.6. 文件上传
      1. 8.6.1. 阿里云OSS
    7. 8.7. 参数配置化
      1. 8.7.1. yml配置文件
      2. 8.7.2. @ConfigurationProperties
    8. 8.8. 登录校验
    9. 8.9. 会话技术
    10. 8.10. 统一拦截技术会话跟踪技术有两种:
      1. 8.10.1. JWT令牌
    11. 8.11. Servlet规范中的Filter过滤器
    12. 8.12. Spring提供的interceptor拦截器
    13. 8.13. 异常处理
      1. 8.13.1. 全局异常处理器
  9. 9. 事务&AOP
    1. 9.1. Spring事务管理
      1. 9.1.1. Transactional注解
      2. 9.1.2. rollbackFor
      3. 9.1.3. propagation
    2. 9.2. AOP
      1. 9.2.1. JoinPoint
  10. 10. SpringBoot 原理
    1. 10.0.1. Advice
    2. 10.0.2. PointCut
      1. 10.0.2.1. execution
      2. 10.0.2.2. @annotation
    3. 10.0.3. Aspect
    4. 10.0.4. Target
    5. 10.0.5. 通知类型
    6. 10.0.6. @PointCut
    7. 10.0.7. @Order