写代码应该向报纸学习,在顶部,你希望有个头条,告诉你故事的主题,好让你决定是否读下去。第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事的细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说法以及其他细节。
日志记录(lombok + logback)
依赖配置:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>provided</scope> <!-- 只在编译时使用 -->
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
lombok
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
public void createUser(String name) {
log.info("创建用户: {}", name);
log.debug("调试信息: name长度={}", name.length());
}
}
在每一个需要进行日志记录的类中,我们都需要手动创建 log对象:
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
如果我们使用lombok,可以简化如下:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserService {
public void createUser(String name) {
log.info("创建用户: {}", name);
log.debug("调试信息: name长度={}", name.length());
}
}
通过 @Slf4j注解,lombook将在编译时,在类中自动添加log对象的创建。
日志级别
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class DemoController {
@GetMapping("/test")
public String testLog() {
log.trace("Trace 级别日志");
log.debug("Debug 级别日志");
log.info("Info 级别日志");
log.warn("Warn 级别日志");
log.error("Error 级别日志");
return "OK";
}
}
支持占位符:
log.info("用户ID: {}, 状态: {}", userId, status);,避免字符串拼接的性能损耗
简单配置
Spring Boot 已经帮我们封装了常用的日志路径配置,你只需要在 application.properties 中加上:
# 日志文件完整路径(包含文件名)
logging.file.name=logs/app.log
# 或者只指定目录,文件名默认 spring.log
logging.file.path=logs
# 日志级别(可选)
logging.level.root=info
Spring Boot 2.2+ 默认支持基于 Logback 的 按天切割 ,只要用
logging.file.name或logging.file.path配置,Logback 会自动生成:logs/app.log logs/app.2025-08-28.log
文件上传
前端上传数据
前端可以通过表单进行简单的文件上传:
<form action="/upload" method="post" enctype="multipart/form-data">
用户名: <input type="text" name="username">
年龄: <input type="text" name="age">
头像: <input type="file" name="image">
<input type="submit" value="提交">
</form>
关键点:
enctype="multipart/form-data":必须设置,否则文件不会被正确传输。<input type="file"/>:浏览器会弹出文件选择框。method="post":文件上传必须用 POST。
后端接收数据
使用Spring开发的后端:
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image){
return Result.success();
}
}
- 除文件外的其他参数接收方法不变。
MultipartFile image:Spring MVC 自动将前端name="image"的文件绑定到这个参数。
在一次请求完全处理结束之前,上传的文件会临时存储在目录:C:\Users\<用户名>\AppData\Local\Temp\tomcat.8080.2799433456118150306\work\Tomcat\localhost\ROOT

