运行DEMO可以在百度网盘中获取:通过网盘分享的文件:https://pan.baidu.com/s/1Ut9-STQL_8ColkAa4-kMkQ?pwd=edar

资料来源:https://heuqqdmbyk.feishu.cn/wiki/space/7413668442156498972?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home

Spring

官网:https://spring.io

SpringBootWeb快速入门

需求:使用 SpringBoot 开发一个web应用,浏览器发起请求/hello后,给浏览器返回字符串”Hello World”

  1. 创建SprintBoot工程,并勾选web开发相关依赖

1

2

  1. 定义HelloController类,添加方法hello,并添加注解

3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package app.netlify.norlcyan.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// 请求处理类
@RestController
public class HelloController {


@RequestMapping("/hello")
public String hello() {
System.out.println("Hello World");
return "Hello World";
}
}
  1. 运行测试

4

5

  1. 在浏览器中输入网址:127.0.0.1:8080/hello,网页中显示Hello World表示启动成功。

HTTP协议

概念:超文本传输协议,规定了浏览器和服务器之间数据传输的规则

特点:

  • 基于TCP协议:面向连接,安全
  • 基于请求-响应模型的:一次请求对应一次响应
  • HTTP协议是无状态的协议:对于事物处理没有记忆能力。每次请求-响应都是独立的
    • 缺点:多次请求间不能共享数据
    • 优点:速度快

请求协议

请求行:请求数据第一行(请求方式、资源路径、协议)

请求头:第二行开始,格式:key : value

Key 说明
Host 请求的主机名
User-Agent 浏览器版本
Accept 表示浏览器能够接收的资源类型,如test/*,image/*或者*/*表示所有
Accept-Language 表示浏览器偏好的语言,服务器可以据此返回不同语言的网页
Accept-Encoding 表示浏览器可以支持的压缩类型
Content-Type 请求主体的数据类型
Content-Length 请求主体的大小(单位:字节)

请求体:POST请求,存放请求参数

请求方式-GET:请求参数在请求行中,没有请求体,GET请求大小是有限制的

请求方式-POST:请求参数在请求体中,POST请求大小是没有限制的

响应协议

响应行:响应数据第一行(协议、状态码、描述)

状态码 说明
1xx 响应中-临时状态码,表示请求已经接收,告诉客户端应该继续请求或者如果它已经完成则忽略它
2xx 成功-表示请求已经被成功接收,处理已完成
3xx 重定向-重定向到其他地方;让客户端再发起一次请求以完成整个处理
4xx 客户端错误-处理发生错误,责任在客户端。如:请求了不存在的资源、客户端未被授权、禁止访问等
5xx 服务器错误-处理发生错误,责任在服务端。如:程序抛出异常等

响应头:第二行开始,格式:key : value

Key 说明
Content-Type 表示该响应内容的类型,例如:text/html,application/json
Content-Length 表示该响应内容的长度(字节数)
Content-Encoding 表示该响应压缩算法,如:gzip
Cache-Control 指示客户端应如何缓存,例如:max-age=300表示可以最多缓存300秒
Set-Cookie 告诉浏览器为当前页面所在的域设置cookie

响应体:最后一部分,存放响应数据

Web服务器

Web服务器是一个软件程序,对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是“提供上网信息浏览服务”

Tomcat

  • 概念:Tomcat是Apache软件基金会的一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet/JSP少量JavaEE规范

  • Tomcat也被称为Web容器、Servlet容器。Servlet程序需要依赖于Tomcat才能运行

  • 官网:https://tomcat.apache.org

  • Tomcat部署项目:将项目放置到webapps目录下,即部署完成

请求响应

  • 请求(HTTPServletRequest):获取请求数据
  • 响应(HTTPServletResponse):设置响应数据
  • BS架构:Browser/Server ,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端

简单参数

  • 原始方式:在原始的Web程序中,获取请求参数,需要通过HttpServletRequest对象手动获取

    1
    2
    3
    4
    5
    6
    7
    8
    @RequestMapping("/simpleParam")
    public String simpleParam(HttpServletRequest request) {
    String name = request.getParameter("name");
    String agestr = request.getParameter("age");
    int age = Integer.parseInt(ageStr);
    System.out.println(name + ":" + age);
    return "OK";
    }
  • SpringBoot方式:参数名与形参变量名相同,定义形参即可接收参数(会自动转换数据类型)

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

    如果方法形参名称与请求参数名称不匹配,可以使用@RequestParam完成映射

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

    注意:@RequestParam中的required属性默认为true,代表该请求参数必须传递,如果不传递将报错。如果该参数是可选的,可以将required属性设置为false

    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";
    }

实体参数

简单实体参数

  • 简单实体参数:请求参数与形参对象属性名相同,定义POJO接收即可

示例:

生成一个User类:

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
package app.netlify.norlcyan.POJO;

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

public User() {};

public User(String name, Integer age) {
this.name = name;
this.age = age;
}

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

public String getName() {
return name;
}

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

public Integer getAge() {
return age;
}

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

保证类中有正确的”getter”和”setter”

编写处理函数:

1
2
3
4
5
6
// 实体参数
@RequestMapping("/simplePojo")
public String simplePojo(User u) {
System.out.println(u);
return "OK";
}

其中形参是之前创建的User类

发送GET请求:

1
http://127.0.0.1:8080/simplePojo?name=张三&age=20

控制台打印以下结果:

1
User{name='张三', age=20}

复杂实体参数

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

以简单实体参数为例,只需要额外添加一个Address对象即可

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 app.netlify.norlcyan.POJO;

public class Address {
private String province;
private String city;

public String getProvince() {
return province;
}

public void setProvince(String province) {
this.province = province;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", city='" + city + '\'' +
'}';
}
}

处理函数没有区别

发送GET请求:

1
http://127.0.0.1:8080/complexPojo?name=张三GET&age=44&address.province=江苏&address.city=苏州

数组集合参数

数组

数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数

1
2
3
4
5
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby) {
System.out.println(Arrays.toString(hobby));
return "OK";
}

发送GET请求:

1
http://127.0.0.1:8080/arrayParam?hobby=打游戏&hobby=敲代码&hobby=看电影

集合

集合参数:请求参数名与形参集合名称相同且请求参数为多个,@RequestParam绑定参数关系(不加这个注解会默认将参数添加到数组中而不是集合中)

1
2
3
4
5
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobby) {
System.out.println(hobby);
return "OK";
}

发送GET请求:

1
http://127.0.0.1:8080/listParam?hobby=打游戏&hobby=敲代码&hobby=看电影

日期参数

日期参数:使用@DateTimeFormat注解完成日期参数格式转换

