Web开发-后端拓展篇

25366 字
64 分钟

写代码应该向报纸学习,在顶部,你希望有个头条,告诉你故事的主题,好让你决定是否读下去。第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事的细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说法以及其他细节。

日志记录(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.namelogging.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

1756352895419

每个参数都对应一个文件。请求结束后这些临时文件都会被删除。所以后端需要存储文件

本地存储文件

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适用于多文件批量上传场景

云服务——对象存储

OSS SDK快速入门_对象存储(OSS)-阿里云帮助中心

配置文件

以对象存储云服务为例:

@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.ymlapplication.properties 都可以,只保留一个就能正常运行——它们本质上是两种格式的等价表达。

核心逻辑

Spring Boot 启动时会自动读取 src/main/resources 下的

  • application.properties
  • application.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)对大小写敏感,例如 Portport 是不同的键。

  • 层级关系通过缩进表示

    使用空格表示缩进,推荐两个空格或四个空格,必须统一。

  • 禁止使用 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)

业务处理

1756433329767

会话技术

会话

  • 用户打开浏览器访问 Web 服务器资源时,服务器会为该用户创建一个会话对象,用于在多次请求之间保持状态。
    • 会话在以下情况结束:
      • 浏览器关闭
      • 会话超时
      • 用户主动退出

会话跟踪技术

  • 识别同一浏览器发起的多次请求,并在同一次会话中实现数据共享

会话跟踪方案

技术类型存储位置特点常见用途
Cookie(客户端会话跟踪)浏览器端轻量、可跨会话保存、依赖加密保证安全记住用户名、偏好设置
Session(服务器会话跟踪)服务器端安全性高、占用服务器资源、需依赖 Cookie/URL 传递 Session ID登录状态、购物车
Token / JWT(令牌技术)客户端(LocalStorage / Cookie)无状态、可跨服务验证、适合分布式系统API 鉴权、移动端登录
// 设置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();
}

1756434542215

优点: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一一对应。

1756436103659

优点:数据存储在服务器端,安全。

缺点:

  • 服务器集群环境无法直接使用Session
  • 无法跨域

令牌技术

1756436352937

优点:

  • 解决集群环境下的认证问题
  • 减轻服务端存储压力

缺点:需要自己实现

JWT

官网:JSON Web Tokens - jwt.io

定义 :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)-

工作流程

  1. 生成 Token

    • 服务端根据用户信息生成 Payload
    • 使用指定算法(如 HS256)和密钥生成签名
    • 拼接成 Header.Payload.Signature
  2. 传输 Token

    • 一般放在 HTTP 请求头 Authorization: Bearer <token></token>
  3. 验证 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 过期时间(标准字段 expPayload
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 部分关键注意事项
1Jwts.parser()创建 JWT 解析器-必须使用 parser() 而不是 builder()
2setSigningKey(secretString)设置签名密钥,用于验证签名合法性Signature必须与生成 JWT 时的密钥完全一致
3parseClaimsJws(token)解析 JWT 字符串并校验签名Header + Payload + Signature签名不匹配会抛异常,说明令牌被篡改或伪造
4getBody()获取载荷(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 按顺序依次执行,形成一个“过滤器链”。
  • 执行顺序 :默认按照 过滤器类名(字符串)的自然排序 执行,除非通过 @OrderFilterRegistrationBean.setOrder() 显式指定顺序。
  • 作用 :实现请求的多阶段处理,例如日志记录、权限校验、数据预处理、响应包装等。

1756466027984

登录校验

示例代码:

@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

1756477546358

实现方式

实现 HandlerInterceptor 接口,并重写其三个方法。

方法调用时机返回值/参数常见用途
preHandle()控制器方法调用前booleantrue 放行,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

执行流程

1756479139737

登录校验

拦截器:

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,提高代码整洁度与可维护性。

1756479921015

示例:

@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:指定处理某类异常的方法。

创建步骤

  1. 创建一个类并加上 @RestControllerAdvice
  2. 在类中定义方法,使用 @ExceptionHandler 指定要捕获的异常类型。
  3. 在方法中记录日志(可用 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 事务默认只在 RuntimeExceptionError 发生时回滚。 对于 受检异常(Checked Exception) ,默认不会回滚。
  • 问题 : 某些业务中,受检异常(如 ExceptionIOException)也需要回滚,这时就要用 rollbackFor 属性。
属性类型说明示例
rollbackForClass<? 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() {
    ...
}
  • 默认 REQUIREDa() 调用 b() 时,b() 会加入 a() 的事务,二者成一个整体,要么一起提交,要么一起回滚。
  • 改为 REQUIRES_NEWb() 会新建事务,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();
}

改进点

  1. 业务方法只关注核心逻辑
  2. 耗时统计交由切面统一处理

快速入门

引入依赖

<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 生成的代理对象,执行时替代 TargetJDK 动态代理或 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 ...");
    }
}
  • 访问修饰符可以是 privateprotectedpublic,但如果要跨类引用,必须是 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 异常?)
  • ? 表示可省略的部分

    • 访问修饰符 :可省略(如 publicprotected
    • 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
  • 只能匹配方法级别的注解(不能直接匹配类注解)

使用示例:

  1. 自定义注解:

    package com.itheima.anno;
    
    import java.lang.annotation.*;
    
    @Target(ElementType.METHOD)          // 作用于方法
    @Retention(RetentionPolicy.RUNTIME)  // 运行时可反射获取
    @Documented
    public @interface Log {
        String value() default "";       // 可选参数,用于描述日志内容
    }
  2. 在业务类中添加自定义注解

    @Service
    public class UserService {
    
        @Log("添加用户操作")
        public void addUser(String username) {
            System.out.println("执行添加用户逻辑: " + username);
        }
    
        @Log("删除用户操作")
        public void deleteUser(Long id) {
            System.out.println("执行删除用户逻辑: " + id);
        }
    }
  3. 定义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 中,连接点通常是方法的执行。
ProceedingJoinPointJoinPoint 的子接口,专用于 @Around 环绕通知,允许显式调用 proceed() 来执行目标方法。

常用方法

方法返回值作用
getTarget()Object获取目标对象(被代理对象)
getSignature().getName()String获取方法名
getArgs()Object[]获取方法参数数组
proceed()Object执行目标方法,并返回执行结果(仅 ProceedingJoinPoint 可用)