每个参数都对应一个文件。请求结束后这些临时文件都会被删除。所以后端需要存储文件。
本地存储文件
MultipartFile 对象中封装了上传文件的元信息:
获取文件元信息:
| 方法 | 返回类型 | 作用 | 典型用途 |
|---|---|---|---|
getName() | String | 获取表单字段名(即 <input name="..."> 的值) | 判断是哪个字段上传的文件 |
getOriginalFilename() | String | 获取客户端原始文件名(可能包含扩展名) | 保留用户上传的文件名或提取扩展名 |
getContentType() | String | 获取 MIME 类型(如 image/png) | 校验文件类型 |
判断与大小:
| 方法 | 返回类型 | 作用 | 典型用途 |
|---|---|---|---|
isEmpty() | boolean | 判断文件是否为空(无内容或未选择) | 上传前后端双重校验 |
getSize() | long | 获取文件大小(字节) | 限制上传大小、防止恶意文件 |
获取文件内容:
| 方法 | 返回类型 | 作用 | 典型用途 |
|---|---|---|---|
getBytes() | byte[] | 将文件内容读成字节数组 | 存入数据库 BLOB、内存处理 |
getInputStream() | InputStream | 获取输入流读取文件内容 | 流式处理、大文件分片上传 |
getResource() | Resource | 将文件包装成 Spring Resource 对象 | 与 Spring 资源加载机制配合使用 |
保存文件:
| 方法 | 返回类型 | 作用 | 典型用途 |
|---|---|---|---|
transferTo(File dest) | void | 将文件保存到指定 File 路径 | 保存到本地磁盘 |
transferTo(Path dest) | void | 将文件保存到指定 Path 路径 | NIO 方式保存文件 |
文件名避免重复
使用UUID生成唯一字符串
import java.util.UUID;
public class UUIDExample {
public static void main(String[] args) {
UUID uuid = UUID.randomUUID();
System.out.println("生成的UUID: " + uuid.toString());
}
}
输出示例:
生成的UUID: d3f29e88-7e44-4f5e-8c6f-02089f4bb20e
每秒生成数十亿个 UUID,重复的概率也几乎为零
获取文件类型
// 获取原始文件名
String originalFilename = image.getOriginalFilename();
// 截取文件后缀名
int index = originalFilename.lastIndexOf(".");
String extname = originalFilename.substring(index);
拼接唯一文件名
String newFileName = UUID.randomUUID().toString() + extname;
推荐策略::业务前缀 + 日期目录 + UUID
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = "avatar_" + UUID.randomUUID() + ext; Path path = Paths.get(uploadBaseDir, datePath); Files.createDirectories(path); file.transferTo(path.resolve(newFileName).toFile());
文件大小限制
Spring默认限制上传的最大单个文件大小为1M。单次请求上传数据总限制为10M。
文件 :application.properties
# 配置单个文件上传大小限制
spring.servlet.multipart.max-file-size=10MB
# 配置单个请求最大大小限制(一次请求可上传多个文件)
spring.servlet.multipart.max-request-size=100MB
| 参数 | 含义 | 示例 | 注意事项 |
|---|---|---|---|
spring.servlet.multipart.max-file-size | 单个文件的最大允许大小 | 10MB | 超过会抛出 MaxUploadSizeExceededException |
spring.servlet.multipart.max-request-size | 单次请求允许的最大总大小 | 100MB | 适用于多文件批量上传场景 |
云服务——对象存储
配置文件
以对象存储云服务为例:
@Component
public class AliOSSUtils {
private String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
private String accessKeyId = "LTAI4GCH1xV6DKqwX6dnEuW";
private String accessKeySecret = "yBshVveHOpqDuhCarrVWiBlkYpqSL";
private String bucketName = "web-tlias";
public String upload(MultipartFile file) throws IOException {
// 文件上传实现逻辑
}
}
我们有许多配置信息直接硬编码在代码中,看似方便,但风险和隐患都很大。
- 维护困难:环境或账号变更时,需要重新改代码并重新发布。
- 多环境不便:开发、测试、生成环境的配置不同,无法灵活切换。
application.properties
首先在 application.properties中自定义键名与值:
aliyun.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=你的AccessKeyId
aliyun.oss.accessKeySecret=你的AccessKeySecret
aliyun.oss.bucketName=你的BucketName
Java类注入:
@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;
// 上传等业务方法...
}
@Value注解常用于外部配置的属性注入,具体用法为:@Value("${配置文件中的KEY}")
- static字段上使用
@Value注解无法注入。
yml配置文件
Spring Boot 配置文件用 application.yml 或 application.properties 都可以,只保留一个就能正常运行——它们本质上是两种格式的等价表达。
核心逻辑
Spring Boot 启动时会自动读取 src/main/resources 下的
application.propertiesapplication.yml/application.yaml
同时存在时 :都会加载,但 properties 会覆盖 yml 中相同 key (取决于加载顺序和 profile 激活情况)。
项目中通常 统一使用一种格式 ,避免混乱和重复配置。
properties:层级不清晰
# application.properties
server.port=8080
server.address=127.0.0.1
yml:结构清晰,无重复
# application.yml
server:
port: 8080
address: 127.0.0.1
基本语法
-
区分大小写
键名(key)对大小写敏感,例如
Port与port是不同的键。 -
层级关系通过缩进表示
使用空格表示缩进,推荐两个空格或四个空格,必须统一。
-
禁止使用 Tab 制表符
缩进只能用空格,混用空格与 Tab 会导致解析错误。
-
缩进层级一致
同一层级的所有元素缩进必须完全一致。
-
格式错误会引发解析失败
常见错误包括:缩进不对齐、多余的空格、冒号后缺少空格等。
数据结构
对象/Map集合
-
定义方式 :
key: value -
层级表示 :冒号后换行并缩进表示子属性
-
适用场景 :表示一组键值对,如配置项、实体对象
示例:
user:
name: zhangsan # 用户名
age: 18 # 年龄
password: 123456 # 密码
数组/List/Set集合
- 定义方式 :每个元素前加
-,同级对齐 - 适用场景 :
- List :有序集合,可重复
- Set :无序集合,不重复(语法同 List,但解释方式依赖解析器) 示例:
hobby:
- java
- game
- sport
@ConfigurationProperties
SpringBoot中,由Spring容器管理的类,可以由Spring自动注入在配置文件中设置的参数。只需满足一个条件:
- key值与变量名相同
applicatiton.yml:
aliyun:
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
accessKeyId: 你的keyId
accessKeySecret: 你的KeySecret
bucketName: web-framework
java配置属性类绑定:
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
@ConfigurationProperties(prefix = "..."):按前缀自动映射配置文件字段@Component自动注册到Spring容器
可选依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
当你在项目中使用 @ConfigurationProperties 来绑定配置文件(如 YAML、Properties)时,这个依赖会在编译期扫描并生成属性元数据文件。
这些元数据会被 IDE(如 IntelliJ IDEA)读取,从而提供:
- 自动补全 (属性名提示)
- 类型校验
- 文档提示 (鼠标悬停显示注释)
没有它,代码功能依然能运行,但编写
application.yml时不会有属性提示和检查,容易写错。
登录功能
基础登录功能
控制器层:
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工信息: {}", emp);
Emp e = empService.login(emp);
return e != null ? Result.success() : Result.error("用户名或者密码错误");
}
}
服务层:
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
数据库层:
/**
* 根据用户名和密码查询员工
* @param emp
* @return
*/
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
登录校验
登录校验是认证(Authentication)环节,通常位于请求进入系统的最前端:
客户端请求
↓
登录校验(用户名+密码 / Token / OAuth)
↓
权限判断(Authorization)
↓
业务处理