1
2
3
4
5
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime) {
System.out.println(updateTime);
return "OK";
}

发送GET请求:

1
http://127.0.0.1:8080/dateParam?updateTime=2024-10-07 21:50:30

JSON参数

GET请求并不能直接使用JSON参数

JSON参数:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用@RequestBody注解

1
2
3
4
5
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user) {
System.out.println(user);
return "OK";
}

User对象和之前的复杂实体参数是一样的

发送POST请求:

1
http://127.0.0.1:8080/jsonParam

在Postman中选择body➡raw,选择JSON格式,JSON数据如下

1
2
3
4
5
6
7
8
{
"name":"Tom",
"age":21,
"address":{
"province":"江苏",
"city":"苏州"
}
}

路径参数

路径参数:通过请求URL直接传递参数,使用{...}来标识该路径参数,需要使用@PathVariable获取路径参数

1
2
3
4
5
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id) {
System.out.println(id);
return "OK";
}

发送GET请求:

1
http://127.0.0.1:8080/path/1

URL中的1可以变为其他整数,并且发送后控制台可以获取到该数值

响应

ResponseBody

@ResponseBody注解:

  • 类型:方法注解、类注解
  • 位置:Controller方法上/类上
  • 作用:将方法返回值直接响应,如果返回值类型是实体对象/集合,将会转换为JSON格式响应
  • 说明:@RestController = @Controller + @ResponseBody;

返回对象:

1
2
3
4
5
@RequestMapping("/getUserInfo")
public User getUserInfo(User u) {
System.out.println(u);
return u;
}

返回集合:

1
2
3
4
5
6
// 返回集合
@RequestMapping("/getList")
public List<String> getList(@RequestParam List<String> hobby) {
System.out.println(hobby);
return hobby;
}

统一响应结果

使用一个实体对象,将响应的结果进行统一处理

1
2
3
4
5
6
7
8
public static Result{
// 响应码
private Integer code;
// 提示信息
private String msg;
// 返回的数据
private Object data;
}

分层解耦

三层架构

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

分层解耦

耦合:衡量软件中各个层/各个模块的依赖关联程度

内聚:软件中各个功能模块内部的功能联系

控制反转:Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转

依赖注入:Dependecy Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入

Bean对象:IOC容器中创建、管理的对象,称之为Bean

实现思路

实现思路如下:

  • 将项目中的类交给IOC容器管理(IOC,控制反转)
  • 应用程序运行时需要什么对象,直接依赖容器为其提供(DI,依赖注入)

核心注解

@Component:

@Component注解用于将一个类标记为Spring的组件,使其成为Spring容器管理的Bean。Spring会自动扫描标有@Component注解的类,并将其实例化、配置并注册到应用程序上下文中。

@Autowired

@Autowired注解用于自动注入Spring容器中的Bean。这意味着Spring会自动将需要的Bean注入到标注了@Autowired的字段、构造函数或方法中。

IOC详解

  • 要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解 说明 位置
@Component 声明bean的基础注解 不属于以下三类时,用此注解
@Controller @Component的衍生注解 标注在控制层类上
@Service @Component的衍生注解 标注在业务层类上
@Repository @Component的衍生注解 标注在数据访问层类上(由于与mybatis整合,用的较少)
  • 前面声明bean的四大注解,要想生效,还需要被组件扫描注解@ComponentScan扫描
  • 该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包

DI详解

基于@Autowired进行依赖注入的常见方式有如下三种:

  1. 属性注入
1
2
3
4
5
6
@RestController
public class UserController {
@Autowired
private UserService userService;
// ......
}
  1. 构造函数注入
1
2
3
4
5
6
7
8
@RestController
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
}
  1. setter注入
1
2
3
4
5
6
7
8
@RestController
public class UserController {
private final UserService userService;
@Autowired
public void setUserController(UserService userService) {
this.userService = userService;
}
}

属性注入——优点:代码简洁、方便快速开发

属性注入——缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性

构造函数注入优点——能清晰地看到类地依赖关系、提高了代码的安全性

构造函数注入缺点——代码繁琐、如果构造参数过多,可能会导致构造函数臃肿

构造函数注入注意点——如果只有一个构造函数,@Autowired注解可以省略

setter注入优点——保持了类的封装性,依赖关系更清晰

setter注入缺点——需要额外编写setter方法,增加了代码量

  • @Autowired注解,默认是按照类型进行注入的

  • 如果存在多个相同类型的bean,会报错,报错信息为:

    1
    Field xxx in xxxxxx required a single bean, but xx were found: ...

解决方案一:@Primary注解,表示优先注入的对象

解决方案二:@Qualifier注解,搭配@Autowired注解,并在@Qualifier中填写指定的类名,如@Qualifier(“xxx”)

解决方案三:@Resource注解,用法和@Qualifier类似,@Resource(name = “xxx”)

JDBC

JDBC:(Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API

本质:

  • Sun公司官方定义的一套操作所有关系型数据库的规范,即接口
  • 各个数据库厂商去实现这套接口,提供数据库驱动jar包
  • 用户可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类

入门使用

修改数据

案例需求:基于JDBC程序,执行update语句(update user set age = 25 where id = 1);

步骤:

  1. 准备工作:创建一个Maven项目,引入依赖;并准备数据库表user
  2. 代码实现:编写JDBC程序,操作数据库
1
2
3
4
5
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 创建数据表
create table user(
id int unsigned primary key auto_increment comment 'ID,主键',
username varchar(20) comment '用户名',
password varchar(32) comment '密码',
name varchar(10) comment '姓名',
age tinyint unsigned comment '年龄'
) comment '用户表';

insert into user(id, username, password, name, age) values (1, 'daqiao', '123456', '大乔', 22),
(2, 'xiaoqiao', '123456', '小乔', 18),
(3, 'diaochan', '123456', '貂蝉', 24),
(4, 'lvbu', '123456', '吕布', 28),
(5, 'zhaoyun', '12345678', '赵云', 27);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 2. 获取连接
String url = "jdbc:mysql://localhost:3306/web01";
String username = "root";
String password = "1234";
Connection connection = DriverManage.getConnect(url,username,password);

// 3. 获取SQL语句执行对象
Statement statement = connection.createStatement();

// 4. 执行SQL
int i = statement.executeUpdate("Update user set age = 25 where id = 1");

// 5. 释放资源
statement.close();
Connection.close();

查询数据

案例需求:基于JDBC执行如下select语句,将查询结果封装到User对象中

1
select * from user where username = 'daqiao' and password = '123456'

案例代码如下:

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
public void testSelect(){
// 数据库连接信息,请根据实际情况修改
String url = "jdbc:mysql://localhost:3306/web01";
String dbUser = "root";
String dbPassword = "1234";

// SQL 查询语句
String sql = "SELECT id, username, password, name, age FROM user WHERE username = ? AND password = ?";

// 用于存储查询结果的列表
List<User> userList = new ArrayList<>();

// 声明数据库资源
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;

try {
// 加载数据库驱动程序
Class.forName("com.mysql.cj.jdbc.Driver");

// 建立数据库连接
conn = DriverManager.getConnection(url, dbUser, dbPassword);

// 创建 PreparedStatement 对象,防止 SQL 注入
stmt = conn.prepareStatement(sql);
stmt.setString(1, "daqiao");
stmt.setString(2, "123456");

// 执行查询
rs = stmt.executeQuery();

// 迭代结果集,将每一行数据封装到 User 对象中
while (rs.next()) {
User userObj = new User();
userObj.setId(rs.getInt("id"));
userObj.setUsername(rs.getString("username"));
userObj.setPassword(rs.getString("password"));
userObj.setName(rs.getString("name"));
userObj.setAge(rs.getInt("age"));
userList.add(userObj);
}

} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
try {
if (rs != null) rs.close();
} catch (SQLException se) {
se.printStackTrace();
}
try {
if (stmt != null) stmt.close();
} catch (SQLException se) {
se.printStackTrace();
}
try {
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}

// 输出每个 User 对象的数据
for (User user : userList) {
System.out.println(user);
}
}

ResultSet(结果集对象)

ResultSet rs = statement.executeQuery()

  • next():将光标从当前位置向前移动一行,并判断当前行是否为有效行,返回值为boolean
    • true:有效行,当前行有数据
    • false:无效行,当前行没有数据
  • getXxx(…):获取数据,可以根据列的标号获取,也可以根据列名获取(推荐)

预编译SQL

静态SQL(参数硬编码):

1
2
3
Statement statement = connection.createStatement();
int i = statement.executeUpdate("update user set age = 25 where id = 1");
System.out.println("SQL执行完毕,影响的记录数为:" + i);

预编译SQL(参数动态传递)

1
2
3
4
PreparedStatement pstmt = conn.prepareStatement("Select * FROM user WHERE username = ? AND password = ?");
pstmt.setString(1,"daqiao");
pstmt.setString(2,"123456");
ResultSet resultset = pstmt.executeQuery();

优势

  1. 安全:
    • 防止SQL注入:SQL注入即通过控制输入来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法
  2. 性能更高

MyBatis

MyBatis是一款优秀的持久层(dao层)框架,用于简化JDBC的开发

查看之前的案例

对比使用MyBatis:

1
2
3
4
5
6
@Mapper
public interface UserMapper {
// 查询全部
@Select("select * from user");
public List<User> findAll();
}

MyBatis官网:https://mybatis.org/mybatis-3/zh_CN/index.html

入门程序

  • 准备工作:

    1. 创建SpringBoot工程、引入MyBatis相关依赖
    6
    1. 准备数据库表user、实体类User,数据库表依旧使用之前的表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package app.netlify.norlcyan.pojo;

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
    private Integer id; //ID
    private String username; //用户名
    private String password; //密码
    private String name; //姓名
    private Integer age; //年龄
    }
    1. 配置MyBatis(在application.properties中数据库连接信息)
    7
  • 编写Mybatis程序:编写Mybatis的持久层接口,定义SQL(注解/XML)

    • 项目目录结构:
    8
    • 项目详细代码:

      • UserMapper
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      package app.netlify.norlcyan.mapper;

      import app.netlify.norlcyan.pojo.User;
      import org.apache.ibatis.annotations.Mapper;
      import org.apache.ibatis.annotations.Select;

      import java.util.List;

      @Mapper // 应用程序在运行时,会自动扫描到这个接口,并创建一个实现类,这个实现类会自动注入到容器中
      public interface UserMapper {
      /*
      查询所有用户
      */
      @Select("select * from user")
      public List<User> findAll();
      }
      • MybatisDemoApplicationTests
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package app.netlify.norlcyan;

      import app.netlify.norlcyan.mapper.UserMapper;
      import app.netlify.norlcyan.pojo.User;
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;

      import java.util.List;

      @SpringBootTest // SpringBoot单元测试的注解 - 当前测试类中的测试方法运行时,会启动springboot项目 - IOC容器
      class MybatisDemoApplicationTests {
      @Autowired
      private UserMapper userMapper;

      @Test
      public void testFindAll() {
      List<User> userList = userMapper.findAll();
      userList.forEach(System.out::println);
      }

      }

辅助配置

SQL语句识别

  • 默认在MyBatis中配置的SQL语句是不识别的。可以做如下配置:
9 10 11

做完以上配置,user会报错:

12

  • 产生原因:idea和数据库没有建立连接,不识别表信息
  • 解决方式:在idea中配置MySQL数据库连接
13

MyBatis日志输出

  • 默认情况下,在MyBatis中,SQL语句执行时,用户并不能直接看到SQL语句的执行日志。加入如下配置,即可查看日志:
1
2
3
4
5
6
7
8
9
10
spring.application.name=Mybatis_Demo

#配置数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/web01
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=1234

#配置MyBatis
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

数据库连接池

  • 数据库连接池是个容器,负责分配、管理数据库连接(Connection)
  • 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
  • 释放空间时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
  • 优势:
    1. 资源重用
    2. 提升系统响应速度
    3. 避免数据库连接遗漏

标准接口:DataSource

  • 官方(sun)提供的数据库连接池接口,由第三方组织实现此接口
  • 功能:获取连接
1
Connection	getConnetion() throws SQLException;

常见产品:

  1. C3P0
  2. DBCP
  3. Druid(阿里巴巴开源的数据库连接池项目)
  4. Hikari(SpringBoot默认)

切换数据库连接池

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.19</version>
</dependency>
1
2
3
4
5
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/web01
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=1234

增删改查操作

删除

  • 需求:根据ID删除用户信息
  • SQL:delete from user where id = 5;
  • Mapper接口:
1
2
3
// @Delete("delete from user where id = 5");	// 硬编码,不推荐
@Delete("delete from user where id = #{id}")
public void deleteById(Integer id);
1
2
@Delete("delete from user where id = #{id}")
public Integer deleteById(Integer id); // DML语句执行完毕的返回值,表示该DML语句执行完毕影响的行数

MyBatis中的#号与$号:

符号 说明 场景 优缺点
#{…} 占位符。执行时,会将#{…}替换为?,生成预编译SQL 参数值传递 安全、性能高(推荐)
${…} 拼接符。直接将参数拼接在SQL语句中,存在SQL注入问题 表名、字段名动态设置时使用 不安全、性能低