会话技术
会话:
- 用户打开浏览器访问 Web 服务器资源时,服务器会为该用户创建一个会话对象,用于在多次请求之间保持状态。
- 会话在以下情况结束:
- 浏览器关闭
- 会话超时
- 用户主动退出
- 会话在以下情况结束:
会话跟踪技术:
- 识别同一浏览器发起的多次请求,并在同一次会话中实现数据共享
会话跟踪方案:
| 技术类型 | 存储位置 | 特点 | 常见用途 |
|---|---|---|---|
| Cookie(客户端会话跟踪) | 浏览器端 | 轻量、可跨会话保存、依赖加密保证安全 | 记住用户名、偏好设置 |
| Session(服务器会话跟踪) | 服务器端 | 安全性高、占用服务器资源、需依赖 Cookie/URL 传递 Session ID | 登录状态、购物车 |
| Token / JWT(令牌技术) | 客户端(LocalStorage / Cookie) | 无状态、可跨服务验证、适合分布式系统 | API 鉴权、移动端登录 |
Cookie
// 设置Cookie
@GetMapping("/C1")
public Result c1(HttpServletResponse response){
response.addCookie(new Cookie("login_username", "itheima")); // 设置cookie
return Result.success();
}
// 获取Cookie
@GetMapping("/C2")
public Result c2(HttpServletRequest request){
Cookie[] cookies = request.getCookies(); // 获取所有的Cookie
for (Cookie cookie : cookies){
if(cookie.getName().equals("login_username")){ // 判断 name 为 login_username
System.out.println("login_username:" + cookie.getValue());
}
}
return Result.success();
}

优点:HTTP协议中支持的技术
缺点:不支持跨域
跨域是浏览器的同源策略(Same-Origin Policy)限制下的一个概念。
同源指的是:
- 协议 (http / https)相同
- 域名 / 主机名 / IP 相同
- 端口号 相同
只要三者中有一个不同,就算 不同源 ,也就是 跨域 。
如果前端页面和后端 API 不同源(跨域),即使后端设置了
Set-Cookie,浏览器默认也不会保存或发送它
Session
@RequestMapping("/s1")
public String session1(HttpSession session) {
session.setAttribute("loginUser", "tom"); // Session设置值
return "success";
}
@RequestMapping("/s2")
public String session2(HttpServletRequest request) {
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser"); // Session获取值
System.out.println("loginUser: {}", loginUser);
return "success";
}
Session通过在Cookie中设置 JSESSIONID来确保客户端与Session一一对应。

优点:数据存储在服务器端,安全。
缺点:
- 服务器集群环境无法直接使用Session
- 无法跨域
令牌技术