增加

  • 需求:添加一个用户
  • SQL:insert into user(username,password,name,age) values('zhouyu','123456','周瑜',20);
  • Mapper接口:
1
2
3
// @Insert("insert into user(username,password,name,age) values('zhouyu','123456','周瑜',20)"); 
@Insert("insert into user(username,password,name,age) values(#{username},#{password},#{name},#{age})");
public void insert(String username,String password,String name,Integer age);

假设参数过多,可以使用对象封装起来

1
2
@Insert("insert into user(username,password,name,age) values(#{username},#{password},#{name},#{age})");
public void insert(User user);

修改

  • 需求:根据ID更新用户信息
  • SQL:update user set username = 'zhouyu',password = '123456', name = '周瑜', age = 20 where id = 1
  • Mapper接口:
1
2
@Update("update user set username = #{username},password = #{password}, name = #{name}, age = #{age} where id = #{id}")
public void update(User user);

查询

  • 需求:根据用户名和密码查询用户信息
  • SQL:select * from user where username = 'zhouyu' and password = '666888'
  • Mapper接口:
1
2
3
4
@Select("select * from user where username = #{username} and password = #{password}")
public User findByUsernameAndPassword(String username,String password);
// 也可以写成以下形式:
// public User findByUsernameAndPassword(@Param("username") String username,@Param("password") String password);

@Param注解的作用是为接口的方法形参起名字的

XML映射配置

  • 在Mybatis中,既可以通过注解配置SQL语句,也可以通过XML配置文件配置SQL语句
  • 默认规则:
    1. XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
    2. XML映射文件的namespace属性为Mapper接口全限定名一致
    3. XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致

14

15

Mapper接口:

1
2
3
4
@Mapper
public interface UserMapper {
public List<User> findAll();
}

XML:

1
2
3
4
5
<mapper namespace="app.netlify.norlcyan.mapper.UserMapper">
<select id="findAll" resultType="app.netlify.norlcyan.pojo.User">
select id, username, password, name, age from user
</select>
</mapper>

辅助配置

现在XML文件位置如下:

16

配置XML映射文件的位置:

application.properties:

1
2
# 指定XML映射配置文件的位置
mybatis.mapper-location=classpath:mapper/*.xml

对于XML映射和注解的选择

  • 在Mybatis的开发中,如果只是简单的增删改查功能,选择注解即可。但是如果要实现复杂的SQL功能,推荐使用XML来配置映射语句
  • 官方说明:https://mybatis.net.cn/getting-started.html

SpringBoot配置文件

  • SpringBoot项目提供了多种属性配置方式(properties、yaml、yml)

application.properties:

1
2
3
4
5
6
7
8
spring.application.name=Mybatis_Demo

#配置数据库连接信息
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/web01
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=1234

缺点:

  • 臃肿
  • 层次结构不清晰

application.yaml/application.yml:

1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/web01
username: root
password: 1234

优点:

  • 简洁
  • 以数据为中心

yml配置文件

格式:

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

定义对象/Map集合:

1
2
3
4
user:
name: 张三
age: 18
password: 123456

定义数组/List/Set集合:

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

注意:在yml格式的配置文件中,如果配置项的值是以0开头的,值需要使用' '引用他起来,因为以0开头在yml中表示8进制的数据

日志技术

Java中的日志技术提供了多种选择,开发人员可以根据项目需求选择适合的日志框架,记录和分析应用程序的运行状态,提高代码质量和维护效率

优势:

  • 数据追踪
  • 性能优化
  • 问题排查
  • 系统监控
  • ……

主流的日志框架:

  1. JUL(java.util.logging):这是JavaSE平台提供的官方日志框架,配置相对简单,但不够灵活,性能较差
  2. Log4j:主流的日志框架,提供了灵活的配置选项,支持多种输出目标
  3. Logback:基于Log4j升级而来,提供了更多的功能和配置选项,性能也优于Log4j(推荐)
  4. Slf4j(Simple Logging Facade For Java):简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架

快速入门

  • 准备工作:引入Logback的依赖(springboot项目中该依赖已传递)、配置文件Logback.xml
  • 记录日志:定义日志记录对象Logger,记录日志
1
2
3
4
5
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classid</artifactId>
<version>1.4.11</version>
</dependency>

Logback.xml文件具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger{50}: 最长50个字符(超出.切割) %msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

<!-- 日志输出级别 -->
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>

测试案例:

1
2
3
4
5
6
7
8
9
10
11
12
private static final Logger log = LoggerFactory.getLogger(LogTest.class);
@Test
public void testLog() {
log.debug("开始计算...");
int sum = 0;
int[] nums = {1,5,3,2,1,4,5,4,6,7,4,34,2,23};
for (int i = 0;i <= nums.length;i++) {
sum += nums[i];
}
log.info("计算结果为:" + sum);
log.debug("结束计算...");
}

配置文件

  • 配置文件名:logback.xml
  • 该配置文件是对Logback日志框架输出的日志进行控制的,可以来配置输出的格式、位置及日志开关等
  • 常用的两种输出日志的位置:控制台、系统文件
1
2
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">...</appender>
1
2
<!-- 系统文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">...</appender>

详细的配置修改查询AI即可

日志级别

  • 日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(级别由低到高):
日志级别 说明 记录方式
trace 追踪,记录程序运行轨迹【使用很少】 log.trace(“…”)
debug 调试,记录程序调试过程中的信息,实际应用中一般将其视为最低级别【使用较多】 log.debug(“…”)
info 记录一般信息,描述程序运行的关键时间,如:网络连接、IO操作等【使用较多】 log.info(“…”)
warn 警告信息,记录潜在有害的情况【使用较多】 log.warn(“…”)
error 错误信息【使用较多】 log.error(“…”)

可以在配置文件中,灵活的控制输出哪些类型的日志

1
2
3
4
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>

事务管理

  • 概念:事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败
  • 比如在WebAi实战项目中的添加员工信息,假设添加员工基本信息成功,但是添加工作经历的代码出现了错误,会导致数据库数据的不完整(只有员工基本信息,没有工作经历信息)

默认MySQL的事务是自动提交的,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务

操作

  • 事务控制主要三步操作:开启事务、提交事务/回滚事务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 开启事务
start transaction; / begin;

-- 1. 保存员工信息
insert into emp values (39,'Tom','123456','汤姆',1,'13300001111',1,4000,'1.jpg','2023-11-01',1,now(),now());

-- 2. 保存员工工作经历
insert into emp_expr(emp_id, begin, end, company, job)
values (37,'2020-01-01', '2021-01-01', '百度', 'Java开发'),
(37,'2022-01-01', '2023-01-01', '阿里巴巴', '运维')

-- 提交事物(全部成功)
commit;
-- 回滚事务(有一个失败)
rollback;

Spring事务管理——控制事务

  • 注解:@Transactional
  • 作用:将当前方法交给Spring进行事务管理,方法执行前,开启事务;成功执行则提交事务;出现异常则回滚事务
  • 位置:在业务(Service)层的方法上、类上、接口上

方法(推荐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Transactional
@Override
public void save(Emp emp) {
// 设置默认值
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
// 保存基本员工信息
empMapper.insert(emp);
// 保存员工工作经历
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)) {
// 遍历集合,为empId赋值
exprList.forEach(expr -> expr.setEmpId(emp.getId()));
empExprMapper.insertBatch(exprList);
}
}

接口:

1
2
@Transactional
public interface EmpService {}

类:

1
2
3
@Transactional
@Service
public class EmpServiceImpl implements EmpService {}

配置日志信息,查看Spring事务管理的底层日志

1
2
3
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManage: debug

事务进阶——rollbackFor

  • rollbackFor属性用于控制出现何种异常类型,回滚事务
  • Transactional默认为出现运行时异常(RuntimeException)才会回滚

以下代码,保存员工工作经历不会执行,但是并不会回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional
@Override
public void save(Emp emp) throws Exception {
// 设置默认值
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
// 保存基本员工信息
empMapper.insert(emp);

if (true) {
throw new Exception("出错信息!!!");
}

// 保存员工工作经历
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)) {
// 遍历集合,为empId赋值
exprList.forEach(expr -> expr.setEmpId(emp.getId()));
empExprMapper.insertBatch(exprList);
}
}

修改后就可以正常使用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional(rollbackFor = {Exception.class})
@Override
public void save(Emp emp) {
// 设置默认值
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
// 保存基本员工信息
empMapper.insert(emp);

if (true) {
throw new Exception("出错信息!!!");
}

// 保存员工工作经历
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)) {
// 遍历集合,为empId赋值
exprList.forEach(expr -> expr.setEmpId(emp.getId()));
empExprMapper.insertBatch(exprList);
}
}

事务进阶——propagation

  • 事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制

例如:

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void a() {
// ......
userService.b();
// ......
}

@Transactional
public void b() {
// ......
}

在a方法中的b方法的事务行为(加入、新建)可以通过propagation属性指定

如:

1
2
3
4
@Transactional(propagation = Propagation.REQUIRED)
public void b() {
// ......
}

其中propagation的常见属性值有如下几种

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

四大特性

  • 原子性:事务是不可分割的最小单元,要么全部成功,要么全部失败
  • 一致性:事务完成时,必须使所有的数据都保持一致状态
  • 隔离性:数据库系统提供的隔离机制,保证事务在不受外部并发影响的独立环境下运行
  • 持久性:事务一旦提交或回滚,它对数据库中的数据的改变就是永久的

文件上传

  • 文件上传:是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程

前端:

1
2
3
4
5
6
<form action="/uploda" method="post" enctype="multipart/form-data">
姓名:<input type="text" name="name"><br>
年龄:<input type="text" name="age"><br>
图像:<input type="file" name="file"><br>
<input type="submit" value="上传文件" name="submit">
</form>

后端:

1
2
3
4
5
6
7
8
@RestController
public class UploadController {
@PostMapping("/upload")
public Result handleFileUpload(String name,Integer age,MultipartFile file) {
log.info("文件上传:{}",file);
return Result.success;
}
}

接收到前端发来的数据后,后端会将其保存为临时文件xxx.tmp

本地存储

对之前的后端代码进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/upload")
public Result upload(String name, Integer age, MultipartFile file) throws IOException {
log.info("接收参数:{},{},{}",name,age,file);
// 1. 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 2. 生成新文件名
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFileName = UUID.randomUUID().toString() + extension;
// 2. 保存文件
file.transferTo(new File("E:/files/" + originalFilename));
return Result.success();
}

注意:

  1. 为了确保上传的文件名不能与本地存储的文件名重复,需要使用UUID生成文件名
  2. Spring Boot 中,默认的文件上传大小限制为 1MB,如果上传文件过大,需要在application.yaml中修改配置:
1
2
3
4
5
6
7
spring:
servlet:
multipart:
# 最大单个文件大小
max-file-size: 10MB
# 最大请求大小(包括所有文件和表单数据)
max-request-size: 100MB

阿里云OSS

阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件

使用教程09-后端Web实战(员工管理) - 飞书云文档

登录校验

会话技术

  • 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应

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

  • 会话跟踪方案:

    • 客户端会话跟踪技术:Cookie
    • 服务端会话跟踪技术:Session
    • 令牌技术

案例来自黑马程序员

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
package com.itheima.controller;

import com.itheima.pojo.Result;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* HttpSession演示
*/
@Slf4j
@RestController
public class SessionController {

//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}

//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}

Cookie的优点:HTTP协议中支持的技术

Cookie的缺点:

  • 移动端APP无法使用Cookie
  • 不安全,用户可以自己禁用Cookie
  • Cookie不能跨域

Session

案例来自黑马程序员

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
package com.itheima.controller;

import com.itheima.pojo.Result;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* HttpSession演示
*/
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());

session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}

@GetMapping("/s2")
public Result session2(HttpSession session){
log.info("HttpSession-s2: {}", session.hashCode());

Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}

Session的优点:存储在服务端,安全

Session的缺点:

  • 服务器集群环境下无法直接使用Session
  • 包括所有Cookie中的缺点

令牌

令牌的优点:

  • 支持PC端、移动端
  • 解决集群环境下的认证问题
  • 减轻服务端存储压力

令牌的缺点:

  • 需要程序员自己实现

JWT令牌

  • 全称:JSON Web Token(http://jwt.io)
  • 定义了一种简洁的、自包含的格式,用于在通信双方以JSON数据格式安全的传输信息
  • 组成:
    1. 第一部分:Header(头),记录令牌类型、算法签名等,例如:{"alg":"HS256", "type":"JWT"}
    2. 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom"}
    3. 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload融入,并加入指定密钥,通过指定签名算法计算而来

使用步骤:

  1. 引入jjwt的依赖
  2. 调用官方提供的工具类Jwts来生成或解析jwt令牌
1
2
3
4
5
<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
@Test
public void testGenerateJwt() {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("id", 1);
dataMap.put("username", "norlcyan");
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "bm9ybGN5YW4=") // 指定加密算法和密钥
.addClaims(dataMap) // 添加自定义信息
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置过期时间
.compact(); // 生成令牌
System.out.println(jwt);
}
1
2
3
4
5
6
7
// 解析JWT令牌
@Test
public void testParseJwt() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJub3JsY3lhbiIsImV4cCI6MTczOTA5NTAzOH0.grGPR6b2Dib5qW74VIHPSujQec-TuKRTMduoTSnBEis";
Claims claims = Jwts.parser().setSigningKey("bm9ybGN5YW4=").parseClaimsJws(token).getBody();
System.out.println(claims);
}

过滤器Filter

  • 概念:Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一
  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
  • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等
快速入门
  1. 定义Filter:定义一个类,实现Filter接口,并实现其所有方法
  2. 配置Filter:Filter类上加@WebFilter注解,配置拦截路径。引导类上加@ServletComponentScan开启Servlet组件支持
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 app.netlify.norlcyan.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

@WebFilter("/*") // 拦截所有请求
public class DemoFilter implements Filter {
@Override
// 初始化方法,web服务器启动,创建Filter实例时调用,只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init...");
}

@Override
// 拦截到请求时,调用该方法,可以调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain chain) throws ServletException, IOException {
System.out.println("拦截到了请求...");
// 放行
chain.doFilter(servletRequest, servletResponse);
}

@Override
// 销毁方法,web服务器关闭时调用,只调用一次
public void destroy() {
System.out.println("destroy...");
}
}

1
2
3
4
// 引导类
@servletComponentScan
@SpringBootApplication
public class TliasManagementApplication {...}

注意:

  1. 拦截到请求后需要执行放行操作,否则服务器不会返回数据
  2. 并不是所有请求都需要校验令牌,如登录请求、注册请求
  3. 当有令牌,且令牌校验通过后,放行;否则返回未登录错误结果
令牌校验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
64
package app.netlify.norlcyan.filter;

import app.netlify.norlcyan.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {
// logback
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TokenFilter.class);

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1. 获取到请求路径
String requestURI = request.getRequestURI();

// 2. 判断是否为登录请求(/login),是则放行
if (requestURI.contains("/login")) {
log.info("登录请求,放行");
filterChain.doFilter(servletRequest,servletResponse);
return;
}

// 3. 获取请求头中的Token
String token = request.getHeader("token");

// 4. 判断Token是否存在,不存在则返回401状态码
if (token == null || token.isEmpty()) {
log.info("令牌为空,返回401状态码");
response.setStatus(401);
return;
}

// 5. 解析Token,获取其中的用户信息
try {
JwtUtils.parseJwt(token);
log.info("解析Token成功");
} catch (Exception e) {
log.info("解析Token失败,返回401状态码");
response.setStatus(401);
return;
}

// 6. 校验通过
log.info("校验通过,放行");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {
Filter.super.destroy();
}
}
Filter拦截路径
拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问/login路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截
Filter过滤器链
  • 一个Web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
  • 注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序

拦截器Interceptor

  • 概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,主要用于动态拦截控制器方法的执行
  • 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
快速入门
  1. 定义拦截器,实现HandlerInterceptor接口,并实现其所有方法
  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
package app.netlify.norlcyan.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class DemoInterceptor implements HandlerInterceptor {
// logback
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DemoInterceptor.class);

@Override // 目标资源方法执行前执行。返回true:放行;返回false:不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle...");
return true;
}

@Override // 目标资源方法执行后执行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle...");
}

@Override // 视图渲染完毕后执行,最后执行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion...");
}
}

1
2
3
4
5
6
7
8
9
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private DemoInterceptor demoInterceptor
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
}
}
令牌校验Interceptor

流程与Filter类似

TokenInterceptor:

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
package app.netlify.norlcyan.filter;

import app.netlify.norlcyan.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

//@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {
// logback
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TokenFilter.class);

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1. 获取到请求路径
String requestURI = request.getRequestURI();

// 2. 判断是否为登录请求(/login),是则放行
if (requestURI.contains("/login")) {
log.info("登录请求,放行");
filterChain.doFilter(servletRequest,servletResponse);
return;
}

// 3. 获取请求头中的Token
String token = request.getHeader("token");

// 4. 判断Token是否存在,不存在则返回401状态码
if (token == null || token.isEmpty()) {
log.info("令牌为空,返回401状态码");
response.setStatus(401);
return;
}

// 5. 解析Token,获取其中的用户信息
try {
JwtUtils.parseJwt(token);
log.info("解析Token成功");
} catch (Exception e) {
log.info("解析Token失败,返回401状态码");
response.setStatus(401);
return;
}

// 6. 校验通过
log.info("校验通过,放行");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {
Filter.super.destroy();
}
}

WebConfig:

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

import app.netlify.norlcyan.interceptor.DemoInterceptor;
import app.netlify.norlcyan.interceptor.TokenInterceptor;
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.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**"); // 拦截所有请求
}
}

拦截路径

拦截器可以根据需求,配置不同的拦截路径

1
2
3
4
public void addInterceptors(InterceptorRegistry registry) {
@Override
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}

excludePathPatterns:指定哪些请求不拦截