优点:
- 解决集群环境下的认证问题
- 减轻服务端存储压力
缺点:需要自己实现
JWT
定义 :JWT 是一种紧凑(Compact)、自包含(Self-contained)的方式,用 JSON 对象在各方之间安全传输信息。
组成结构
JWT 由三部分组成,使用 . 分隔:
Header.Payload.Signature
| 部分 | 作用 | 示例 | 编码方式 |
|---|---|---|---|
| Header | 描述元数据,如签名算法、类型 | {"alg":"HS256","typ":"JWT"} | Base64Url |
| Payload | 存放声明(Claims),即传递的数据 | {"sub":"1234567890","name":"Tom"} | Base64Url |
| Signature | 校验 Token 完整性与真实性 | HMACSHA256(base64UrlEncode(Header) + "." + base64UrlEncode(Payload), secret) | - |
工作流程
-
生成 Token
- 服务端根据用户信息生成 Payload
- 使用指定算法(如 HS256)和密钥生成签名
- 拼接成
Header.Payload.Signature
-
传输 Token
- 一般放在 HTTP 请求头
Authorization: Bearer <token></token>
- 一般放在 HTTP 请求头
-
验证 Token
- 服务端用相同算法和密钥验证签名
- 校验通过则信任 Payload 中的信息
Java实现
引入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
生成钥匙
@Test
public void generateJwtToken(){
// 1. 定义自定义载荷(Claims)
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "Tom");
// 2. 定义密钥(HS256 要求至少 32 个字符)
String secretString = "s+0jBv4X7n1N2fH2Xg1Y3+8S+2v1X1o0Y3+8S+2v1o=1";
// 3. 构建 JWT
String jwt = Jwts.builder()
.setClaims(claims) // 设置载荷
.signWith(SignatureAlgorithm.HS256, secretString) // 指定签名算法和密钥
.setExpiration(new Date(System.currentTimeMillis() + 12*3600*1000)) // 设置过期时间(12小时)
.compact(); // 生成最终的 token 字符串
// 4. 输出 JWT
System.out.println(jwt);
}
| 步骤 | 作用 | 对应 JWT 部分 |
|---|---|---|
setClaims(claims) | 设置业务数据(如 id、username 等) | Payload |
signWith(SignatureAlgorithm.HS256, secretString) | 指定签名算法(HS256)和密钥,用于生成签名 | Signature |
setExpiration(...) | 设置 Token 过期时间(标准字段 exp) | Payload |
compact() | 将 Header、Payload、Signature 进行 Base64URL 编码并拼接成最终字符串 | 整个 JWT |
打印字符串:
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJUb20iLCJleHAiOjE3NTY0ODI2MjZ9.qtCSyqgUzbGOgTmoyRlDej7dkRRzmFkpXG21w6ejm2Q
解析JWT
@Test
public void parseJWT() {
String secretString = "s+0jBv4X7n1N2fH2Xg1Y3+8S+2v1X1o0Y3+8S+2v1o=1";
Claims claims = Jwts.parser()
.setSigningKey(secretString) // ① 设置签名密钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJUb20iLCJleHAiOjE3NTY0ODI2MjZ9.qtCSyqgUzbGOgTmoyRlDej7dkRRzmFkpXG21w6ejm2Q") // ② 解析并校验
.getBody(); // ③ 获取载荷(Payload)
System.out.println(claims);
}
| 步骤 | 方法调用 | 作用 | 对应 JWT 部分 | 关键注意事项 |
|---|---|---|---|---|
| 1 | Jwts.parser() | 创建 JWT 解析器 | - | 必须使用 parser() 而不是 builder() |
| 2 | setSigningKey(secretString) | 设置签名密钥,用于验证签名合法性 | Signature | 必须与生成 JWT 时的密钥完全一致 |
| 3 | parseClaimsJws(token) | 解析 JWT 字符串并校验签名 | Header + Payload + Signature | 签名不匹配会抛异常,说明令牌被篡改或伪造 |
| 4 | getBody() | 获取载荷(Claims),即业务数据 | Payload | 可直接读取自定义字段和标准字段(如 exp) |
打印结果:
{id=1, username=Tom, exp=1756482626}
校验机制:
- 签名校验:
- 解析器会用
setSigningKey提供的密钥,对 JWT 的 Header + Payload 重新计算签名,与令牌中的 Signature 比对。 - 如果不一致 → 抛出
SignatureException,说明令牌被改动或伪造。
- 解析器会用
- 过期校验:
- 如果 Payload 中有 exp(过期时间),解析时会自动检查当前时间是否已超过 exp。
- 超时会抛出 ExpiredJwtException。
Claims对象可通过 get(key)方法获取 Value
claims.get("id") -> 1;
统一拦截
过滤器Filter
Filter详细介绍可以看:java-Web基础之Servlet、Filter、Listener -
定义 :Filter 是 Java Web 三大组件之一(Servlet、Filter、Listener)。
作用:在请求到达 Servlet 之前、响应返回客户端之前,对请求/响应进行拦截与处理。
特点 :
- 不直接生成响应内容
- 可实现请求与响应的预处理/后处理
- 可形参过滤链(多个Filter顺序执行)
Filter快速入门
核心接口与方法:
| 方法 | 触发时机 | 作用 |
|---|---|---|
init(FilterConfig filterConfig) | Filter 初始化时 | 读取配置、资源初始化 |
doFilter(ServletRequest request, ServletResponse response, FilterChain chain) | 每次请求经过 Filter 时 | 编写过滤逻辑,调用 chain.doFilter() 继续执行链 |
destroy() | Filter 销毁时 | 释放资源 |
实现Filter接口:
@WebFilter(urlPatterns = "/*")
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("doFilter");
chain.doFilter(request, response); // 放行
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
注册Filter:在启动类上添加注解 @ServletComponentScan
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
@ServletComponentScan会扫描@WebFilter、@WebServlet、@WebListener等注解并注册到容器中。
Filter详解
执行流程
┌───────────────────────────────┐
│ 客户端请求 │
└───────────────┬───────────────┘
│
▼
┌─────────────────┐
│ Filter 前处理 │ ← 记录日志 / 权限校验 / 编码设置
└───────┬─────────┘
│
▼
┌─────────────────┐
│ 目标 Servlet │ ← 核心业务逻辑
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Filter 后处理 │ ← 响应压缩 / 统一格式 / 数据加密
└───────┬─────────┘
│
▼
┌───────────────────────────────┐
│ 客户端响应 │
└───────────────────────────────┘
chain.doFilter(request, response)是连接前处理和后处理的关键调用点。
拦截路径
常见拦截路径配置:
| 拦截路径类型 | urlPatterns 值 | 含义 |
|---|---|---|
| 精确路径匹配 | /login | 仅拦截访问 /login 的请求 |
| 目录路径匹配 | /emps/* | 拦截 /emps 目录下的所有请求 |
| 全局路径匹配 | /* | 拦截所有请求路径 |
目录匹配 :
/path/*会匹配该目录下的所有子路径,但不匹配/path本身。
过滤器链
- 定义 :在一个 Web 应用中,可以配置多个 Filter,这些 Filter 按顺序依次执行,形成一个“过滤器链”。
- 执行顺序 :默认按照 过滤器类名(字符串)的自然排序 执行,除非通过
@Order或FilterRegistrationBean.setOrder()显式指定顺序。 - 作用 :实现请求的多阶段处理,例如日志记录、权限校验、数据预处理、响应包装等。

登录校验
示例代码:
@WebFilter(urlPatterns = "/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String uri = req.getRequestURI();
// 1. 如果是登录接口,直接放行
if ("/login".equals(uri)) {
chain.doFilter(request, response);
return;
}
// 2. 获取请求头中的 token
String token = req.getHeader("token");
// 3. 判断 token 是否存在
if (token == null || token.isEmpty()) {
sendNotLogin(resp);
return;
}
// 4. 校验 token 合法性(示例:JWT 校验)
try {
boolean valid = JwtUtils.verifyToken(token); // 你需要实现 JwtUtils
if (!valid) {
sendNotLogin(resp);
return;
}
} catch (Exception e) {
sendNotLogin(resp);
return;
}
// 5. 如果合法,放行
chain.doFilter(request, response);
}
}
拦截器Interceptor
- 定义 :拦截器是一种 动态拦截方法调用 的机制,类似于过滤器(Filter),但作用范围和实现方式不同。
- 作用 :在方法调用 前后 根据业务需求执行自定义逻辑。
- Spring 中的应用 :基于 动态代理 的 AOP(面向切面编程)机制实现,常用于请求预处理、权限校验、日志记录等。
| 对比项 | 拦截器(Interceptor) | 过滤器(Filter) |
|---|---|---|
| 所属规范 | Spring 框架 | Servlet 规范 |
| 拦截范围 | Spring MVC 控制器方法 | 所有 Servlet 请求 |
| 依赖容器 | Spring 容器 | Web 容器 |
| 使用场景 | 登录校验、权限控制、性能监控、日志记录 | 编码设置、跨域处理、通用请求过滤 |
| 实现方式 | 实现 HandlerInterceptor 接口 | 实现 Filter 接口 |
| 执行顺序 | 在过滤器之后执行 | 在拦截器之前执行 |
| 是否依赖 Spring | 是 | 否 |

实现方式
实现 HandlerInterceptor 接口,并重写其三个方法。
| 方法 | 调用时机 | 返回值/参数 | 常见用途 |
|---|---|---|---|
preHandle() | 控制器方法调用前 | boolean(true 放行,false 拦截) | 权限校验、参数验证、日志记录 |
postHandle() | 控制器方法调用后,视图渲染前 | 可修改 ModelAndView | 数据加工、统一添加模型数据 |
afterCompletion() | 请求完成后(视图渲染后) | 可获取异常信息 | 资源清理、异常处理、性能统计 |
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
System.out.println("preHandle...");
return true; // 返回 false 则中断请求
}
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
注册拦截器
需要编写一个配置类,用于注册:Interceptor
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor myInterceptor; // 注入你自定义的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login", "/error"); // 可选:排除不需要拦截的路径
}
}
拦截路径
| 拦截器路径模式 | 含义 | 示例匹配 | 示例不匹配 |
|---|---|---|---|
/depts/** | 匹配 /depts 下的所有路径(多级) | /depts/1、/depts/1/2 | /login |
/** | 匹配任意路径(多级) | /depts/1、/emps | /login(若被排除) |
/depts/* | 匹配 /depts 下的一级路径 | /depts/1 | /depts/1/2 |
/depts | 精确匹配路径 | /depts | /depts/1 |
执行流程

登录校验
拦截器:
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
String uri = req.getRequestURI();
// 1. 如果是登录接口,直接放行
if ("/login".equals(uri)) {
return true;
}
// 2. 获取请求头中的 token
String token = req.getHeader("token");
// 3. 判断 token 是否存在
if (token == null || token.isEmpty()) {
sendNotLogin(resp);
return false; // 拦截请求
}
// 4. 校验 token 合法性(示例:JWT 校验)
try {
boolean valid = JwtUtils.verifyToken(token); // 你需要实现 JwtUtils
if (!valid) {
sendNotLogin(resp);
return false;
}
} catch (Exception e) {
sendNotLogin(resp);
return false;
}
// 5. 如果合法,放行
return true;
}
}
配置类:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/login", "/error", "/css/**", "/js/**", "/images/**"); // 放行登录和静态资源
}
}
异常处理
在Controller中try-catch
每一个Controller都要进行异常处理,不推荐。
全局异常处理器
在 Spring Boot 中,通过统一的异常捕获机制,将 Controller、Service、Mapper 等各层抛出的异常集中处理,避免在每个方法中重复写 try-catch,提高代码整洁度与可维护性。

示例:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) // 捕获所有异常
public Result ex(Exception ex){
ex.printStackTrace(); // 记录日志
return Result.error(); // 返回统一错误响应
}
}
关键注解
@RestControllerAdvice:组合注解,等价于@ControllerAdvice + @ResponseBody,用于全局异常处理并返回 JSON。@ControllerAdvice:标记该类为全局控制器增强类,可拦截所有 Controller 层抛出的异常。@ResponseBody:将返回值序列化为 JSON。@ExceptionHandler:指定处理某类异常的方法。
创建步骤:
- 创建一个类并加上
@RestControllerAdvice。 - 在类中定义方法,使用
@ExceptionHandler指定要捕获的异常类型。 - 在方法中记录日志(可用
log.error)并返回统一响应对象。
事务管理
Spring @Transactional注解笔记
| 项目 | 内容 |
|---|---|
| 注解 | @Transactional |
| 常用位置 | - Service 层方法上 - Service 类上 - Service 接口上 |
| 主要作用 | 将方法交由 Spring 进行事务管理: 1. 方法执行前开启事务 2. 成功执行后提交事务 3. 出现异常时回滚事务 |
示例:
@Transactional
@Override
public void delete(Integer id) {
deptMapper.deleteById(id);
int i = 1 / 0; // 模拟异常
deptMapper.deleteByDeptId(id);
}
事务进阶
rollbackFor
背景
- 默认回滚规则 :
Spring 事务默认只在
RuntimeException或Error发生时回滚。 对于 受检异常(Checked Exception) ,默认不会回滚。 - 问题 :
某些业务中,受检异常(如
Exception、IOException)也需要回滚,这时就要用rollbackFor属性。
| 属性 | 类型 | 说明 | 示例 |
|---|---|---|---|
| rollbackFor | Class<? extends Throwable>[] | 指定遇到哪些异常类型时回滚事务 | rollbackFor = Exception.class |
示例:
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(Integer id) throws Exception {
Dept dept = empMapper.selectDeptById(id);
if (true) {
throw new Exception("删除前异常...");
}
empMapper.deleteByDeptId(id); // 删除部门
}
| 配置方式 | 回滚异常范围 | 适用场景 |
|---|---|---|
| 默认(无属性) | RuntimeException 及其子类 | 大多数业务逻辑异常 |
| rollbackFor = Exception.class | 所有异常(受检 + 非受检) | 需要对受检异常也回滚的场景 |
| rollbackFor = {IOException.class, SQLException.class} | 指定异常类型 | 精确控制回滚条件 |
事务传播行为(Propagation)
事务传播行为 :指一个事务方法调用另一个事务方法时,第二个方法应如何参与事务控制。
常用传播行为类型:
| 传播行为 | 说明 | 特点与适用场景 |
|---|---|---|
| REQUIRED(默认) | 如果当前存在事务,则加入;否则新建事务 | 绝大多数业务场景的默认选择 |
| REQUIRES_NEW | 无论当前是否存在事务,都新建事务,原事务挂起 | 需要隔离执行、互不影响的操作 |
| SUPPORTS | 如果当前存在事务,则加入;否则以非事务方式执行 | 可选事务场景 |
| NOT_SUPPORTED | 总是以非事务方式执行,若存在事务则挂起 | 只读查询、日志记录等不需要事务的操作 |
| MANDATORY | 必须在事务中执行,否则抛异常 | 强制要求调用方已有事务 |
| NEVER | 必须在非事务环境执行,否则抛异常 | 禁止事务的场景 |
| NESTED | 如果当前存在事务,则在嵌套事务中执行(有独立回滚点);否则新建事务 | 局部回滚需求,如批处理中的单条失败回滚 |
示例:
@Transactional
public void a() {
...
userService.b(); // 调用另一个事务方法
...
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void b() {
...
}
- 默认 REQUIRED :
a()调用b()时,b()会加入a()的事务,二者成一个整体,要么一起提交,要么一起回滚。 - 改为 REQUIRES_NEW :
b()会新建事务,a()的事务会挂起,b()的提交/回滚互不影响a()。
AOP
AOP (Aspect Oriented Programming,面向切面编程 / 面向方面编程)
本质:针对特定方法或特定关注点进行编程
目标:将横切关注点(如日志、性能统计、安全校验)从核心业务逻辑中分离出来,实现解耦与复用
实现方式 :在不修改源码的前提下,通过 代理模式 在运行时动态织入增强逻辑。
代理模式:注入时,不注入原始的类对象,而是注入功能增强后的代理类。
代理机制:
代理方式 适用场景 实现原理 特点 JDK 动态代理 目标类实现了接口 基于 java.lang.reflect.Proxy生成代理类,通过InvocationHandler调用目标方法只能代理接口方法,性能较好,生成速度快 CGLIB 动态代理 目标类没有实现接口 基于 ASM 字节码技术生成目标类的子类,并重写方法实现增强 可代理普通类,不能代理 final类/方法,生成代理类速度稍慢但调用性能较高Spring 选择策略:
- 默认:有接口 → 使用 JDK 动态代理;无接口 → 使用 CGLIB
- 可通过
@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用 CGLIB
典型应用场景:
- 性能监控 :统计方法执行耗时
- 日志记录 :统一记录方法调用信息
- 权限控制 :在方法执行前进行权限校验
- 事务管理 :统一处理事务的开启、提交、回滚
以性能监控为例:
无AOP:
public List<User> list() {
long start = System.currentTimeMillis();
List<User> list = userMapper.list();
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
return list;
}
public List<User> list2() {
long start = System.currentTimeMillis();
List<User> list = userMapper.list2();
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
return list;
}
有AOP:
public List<User> list() {
return userMapper.list();
}
public List<User> list2() {
return userMapper.list2();
}
改进点 :
- 业务方法只关注核心逻辑
- 耗时统计交由切面统一处理
快速入门
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义切面
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.itheima.service..*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object result = pjp.proceed();
long end = System.currentTimeMillis();
System.out.println(pjp.getSignature() + " 耗时:" + (end - begin) + "ms");
return result;
}
}
| 注解 | 作用 | 常用位置 | 易错点 |
|---|---|---|---|
@Aspect | 声明一个类是切面类(Aspect) | 类上 | 忘记加 @Component 会导致切面不被 Spring 管理 |
@Around | 环绕通知(可在方法执行前后自定义逻辑,并可决定是否执行原方法) | 切面类方法上 | 必须调用 pjp.proceed() 才会执行原方法,否则会被拦截掉 |
AOP 核心概念
| 概念 | 含义 | 备注 / 易错点 |
|---|---|---|
| JoinPoint(连接点) | 程序执行过程中的一个点,例如方法调用、异常抛出等 | Spring AOP 仅支持方法级别的 JoinPoint(基于代理) |
| Advice(通知) | 在 JoinPoint 上执行的代码逻辑 | 分为 @Before、@After、@AfterReturning、@AfterThrowing、@Around |
| PointCut(切点) | 匹配 JoinPoint 的表达式规则 | 常用 execution()、within()、@annotation() 等 |
| Aspect(切面) | 封装 PointCut 和 Advice 的模块 | 用 @Aspect 声明,通常配合 @Component |
| Target(目标对象) | 被 AOP 增强的原始对象 | 业务逻辑类 |
| Proxy(代理对象) | AOP 生成的代理对象,执行时替代 Target | JDK 动态代理或 CGLIB |
通知类型
| 注解 | 中文名称 | 执行时机 | 特点 | 易错点 |
|---|---|---|---|---|
@Around | 环绕通知 | 在目标方法执行前后都会执行 | 可完全控制目标方法是否执行,可在前后添加逻辑 | 必须调用 ProceedingJoinPoint.proceed() 才会执行目标方法,否则会被拦截掉 |
@Before | 前置通知 | 在目标方法执行前执行 | 可获取方法入参,适合做权限校验、日志记录等 | 无法获取返回值 |
@After | 后置通知 | 在目标方法执行后执行(无论是否异常) | 类似 finally,常用于资源释放 | 无法区分正常返回还是异常退出 |
@AfterReturning | 返回后通知 | 在目标方法正常返回后执行 | 可获取返回值,适合做结果处理、缓存更新等 | returning 参数名必须与方法形参名一致 |
@AfterThrowing | 异常后通知 | 在目标方法抛出异常后执行 | 可获取异常对象,适合做异常日志、告警等 | throwing 参数名必须与方法形参名一致 |
@Around 的织入逻辑需要自己调用
ProceedingJoinPoint.proceed()来让底层的方法执行,其他注解不需要考虑原始方法执行
执行顺序:
@Around(前半段)
↓
@Before
↓
目标方法执行
↓
@AfterReturning / @AfterThrowing
↓
@After
↓
@Around(后半段)
抽取切入点表达式
示例:
@Aspect
@Component
public class MyAspect1 {
// ① 定义切入点方法(方法体必须为空)
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
private void pt() {}
// ② 在通知中引用切入点方法
@Before("pt()")
public void before() {
log.info("before ...");
}
@After("pt()")
public void after() {
log.info("after ...");
}
}
- 访问修饰符可以是
private、protected、public,但如果要跨类引用,必须是public。
通知执行顺序
默认规则:
- 多个切面类的执行顺序默认按类名字母顺序排序。
推荐做法:
- 使用
@Order注解指定优先级 - 数字越小、优先级越高、越先执行
@Aspect
@Order(1) // 优先级高,先执行
@Component
public class MyAspect1 {
@Before("execution(* com.itheima.service.impl.LogServiceImpl.*(..))")
public void before() {
log.info("MyAspect1...");
}
}
@Aspect
@Order(2) // 优先级低,后执行
@Component
public class MyAspect2 {
@Before("execution(* com.itheima.service.impl.LogServiceImpl.*(..))")
public void before() {
log.info("MyAspect2...");
}
}
切入点表达式
切入点表达式(Pointcut Expression) :用于匹配目标方法的规则表达式。
作用 :决定项目中哪些方法需要织入通知(Advice)。
常用两种表达式:
| 表达式 | 匹配规则 | 示例 | 说明 |
|---|---|---|---|
execution(...) | 按方法签名匹配 | execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)) | 精确匹配某个方法 |
@annotation(...) | 按方法上的注解匹配 | @annotation(com.itheima.anno.Log) | 匹配被特定注解标记的方法 |
execution表达式
execution(访问修饰符? 返回值 包名.?类名.?方法名(参数类型) throws 异常?)
-
?表示可省略的部分- 访问修饰符 :可省略(如
public、protected) - throws 异常 :可省略(指方法声明的异常,而非实际抛出的异常)
- 包名:省略包名,即全局匹配类名和方法名
- 类名:省略类名,即全局匹配方法名
- 访问修饰符 :可省略(如
-
返回值 :必填(可用
*表示任意类型) -
包名.类名.方法名 :可用
*或..进行通配*:单个独立的任意符号..:多个连续的任意符号,可以通配任意层级的包,或者任意类型、任意个数的参数
同时匹配多个没有共同点的方法:
@Pointcut("execution(* com.itheima.service.DeptService.list()) || " +
"execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
根据业务需要,可以使用
&&、||、!来组合比较复杂的切入点表达式。
annotation
@annotation 切入点表达式 :用于匹配方法上标注了指定注解的连接点(Join Point)。
语法:
@annotation(注解全限定类名)
- 注解全限定类名 :必须包含完整包路径,例如
com.itheima.anno.Log - 只能匹配方法级别的注解(不能直接匹配类注解)
使用示例:
-
自定义注解:
package com.itheima.anno; import java.lang.annotation.*; @Target(ElementType.METHOD) // 作用于方法 @Retention(RetentionPolicy.RUNTIME) // 运行时可反射获取 @Documented public @interface Log { String value() default ""; // 可选参数,用于描述日志内容 } -
在业务类中添加自定义注解
@Service public class UserService { @Log("添加用户操作") public void addUser(String username) { System.out.println("执行添加用户逻辑: " + username); } @Log("删除用户操作") public void deleteUser(Long id) { System.out.println("执行删除用户逻辑: " + id); } } -
定义AOP组件
@Slf4j @Aspect @Component public class LogAspect { @Before("@annotation(com.itheima.anno.Log)") public void before(JoinPoint joinPoint) { // 获取方法签名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 获取注解 Log logAnno = method.getAnnotation(Log.class); String desc = logAnno.value(); // 获取方法名和参数 String methodName = signature.getDeclaringTypeName() + "." + signature.getName(); Object[] args = joinPoint.getArgs(); log.info("【AOP日志】方法调用前: {}", methodName); log.info("【AOP日志】注解描述: {}", desc); log.info("【AOP日志】参数: {}", args); } }Spring中可以通过
@Autowired private HttpServletRequest request;在任何由容器托管的类中获取
HttpServletRequest对象
连接点
基本概念:
| 概念 | 说明 |
|---|---|
| JoinPoint | 连接点,表示程序执行过程中的某个点(如方法调用、异常抛出等)。在 Spring AOP 中,连接点通常是方法的执行。 |
| ProceedingJoinPoint | JoinPoint 的子接口,专用于 @Around 环绕通知,允许显式调用 proceed() 来执行目标方法。 |
常用方法:
| 方法 | 返回值 | 作用 |
|---|---|---|
getTarget() | Object | 获取目标对象(被代理对象) |
getSignature().getName() | String | 获取方法名 |
getArgs() | Object[] | 获取方法参数数组 |
proceed() | Object | 执行目标方法,并返回执行结果(仅 ProceedingJoinPoint 可用) |