拦截路径 含义 举例
/* 一级路径 能匹配/depts,/emps等,不能匹配/depts/1
/** 任意级路径 所有路径都能匹配
/depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/** /depts下的任意级路径 /depts下的所有路径,如/depts/1,/depts/1/2
执行流程

优先Filter,其次才是拦截器(拦截器是在Spring框架下的)

实际项目中,二选一即可

AOP

  • AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),可以简单理解为就是面向特定方法编程
  • 场景:案例中部分业务方法运行较慢,定位执行耗时较长的方法,此时需要统计每个业务方法的执行耗时

原始方式:

1
2
3
4
5
6
7
public List<Dept> list() {
long beginTime = System.currentTimeMillis();
List<Dept> deptList = deptMapper.list();
long endTime = System.currentTimeMillis();
log.info("执行耗时:{}",endTime - beginTime);
return deptList;
}

AOP方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Aspect
@Component
public class RecordTimeAspect {
@Around("execution(* xxx.xxx.service.*.*(..))")
public Object recordTime(ProceedingJointPoint pjp) {
long beginTime = System.currentTimeMillis();
Object result = pjp.proceed();
long endTime = System.currentTimeMillis();
log.info("执行耗时:{}",endTime - beginTime);
return result;
}
}

优势:

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

提示:AOP是一种思想,而在Spring框架中对这种思想进行的实现,就是Spring AOP

个人理解:相当于给方法装了个插件,方法本身不会被修改

AOP基础

快速入门

需求:统计所有业务层方法的执行耗时

  1. 导入依赖:在pom.xml中引入AOP的依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 编写AOP程序:针对特定的方法根据业务需要进行编程
1
2
3
4
5
6
7
8
9
@Aspect
@Component
public class RecordTimeAspect {
@Around("execution(* xxx.xxx.service.impl.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {


}
}

AOP核心概念

  • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
AOP01
  • 通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
AOP02
  • 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
AOP03
  • 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
AOP04
  • 目标对象:Target,通知所应用的对象
AOP05

AOP进阶

通知类型

  • 根据通知方法执行时机的不同,将通知类型分为以下常见的五类:
    1. @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
    2. @Before:前置通知,此注解标注的通知方法在目标方法前被执行
    3. @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
    4. @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
    5. @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

注意:

  1. @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
  2. @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值

@PointCut

该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可

1
2
3
4
5
@PointCut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pt() {};

@Around("pt()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {...}

通知顺序

  • 当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
  • 执行顺序:
    • 不同切面类中,默认按照切面类的类名字母排序:
      • 目标方法前的通知方法:字母排名靠前的先执行
      • 目标方法后的通知方法:字母排名靠前的后执行
    • @Order(数字)加在切面类上来控制顺序
      • 目标方法前的通知方法:数字小的先执行
      • 目标方法前的通知方法:数字小的后执行
1
2
3
4
5
@Slf4j
@Order(5)
@Aspect
@Component
public class RecordTimeAspect{...}

切入点表达式

  • 介绍:描述切入点方法的一种表达式

  • 作用:用来决定项目中的哪些方法需要加入通知

  • 常见形式:

    1. execution(...):根据方法的签名来匹配
    1
    2
    @Before("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
    public void before(JoinPoint joinPoint) {..}
    1. @annotation(...):根据注解匹配
    1
    2
    @Before("@annotation(com.itheima.anno.Log)")
    public void before() {}

execution

主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

1
execution(访问修饰符? 返回值 + 包名.类名.?方法名(方法参数) throws 异常?)
  • 其中带?的表示可以省略的部分

    1. 访问修饰符:可省略(比如:public、protected)

    2. 包名.类名:可省略(不建议省略)

    3. throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

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

    1. *:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
    1
    execution(* com.*.service.*.update*(*))
    1. ..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
    1
    execution(* com.itheima..DeptService.*(..))

书写建议:

  1. 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxxupdateXxx
  2. 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
  3. 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用..,使用*匹配单个包

@annotation

  • @annotation切入点表达式,用于匹配标识有特定注解的方法
1
2
3
4
5
@Around("@annotation(com.itheima.anno.LogOperation)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
}
1
2
3
4
5
6
7
@LogOperation	// 匹配
@DeleteMapping
public Result delete(Integer id) {
System.out.println("根据ID删除部门数据:" + id);
deptService.delete(id);
return Result.success();
}

注:@LogOperation该注解为用户自定义注解,关于创建用户自定义注解在Java | Norlcyan’s Blog提及

连接点

  • 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
    • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
    • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
1
2
3
4
5
6
7
8
9
@Around("execution(* com.itheima.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getName(); // 获取目标类名
Signature signature = joinPoint.getSignature(); // 获取目标方法签名
String methodName = joinPoint.getSignature().getName(); // 获取目标方法名
Object[] args = joinPoint.getArgs(); // 获取目标方法运行参数
Object res = joinPoint.proceed();
return res;
}
1
2
3
4
5
6
7
@Before("execution(* com.itheima.service.DeptService.*(..))")
public void before(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName(); // 获取目标类名
Signature signature = joinPoint.getSignature(); // 获取目标方法签名
String methodName = joinPoint.getSignature().getName(); // 获取目标方法名
Object[] args = joinPoint.getArgs(); // 获取目标方法运行参数
}

AOP案例

  • 将Tlias智能学习辅助系统案例中增删改接口的操作日志记录到数据库中
  • 日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

创建操作日志表:

1
2
3
4
5
6
7
8
9
10
11
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_emp_id int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(2000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint unsigned comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

创建AOP类:

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
package app.netlify.norlcyan.aop;

import app.netlify.norlcyan.mapper.OperateLogMapper;
import app.netlify.norlcyan.pojo.OperateLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Arrays;

@Aspect
@Component
public class LogAspect {
// logback
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogAspect.class);

@Autowired
private OperateLogMapper operateLogMapper;

@Around("@annotation(app.netlify.norlcyan.anno.Log)")
public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();

// 执行方法
Object result = joinPoint.proceed();

// 计算耗时
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;

// 构建日志实体
OperateLog olog = new OperateLog();
olog.setOperateEmpId(1);
olog.setOperateTime(LocalDateTime.now());
olog.setClassName(joinPoint.getTarget().getClass().getName());
olog.setMethodName(joinPoint.getSignature().getName());
olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
olog.setReturnValue(result != null ? result.toString() : "void");
olog.setCostTime(costTime);

// 保存日志
log.info("日志记录:{}", olog);
operateLogMapper.insert(olog);

return result;
}
}

由于只记录增删改操作,所以使用@annotation注解,创建自定义注解:

1
2
3
4
5
6
7
8
9
10
11
package app.netlify.norlcyan.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

根据数据库结构创建实体类:

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package app.netlify.norlcyan.pojo;

import lombok.Data;
import java.time.LocalDateTime;


public class OperateLog {
private Integer id; //ID
private Integer operateEmpId; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时

public OperateLog() {
}

public OperateLog(Integer id, Integer operateEmpId, LocalDateTime operateTime, String className, String methodName, String methodParams, String returnValue, Long costTime) {
this.id = id;
this.operateEmpId = operateEmpId;
this.operateTime = operateTime;
this.className = className;
this.methodName = methodName;
this.methodParams = methodParams;
this.returnValue = returnValue;
this.costTime = costTime;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Integer getOperateEmpId() {
return operateEmpId;
}

public void setOperateEmpId(Integer operateEmpId) {
this.operateEmpId = operateEmpId;
}

public LocalDateTime getOperateTime() {
return operateTime;
}

public void setOperateTime(LocalDateTime operateTime) {
this.operateTime = operateTime;
}

public String getClassName() {
return className;
}

public void setClassName(String className) {
this.className = className;
}

public String getMethodName() {
return methodName;
}

public void setMethodName(String methodName) {
this.methodName = methodName;
}

public String getMethodParams() {
return methodParams;
}

public void setMethodParams(String methodParams) {
this.methodParams = methodParams;
}

public String getReturnValue() {
return returnValue;
}

public void setReturnValue(String returnValue) {
this.returnValue = returnValue;
}

public Long getCostTime() {
return costTime;
}

public void setCostTime(Long costTime) {
this.costTime = costTime;
}
}

创建Mapper对数据库进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package app.netlify.norlcyan.mapper;


import app.netlify.norlcyan.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperateLogMapper {

//插入日志数据
@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);

}

对需要记录的操作加上@annotation注解:

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
package app.netlify.norlcyan.controller;

import app.netlify.norlcyan.anno.Log;
import app.netlify.norlcyan.pojo.*;
import app.netlify.norlcyan.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

// 员工管理Controller
@RequestMapping("/emps")
@RestController
public class EmpController {
private static final org.slf4j.Logger log
= org.slf4j.LoggerFactory.getLogger(EmpController.class);
@Autowired
private EmpService empService;

// 分页查询

@GetMapping
public Result page(EmpQueryParam empQueryParam) {
log.info("分页查询,参数:{}",empQueryParam);
PageResult<Emp> pageResult = empService.page(empQueryParam);
return Result.success(pageResult);
}

// 保存员工信息
@Log
@PostMapping
public Result save(@RequestBody Emp emp) {
log.info("保存员工信息,参数:{}",emp);
empService.save(emp);
return Result.success();
}

// 删除员工信息
@Log
@DeleteMapping
public Result delete(@RequestParam List<Integer> ids) {
log.info("删除员工信息,参数:{}", ids);
empService.delete(ids);
return Result.success();
}

// 查询回显,返回数据包括员工基本信息和员工工作经历
@GetMapping("/{id}")
public Result getInfo(@PathVariable Integer id) {
log.info("查询回显,参数:{}", id);
Emp emp = empService.getInfo(id);
return Result.success(emp);
}

// 修改员工信息
@Log
@PutMapping
public Result update(@RequestBody Emp emp) {
log.info("修改员工信息,参数:{}", emp);
empService.update(emp);
return Result.success();
}

// 查询所有员工信息
@GetMapping("/list")
public Result list() {
log.info("查询所有员工信息");
List<Emp> list = empService.list();
return Result.success(list);
}
}

在之前创建的AOP类代码中,获取员工数据是写死的:

1
olog.setOperateEmpId(1)

想要获取员工动态数据,可以从令牌的Token中解析得到:

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 app.netlify.norlcyan.interceptor;

import app.netlify.norlcyan.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class TokenInterceptor implements HandlerInterceptor {
// logback
private static final Logger log = LoggerFactory.getLogger(DemoInterceptor.class);

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取到请求路径
String requestURI = request.getRequestURI();

// 2. 判断是否为登录请求(/login),是则放行
if (requestURI.contains("/login")) {
log.info("登录请求,放行");
return true;
}

// 3. 获取请求头中的Token
String token = request.getHeader("token");

// 4. 判断Token是否存在,不存在则返回401状态码
if (token == null || token.isEmpty()) {
log.info("令牌为空,返回401状态码");
response.setStatus(401);
return false;
}
// 5. 解析Token,获取其中的用户信息
try {
JwtUtils.parseJwt(token);
log.info("解析Token成功");
} catch (Exception e) {
log.info("解析Token失败,返回401状态码");
response.setStatus(401);
return false;
}
// 6. 校验通过
log.info("校验通过,放行");
return true;
}
}

想要将其中的参数发送给AOP、Controller、Serivce,需要通过ThreadLocal来实现

ThreadLocal

  • ThreadLocal并不是一个Thread,而是Thread的局部变量
  • ThreadLocal为每个线程提供一份单独的存储空间,具有线程隔离效果,不同的线程之间不会相互干扰
  • ThreadLocal常用方法:
    • public void set(T value):设置当前线程的线程局部变量
    • public T get():返回当前线程所对应的线程局部变量
    • public void remove():移除当前线程的线程局部变量

获取当前登录员工

具体操作步骤:

  1. 定义ThreadLocal操作的工具类,用于操作当前登录员工ID
  2. 在拦截器类中,解析当前完成登录员工ID,将其存入ThreadLocal(用完之后需要将其删除)
  3. 在AOP程序中,从ThreadLocal中获取当前登录员工的ID

ThreadLocal工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package app.netlify.norlcyan.utils;

public class CurrentHolder {

private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();

public static void setCurrentId(Integer employeeId) {
CURRENT_LOCAL.set(employeeId);
}

public static Integer getCurrentId() {
return CURRENT_LOCAL.get();
}

public static void remove() {
CURRENT_LOCAL.remove();
}
}

修改拦截器相关代码:

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
package app.netlify.norlcyan.interceptor;

import app.netlify.norlcyan.utils.CurrentHolder;
import app.netlify.norlcyan.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class TokenInterceptor implements HandlerInterceptor {
// logback
private static final Logger log = LoggerFactory.getLogger(DemoInterceptor.class);

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取到请求路径
String requestURI = request.getRequestURI();

// 2. 判断是否为登录请求(/login),是则放行
if (requestURI.contains("/login")) {
log.info("登录请求,放行");
return true;
}

// 3. 获取请求头中的Token
String token = request.getHeader("token");

// 4. 判断Token是否存在,不存在则返回401状态码
if (token == null || token.isEmpty()) {
log.info("令牌为空,返回401状态码");
response.setStatus(401);
return false;
}
// 5. 解析Token,获取其中的用户信息
try {
Claims claims = JwtUtils.parseJwt(token);
Integer empId = Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId);
log.info("当前用户id为:{}",empId);
log.info("解析Token成功");
} catch (Exception e) {
log.info("解析Token失败,返回401状态码");
response.setStatus(401);
return false;
}
// 6. 校验通过
log.info("校验通过,放行");
return true;
}

// 7. 释放资源
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
CurrentHolder.remove();
}
}

AOP动态获取当前登录员工数据:

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
package app.netlify.norlcyan.aop;

import app.netlify.norlcyan.mapper.OperateLogMapper;
import app.netlify.norlcyan.pojo.OperateLog;
import app.netlify.norlcyan.utils.CurrentHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Arrays;

@Aspect
@Component
public class LogAspect {
// logback
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogAspect.class);

@Autowired
private OperateLogMapper operateLogMapper;

@Around("@annotation(app.netlify.norlcyan.anno.Log)")
public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();

// 执行方法
Object result = joinPoint.proceed();

// 计算耗时
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;

// 构建日志实体
OperateLog olog = new OperateLog();
olog.setOperateEmpId(CurrentHolder.getCurrentId()); // 只需要修改这里
olog.setOperateTime(LocalDateTime.now());
olog.setClassName(joinPoint.getTarget().getClass().getName());
olog.setMethodName(joinPoint.getSignature().getName());
olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
olog.setReturnValue(result != null ? result.toString() : "void");
olog.setCostTime(costTime);

// 保存日志
log.info("日志记录:{}", olog);
operateLogMapper.insert(olog);

return result;
}
}