Spring Boot企业级开发入门

1.1、Spring Boot和MVC架构

Spring Boot 和 MVC 架构是 Java 开发中两个不同层面的概念,它们的定位、目标和应用场景有显著区别,下面从多个维度进行对比:

1. 本质与定位

  • MVC 架构

    是一种软件设计模式(Model-View-Controller),用于分离应用程序的业务逻辑、数据展示和用户交互。

    • Model:处理数据和业务逻辑
    • View:负责数据展示(如页面)
    • Controller:接收用户请求,协调 Model 和 View 完成响应

    MVC 是一种思想,不依赖特定技术,可用于任何语言或框架(如 Java 的 Spring MVC、前端的 Angular 等)。

  • Spring Boot

    基于 Spring 框架的快速开发工具 ,本质是一套脚手架 ,用于简化 Spring 应用的配置和部署。

    它内置了自动配置、依赖管理、嵌入式服务器等特性,让开发者无需手动配置大量 XML 或注解就能快速构建应用。

2. 核心目标

  • MVC :解决应用程序的代码结构混乱问题,通过分离关注点提高代码复用性和可维护性。
  • Spring Boot :解决 Spring 框架的配置繁琐、部署复杂问题,实现"开箱即用",加速开发流程。

3. 技术层级

  • MVC 属于架构设计层面,是组织代码的方法论。
  • Spring Boot 属于开发工具层面,是简化 Spring 应用开发的技术栈。

关系:Spring Boot 可以集成 Spring MVC(MVC 模式的一种实现)作为 Web 层框架,即 Spring Boot 是容器,Spring MVC 是其中的一个组件。

4. 功能范围

  • MVC:仅关注请求处理流程(接收请求→处理业务→返回响应),不涉及应用部署、依赖管理等。
  • Spring Boot :涵盖更广泛的功能:
    • 自动配置 Spring 及第三方库(如默认配置数据源、Web 服务器)
    • 内置 Tomcat、Jetty 等服务器,无需单独部署
    • 提供 Actuator 监控应用健康状态
    • 支持快速集成各种组件(如 MyBatis、Redis 等)

5. 使用场景

  • MVC:适用于所有需要清晰分离业务逻辑和展示层的应用,尤其是 Web 应用。
  • Spring Boot:适用于需要快速开发、简化配置的 Spring 应用,特别是微服务架构(可配合 Spring Cloud 使用)。

6. 典型示例对比

  • 传统 Spring MVC 应用

    需要手动配置 web.xmlDispatcherServlet、组件扫描等,且需单独部署到外部服务器:

    xml 复制代码
    <!-- web.xml 配置 DispatcherServlet -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
    </servlet>
  • Spring Boot 应用

    无需 XML 配置,通过注解和 main 方法即可启动嵌入式服务器:

    java 复制代码
    @SpringBootApplication
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
  • MVC 是架构模式 ,解决代码组织问题;Spring Boot 是开发工具,解决 Spring 应用的效率问题。

  • 实际开发中,两者常结合使用:用 Spring Boot 简化配置,用 Spring MVC(MVC 模式的实现)处理 Web 请求。

  • 选择依据:若需快速开发 Spring 应用,优先用 Spring Boot;若仅需规范代码结构,MVC 模式是基础原则。

1.2、Spring Boot的特征

Spring Boot 作为简化 Spring 应用开发的框架,具有以下核心特征,这些特征使其成为快速构建 Java 应用的首选工具:

1. 自动配置(Auto-configuration)

  • 核心特性:根据类路径中的依赖自动配置 Spring 应用,减少手动配置。
  • 举例 :若项目依赖 spring-boot-starter-web,Spring Boot 会自动配置 Tomcat 服务器、DispatcherServlet(Spring MVC 核心)等 Web 组件,无需手动编写 XML 或注解配置。
  • 灵活性 :可通过 @EnableAutoConfiguration 控制自动配置范围,或用 @Configuration 自定义配置覆盖默认行为。

2. 起步依赖(Starter Dependencies)

  • 核心特性:将常用依赖打包成" starter ",简化 Maven/Gradle 依赖管理。
  • 举例 :引入 spring-boot-starter-data-jpa 即可自动包含 Hibernate、Spring Data JPA 及相关数据库依赖,无需手动添加多个 jar 包。
  • 优势:避免版本冲突(Starter 内部已协调依赖版本),降低依赖管理复杂度。

3. 嵌入式服务器(Embedded Servers)

  • 核心特性:内置 Tomcat、Jetty、Undertow 等服务器,无需单独部署应用到外部服务器。
  • 效果 :应用可通过 java -jar 直接运行,简化开发、测试和部署流程(如微服务场景)。
  • 自定义 :可通过配置文件修改服务器端口(如 server.port=8081)或切换服务器类型。

4. 无代码生成与 XML 配置

  • 核心特性 :通过注解(如 @SpringBootApplication)和约定优于配置(Convention over Configuration)原则,替代传统 XML 配置。
  • 优势:减少模板代码和配置文件,专注业务逻辑开发。
  • 例外 :若需特殊配置,仍可通过 Java 配置类(@Configuration)或少量 YAML/Properties 文件实现。

5. Actuator 监控

  • 核心特性:提供应用监控和管理功能,无需额外开发。
  • 功能 :通过 HTTP 端点暴露应用健康状态(/health)、内存使用(/metrics)、Bean 信息(/beans)等,便于运维和问题排查。
  • 扩展:支持集成 Prometheus、Grafana 等工具实现可视化监控。

6. 命令行界面(CLI,可选)

  • 核心特性:通过命令行快速创建和运行 Spring Boot 应用,支持 Groovy 脚本(无需编译即可执行)。
  • 场景 :适合快速原型开发,例如通过 spring run app.groovy 直接启动应用。

7. 与 Spring 生态无缝集成

  • 核心特性:完美兼容 Spring 框架的所有功能(如 DI、AOP、Spring Security 等),并简化其使用。
  • 扩展:可轻松集成 Spring Cloud 实现微服务(服务注册、配置中心等),或与 MyBatis、Redis 等第三方库协作。

8. 开发工具支持

  • 核心特性 :集成开发者友好工具,提升开发效率。
    • 热部署 :通过 spring-boot-devtools 实现代码修改后自动重启,减少手动重启时间。
    • IDE 集成:支持 IntelliJ IDEA、Eclipse 等主流 IDE 的一键创建和运行。

Spring Boot 的核心设计理念是"简化开发 "和"开箱即用",通过自动配置、起步依赖和嵌入式服务器等特性,大幅降低 Spring 应用的入门门槛,同时保持灵活性和扩展性。它特别适合快速开发微服务、RESTful API、企业级应用等场景,是当前 Java 开发领域的主流框架之一。

1.3、Maven简介及pom文件构成

Maven简介

Maven 是 Apache 推出的项目管理和构建自动化工具,主要用于 Java 项目,通过标准化的项目结构、依赖管理和构建流程,解决了传统项目中"jar 包冲突""构建步骤繁琐"等问题。

其核心功能包括:

  1. 依赖管理:自动下载、管理项目所需的第三方库(jar 包),并处理依赖间的版本冲突。
  2. 标准化构建 :提供编译、测试、打包、部署等统一的生命周期命令(如 mvn clean package)。
  3. 项目结构标准化 :规定了固定的目录结构(如 src/main/java 存放源代码),避免团队开发中的结构混乱。

POM 文件(Project Object Model)

POM 是 Maven 的核心配置文件,以 XML 格式存在(命名为 pom.xml),位于项目根目录,用于描述项目信息、依赖、构建规则等。其核心构成如下:

1. 基本项目信息

定义项目的基本元数据,是 POM 的基础。

xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- POM 模型版本(固定为 4.0.0) -->
    <modelVersion>4.0.0</modelVersion>

    <!-- 项目坐标(唯一标识项目,用于依赖引用) -->
    <groupId>com.example</groupId>   <!-- 组织/公司标识(如域名反转) -->
    <artifactId>demo-project</artifactId>  <!-- 项目名称 -->
    <version>1.0.0</version>  <!-- 版本号 -->
    <name>Demo Project</name>  <!-- 项目显示名称(可选) -->
    <description>A sample Maven project</description>  <!-- 项目描述(可选) -->
</project>
2. 依赖管理(dependencies)

声明项目所需的第三方库或模块,Maven 会自动从中央仓库(默认 https://repo.maven.apache.org/maven2)下载并添加到类路径。

xml 复制代码
<dependencies>
    <!-- 单个依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>  <!-- 依赖的组织标识 -->
        <artifactId>spring-boot-starter-web</artifactId>  <!-- 依赖的名称 -->
        <version>2.7.0</version>  <!-- 依赖的版本号 -->
        <!-- 可选配置 -->
        <scope>compile</scope>  <!-- 依赖范围(默认 compile,其他如 test、provided 等) -->
        <optional>false</optional>  <!-- 是否可选依赖(避免传递依赖) -->
        <exclusions>  <!-- 排除依赖中的子依赖(解决冲突) -->
            <exclusion>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>
  • 依赖范围(scope) :控制依赖在不同构建阶段(编译、测试、运行)的可见性,常见值:
    • compile:默认值,编译、测试、运行阶段均有效。
    • test:仅测试阶段有效(如 JUnit)。
    • provided:编译和测试阶段有效,运行时由容器提供(如 Servlet API)。
3. 依赖管理(dependencyManagement)

用于统一管理依赖版本,避免子模块重复声明版本号(常用于多模块项目)。

xml 复制代码
<dependencyManagement>
    <dependencies>
        <!-- 声明 Spring Boot 版本,子模块引用时无需指定 version -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>
4. 构建配置(build)

自定义项目的构建流程,如指定源代码目录、插件配置等。

xml 复制代码
<build>
    <!-- 最终构建产物的名称(默认 ${artifactId}-${version}) -->
    <finalName>my-project</finalName>

    <!-- 插件配置(扩展 Maven 功能) -->
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.7.0</version>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>  <!-- 将 jar 包转为可执行 jar -->
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
5. 父项目(parent)

用于继承父项目的配置(如依赖管理、插件),实现配置复用(典型如 Spring Boot 项目继承 spring-boot-starter-parent)。

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
    <relativePath/> <!-- 从仓库查找父项目,而非本地目录 -->
</parent>
6. 模块(modules)

多模块项目中,声明当前项目包含的子模块(子模块需在 pom.xml 中声明 parent 指向当前项目)。

xml 复制代码
<modules>
    <module>module-1</module>  <!-- 子模块目录名称 -->
    <module>module-2</module>
</modules>

pom.xml 是 Maven 项目的"灵魂",通过项目坐标 唯一标识项目,通过依赖管理 简化第三方库引用,通过构建配置定制编译、打包等流程。掌握 POM 文件的结构,是使用 Maven 进行高效开发的基础。

1.4、Spring Boot的文件目录

Spring Boot 项目遵循 Maven/Gradle 的标准化目录结构,同时结合自身特点进行了优化,使项目结构清晰、职责明确。以下是典型的 Spring Boot 项目文件目录结构及各部分的作用:

一、基本目录结构(Maven 为例)

复制代码
project-root/                  # 项目根目录
├── src/
│   ├── main/                  # 主程序目录(生产环境代码)
│   │   ├── java/              # Java 源代码目录
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── demo/  # 项目包路径(通常为域名反转+项目名)
│   │   │               ├── DemoApplication.java  # 启动类(入口)
│   │   │               ├── controller/           # 控制器(处理HTTP请求)
│   │   │               ├── service/              # 业务逻辑层
│   │   │               ├── repository/           # 数据访问层(如DAO)
│   │   │               ├── model/                # 数据模型(实体类)
│   │   │               └── config/               # 配置类(自定义Bean等)
│   │   └── resources/         # 资源文件目录
│   │       ├── application.properties           # 主配置文件(默认)
│   │       ├── application.yml                  # 主配置文件(YAML格式,推荐)
│   │       ├── static/        # 静态资源(CSS、JS、图片等)
│   │       ├── templates/     # 模板文件(如Thymeleaf、Freemarker)
│   │       └── META-INF/      # 元数据文件(如spring.factories)
│   └── test/                  # 测试目录
│       ├── java/              # 测试代码(对应main/java的包结构)
│       │   └── com/
│       │       └── example/
│       │           └── demo/
│       │               └── controller/
│       │                   └── UserControllerTest.java  # 测试类
│       └── resources/         # 测试资源文件(如测试专用配置)
├── pom.xml                    # Maven 依赖配置文件
├── .gitignore                 # Git 忽略文件配置
└── README.md                  # 项目说明文档

二、核心目录/文件详解

1. src/main/java

存放项目的核心 Java 源代码,遵循包结构规范:

  • 启动类(如 DemoApplication.java :带有 @SpringBootApplication 注解,是项目入口,通过 main 方法启动 Spring Boot 应用。
  • controller/ :存放控制器类(带 @Controller@RestController 注解),负责接收 HTTP 请求、调用业务逻辑并返回响应。
  • service/ :存放业务逻辑类(带 @Service 注解),实现核心业务功能,被控制器调用。
  • repository/ :数据访问层(如带 @Repository 注解的类),负责与数据库交互(通常结合 MyBatis、JPA 等框架)。
  • model/ :存放实体类(如 User.java),映射数据库表或封装数据。
  • config/ :存放配置类(带 @Configuration 注解),用于定义自定义 Bean、配置第三方组件等。
2. src/main/resources

存放项目资源文件,包括配置、静态资源、模板等:

  • 配置文件
    • application.propertiesapplication.yml:Spring Boot 主配置文件,用于配置端口(server.port)、数据库连接、日志级别等。支持多环境配置(如 application-dev.yml 对应开发环境)。
  • static/ :存放静态资源,如 CSS、JavaScript、图片、HTML 等,Spring Boot 会自动映射访问路径(例如 static/index.html 可通过 http://localhost:8080/index.html 直接访问)。
  • templates/ :存放模板引擎文件(如 Thymeleaf 的 .html、Freemarker 的 .ftl),用于动态生成 HTML 页面(需配合控制器返回逻辑视图名)。
  • META-INF/ :存放元数据文件,例如 spring.factories 用于自动配置扩展(Spring Boot 自动配置的核心机制之一)。
3. src/test/java

存放单元测试和集成测试代码,通常与 src/main/java 保持一致的包结构,使用 JUnit、Mockito 等框架编写测试用例。例如 UserControllerTest 测试 UserController 的接口功能。

4. 根目录文件
  • pom.xml :Maven 项目的核心配置文件,声明项目依赖(如 spring-boot-starter-web)、插件(如 spring-boot-maven-plugin)等。
  • .gitignore :指定 Git 版本控制中需要忽略的文件(如 target/ 编译目录、IDE 配置文件等)。

三、其他常见目录(视项目需求而定)

  • src/main/java/com/example/demo/exception/ :存放自定义异常类及全局异常处理器(带 @RestControllerAdvice 注解)。
  • src/main/java/com/example/demo/util/:存放工具类(如日期处理、加密解密等)。
  • src/main/resources/db/migration/:若使用 Flyway/Liquibase 进行数据库版本管理,存放 SQL 迁移脚本。

Spring Boot 目录结构遵循"约定优于配置"原则,通过标准化的目录划分,使开发者能快速定位代码位置,降低团队协作成本。核心原则是:

  • 源代码与资源文件分离(java/ vs resources/
  • 主程序与测试代码分离(main/ vs test/
  • 业务逻辑按分层职责划分(控制器、服务、数据访问等)

这种结构在微服务、RESTful API 等场景中尤为适用,是 Spring Boot 提高开发效率的重要基础。

1.5、Spring Boot的两种运行方式

Spring Boot 应用有两种主要的部署和运行方式:JAR 包方式和 WAR 包方式,它们适用于不同的场景,各有特点。以下是两种方式的详细介绍:

一、JAR 包运行方式(推荐)

Spring Boot 默认支持将应用打包为可执行 JAR(Java ARchive)文件,这是最常用的方式,尤其适合微服务架构。

1. 打包为 JAR 包
  • 配置基础 :Spring Boot 项目的 pom.xml 中默认包含 spring-boot-maven-plugin 插件,无需额外配置即可打包为可执行 JAR。

    xml 复制代码
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
  • 打包命令 :在项目根目录执行 Maven 命令

    bash 复制代码
    mvn clean package

    执行成功后,JAR 包会生成在 target/ 目录下,命名格式通常为 [项目名]-[版本号].jar(如 demo-0.0.1-SNAPSHOT.jar)。

2. 运行 JAR 包

直接通过 java -jar 命令启动,无需依赖外部 Web 服务器:

bash 复制代码
java -jar target/demo-0.0.1-SNAPSHOT.jar
3. 特点与优势
  • 内置服务器:JAR 包中包含嵌入式 Tomcat(默认)、Jetty 或 Undertow 服务器,无需额外部署到外部容器。
  • 独立运行:一个 JAR 包就是一个独立的应用,可直接在任何安装了 JRE 的环境中运行。
  • 简化部署:适合 CI/CD 流程和容器化部署(如 Docker),只需复制 JAR 包即可。
  • 默认选择:Spring Boot 官方推荐,大多数场景(如微服务、REST API)优先使用。

二、WAR 包运行方式

WAR(Web Application Archive)包是传统 Java Web 应用的打包格式,适用于需要部署到外部 Web 服务器(如 Tomcat、JBoss)的场景。

1. 配置为 WAR 包

需要修改项目配置以支持 WAR 打包:

  • 步骤 1 :在 pom.xml 中修改打包类型为 war

    xml 复制代码
    <packaging>war</packaging>
  • 步骤 2 :排除内置服务器依赖(避免与外部服务器冲突)

    xml 复制代码
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- 排除内置 Tomcat -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 添加 Servlet API 依赖(由外部服务器提供) -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
  • 步骤 3 :修改启动类,继承 SpringBootServletInitializer 并重写 configure 方法

    java 复制代码
    @SpringBootApplication
    public class DemoApplication extends SpringBootServletInitializer {
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
            return application.sources(DemoApplication.class);
        }
        
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
2. 打包与运行 WAR 包
  • 打包命令 :同 JAR 包,执行后在 target/ 目录生成 WAR 包

    bash 复制代码
    mvn clean package
  • 运行方式 :将 WAR 包复制到外部 Web 服务器(如 Tomcat)的 webapps/ 目录下,启动服务器后自动部署,访问路径通常为 http://localhost:8080/[WAR包名]

3. 特点与适用场景
  • 依赖外部服务器:需要部署到独立的 Web 服务器,由外部服务器管理生命周期。
  • 兼容性:适合需要集成到传统 Java EE 环境或依赖特定服务器功能的场景。
  • 灵活性低:部署流程比 JAR 包复杂,需手动管理服务器配置。

三、两种方式的核心区别

对比维度 JAR 包方式 WAR 包方式
服务器依赖 内置嵌入式服务器 依赖外部 Web 服务器
启动方式 java -jar 直接运行 部署到外部服务器后启动
适用场景 微服务、独立应用、容器化 传统 Java Web 环境、服务器集群
部署复杂度 简单(复制 JAR 包即可) 较复杂(需配置外部服务器)
官方推荐程度 推荐(默认方式) 按需使用(兼容传统场景)
  • 优先选择 JAR 包方式:适合大多数 Spring Boot 应用,尤其是微服务、云原生场景,简化部署流程。
  • 必要时使用 WAR 包方式:当需要集成到现有外部 Web 服务器或依赖特定服务器功能时采用。

两种方式均能正常运行 Spring Boot 应用,选择时主要根据部署环境和项目需求决定。

2.1、Spring Boot的自动化配置

Spring Boot 的自动化配置(Auto-configuration)是其核心特性之一,它通过"约定优于配置"的原则,自动完成 Spring 应用的各项配置,大幅减少了传统 Spring 应用中繁琐的 XML 或注解配置。以下从实现原理、核心机制、自定义配置等方面详细介绍:

一、自动化配置的核心目标

  • 消除重复配置:对于常见场景(如 Web 开发、数据库连接),Spring Boot 预定义了一套默认配置,无需开发者手动编写。
  • 按需激活配置 :仅当特定依赖存在于类路径时,才激活对应的自动配置(例如引入 spring-boot-starter-web 才会自动配置 Tomcat 和 Spring MVC)。
  • 保持灵活性:允许开发者通过自定义配置覆盖默认行为,平衡"自动"与"可控"。

二、自动化配置的实现原理

Spring Boot 的自动配置基于 Spring 框架的 条件注解(Condition)SPI(Service Provider Interface) 机制,核心流程如下:

1. 扫描自动配置类
  • Spring Boot 启动时,会通过 @EnableAutoConfiguration 注解(被 @SpringBootApplication 间接包含)触发自动配置。

  • 该注解会扫描类路径下 META-INF/spring.factories 文件中声明的自动配置类(全类名),这些类是 Spring Boot 预定义的配置模板。

    示例 spring.factories 内容(来自 spring-boot-autoconfigure 包):

    properties 复制代码
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
    org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
    org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
2. 条件筛选有效配置

自动配置类通过 条件注解 决定是否生效,确保配置仅在满足特定条件时被激活。常见条件注解包括:

  • @ConditionalOnClass:当类路径中存在指定类时生效(如 WebMvcAutoConfiguration 依赖 DispatcherServlet.class)。

  • @ConditionalOnMissingClass:当类路径中不存在指定类时生效。

  • @ConditionalOnBean:当 Spring 容器中存在指定 Bean 时生效。

  • @ConditionalOnMissingBean:当 Spring 容器中不存在指定 Bean 时生效(允许开发者自定义 Bean 覆盖默认配置)。

  • @ConditionalOnProperty:当配置文件中存在指定属性且值匹配时生效(如 @ConditionalOnProperty(name = "spring.mvc.enabled", havingValue = "true"))。

    示例:WebMvcAutoConfiguration 的条件判断

    java 复制代码
    @Configuration
    @ConditionalOnWebApplication(type = Type.SERVLET)  // 仅在 Servlet Web 应用中生效
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })  // 存在相关类时生效
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    public class WebMvcAutoConfiguration {
        // 自动配置内容(如 DispatcherServlet、视图解析器等)
    }
3. 加载默认配置与绑定属性
  • 生效的自动配置类会向 Spring 容器中注册默认 Bean(如 DispatcherServletDataSource 等)。

  • 这些 Bean 的配置参数会通过 @ConfigurationProperties 注解与 application.yml/application.properties 中的属性绑定,实现配置定制。

    示例:DataSourceAutoConfiguration 绑定数据库配置

    java 复制代码
    @ConfigurationProperties(prefix = "spring.datasource")  // 绑定配置文件中 spring.datasource 前缀的属性
    public class DataSourceProperties {
        private String url;
        private String username;
        private String password;
        // getter/setter
    }

    对应的配置文件(application.yml):

    yaml 复制代码
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/test
        username: root
        password: 123456
4. 允许自定义配置覆盖默认值

Spring Boot 遵循"用户配置优先"原则:

  • 若开发者手动定义了某个 Bean(如自定义 DataSource),则自动配置类中通过 @ConditionalOnMissingBean 声明的默认 Bean 会失效。
  • 配置文件中的属性会覆盖自动配置类的默认参数(如通过 server.port=8081 修改默认端口)。

三、核心组件与注解

  1. @SpringBootApplication :组合注解,包含 @EnableAutoConfiguration(开启自动配置)、@ComponentScan(扫描组件)、@Configuration(标识配置类)。
  2. @EnableAutoConfiguration :触发自动配置的核心注解,通过 AutoConfigurationImportSelector 加载 spring.factories 中的配置类。
  3. @Conditional 系列注解:用于条件筛选,决定自动配置是否生效。
  4. @ConfigurationProperties:将配置文件中的属性绑定到 Java 类,实现配置参数注入。
  5. spring-boot-autoconfigure:包含大量预定义的自动配置类(如 Web、数据库、缓存等场景),是自动化配置的核心依赖。

四、自定义自动化配置(扩展场景)

若需为自定义组件实现自动配置,可遵循以下步骤:

  1. 创建自动配置类 :使用 @Configuration 和条件注解定义配置逻辑。

    java 复制代码
    @Configuration
    @ConditionalOnClass(MyService.class)  // 当类路径存在 MyService 时生效
    public class MyAutoConfiguration {
        @Bean
        @ConditionalOnMissingBean  // 若用户未定义 MyService,则使用默认实现
        public MyService myService() {
            return new MyService();
        }
    }
  2. 注册自动配置类 :在项目的 src/main/resources/META-INF/spring.factories 中声明:

    properties 复制代码
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.example.MyAutoConfiguration
  3. 绑定配置属性 (可选):通过 @ConfigurationProperties 允许用户自定义参数。

    java 复制代码
    @ConfigurationProperties(prefix = "my.service")
    public class MyServiceProperties {
        private boolean enabled = true;
        // getter/setter
    }

    在自动配置类中引入:

    java 复制代码
    @EnableConfigurationProperties(MyServiceProperties.class)
    public class MyAutoConfiguration {
        // 使用 MyServiceProperties 配置 MyService
    }

五、调试与排查自动配置

若需查看自动配置的生效情况,可通过以下方式调试:

  1. 开启 debug 日志 :在配置文件中添加 debug: true,启动时会输出自动配置报告,显示哪些配置生效(Positive matches)、哪些未生效(Negative matches)。

  2. 排除特定自动配置 :若某个自动配置不符合需求,可通过 @SpringBootApplicationexclude 属性排除:

    java 复制代码
    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    public class DemoApplication { ... }

    或在配置文件中排除:

    yaml 复制代码
    spring:
      autoconfigure:
        exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

Spring Boot 的自动化配置通过 SPI 扫描条件筛选属性绑定 三大机制,实现了"按需配置"和"用户优先"的核心逻辑。它既简化了开发流程(无需手动配置常见组件),又保留了足够的灵活性(允许覆盖默认配置),是 Spring Boot 实现"开箱即用"的关键所在。理解自动配置的原理,有助于开发者更好地掌控 Spring Boot 应用的配置逻辑,解决配置冲突等问题。

2.2、Spring Boot的全局配置

Spring Boot 的全局配置是管理应用行为的核心方式,通过配置文件可以统一设置应用端口、数据库连接、日志级别等各种参数,无需修改代码即可调整应用特性。以下从配置文件类型、核心配置项、多环境配置、配置原理等方面详细介绍:

一、全局配置文件的类型与位置

Spring Boot 支持两种格式的全局配置文件,用于设置应用的各种参数:

1. 配置文件类型
  • application.properties :传统的键值对格式,语法为 key=value

    示例:

    properties 复制代码
    server.port=8080
    spring.datasource.url=jdbc:mysql://localhost:3306/test
  • application.yml :YAML 格式(推荐),采用缩进表示层级关系,语法更简洁

    示例:

    yaml 复制代码
    server:
      port: 8080
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/test

两种格式功能完全一致,YAML 更适合复杂层级配置,Properties 适合简单键值对场景。

2. 配置文件存放位置(优先级从高到低)

Spring Boot 会按以下顺序加载配置文件,高优先级配置会覆盖低优先级:

  1. file:./config/ :项目根目录下的 config 文件夹(适合生产环境外部配置)。
  2. file:./ :项目根目录(如 pom.xml 同级目录)。
  3. classpath:/config/src/main/resources/config 目录(适合开发环境)。
  4. classpath:/src/main/resources 目录(默认存放位置)。

注意 :若多个位置存在配置文件,Spring Boot 会合并所有配置,冲突时以高优先级为准。

二、核心全局配置项分类

全局配置项覆盖应用开发的各个方面,以下是常用分类及示例:

1. 服务器配置(server 前缀)
yaml 复制代码
server:
  port: 8081  # 应用端口(默认 8080)
  servlet:
    context-path: /api  # 应用上下文路径(默认 /)
  tomcat:
    max-threads: 200  # Tomcat 最大线程数
2. 数据源配置(spring.datasource 前缀)
yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver  # 数据库驱动(多数情况可自动推断)
    hikari:  # 连接池配置(HikariCP 是默认连接池)
      maximum-pool-size: 10  # 最大连接数
      idle-timeout: 300000  # 连接空闲超时(毫秒)
3. 日志配置(logging 前缀)
yaml 复制代码
logging:
  level:
    root: INFO  # 全局日志级别(ERROR/WARN/INFO/DEBUG/TRACE)
    com.example.demo.controller: DEBUG  # 特定包的日志级别
  file:
    name: ./logs/app.log  # 日志文件路径
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"  # 控制台日志格式
4. Spring MVC 配置(spring.mvc 前缀)
yaml 复制代码
spring:
  mvc:
    view:
      prefix: /templates/  # 视图前缀(如 Thymeleaf 模板路径)
      suffix: .html  # 视图后缀
    format:
      date: yyyy-MM-dd  # 日期格式化
5. 静态资源配置(spring.web.resources 前缀)
yaml 复制代码
spring:
  web:
    resources:
      static-locations: classpath:/static/,classpath:/public/  # 静态资源目录(默认已包含)
6. 自定义配置(无固定前缀)

可自定义配置项,通过 @Value@ConfigurationProperties 注入到代码中:

yaml 复制代码
app:
  name: "我的应用"
  version: "1.0.0"
  features:
    - "用户管理"
    - "订单系统"

三、配置参数的注入方式

全局配置中的参数可通过以下方式注入到 Java 代码中使用:

1. @Value 注解(适合简单参数)

直接注入单个配置项,支持 SpEL 表达式:

java 复制代码
@RestController
public class MyController {
    @Value("${server.port}")
    private int serverPort;  // 注入服务器端口
    
    @Value("${app.name:默认名称}")  // 冒号后为默认值(配置不存在时使用)
    private String appName;
    
    @GetMapping("/info")
    public String info() {
        return "端口:" + serverPort + ",应用名:" + appName;
    }
}
2. @ConfigurationProperties 注解(适合复杂参数)

将一组相关配置绑定到 Java 类,适合多个参数的场景:

java 复制代码
// 绑定前缀为 "app" 的配置
@ConfigurationProperties(prefix = "app")
@Component
public class AppConfig {
    private String name;
    private String version;
    private List<String> features;
    
    // getter 和 setter(必须存在,否则无法绑定)
}

使用时直接注入该类:

java 复制代码
@Service
public class MyService {
    @Autowired
    private AppConfig appConfig;
    
    public void printInfo() {
        System.out.println("应用名:" + appConfig.getName());
        System.out.println("功能列表:" + appConfig.getFeatures());
    }
}

注意 :需在启动类添加 @EnableConfigurationProperties 或在配置类添加 @Component 使绑定生效。

四、多环境配置

实际开发中,应用需要在开发(dev)、测试(test)、生产(prod) 等不同环境运行,配置参数可能不同(如数据库地址)。Spring Boot 支持多环境配置,无需修改主配置文件即可切换环境。

1. 多环境配置文件命名规则

创建多个配置文件,命名格式为:
application-{profile}.yml(或 .properties),其中 {profile} 为环境标识(如 devprod)。

示例:

  • application-dev.yml:开发环境配置
  • application-test.yml:测试环境配置
  • application-prod.yml:生产环境配置
2. 环境配置内容示例

application-dev.yml(开发环境):

yaml 复制代码
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev_db  # 开发库

application-prod.yml(生产环境):

yaml 复制代码
server:
  port: 80  # 生产环境用 80 端口
spring:
  datasource:
    url: jdbc:mysql://prod-server:3306/prod_db  # 生产库
3. 激活指定环境

通过以下方式指定当前环境(优先级从高到低):

  • 方式 1:在主配置文件中指定
    application.yml

    yaml 复制代码
    spring:
      profiles:
        active: dev  # 激活开发环境
  • 方式 2:启动命令参数指定

    运行 JAR 包时通过 --spring.profiles.active 指定:

    bash 复制代码
    java -jar app.jar --spring.profiles.active=prod  # 激活生产环境
  • 方式 3:IDE 启动参数

    在 IDE 中配置启动参数(如 VM options):-Dspring.profiles.active=test

五、配置的优先级

当同一配置项在多个地方被定义时,Spring Boot 按以下优先级(从高到低)生效:

  1. 命令行参数(如 --server.port=8081
  2. 系统环境变量(如操作系统的环境变量)
  3. application-{profile}.yml(激活的环境配置)
  4. application.yml(主配置文件)
  5. 类路径下的默认配置(如 spring-boot-autoconfigure 中的默认值)

示例 :若命令行指定 --server.port=8082,同时 application-dev.yml 中设置 server.port=8080,最终端口为 8082(命令行优先级更高)。

六、外部化配置扩展

除了默认配置文件,Spring Boot 还支持多种外部化配置方式,适合生产环境动态调整参数:

  1. 外部配置文件 :将 application.yml 放在应用外部(如 /etc/app/),通过启动参数指定路径:

    bash 复制代码
    java -jar app.jar --spring.config.location=/etc/app/application.yml
  2. 配置中心:结合 Spring Cloud Config 或 Nacos 等工具,实现分布式环境下的配置集中管理和动态刷新。

  3. 操作系统环境变量:适合容器化部署(如 Docker),通过环境变量注入配置:

    bash 复制代码
    # 启动 Docker 容器时注入环境变量
    docker run -e "SPRING_PROFILES_ACTIVE=prod" -e "SERVER_PORT=80" myapp:latest

    (注意:环境变量名需将配置中的 . 改为 _,并大写,如 server.port 对应 SERVER_PORT

Spring Boot 的全局配置通过标准化的配置文件和灵活的注入方式,实现了应用参数的集中管理和动态调整。核心优势包括:

  • 支持多格式(Properties/YAML)和多环境配置,适应不同开发阶段需求。
  • 提供多种配置注入方式,满足简单和复杂场景。
  • 灵活的优先级机制和外部化配置支持,便于生产环境部署。

掌握全局配置是使用 Spring Boot 开发的基础,合理使用可大幅提升应用的可配置性和可维护性。

2.3、Spring Boot的自定义配置

在 Spring Boot 中,除了使用全局配置文件管理默认参数外,还可以通过"自定义配置"灵活开发者根据业务需求定义配置项,并在代码中灵活使用。这种方式能让配置与业务逻辑解耦,便于维护和扩展。以下从自定义配置的定义、注入方式、高级特性等方面详细介绍:

一、自定义配置的基本定义

自定义配置指开发者在全局配置文件(application.ymlapplication.properties)中添加业务相关的配置项,例如应用名称、功能开关、第三方 API 密钥等。

1. 配置项格式
  • YAML 格式(推荐):

    yaml 复制代码
    # 自定义应用信息
    app:
      name: "电商平台"
      version: "2.1.0"
      enabled: true
      timeout: 3000
      contact:
        email: "support@example.com"
        phone: "123456789"
      features:  # 列表类型
        - "支付功能"
        - "会员系统"
        - "优惠券"
  • Properties 格式

    properties 复制代码
    # 自定义应用信息
    app.name=电商平台
    app.version=2.1.0
    app.enabled=true
    app.timeout=3000
    app.contact.email=support@example.com
    app.contact.phone=123456789
    app.features[0]=支付功能
    app.features[1]=会员系统
    app.features[2]=优惠券

二、自定义配置的注入方式

定义配置项后,需将其注入到 Java 代码中使用。Spring Boot 提供了多种注入方式,适用于不同场景:

1. @Value 注解(简单配置项)

@Value 适合注入单个简单类型的配置项(如字符串、数字、布尔值),支持直接指定默认值。

示例

java 复制代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AppController {
    // 注入字符串类型(带默认值,配置不存在时使用)
    @Value("${app.name:默认应用}")
    private String appName;

    // 注入数字类型
    @Value("${app.version}")
    private String appVersion;

    // 注入布尔类型
    @Value("${app.enabled}")
    private boolean isEnabled;

    // 注入嵌套配置项(contact.email)
    @Value("${app.contact.email}")
    private String contactEmail;

    @GetMapping("/app/info")
    public String getAppInfo() {
        return String.format(
            "应用名称:%s,版本:%s,状态:%s,联系邮箱:%s",
            appName, appVersion, isEnabled ? "启用" : "禁用", contactEmail
        );
    }
}

注意

  • 若配置项不存在且未指定默认值,启动时会抛出 IllegalArgumentException
  • @Value 不适合注入复杂类型(如列表、对象),需配合 SpEL 表达式,较繁琐。
2. @ConfigurationProperties 注解(复杂配置项)

@ConfigurationProperties 适合注入一组相关的配置项(尤其是包含对象、列表的复杂结构),通过"前缀匹配"将配置项绑定到 Java 类的属性中,更清晰且类型安全。

步骤 1:定义配置类并绑定前缀

java 复制代码
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;

// 绑定前缀为 "app" 的所有配置项
@ConfigurationProperties(prefix = "app")
@Component  // 注册为 Spring 组件,使其被扫描并注入
public class AppConfig {
    // 基本类型
    private String name;
    private String version;
    private boolean enabled;
    private int timeout;

    // 嵌套对象(对应 app.contact 配置)
    private Contact contact = new Contact();  // 初始化避免空指针

    // 列表类型(对应 app.features 配置)
    private List<String> features;

    // 内部类:对应嵌套配置 app.contact
    public static class Contact {
        private String email;
        private String phone;

        // getter 和 setter(必须存在,否则无法绑定)
        public String getEmail() { return email; }
        public void setEmail(String email) { this.email = email; }
        public String getPhone() { return phone; }
        public void setPhone(String phone) { this.phone = phone; }
    }

    // 外部类的 getter 和 setter(必须存在)
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getVersion() { return version; }
    public void setVersion(String version) { this.version = version; }
    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }
    public int getTimeout() { return timeout; }
    public void setTimeout(int timeout) { this.timeout = timeout; }
    public Contact getContact() { return contact; }
    public void setContact(Contact contact) { this.contact = contact; }
    public List<String> getFeatures() { return features; }
    public void setFeatures(List<String> features) { this.features = features; }
}

步骤 2:在代码中注入并使用配置类

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AppController {
    // 注入自定义配置类
    @Autowired
    private AppConfig appConfig;

    @GetMapping("/app/detail")
    public String getAppDetail() {
        StringBuilder info = new StringBuilder();
        info.append("应用详情:").append("\n")
            .append("名称:").append(appConfig.getName()).append("\n")
            .append("版本:").append(appConfig.getVersion()).append("\n")
            .append("超时时间:").append(appConfig.getTimeout()).append("ms\n")
            .append("联系电话:").append(appConfig.getContact().getPhone()).append("\n")
            .append("支持功能:").append(appConfig.getFeatures());
        return info.toString();
    }
}

注意

  • 配置类必须包含 getter 和 setter,否则 Spring 无法绑定配置值。
  • 若配置类未使用 @Component,需在启动类或配置类上添加 @EnableConfigurationProperties(AppConfig.class) 手动激活绑定。
3. 两种注入方式的对比
方式 优点 缺点 适用场景
@Value 简单直观,适合单个参数 不适合复杂类型,配置项多时代码冗余 少量简单配置(如单个密钥、端口)
@ConfigurationProperties 支持复杂类型,配置与代码解耦,类型安全 需定义专门的配置类,稍显繁琐 多个相关配置(如应用信息、第三方服务配置)

三、自定义配置的高级特性

1. 配置校验(@Validated

通过 JSR-303 注解(如 @NotBlank@Min)对配置项进行校验,确保配置值符合业务规则,避免无效配置导致的问题。

步骤

  1. 引入校验依赖(Spring Boot 已包含在 spring-boot-starter-web 中):

    xml 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
  2. 在配置类上添加 @Validated 并使用校验注解:

    java 复制代码
    import org.springframework.validation.annotation.Validated;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.Min;
    import javax.validation.constraints.Email;
    
    @ConfigurationProperties(prefix = "app")
    @Component
    @Validated  // 开启校验
    public class AppConfig {
        @NotBlank(message = "应用名称不能为空")  // 非空校验
        private String name;
    
        @Min(value = 1000, message = "超时时间不能小于 1000ms")  // 最小值校验
        private int timeout;
    
        public static class Contact {
            @Email(message = "邮箱格式不正确")  // 邮箱格式校验
            private String email;
            // ...
        }
        // ... getter/setter
    }

    若配置不符合规则(如 app.timeout=500),应用启动时会抛出 ConstraintViolationException,明确提示错误原因。

2. 配置提示(IDE 友好)

当使用 @ConfigurationProperties 时,IDE(如 IntelliJ IDEA)默认不会提示自定义配置项的名称和类型。通过添加依赖可生成配置元数据,让 IDE 支持自动补全和文档提示。

步骤

  1. 引入配置处理器依赖:

    xml 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>  <!-- 打包时排除,仅开发时使用 -->
    </dependency>
  2. 重新编译项目,处理器会在 target/classes/META-INF 下生成 spring-configuration-metadata.json,包含配置项的名称、类型、描述等信息。

  3. 可通过 @ConfigurationPropertiesdescription 属性或 @Deprecated 注解添加文档:

    java 复制代码
    @ConfigurationProperties(prefix = "app", description = "应用基础配置")
    public class AppConfig {
        /**
         * 应用名称,会显示在页面标题中
         */
        private String name;
    
        @Deprecated  // 标记为过时配置
        private String oldProperty;
        // ...
    }

    之后在配置文件中输入 app. 时,IDE 会自动提示配置项及说明,提升开发效率。

3. 多环境下的自定义配置

结合 Spring Boot 的多环境配置(application-{profile}.yml),可针对不同环境(开发、测试、生产)定义不同的自定义配置值。

示例

  • application-dev.yml(开发环境):

    yaml 复制代码
    app:
      name: "电商平台-开发版"
      timeout: 2000  # 开发环境超时时间较短
  • application-prod.yml(生产环境):

    yaml 复制代码
    app:
      name: "电商平台-正式版"
      timeout: 5000  # 生产环境超时时间较长

启动时通过 spring.profiles.active=devprod 激活对应环境,配置类会自动加载当前环境的配置值。

4. 外部化自定义配置

除了内置配置文件,自定义配置还支持从外部源加载(如命令行、环境变量、配置中心),适合生产环境动态调整。

  • 命令行参数 :启动时通过 -- 传递自定义配置,覆盖文件中的值:

    bash 复制代码
    java -jar app.jar --app.name="临时测试版" --app.timeout=1000
  • 环境变量 :在操作系统或容器中设置环境变量,Spring Boot 会自动识别(变量名需将 . 改为 _ 并大写):

    bash 复制代码
    # Linux 环境
    export APP_NAME="环境变量配置"
    export APP_TIMEOUT=4000
    java -jar app.jar
  • 配置中心:结合 Spring Cloud Config 或 Nacos,将自定义配置集中管理,支持动态刷新(无需重启应用)。

四、自定义配置的最佳实践

  1. 命名规范 :配置项前缀使用项目或模块标识(如 app.pay.redis.),避免与 Spring 内置配置冲突。
  2. 分层管理 :将不同业务的配置分类(如 pay. 对应支付配置,cache. 对应缓存配置),提高可读性。
  3. 必选配置校验 :通过 @NotBlank 等注解强制校验必选配置,避免应用启动后因缺少配置而报错。
  4. 敏感配置加密 :对于密码、密钥等敏感配置,使用 Spring Boot 加密工具(如 jasypt-spring-boot)加密存储,避免明文泄露。

Spring Boot 的自定义配置通过灵活的注入方式和丰富的高级特性,让开发者能够轻松管理业务相关的配置项。无论是简单的单个参数还是复杂的嵌套结构,都能通过 @Value@ConfigurationProperties 优雅地注入到代码中。结合配置校验、多环境支持和外部化配置,可进一步提升配置的可靠性和灵活性,是 Spring Boot 应用开发中的核心实践之一。

3.1、Thymeleaf的基本语法

Thymeleaf 是一款适用于 Web 和独立环境的现代服务器端 Java 模板引擎,它支持 HTML5 原型,能无缝集成 Spring Boot 应用。其语法简洁且贴近自然 HTML,以下是 Thymeleaf 的基本语法详解:

一、命名空间与基本表达式

在 HTML 页面中使用 Thymeleaf,需先声明命名空间:

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Thymeleaf 示例</title>
  </head>
  <body>
    <!-- 内容 -->
  </body>
</html>

Thymeleaf 通过 th: 前缀标识其属性,核心表达式语法如下:

1. 变量表达式(${...}

用于获取模型(Model)中的变量,等价于 Spring MVC 的 request.getAttribute()

示例

后端控制器添加变量:

java 复制代码
@GetMapping("/user")
public String user(Model model) {
    User user = new User("张三", 25, "zhangsan@example.com");
    model.addAttribute("user", user);
    return "user"; // 指向 user.html 模板
}

前端模板使用:

html 复制代码
<!-- 获取对象属性 -->
<p>姓名:<span th:text="${user.name}">默认姓名</span></p>
<p>年龄:<span th:text="${user.age}">默认年龄</span></p>

<!-- 支持链式调用(需对象有对应 getter 方法) -->
<p>邮箱:<span th:text="${user.email}">默认邮箱</span></p>
  • th:text:替换标签内的文本内容("默认姓名" 会被 ${user.name} 的值替换)。
  • 若变量为 null,则显示空字符串(而非默认文本)。
2. 选择变量表达式(*{...}

用于简化对象属性的访问,需配合 th:object 指定上下文对象。

示例

html 复制代码
<!-- 先通过 th:object 指定对象 -->
<div th:object="${user}">
  <p>姓名:<span th:text="*{name}">默认姓名</span></p> <!-- 等价于 ${user.name} -->
  <p>年龄:<span th:text="*{age}">默认年龄</span></p>   <!-- 等价于 ${user.age} -->
</div>
3. 链接表达式(@{...}

用于生成 URL 路径,自动处理上下文路径(Context Path),支持绝对路径和相对路径。

示例

html 复制代码
<!-- 相对路径(自动拼接上下文路径) -->
<a th:href="@{/home}">首页</a>
<a th:href="@{/user/detail(id=1, name='张三')}">用户详情</a> <!-- 带参数 -->

<!-- 绝对路径 -->
<a th:href="@{https://www.example.com}">外部链接</a>

<!-- 资源路径(CSS/JS/图片) -->
<link th:href="@{/static/css/style.css}" rel="stylesheet">
<script th:src="@{/static/js/app.js}"></script>
<img th:src="@{/static/images/logo.png}" alt="logo">
  • 若应用上下文路径为 /app@{/home} 会生成 /app/home
  • 参数通过 (key=value) 形式添加,多个参数用逗号分隔。
4. 片段表达式(~{...}

用于引用模板片段(可复用的 HTML 片段),配合 th:insertth:replace 使用。

示例

定义片段(footer.html):

html 复制代码
<footer th:fragment="copyright">
  <p>© 2023 示例网站 版权所有</p>
</footer>

引用片段(在其他页面中):

html 复制代码
<!-- 插入片段(保留当前标签,片段作为子元素) -->
<div th:insert="~{footer :: copyright}"></div>

<!-- 替换片段(用片段替换当前标签) -->
<div th:replace="~{footer :: copyright}"></div>
  • footer :: copyright 表示引用 footer.html 中名为 copyright 的片段。

二、常用属性

Thymeleaf 提供了丰富的属性用于动态控制页面元素,以下是常用属性:

1. 文本与属性操作
  • th:text:设置标签内文本(会转义特殊字符,防止 XSS)。
  • th:utext:设置标签内文本(不转义特殊字符,适合输出 HTML 内容)。
  • th:attr:设置标签的任意属性(如 th:attr="src=@{/img.png}, title='图片'")。
  • 简化属性:th:srcth:hrefth:titleth:class 等(直接对应 HTML 属性)。

示例

html 复制代码
<!-- 转义文本(< 会变为 &lt;) -->
<p th:text="${htmlContent}">包含 HTML 的文本</p>

<!-- 不转义文本(直接渲染 HTML) -->
<p th:utext="${htmlContent}">包含 HTML 的文本</p>

<!-- 动态设置图片和样式 -->
<img th:src="@{/images/{filename}(filename=${imageName})}" th:alt="${imageDesc}">
<div th:class="${isActive ? 'active' : 'inactive'}">状态样式</div>
2. 条件判断
  • th:if:当表达式为 true 时显示标签。
  • th:unless:当表达式为 false 时显示标签(与 th:if 相反)。
  • th:switch + th:case:多条件判断(类似 switch-case)。

示例

html 复制代码
<!-- th:if 条件 -->
<p th:if="${user.age >= 18}">已成年</p>

<!-- th:unless 条件(等价于 th:if="${!(user.age >= 18)}") -->
<p th:unless="${user.age >= 18}">未成年</p>

<!-- switch-case -->
<div th:switch="${user.role}">
  <p th:case="'ADMIN'">管理员</p>
  <p th:case="'USER'">普通用户</p>
  <p th:case="*">未知角色</p> <!-- * 表示默认 case -->
</div>
  • 表达式结果为 null 时,th:if 视为 false;非 null 的数值/字符串/布尔值按实际值判断。
3. 循环遍历

th:each:用于遍历集合(List、Map、数组等),语法为 变量名, 状态变量 : ${集合}

示例

后端传递集合:

java 复制代码
model.addAttribute("users", Arrays.asList(
    new User("张三", 25),
    new User("李四", 30)
));

前端遍历:

html 复制代码
<!-- 遍历用户列表 -->
<table>
  <tr>
    <th>序号</th>
    <th>姓名</th>
    <th>年龄</th>
  </tr>
  <!-- status 是状态变量,包含 index(索引,从 0 开始)、count(计数,从 1 开始)等 -->
  <tr th:each="user, status : ${users}">
    <td th:text="${status.count}">1</td>
    <td th:text="${user.name}">姓名</td>
    <td th:text="${user.age}">年龄</td>
  </tr>
</table>

状态变量常用属性:

  • index:当前元素索引(0 开始)。
  • count:当前元素计数(1 开始)。
  • size:集合总大小。
  • even/odd:是否为偶/奇数行(布尔值)。
  • first/last:是否为第一个/最后一个元素(布尔值)。
4. 表单处理

Thymeleaf 提供表单相关属性,与 Spring MVC 表单绑定配合使用:

  • th:action:表单提交地址(结合 @{...} 表达式)。
  • th:object:绑定表单对象(后端 @ModelAttribute 对应的对象)。
  • th:field:绑定对象属性(自动生成 namevalue 属性)。

示例

html 复制代码
<form th:action="@{/user/save}" th:object="${user}" method="post">
  <!-- 等价于 name="name" value="${user.name}" -->
  <input type="text" th:field="*{name}" placeholder="姓名">
  
  <!-- 等价于 name="age" value="${user.age}" -->
  <input type="number" th:field="*{age}" placeholder="年龄">
  
  <button type="submit">提交</button>
</form>
  • th:field 会自动处理表单回显(如提交失败后保留用户输入)。

三、内置对象

Thymeleaf 提供了多个内置对象,简化常见操作:

1. Web 相关对象
  • #request:HttpServletRequest 对象(如 #request.getContextPath())。
  • #session:HttpSession 对象(如 #session.getAttribute('user'))。
  • #servletContext:ServletContext 对象。

示例

html 复制代码
<p>上下文路径:<span th:text="${#request.getContextPath()}"></span></p>
<p>Session 中的用户:<span th:text="${#session.getAttribute('username')}"></span></p>
2. 工具类对象
  • #strings:字符串工具类(isEmptytoUpperCasesubstring 等)。
  • #numbers:数字工具类(格式化数字、百分比等)。
  • #dates/#calendars:日期工具类(格式化日期、获取年/月/日等)。
  • #lists:集合工具类(sizecontainssort 等)。

示例

html 复制代码
<!-- 字符串处理 -->
<p>大写姓名:<span th:text="${#strings.toUpperCase(user.name)}"></span></p>
<p>是否为空:<span th:text="${#strings.isEmpty(user.email)}"></span></p>

<!-- 日期格式化 -->
<p>注册时间:<span th:text="${#dates.format(user.regTime, 'yyyy-MM-dd HH:mm:ss')}"></span></p>

<!-- 集合操作 -->
<p>用户总数:<span th:text="${#lists.size(users)}"></span></p>

四、注释与内联表达式

1. Thymeleaf 注释
html 复制代码
<!-- 标准 HTML 注释(会被浏览器解析) -->
<!--/* Thymeleaf 解析注释(仅模板引擎可见,不会输出到页面) */-->
<!--/*
  多行 Thymeleaf 注释
  不会被渲染到最终 HTML 中
*/-->
2. 内联表达式

在 HTML 文本或 JavaScript 中直接使用表达式,无需通过 th:text 等属性:

  • 文本内联[[${表达式}]](等价于 th:text,会转义),[(${表达式})](等价于 th:utext,不转义)。
  • JavaScript 内联/*[[${表达式}]]*/(在 JS 中注入变量)。

示例

html 复制代码
<!-- 文本内联 -->
<p>欢迎您:[[${user.name}]]</p> <!-- 等价于 <p th:text="${user.name}"></p> -->
<p>简介:[(${user.intro})]</p> <!-- 渲染 HTML 内容 -->

<!-- JavaScript 内联 -->
<script th:inline="javascript">
  var username = /*[[${user.name}]]*/ '默认名称';
  console.log("当前用户:" + username);
</script>
  • 使用 JavaScript 内联时,需在 <script> 标签添加 th:inline="javascript"

Thymeleaf 的核心语法围绕 表达式属性 展开,通过 th: 前缀实现模板动态渲染。其特点是:

  • 语法贴近 HTML,易于理解和维护。
  • 支持丰富的表达式(变量、链接、片段等)和操作(条件、循环、表单绑定)。
  • 内置工具类简化常见处理(字符串、日期、集合等)。

掌握这些基本语法后,可轻松实现动态页面渲染,结合 Spring Boot 开发高效的 Web 应用。

3.2、Spring Boot实现页面国际化

Spring Boot 实现页面国际化(i18n)可以让应用根据不同语言环境显示对应的文本内容,核心是通过配置消息源(MessageSource)和切换Locale(语言环境)来实现。以下是详细实现步骤:

一、准备工作:创建国际化资源文件

首先在 src/main/resources 目录下创建国际化消息文件,命名规则为 messages_{语言}_{国家}.properties,默认文件为 messages.properties

1. 文件结构
复制代码
resources/
├── i18n/  # 建议统一放在i18n目录下(可选)
│   ├── messages.properties       # 默认(未指定语言时使用)
│   ├── messages_en_US.properties # 英文(美国)
│   └── messages_zh_CN.properties # 中文(中国)
2. 资源文件内容
  • messages.properties(默认):

    properties 复制代码
    welcome=欢迎访问网站
    username=用户名
    password=密码
    login=登录
  • messages_en_US.properties(英文):

    properties 复制代码
    welcome=Welcome to the website
    username=Username
    password=Password
    login=Login
  • messages_zh_CN.properties(中文):

    properties 复制代码
    welcome=欢迎访问网站
    username=用户名
    password=密码
    login=Login

二、配置Spring Boot支持国际化

需要配置消息源位置和Locale解析器,告诉Spring Boot如何加载国际化资源和切换语言。

1. 配置application.yml

指定国际化资源文件的位置:

yaml 复制代码
spring:
  messages:
    basename: i18n/messages  # 资源文件路径(无需扩展名)
    encoding: UTF-8          # 编码格式(解决中文乱码)
2. 自定义Locale解析器(可选)

默认情况下,Spring Boot通过请求头的 Accept-Language 字段判断Locale。若需要通过URL参数(如 ?lang=en_US)切换语言,需自定义LocaleResolver:自定义Locale解析器配置类:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;

@Configuration
public class LocaleConfig implements WebMvcConfigurer {

    // 配置Locale解析器(存储用户选择的语言)
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        // 设置默认语言(中文)
        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return resolver;
    }

    // 配置语言切换拦截器(通过URL参数lang切换语言)
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        // 指定切换语言的参数名(如 ?lang=en_US)
        interceptor.setParamName("lang");
        return interceptor;
    }

    // 注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}
  • SessionLocaleResolver:将用户选择的Locale存储在Session中,保持会话内语言一致。
  • LocaleChangeInterceptor:拦截URL中包含 lang 参数的请求,动态切换Locale(如 http://localhost:8080?lang=en_US 切换为英文)。

三、在页面中使用国际化文本

以Thymeleaf模板为例,通过 #{key} 表达式引用国际化资源文件中的key:

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>国际化示例</title>
</head>
<body>
    <!-- 引用国际化文本 -->
    <h1 th:text="#{welcome}">欢迎信息</h1>
    
    <form>
        <label th:text="#{username}">用户名</label>
        <input type="text" name="username"><br>
        
        <label th:text="#{password}">密码</label>
        <input type="password" name="password"><br>
        
        <button type="submit" th:text="#{login}">登录</button>
    </form>
    
    <!-- 语言切换链接 -->
    <div>
        <a th:href="@{/?lang=zh_CN}">中文</a> | 
        <a th:href="@{/?lang=en_US}">English</a>
    </div>
</body>
</html>
  • #{welcome} 会根据当前Locale自动匹配 messages_*.properties 中对应的value。
  • 点击"中文"或"English"链接时,通过 lang 参数触发语言切换。

四、在Java代码中使用国际化文本

通过 MessageSource 接口在Service或Controller中获取国际化文本:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
import java.util.Locale;

@Service
public class HelloService {

    // 注入消息源
    @Autowired
    private MessageSource messageSource;

    // 获取国际化消息
    public String getWelcomeMessage() {
        // LocaleContextHolder.getLocale() 获取当前Locale(由LocaleResolver管理)
        Locale locale = LocaleContextHolder.getLocale();
        // 第一个参数:资源文件中的key;第二个参数:占位符参数(可选);第三个参数:Locale
        return messageSource.getMessage("welcome", null, locale);
    }
}
  • LocaleContextHolder.getLocale():获取当前请求的Locale(自动适配自定义的LocaleResolver)。
  • messageSource.getMessage():第一个参数为资源文件的key,第二个参数用于填充带占位符的消息(如 welcome=欢迎 {0} 访问,可传递 new Object[]{"张三"})。

五、测试与验证

  1. 启动应用,访问页面默认显示中文。
  2. 点击"English"链接(URL变为 ?lang=en_US),页面切换为英文。
  3. 刷新页面或访问其他路由,语言设置会保持(因使用Session存储)。

关键知识点总结

  1. 资源文件命名 :遵循 messages_{语言}_{国家}.properties 规则,默认文件 messages.properties 必须存在。
  2. Locale解析
    • 默认通过请求头 Accept-Language 解析。
    • 自定义 SessionLocaleResolver + LocaleChangeInterceptor 支持URL参数切换。
  3. 文本引用
    • 页面(Thymeleaf)用 #{key}
    • 代码中用 MessageSource.getMessage(key, params, locale)

通过以上步骤,Spring Boot应用即可实现灵活的页面国际化,满足多语言场景需求。

3.3、Spring Boot集成Spring MVC

Spring Boot 对 Spring MVC 进行了深度集成和自动化配置,让开发者无需手动搭建 Spring MVC的基础架构,就能能快速开发基于基于 MVC 模式的 Web 应用。以下从集成原理、核心配置、组件使用等方面详细讲解:

一、集成原理与自动配置

Spring Boot 通过 spring-boot-starter-web starter 实现对 Spring MVC 的自动集成,底层依赖 Spring 框架的 Web 模块,并通过自动配置类简化传统 Spring MVC 的繁琐配置。

1. 核心依赖

pom.xml 中引入 Web starter 即可自动集成 Spring MVC:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

该依赖包含:

  • Spring MVC 核心组件(DispatcherServlet、控制器、视图解析器等)
  • 嵌入式服务器(默认 Tomcat)
  • 常用 Web 依赖(如 javax.servlet-api
2. 自动配置类

Spring Boot 通过 WebMvcAutoConfiguration 自动配置 Spring MVC 核心组件,主要包括:

  • DispatcherServlet :前端控制器,自动注册并映射到 / 路径
  • 视图解析器 :默认配置 InternalResourceViewResolver,支持 JSP 等视图
  • 静态资源处理器 :映射 /static/public 等目录的静态资源
  • 类型转换:注册默认的类型转换器和格式化器
  • 消息转换器 :配置 HttpMessageConverter,支持 JSON 等数据格式(默认集成 Jackson)

二、核心组件与使用方式

Spring Boot 集成 Spring MVC 后,保留了 MVC 模式的核心组件(控制器、服务、视图等),使用方式与传统 Spring MVC 一致,但配置更简化。

1. 控制器(Controller)

通过 @Controller@RestController 定义控制器,处理 HTTP 请求:

Spring MVC控制器示例:

java 复制代码
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

// @RestController = @Controller + @ResponseBody(返回JSON/文本,不跳转视图)
@Controller
@RequestMapping("/users") // 类级别的URL映射
public class UserController {

    // 处理GET请求,返回视图
    @GetMapping("/{id}")
    public String getUser(@PathVariable("id") Long id, Model model) {
        // 模拟查询用户(实际应调用Service)
        User user = new User(id, "张三", 25);
        model.addAttribute("user", user); // 向视图传递数据
        return "user/detail"; // 跳转至templates/user/detail.html视图
    }

    // 处理POST请求,返回JSON(@ResponseBody单独使用)
    @PostMapping("/save")
    @ResponseBody
    public Result saveUser(@RequestBody User user) {
        // 模拟保存用户
        return Result.success("保存成功", user);
    }
}
    

关键注解说明:

  • @RequestMapping:定义请求路径和方法(可指定 method = RequestMethod.GET 等)
  • @GetMapping/@PostMapping:简化的 HTTP 方法注解(分别对应 GET/POST)
  • @PathVariable:获取 URL 路径参数(如 /users/1 中的 1
  • @RequestBody:接收请求体中的 JSON 数据并绑定到对象
  • @ResponseBody:将返回值直接转换为 JSON/XML 等响应体(无需视图解析)
2. 服务层(Service)

通过 @Service 定义业务逻辑层,被控制器调用:

Spring MVC服务层示例:

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // 模拟查询用户
    public User getUserById(Long id) {
        // 实际应调用DAO层操作数据库
        return new User(id, "张三", 25);
    }

    // 模拟保存用户
    public boolean saveUser(User user) {
        // 实际应包含事务管理和业务校验
        return true;
    }
}
    
3. 视图层(View)

Spring Boot 推荐使用 Thymeleaf 作为视图模板引擎(默认集成),替代传统 JSP。

步骤 1 :引入 Thymeleaf 依赖(已包含在 spring-boot-starter-web 中,如需单独引入):

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

步骤 2 :创建 Thymeleaf 视图(src/main/resources/templates/user/detail.html):

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>用户详情</title>
</head>
<body>
    <h1>用户详情</h1>
    <p>ID:<span th:text="${user.id}"></span></p>
    <p>姓名:<span th:text="${user.name}"></span></p>
    <p>年龄:<span th:text="${user.age}"></span></p>
</body>
</html>
4. 静态资源处理

Spring Boot 自动映射以下目录的静态资源(CSS、JS、图片等):

  • classpath:/static/
  • classpath:/public/
  • classpath:/resources/
  • classpath:/META-INF/resources/

访问方式:直接通过资源路径访问,例如 http://localhost:8080/css/style.css 对应 static/css/style.css

三、自定义 Spring MVC 配置

Spring Boot 允许通过两种方式自定义 Spring MVC 配置,覆盖默认行为:

1. 实现 WebMvcConfigurer 接口(推荐)

通过重写接口方法自定义配置,不会覆盖自动配置:

自定义Spring MVC配置:

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // 配置视图控制器(无需编写Controller即可映射URL到视图)
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index"); // 访问/直接跳转index.html
        registry.addViewController("/login").setViewName("login");
    }

    // 配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册自定义拦截器,排除静态资源
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/login", "/css/**", "/js/**"); // 排除登录页和静态资源
    }

    // 配置跨域资源共享(CORS)
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**") // 对/api/*路径允许跨域
                .allowedOrigins("http://localhost:3000") // 允许的源
                .allowedMethods("GET", "POST") // 允许的方法
                .allowCredentials(true); // 允许携带Cookie
    }
}
    
2. 使用 @EnableWebMvc 注解(谨慎使用)

在配置类上添加 @EnableWebMvc 会完全禁用 Spring Boot 的 MVC 自动配置,需手动配置所有组件(如视图解析器、消息转换器等),适用于需要完全自定义的场景。

四、常用功能配置

1. 全局异常处理

通过 @ControllerAdvice 定义全局异常处理器,统一处理请求中的异常:

java 复制代码
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice // 全局控制器增强
public class GlobalExceptionHandler {

    // 处理自定义异常
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseBody
    public Result handleUserNotFound(UserNotFoundException e) {
        return Result.error(404, e.getMessage());
    }

    // 处理所有未捕获的异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result handleException(Exception e) {
        return Result.error(500, "服务器内部错误:" + e.getMessage());
    }
}
    
2. 数据校验

结合 JSR-303 注解(如 @NotNull@Size)实现请求参数校验:

java 复制代码
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;

public class User {
    private Long id;

    @NotBlank(message = "姓名不能为空")
    private String name;

    @NotNull(message = "年龄不能为空")
    @Positive(message = "年龄必须为正数")
    private Integer age;

    // getter/setter
}
    
java 复制代码
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @PostMapping("/users")
    public Result addUser(@Validated @RequestBody User user) {
        // 若校验失败,会抛出MethodArgumentNotValidException,被全局异常处理器捕获
        return Result.success("添加成功", user);
    }
}
    

五、集成优势总结

  1. 零 XML 配置 :自动配置核心组件(DispatcherServlet、视图解析器等),无需手动配置。
  2. 简化依赖管理:通过 starter 一键引入所需依赖,避免版本冲突。
  3. 开箱即用:默认支持 JSON 转换、静态资源处理、类型转换等功能。
  4. 灵活扩展 :通过 WebMvcConfigurer 接口轻松自定义配置,不破坏自动配置。
  5. 与 Spring 生态无缝衔接:可直接集成 Spring Security、Spring Data 等组件。

通过 Spring Boot 集成 Spring MVC,开发者能专注于业务逻辑实现,大幅提升 Web 应用开发效率。

3.4、Spring Boot返回JSON数据

在 Spring Boot 中,返回 JSON 数据是 Web 开发(尤其是 RESTful API)的常见需求。得益于 Spring Boot 的自动配置,无需额外依赖即可实现 JSON 数据的序列化与返回,核心依赖 Jackson 会被自动引入并配置。以下是详细实现方式:

一、默认 JSON 支持原理

Spring Boot 的 spring-boot-starter-web 依赖中默认包含 jackson-databind 库,它会自动配置 MappingJackson2HttpMessageConverter 消息转换器,负责将 Java 对象与 JSON 数据互相转换。

  • 依赖自动引入 :引入 spring-boot-starter-web 即可(无需额外配置):

    xml 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

二、基本使用:返回 JSON 数据

通过 @ResponseBody 注解(或 @RestController 组合注解)即可将 Java 对象自动转换为 JSON 并返回。

1. 使用 @RestController 注解(推荐)

@RestController@Controller + @ResponseBody 的组合注解,类中所有方法的返回值都会被自动转换为 JSON:

使用@RestController返回JSON:

java 复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;

// @RestController = @Controller + @ResponseBody
@RestController
public class UserController {

    // 返回单个对象
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        // 模拟查询用户(实际应从数据库获取)
        return new User(id, "张三", 25, "zhangsan@example.com");
    }

    // 返回集合
    @GetMapping("/users")
    public List<User> getUsers() {
        List<User> users = new ArrayList<>();
        users.add(new User(1L, "张三", 25, "zhangsan@example.com"));
        users.add(new User(2L, "李四", 30, "lisi@example.com"));
        return users;
    }

    // 返回自定义响应体(通用格式)
    @GetMapping("/api/users/{id}")
    public Result<User> getUserApi(@PathVariable Long id) {
        User user = new User(id, "张三", 25, "zhangsan@example.com");
        return Result.success("查询成功", user);
    }
}
    
2. 相关实体类定义
  • User 实体类

    java 复制代码
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private String email;
        
        // 构造方法、getter、setter(必须存在,否则JSON序列化会丢失字段)
        public User(Long id, String name, Integer age, String email) {
            this.id = id;
            this.name = name;
            this.age = age;
            this.email = email;
        }
        
        // getter 和 setter 省略...
    }
  • 通用响应体 Result 类(统一 API 响应格式):

    java 复制代码
    public class Result<T> {
        private Integer code; // 状态码(如 200 成功,400 失败)
        private String message; // 提示信息
        private T data; // 数据本体
        
        // 静态方法:快速构建成功/失败响应
        public static <T> Result<T> success(String message, T data) {
            Result<T> result = new Result<>();
            result.code = 200;
            result.message = message;
            result.data = data;
            return result;
        }
        
        public static <T> Result<T> error(Integer code, String message) {
            Result<T> result = new Result<>();
            result.code = code;
            result.message = message;
            return result;
        }
        
        // getter 和 setter 省略...
    }
3. 访问效果
  • 访问 http://localhost:8080/users/1 会返回 JSON 格式的用户对象:

    json 复制代码
    {
      "id": 1,
      "name": "张三",
      "age": 25,
      "email": "zhangsan@example.com"
    }
  • 访问 http://localhost:8080/api/users/1 会返回统一格式的响应:

    json 复制代码
    {
      "code": 200,
      "message": "查询成功",
      "data": {
        "id": 1,
        "name": "张三",
        "age": 25,
        "email": "zhangsan@example.com"
      }
    }

三、JSON 序列化自定义配置

默认情况下,Jackson 会按实体类的字段(通过 getter 方法)序列化 JSON,但实际开发中常需要自定义格式(如日期格式化、忽略 null 值等)。

1. 局部配置(通过注解)

在实体类或字段上使用 Jackson 注解自定义序列化规则:

使用注解自定义JSON序列化:

java 复制代码
import com.fasterxml.jackson.annotation.*;
import java.util.Date;

public class User {
    private Long id;
    
    // 序列化时指定字段别名(JSON中显示为"username")
    @JsonProperty("username")
    private String name;
    
    private Integer age;
    
    // 忽略该字段(不序列化到JSON)
    @JsonIgnore
    private String email;
    
    // 日期格式化(默认会序列化为时间戳)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    
    // 忽略值为null的字段
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String remark;

    // 构造方法、getter、setter 省略...
}
    

常用注解说明:

  • @JsonProperty("alias"):指定 JSON 字段的别名。
  • @JsonIgnore:序列化时忽略该字段。
  • @JsonFormat(pattern = "...", timezone = "GMT+8"):格式化日期/时间(解决时区问题)。
  • @JsonInclude(JsonInclude.Include.NON_NULL):忽略值为 null 的字段(也可全局配置)。
2. 全局配置(通过配置类)

通过自定义 Jackson2ObjectMapperBuilderCustomizer 实现全局 JSON 序列化规则:

全局JSON序列化配置:

java 复制代码
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

@Configuration
public class JacksonConfig {

    // 全局配置Jackson
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> {
            // 全局设置时区
            builder.timeZone(TimeZone.getTimeZone("GMT+8"));
            
            // 全局日期格式化(LocalDateTime)
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
            
            // 忽略值为null的字段
            builder.serializationInclusion(JsonInclude.Include.NON_NULL);
            
            // 禁用序列化空对象(避免返回{"empty":false}等)
            builder.featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
            
            // 反序列化时忽略未知字段(避免前端传递多余字段时报错)
            builder.featuresToEnable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
        };
    }
}
    

四、接收 JSON 数据(请求体)

除了返回 JSON,Spring Boot 也能自动将请求体中的 JSON 数据反序列化为 Java 对象,通过 @RequestBody 注解实现:

java 复制代码
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    // 接收JSON请求体并转换为User对象
    @PostMapping("/users")
    public Result<User> addUser(@RequestBody User user) {
        // user 对象已自动填充请求体中的JSON数据
        System.out.println("接收用户:" + user.getName());
        return Result.success("添加成功", user);
    }
}
    

请求示例 (使用 POST 方法,Content-Type 为 application/json):

json 复制代码
// 请求体
{
  "name": "王五",
  "age": 28,
  "email": "wangwu@example.com"
}

Spring Boot 会自动将上述 JSON 转换为 User 对象,无需手动解析。

五、常见问题解决

  1. 中文乱码 :Spring Boot 默认已配置 UTF-8 编码,若出现乱码,检查请求头 Content-Type 是否为 application/json;charset=utf-8

  2. 日期序列化问题 :通过 @JsonFormat 注解或全局配置指定日期格式和时区(GMT+8),避免时间戳或时区偏移问题。

  3. 循环引用报错 :若实体类存在循环引用(如 User 包含 Order,Order 包含 User),可在关联字段添加 @JsonIgnore 忽略一方,或配置 builder.featuresToDisable(SerializationFeature.FAIL_ON_SELF_REFERENCES)

Spring Boot 对 JSON 的支持基于 Jackson 实现,通过 @RestController@ResponseBody 可轻松返回 JSON 数据。核心优势:

  • 自动配置:无需手动配置消息转换器,开箱即用。
  • 灵活定制:通过注解或全局配置自定义序列化规则(日期、null 值、字段别名等)。
  • 双向转换:既支持返回 JSON,也支持接收 JSON 请求体并自动反序列化为对象。

这种方式满足了绝大多数 RESTful API 开发需求,是 Spring Boot 构建 Web 应用的核心能力之一。

3.5、Spring Boot实现RESTful风格的Web应用

Spring Boot 非常适合构建 RESTful 风格的 Web 应用提供了天然支持,通过简洁的注解、自动化配置和内置功能,可快速实现符合 REST 规范的 API 设计。以下是详细实现方法:

一、RESTful 核心原则

RESTful 是一种软件设计风格,核心是通过 HTTP 方法(GET/POST/PUT/DELETE 等)和 URI 路径表示资源的操作,特点包括:

  • 资源导向 :URI 表示资源(如 /users 表示用户集合),而非动作(避免 /getUser)。
  • HTTP 方法语义 :用不同方法表达操作类型:
    • GET:查询资源(幂等,无副作用)
    • POST:创建资源
    • PUT:全量更新资源(幂等)
    • PATCH:部分更新资源
    • DELETE:删除资源(幂等)
  • 状态码表示结果:用 HTTP 状态码(200/201/400/404 等)表示请求结果。
  • 无状态:每次请求独立,服务器不存储客户端状态。

二、Spring Boot 实现 RESTful API 的核心组件

1. 依赖准备

引入 spring-boot-starter-web 即可,包含 RESTful 开发所需的所有组件:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. 控制器(Controller)设计

使用 @RestController 定义 REST 控制器(自动返回 JSON),结合 HTTP 方法注解(@GetMapping/@PostMapping 等)映射请求。

RESTful风格控制器示例:

java 复制代码
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

@RestController
@RequestMapping("/api/users") // 资源路径(用户集合)
public class UserController {

    // 模拟数据库(线程安全)
    private final ConcurrentMap<Long, User> userMap = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    // 1. 查询所有用户(GET /api/users)
    @GetMapping
    public List<User> getAllUsers() {
        return new ArrayList<>(userMap.values());
    }

    // 2. 查询单个用户(GET /api/users/{id})
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userMap.get(id);
        if (user == null) {
            // 资源不存在返回404
            return ResponseEntity.notFound().build();
        }
        // 成功返回200 + 资源
        return ResponseEntity.ok(user);
    }

    // 3. 创建用户(POST /api/users)
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        // 生成ID
        Long id = idGenerator.incrementAndGet();
        user.setId(id);
        userMap.put(id, user);
        // 成功创建返回201 + 资源 +  Location头(新资源路径)
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .header("Location", "/api/users/" + id)
                .body(user);
    }

    // 4. 全量更新用户(PUT /api/users/{id})
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id,
            @RequestBody User updatedUser) {
        if (!userMap.containsKey(id)) {
            return ResponseEntity.notFound().build();
        }
        // 全量更新(覆盖原有字段)
        updatedUser.setId(id);
        userMap.put(id, updatedUser);
        return ResponseEntity.ok(updatedUser);
    }

    // 5. 部分更新用户(PATCH /api/users/{id})
    @PatchMapping("/{id}")
    public ResponseEntity<User> patchUser(
            @PathVariable Long id,
            @RequestBody User partialUser) {
        User existingUser = userMap.get(id);
        if (existingUser == null) {
            return ResponseEntity.notFound().build();
        }
        // 部分更新(仅更新非null字段)
        if (partialUser.getName() != null) {
            existingUser.setName(partialUser.getName());
        }
        if (partialUser.getAge() != null) {
            existingUser.setAge(partialUser.getAge());
        }
        userMap.put(id, existingUser);
        return ResponseEntity.ok(existingUser);
    }

    // 6. 删除用户(DELETE /api/users/{id})
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        if (!userMap.containsKey(id)) {
            return ResponseEntity.notFound().build();
        }
        userMap.remove(id);
        // 成功删除返回204(无内容)
        return ResponseEntity.noContent().build();
    }
}
3. 实体类(Resource)

定义资源模型,对应业务实体:

java 复制代码
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;

    // 无参构造器(反序列化需要)
    public User() {}

    // getter、setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    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; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}
4. 统一响应体(可选)

为了规范响应格式,可定义通用响应类(包含状态码、消息、数据):

RESTful统一响应体:

java 复制代码
import org.springframework.http.HttpStatus;

public class ApiResponse<T> {
    private int code;          // 业务状态码
    private String message;    // 提示消息
    private T data;            // 响应数据

    // 成功响应(带数据)
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = HttpStatus.OK.value();
        response.message = "success";
        response.data = data;
        return response;
    }

    // 成功响应(无数据)
    public static <T> ApiResponse<T> success() {
        return success(null);
    }

    // 错误响应
    public static <T> ApiResponse<T> error(int code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = code;
        response.message = message;
        return response;
    }

    // getter、setter
    public int getCode() { return code; }
    public void setCode(int code) { this.code = code; }
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    public T getData() { return data; }
    public void setData(T data) { this.data = data; }
}

修改控制器方法返回统一响应体(以查询为例):

java 复制代码
@GetMapping("/{id}")
public ApiResponse<User> getUserById(@PathVariable Long id) {
    User user = userMap.get(id);
    if (user == null) {
        return ApiResponse.error(404, "用户不存在");
    }
    return ApiResponse.success(user);
}

三、RESTful 高级特性实现

1. 分页与排序

查询集合时,通过 Pageable 实现分页和排序:

RESTful分页与排序:

java 复制代码
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/articles")
public class ArticleController {

    private final ArticleService articleService;

    // 构造器注入服务
    public ArticleController(ArticleService articleService) {
        this.articleService = articleService;
    }

    // 分页查询文章(默认第0页,每页10条,按创建时间降序)
    @GetMapping
    public Page<Article> getArticles(
            @PageableDefault(
                page = 0, 
                size = 10, 
                sort = "createTime", 
                direction = Sort.Direction.DESC
            ) Pageable pageable) {
        return articleService.findArticles(pageable);
    }
}

前端请求示例:

复制代码
GET /api/articles?page=1&size=20&sort=title,asc  // 第2页,每页20条,按标题升序
2. 异常处理

通过 @ControllerAdvice 统一处理 REST 接口异常,返回标准错误响应:

RESTful异常处理:

java 复制代码
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.NoHandlerFoundException;

@ControllerAdvice
public class GlobalExceptionHandler {

    // 资源未找到异常(404)
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleResourceNotFound(ResourceNotFoundException e) {
        ApiResponse<Void> error = ApiResponse.error(404, e.getMessage());
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    // 请求参数错误(400)
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException e) {
        ApiResponse<Void> error = ApiResponse.error(400, e.getMessage());
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    // 未匹配的路由(404)
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleNoHandlerFound() {
        ApiResponse<Void> error = ApiResponse.error(404, "接口不存在");
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    // 服务器内部错误(500)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception e) {
        ApiResponse<Void> error = ApiResponse.error(500, "服务器内部错误:" + e.getMessage());
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
3. 跨域支持

前后端分离场景下,通过配置允许跨域请求:

RESTful跨域配置:

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")          // 对/api/*路径允许跨域
                .allowedOrigins("http://localhost:3000")  // 允许的前端域名
                .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")  // 允许的HTTP方法
                .allowedHeaders("*")            // 允许的请求头
                .allowCredentials(true)         // 允许携带Cookie
                .maxAge(3600);                  // 预检请求缓存时间(秒)
    }
}

四、RESTful API 测试

可通过工具测试接口是否符合 REST 规范:

  • Postman:手动发送 HTTP 请求,验证响应状态码和数据。
  • Swagger/OpenAPI :自动生成 API 文档,支持在线测试(需引入 springdoc-openapi-ui 依赖)。
  • JUnit:编写单元测试验证接口逻辑:
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    // 测试创建用户
    @Test
    public void testCreateUser() throws Exception {
        String userJson = "{\"name\":\"测试用户\",\"age\":20,\"email\":\"test@example.com\"}";
        
        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(userJson))
                .andExpect(status().isCreated())  // 验证状态码201
                .andExpect(jsonPath("$.name").value("测试用户"))
                .andExpect(header().exists("Location"));  // 验证Location头
    }

    // 测试查询用户
    @Test
    public void testGetUser() throws Exception {
        mockMvc.perform(get("/api/users/1"))
                .andExpect(status().isOk())  // 验证状态码200
                .andExpect(jsonPath("$.id").value(1));
    }
}

五、最佳实践总结

  1. URI 设计

    • 用名词表示资源(复数形式,如 /users),避免动词(/getUsers)。
    • 子资源用嵌套路径(如 /users/1/orders 表示用户1的订单)。
    • 使用查询参数过滤资源(如 /users?role=admin)。
  2. 状态码使用

    • 200 OK:GET/PUT/PATCH 请求成功。
    • 201 Created:POST 请求创建资源成功。
    • 204 No Content:DELETE 请求成功。
    • 400 Bad Request:请求参数错误。
    • 404 Not Found:资源不存在。
    • 500 Internal Server Error:服务器内部错误。
  3. 版本控制

    • 在 URI 中包含版本(如 /api/v1/users),便于兼容旧版本。
  4. 安全性

    • 对敏感接口添加认证授权(如 Spring Security)。
    • 验证请求参数,防止注入攻击。

通过以上方式,Spring Boot 可快速构建规范、可扩展的 RESTful Web 应用,满足前后端分离、微服务等场景需求。

3.6、Spring Boot实现文件的上传和下载

Spring Boot 实现文件上传和下载功能非常便捷,通过内置的 Multipart 支持和资源处理能力,可以可快速开发相关功能。以下是详细的开发流程:

一、准备工作:配置文件上传参数

首先在 application.yml 中配置文件上传的相关参数(如大小限制):

yaml 复制代码
spring:
  servlet:
    multipart:
      max-file-size: 10MB  # 单个文件最大大小
      max-request-size: 100MB  # 单次请求最大文件总大小
  • max-file-size:限制单个上传文件的大小(默认 1MB)。
  • max-request-size:限制单次请求中所有文件的总大小(默认 10MB)。

二、实现文件上传功能

文件上传需要处理 multipart/form-data 类型的请求,通过 MultipartFile 接收上传的文件。

1. 编写文件上传控制器
java 复制代码
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;

@RestController
public class FileController {

    // 上传文件保存路径(实际项目中建议配置在application.yml中)
    private static final String UPLOAD_DIR = "uploads/";

    // 单文件上传
    @PostMapping("/upload/single")
    public ResponseEntity<String> uploadSingleFile(
            @RequestParam("file") MultipartFile file) {

        // 检查文件是否为空
        if (file.isEmpty()) {
            return new ResponseEntity<>("请选择文件", HttpStatus.BAD_REQUEST);
        }

        try {
            // 确保上传目录存在
            File uploadDir = new File(UPLOAD_DIR);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }

            // 生成唯一文件名(避免重名覆盖)
            String originalFilename = file.getOriginalFilename();
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
            String uniqueFilename = UUID.randomUUID().toString() + extension;

            // 保存文件到本地磁盘
            Path filePath = Paths.get(UPLOAD_DIR + uniqueFilename);
            Files.write(filePath, file.getBytes());

            return new ResponseEntity<>("文件上传成功:" + uniqueFilename, HttpStatus.OK);
        } catch (IOException e) {
            return new ResponseEntity<>("文件上传失败:" + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    // 多文件上传
    @PostMapping("/upload/multiple")
    public ResponseEntity<String> uploadMultipleFiles(
            @RequestParam("files") MultipartFile[] files) {

        // 检查是否有文件
        if (files == null || files.length == 0) {
            return new ResponseEntity<>("请选择文件", HttpStatus.BAD_REQUEST);
        }

        StringBuilder result = new StringBuilder();
        for (MultipartFile file : files) {
            if (file.isEmpty()) {
                result.append("跳过空文件;");
                continue;
            }

            try {
                // 生成唯一文件名
                String originalFilename = file.getOriginalFilename();
                String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
                String uniqueFilename = UUID.randomUUID().toString() + extension;

                // 保存文件
                Path filePath = Paths.get(UPLOAD_DIR + uniqueFilename);
                Files.write(filePath, file.getBytes());
                result.append("文件上传成功:").append(uniqueFilename).append(";");
            } catch (IOException e) {
                result.append("文件上传失败(").append(file.getOriginalFilename()).append("):").append(e.getMessage()).append(";");
            }
        }

        return new ResponseEntity<>(result.toString(), HttpStatus.OK);
    }
}
    
2. 核心代码说明
  • @RequestParam("file") MultipartFile file :接收前端上传的文件,"file" 需与前端表单的 name 属性一致。
  • 文件重命名 :使用 UUID 生成唯一文件名,避免因文件名重复导致文件被覆盖。
  • 目录创建 :通过 mkdirs() 确保上传目录存在,防止保存文件时出现路径不存在的错误。
  • 多文件处理 :通过 MultipartFile[] 接收多个文件,循环处理每个文件的上传逻辑。
3. 前端上传页面(可选)

如果需要网页上传界面,可创建一个简单的 HTML 页面(放在 src/main/resources/static 目录下):

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>文件上传</title>
</head>
<body>
    <h3>单文件上传</h3>
    <form action="/upload/single" method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept="*/*">
        <button type="submit">上传</button>
    </form>

    <h3>多文件上传</h3>
    <form action="/upload/multiple" method="post" enctype="multipart/form-data">
        <input type="file" name="files" multiple accept="*/*">
        <button type="submit">上传</button>
    </form>
</body>
</html>
  • 表单必须设置 enctype="multipart/form-data" 才能支持文件上传。
  • multiple 属性允许选择多个文件。

三、实现文件下载功能

文件下载需要读取服务器上的文件,通过 ResponseEntity<Resource>HttpServletResponse 输出文件流。

1. 编写文件下载控制器
java 复制代码
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

// 继续扩展上面的FileController类
public class FileController {

    // 上传文件保存路径(与上传功能保持一致)
    private static final String UPLOAD_DIR = "uploads/";

    // 文件下载
    @GetMapping("/download/{filename:.+}")  // .+ 确保能匹配带扩展名的文件名
    public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
        // 构建文件路径
        Path filePath = Paths.get(UPLOAD_DIR + filename);
        Resource resource = new FileSystemResource(filePath);

        // 检查文件是否存在
        if (!resource.exists()) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }

        try {
            // 构建响应头(指定文件名和下载方式)
            String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.name())
                    .replaceAll("\\+", "%20");  // 处理空格编码问题
            HttpHeaders headers = new HttpHeaders();
            headers.add(HttpHeaders.CONTENT_DISPOSITION, 
                    "attachment; filename*=UTF-8''" + encodedFilename);  // 支持中文文件名

            return new ResponseEntity<>(resource, headers, HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}
    
2. 核心代码说明
  • @GetMapping("/download/{filename:.+}:.+ 用于匹配带扩展名的文件名(如 test.jpg)。
  • FileSystemResource:用于访问本地文件系统中的资源。
  • 响应头设置
    • Content-Disposition: attachment 告诉浏览器以附件形式下载文件(而非直接打开)。
    • 通过 URLEncoder 编码文件名,解决中文文件名乱码问题。
3. 前端下载链接(可选)

在页面中添加下载链接,指向下载接口:

html 复制代码
<!-- 在上传页面中添加 -->
<h3>文件下载</h3>
<!-- 假设已知文件名,实际项目中通常从数据库获取文件名列表 -->
<a href="/download/xxx-xxx-xxx.jpg">下载图片</a><br>
<a href="/download/xxx-xxx-xxx.pdf">下载文档</a>

四、优化与最佳实践

  1. 文件存储路径配置

    建议将上传路径配置在 application.yml 中,而非硬编码:

    yaml 复制代码
    app:
      upload:
        dir: /var/www/uploads/  # 生产环境建议使用绝对路径

    在代码中通过 @Value 注入:

    java 复制代码
    @Value("${app.upload.dir}")
    private String uploadDir;
  2. 文件类型限制

    上传时校验文件类型,防止恶意文件上传:

    java 复制代码
    // 允许的文件类型
    private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "pdf");
    
    // 校验文件类型
    private boolean isAllowed(String filename) {
        String extension = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
        return ALLOWED_EXTENSIONS.contains(extension);
    }
  3. 使用云存储

    生产环境中,建议将文件存储到云存储服务(如 AWS S3、阿里云 OSS),而非本地磁盘,避免服务器存储压力和文件丢失风险。

  4. 数据库记录文件信息

    上传文件后,将文件名、原文件名、大小、上传时间等信息存入数据库,便于管理和查询文件列表。

  5. 大文件分片上传

    对于超大文件(如超过 100MB),可实现分片上传功能,将文件分割成小块依次上传,最后合并。

五、测试流程

  1. 启动应用 :访问 http://localhost:8080 打开上传页面。
  2. 上传文件:选择文件并提交,查看上传结果。
  3. 下载文件 :通过返回的文件名或数据库记录的文件名,访问 /download/文件名 下载文件。

通过以上流程,Spring Boot 可快速实现文件上传和下载功能,结合实际需求进行适当优化后,即可应用于生产环境。

3.7、Spring Boot异常统一处理方式

在 Spring Boot 应用中,异常统一处理是保证接口健壮性和用户体验的关键机制。通过集中处理各类异常,可以避免代码中充斥大量重复的 try-catch 块,同时返回格式统一的错误响应。以下是 Spring Boot 实现异常统一处理的常用方式:

一、核心组件:@ControllerAdvice@ExceptionHandler

Spring Boot 提供 @ControllerAdvice 注解(控制器增强),配合 @ExceptionHandler 注解(异常处理器),可实现全局异常捕获与处理。

  • @ControllerAdvice :标识一个类为全局异常处理器,会拦截所有控制器(@Controller/@RestController)抛出的异常。
  • @ExceptionHandler:定义方法处理特定类型的异常,通过参数指定异常类型。

二、基础实现:统一异常处理类

1. 定义统一响应体

首先创建通用的错误响应类,规范异常返回格式:

java 复制代码
import java.time.LocalDateTime;

public class ErrorResponse {
    private int code; // 错误状态码(建议与HTTP状态码一致或自定义业务码)
    private String message; // 错误提示信息
    private String details; // 错误详情(可选,如堆栈信息简化版)
    private LocalDateTime timestamp; // 错误发生时间

    // 构造方法
    public ErrorResponse(int code, String message, String details) {
        this.code = code;
        this.message = message;
        this.details = details;
        this.timestamp = LocalDateTime.now();
    }

    // getter和setter
    public int getCode() { return code; }
    public void setCode(int code) { this.code = code; }
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    public String getDetails() { return details; }
    public void setDetails(String details) { this.details = details; }
    public LocalDateTime getTimestamp() { return timestamp; }
}
2. 创建全局异常处理器

通过 @ControllerAdvice 定义全局异常处理类,针对不同异常类型编写处理逻辑:

java 复制代码
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.NoHandlerFoundException;

@ControllerAdvice // 标识为全局控制器增强
public class GlobalExceptionHandler {

    // 1. 处理自定义业务异常(最常用)
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(
            BusinessException ex, WebRequest request) {
        ErrorResponse error = new ErrorResponse(
                ex.getCode(), // 自定义业务状态码
                ex.getMessage(), // 异常消息
                request.getDescription(false) // 请求详情(如URI)
        );
        // 返回自定义状态码和错误响应
        return new ResponseEntity<>(error, HttpStatus.valueOf(ex.getCode()));
    }

    // 2. 处理参数校验异常(如@Valid注解触发)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationExceptions(
            MethodArgumentNotValidException ex, WebRequest request) {
        // 提取校验失败的字段和消息
        String errorMessage = ex.getBindingResult().getFieldError().getDefaultMessage();
        ErrorResponse error = new ErrorResponse(
                HttpStatus.BAD_REQUEST.value(), // 400
                "参数校验失败:" + errorMessage,
                request.getDescription(false)
        );
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    // 3. 处理资源未找到异常(如404)
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<ErrorResponse> handleNoHandlerFoundException(
            NoHandlerFoundException ex, WebRequest request) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(), // 404
                "请求的资源不存在:" + ex.getRequestURL(),
                request.getDescription(false)
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    // 4. 处理所有未捕获的异常(兜底处理)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(
            Exception ex, WebRequest request) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(), // 500
                "服务器内部错误:" + ex.getMessage(),
                // 生产环境建议隐藏详细堆栈,开发环境可开启
                request.getDescription(false)
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
3. 自定义业务异常(可选但推荐)

实际开发中,建议定义业务相关的异常类,便于区分不同业务场景的错误:

java 复制代码
public class BusinessException extends RuntimeException {
    private int code; // 业务状态码(如400/403/500等)

    // 构造方法
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    // 快捷构造方法(如通用的400错误)
    public static BusinessException badRequest(String message) {
        return new BusinessException(400, message);
    }

    public static BusinessException forbidden(String message) {
        return new BusinessException(403, message);
    }

    // getter
    public int getCode() {
        return code;
    }
}

三、使用场景与示例

1. 在业务代码中抛出异常

在 Service 或 Controller 中主动抛出异常,由全局处理器统一捕获:

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public User getUserById(Long id) {
        // 模拟查询用户,若不存在则抛出异常
        if (id == null || id <= 0) {
            throw BusinessException.badRequest("用户ID必须为正数");
        }
        
        User user = userRepository.findById(id).orElse(null);
        if (user == null) {
            throw new BusinessException(404, "用户不存在:" + id);
        }
        return user;
    }
}
2. 触发参数校验异常

使用 @Valid 注解校验请求参数,不符合规则时会抛出 MethodArgumentNotValidException

java 复制代码
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;

@RestController
public class UserController {

    @PostMapping("/users")
    public User createUser(@Valid @RequestBody User user) {
        // @Valid 会触发参数校验,失败则抛出MethodArgumentNotValidException
        return userService.save(user);
    }
}

// User实体类(含校验注解)
class User {
    private Long id;
    
    @NotBlank(message = "用户名不能为空")
    private String name;
    
    @Min(value = 18, message = "年龄不能小于18岁")
    private Integer age;
    
    // getter/setter
}
3. 404异常处理

默认情况下,Spring Boot 会返回内置的 404 页面。若需自定义 404 响应,需配置 throwExceptionIfNoHandlerFound=true

yaml 复制代码
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false # 禁用默认静态资源映射(避免掩盖404)

此时访问不存在的接口会抛出 NoHandlerFoundException,被全局处理器捕获并返回统一格式的 404 响应。

四、高级特性与最佳实践

1. 区分环境显示错误详情

开发环境可显示详细堆栈信息,生产环境隐藏敏感信息,通过 @Profile 实现:

java 复制代码
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex, WebRequest request) {
    String details;
    // 根据环境决定是否显示堆栈信息
    if (environment.acceptsProfiles(Profiles.of("dev"))) {
        details = Arrays.toString(ex.getStackTrace()); // 开发环境显示堆栈
    } else {
        details = request.getDescription(false); // 生产环境仅显示请求信息
    }
    
    ErrorResponse error = new ErrorResponse(500, "服务器内部错误", details);
    return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
2. 异常日志记录

在异常处理方法中添加日志记录,便于问题排查:

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
        logger.warn("业务异常:{}", ex.getMessage()); // 警告级别日志
        // ... 构建响应
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
        logger.error("未捕获异常", ex); // 错误级别日志,记录堆栈
        // ... 构建响应
    }
}
3. 结合 AOP 实现更细粒度的异常处理

对于需要在异常处理前后执行额外逻辑(如统计异常次数、发送告警)的场景,可结合 AOP 实现:

java 复制代码
@Aspect
@Component
public class ExceptionAspect {
    @AfterThrowing(pointcut = "within(@org.springframework.web.bind.annotation.RestController *)", 
                   throwing = "ex")
    public void afterThrowing(Exception ex) {
        // 异常发生后执行:如发送邮件告警、记录 metrics
        System.out.println("捕获到异常:" + ex.getMessage());
    }
}

Spring Boot 异常统一处理的核心是通过 @ControllerAdvice + @ExceptionHandler 实现全局拦截,优势包括:

  • 代码简洁 :避免在控制器中重复编写 try-catch
  • 响应统一:所有异常返回格式一致的 JSON,便于前端解析。
  • 可扩展性:轻松添加新的异常类型处理逻辑。

实际开发中,建议结合自定义业务异常、参数校验和日志记录,构建完整的异常处理体系,提升应用的可靠性和可维护性。

4.1、Spring Boot整合JDBC

Spring Boot 对 JDBC 的整合进行了简化,通过 spring-boot-starter-jdbc starter 实现自动配置,无需手动编写繁琐的数据库连接代码。以下是详细的整合流程和使用方式:

一、准备工作:引入依赖

pom.xml 中添加 JDBC starter 和数据库驱动(以 MySQL 为例):

xml 复制代码
<!-- JDBC 自动配置 starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- MySQL 驱动(根据数据库类型选择) -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
  • spring-boot-starter-jdbc 包含:spring-jdbc 核心库、数据源自动配置、事务管理等。
  • 数据库驱动需根据实际使用的数据库(MySQL、PostgreSQL 等)选择。

二、配置数据库连接信息

application.yml 中配置数据库连接参数,Spring Boot 会自动根据配置创建数据源:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver  # MySQL 8.x 驱动类
    hikari:  # HikariCP 连接池配置(默认使用 HikariCP)
      maximum-pool-size: 10  # 最大连接数
      minimum-idle: 5        # 最小空闲连接数
      idle-timeout: 300000   # 连接空闲超时时间(毫秒)
      connection-timeout: 20000  # 连接超时时间(毫秒)
  • 连接池:Spring Boot 2.x 后默认使用 HikariCP(性能最优),无需额外配置即可使用。
  • 时区配置serverTimezone=UTC 解决时区偏移问题(MySQL 8.x 必需)。

三、核心组件与使用方式

Spring Boot 整合 JDBC 后,主要通过 JdbcTemplate 操作数据库,它封装了 JDBC 的繁琐操作(如获取连接、释放资源等)。

1. JdbcTemplate 注入与基本使用

JdbcTemplate 已由 Spring 自动配置,可直接在 Service 或 Controller 中注入使用:JdbcTemplate基本使用:UserService.java

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

@Service
public class UserService {

    // 注入JdbcTemplate(Spring自动配置)
    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 1. 查询单个用户
    public User getUserById(Long id) {
        String sql = "SELECT id, name, age, email FROM user WHERE id = ?";
        // RowMapper:将ResultSet映射为User对象
        return jdbcTemplate.queryForObject(sql, new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                User user = new User();
                user.setId(rs.getLong("id"));
                user.setName(rs.getString("name"));
                user.setAge(rs.getInt("age"));
                user.setEmail(rs.getString("email"));
                return user;
            }
        }, id); // 最后一个参数是SQL中的?占位符值
    }

    // 2. 查询所有用户
    public List<User> getAllUsers() {
        String sql = "SELECT id, name, age, email FROM user";
        return jdbcTemplate.query(sql, (rs, rowNum) -> {
            // 使用Lambda简化RowMapper
            User user = new User();
            user.setId(rs.getLong("id"));
            user.setName(rs.getString("name"));
            user.setAge(rs.getInt("age"));
            user.setEmail(rs.getString("email"));
            return user;
        });
    }

    // 3. 新增用户
    public int addUser(User user) {
        String sql = "INSERT INTO user (name, age, email) VALUES (?, ?, ?)";
        // 返回影响行数
        return jdbcTemplate.update(sql, 
                user.getName(), 
                user.getAge(), 
                user.getEmail());
    }

    // 4. 更新用户
    public int updateUser(User user) {
        String sql = "UPDATE user SET name = ?, age = ?, email = ? WHERE id = ?";
        return jdbcTemplate.update(sql, 
                user.getName(), 
                user.getAge(), 
                user.getEmail(), 
                user.getId());
    }

    // 5. 删除用户
    public int deleteUser(Long id) {
        String sql = "DELETE FROM user WHERE id = ?";
        return jdbcTemplate.update(sql, id);
    }

    // 6. 统计用户总数
    public Integer countUsers() {
        String sql = "SELECT COUNT(*) FROM user";
        return jdbcTemplate.queryForObject(sql, Integer.class);
    }
}
2. 核心方法说明

JdbcTemplate 提供了丰富的方法简化数据库操作:

  • 查询方法
    • queryForObject(sql, rowMapper, params):查询单个对象。
    • query(sql, rowMapper, params):查询集合。
    • queryForObject(sql, Class<T>, params):查询单个值(如数量、总和)。
  • 增删改方法
    • update(sql, params):执行 INSERT/UPDATE/DELETE,返回影响行数。
  • 批量操作
    • batchUpdate(sql, batchArgs):批量执行 SQL(如批量插入)。
3. 批量操作示例

JdbcTemplate批量操作:UserService.java

java 复制代码
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

// 在UserService中添加批量插入方法
public int batchAddUsers(List<User> users) {
    String sql = "INSERT INTO user (name, age, email) VALUES (?, ?, ?)";
    
    // 批量设置参数
    return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            User user = users.get(i);
            ps.setString(1, user.getName());
            ps.setInt(2, user.getAge());
            ps.setString(3, user.getEmail());
        }
        
        @Override
        public int getBatchSize() {
            return users.size(); // 批量大小
        }
    }).length; // 返回成功执行的数量
}

四、事务管理

Spring Boot 整合 JDBC 时,可通过 @Transactional 注解轻松实现事务管理:

JDBC事物管理:OrderService.java

java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 声明事务:发生异常时自动回滚
    @Transactional
    public void createOrder(Order order, List<OrderItem> items) {
        // 1. 插入订单
        String orderSql = "INSERT INTO order (order_no, user_id, total) VALUES (?, ?, ?)";
        jdbcTemplate.update(orderSql, order.getOrderNo(), order.getUserId(), order.getTotal());

        // 2. 插入订单项(若此处抛出异常,订单插入会自动回滚)
        String itemSql = "INSERT INTO order_item (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)";
        for (OrderItem item : items) {
            jdbcTemplate.update(itemSql, order.getId(), item.getProductId(), item.getQuantity(), item.getPrice());
        }
    }
}
  • @Transactional 注解添加在方法上,当方法内抛出未捕获的异常时,所有数据库操作会自动回滚。
  • 可通过 rollbackFor 指定需要回滚的异常类型(默认只回滚 RuntimeException)。

五、初始化数据库(可选)

Spring Boot 支持通过 SQL 脚本自动初始化数据库(建表、插入初始数据):

1. 创建初始化脚本

src/main/resources 目录下创建:

  • schema.sql:数据库表结构脚本(建表语句)。
  • data.sql:初始数据脚本(插入语句)。

示例 schema.sql

sql 复制代码
CREATE TABLE IF NOT EXISTS user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    age INT,
    email VARCHAR(100),
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

示例 data.sql

sql 复制代码
INSERT INTO user (name, age, email) VALUES 
('张三', 25, 'zhangsan@example.com'),
('李四', 30, 'lisi@example.com');
2. 配置初始化参数

application.yml 中配置脚本执行规则:

yaml 复制代码
spring:
  sql:
    init:
      mode: always  # 始终执行初始化脚本(可选:always/embedded/never)
      schema-locations: classpath:schema.sql  # 表结构脚本路径
      data-locations: classpath:data.sql      # 数据脚本路径
      encoding: utf-8  # 脚本编码

六、高级配置:自定义数据源

默认使用 HikariCP 数据源,若需切换为其他数据源(如 Druid),需添加对应依赖并配置:

1. 引入 Druid 依赖
xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
2. 配置 Druid 数据源
yaml 复制代码
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: 123456
    druid:
      initial-size: 5  # 初始连接数
      max-active: 20   # 最大连接数
      min-idle: 5      # 最小空闲连接数
      # 其他配置(如监控、过滤等)

Spring Boot 整合 JDBC 的核心优势:

  1. 自动配置 :引入 starter 后,无需手动创建 DataSourceJdbcTemplate 等组件。
  2. 简化操作JdbcTemplate 封装了 JDBC 繁琐的样板代码,专注于 SQL 逻辑。
  3. 事务支持 :通过 @Transactional 轻松实现事务管理。
  4. 灵活扩展:支持多种数据源(HikariCP、Druid 等)和数据库类型。

适合场景:简单的数据库操作、需要直接控制 SQL 的场景。若项目复杂度较高,建议进一步整合 MyBatis 或 JPA 框架。

5.1、Spring Boot整合MyBatis

Spring Boot 整合 MyBatis 可以充分利用 MyBatis 的 SQL 灵活性和 Spring Boot 的自动配置能力,简化数据访问层开发。以下是详细的整合流程和使用方式:

一、准备工作:引入依赖

pom.xml 中添加 MyBatis Starter 和数据库驱动(以 MySQL 为例):

xml 复制代码
<!-- MyBatis 整合 Spring Boot 的 Starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.1</version> <!-- 版本需与 Spring Boot 兼容 -->
</dependency>

<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
  • mybatis-spring-boot-starter 包含:MyBatis 核心、Spring 整合支持、自动配置等。
  • 版本说明:2.3.x 版本适用于 Spring Boot 2.7+,3.x 版本适用于 Spring Boot 3.0+。

二、配置数据库和 MyBatis

application.yml 中配置数据库连接信息和 MyBatis 相关参数:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  # Mapper XML 文件路径(若使用注解方式可省略)
  mapper-locations: classpath:mybatis/mappers/*.xml
  # 实体类包路径(别名配置,可选)
  type-aliases-package: com.example.entity
  configuration:
    # 开启驼峰命名自动映射(如数据库字段 user_name 映射到 Java 属性 userName)
    map-underscore-to-camel-case: true
    # 日志输出 SQL 语句(开发环境使用)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

三、核心组件与实现方式

Spring Boot 整合 MyBatis 有两种主流方式:注解方式 (适合简单 SQL)和 XML 映射方式(适合复杂 SQL)。

1. 注解方式(推荐简单场景)

通过 @Mapper 注解标识接口,并使用 MyBatis 注解编写 SQL。

步骤 1:定义实体类
java 复制代码
package com.example.entity;

public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;

    // 构造方法、getter、setter
    public User() {}
    
    // getter 和 setter 省略...
}
步骤 2:创建 Mapper 接口(使用注解)
java 复制代码
package com.example.mapper;

import com.example.entity.User;
import org.apache.ibatis.annotations.*;
import java.util.List;

// @Mapper 标识这是MyBatis的Mapper接口(无需手动配置,Spring会自动扫描)
@Mapper
public interface UserMapper {

    // 1. 根据ID查询
    @Select("SELECT id, name, age, email FROM user WHERE id = #{id}")
    User selectById(Long id);

    // 2. 查询所有
    @Select("SELECT id, name, age, email FROM user")
    List<User> selectAll();

    // 3. 新增用户
    @Insert("INSERT INTO user (name, age, email) VALUES (#{name}, #{age}, #{email})")
    // 回填自增ID到实体类(若主键是自增)
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    // 4. 更新用户
    @Update("UPDATE user SET name = #{name}, age = #{age}, email = #{email} WHERE id = #{id}")
    int update(User user);

    // 5. 删除用户
    @Delete("DELETE FROM user WHERE id = #{id}")
    int delete(Long id);
} 
步骤 3:创建 Service 层调用 Mapper
java 复制代码
package com.example.service;

import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {

    // 注入Mapper接口(Spring自动生成实现类)
    @Autowired
    private UserMapper userMapper;

    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }

    public List<User> getAllUsers() {
        return userMapper.selectAll();
    }

    public int addUser(User user) {
        return userMapper.insert(user);
    }

    public int updateUser(User user) {
        return userMapper.update(user);
    }

    public int deleteUser(Long id) {
        return userMapper.delete(id);
    }
}
2. XML 映射方式(推荐复杂场景)

将 SQL 写在 XML 文件中,Mapper 接口仅定义方法签名,适合 SQL 逻辑复杂或需要动态 SQL 的场景。

步骤 1:创建 Mapper 接口(无注解)
java 复制代码
package com.example.mapper;

import com.example.entity.User;
import java.util.List;
import java.util.Map;

@Mapper
public interface UserXmlMapper {
    // 方法签名需与XML中的id一致
    User selectById(Long id);
    List<User> selectByCondition(Map<String, Object> condition); // 动态条件查询
    int insert(User user);
}
步骤 2:创建 Mapper XML 文件

src/main/resources/mybatis/mappers 目录下创建 UserXmlMapper.xml(路径需与 application.ymlmapper-locations 配置一致):

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace需与Mapper接口全类名一致 -->
<mapper namespace="com.example.mapper.UserXmlMapper">

    <!-- 定义结果集映射(可选,若开启驼峰命名可省略) -->
    <resultMap id="BaseResultMap" type="com.example.entity.User">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="age" property="age"/>
        <result column="email" property="email"/>
    </resultMap>

    <!-- 根据ID查询(id需与接口方法名一致) -->
    <select id="selectById" parameterType="java.lang.Long" resultMap="BaseResultMap">
        SELECT id, name, age, email FROM user WHERE id = #{id}
    </select>

    <!-- 动态条件查询(使用if标签) -->
    <select id="selectByCondition" parameterType="map" resultMap="BaseResultMap">
        SELECT id, name, age, email FROM user
        <where>
            <if test="name != null and name != ''">
                AND name LIKE CONCAT('%', #{name}, '%')
            </if>
            <if test="age != null">
                AND age = #{age}
            </if>
        </where>
    </select>

    <!-- 新增用户 -->
    <insert id="insert" parameterType="com.example.entity.User"
            useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user (name, age, email) 
        VALUES (#{name}, #{age}, #{email})
    </insert>

</mapper>

四、扫描 Mapper 接口的两种方式

  1. 在每个 Mapper 接口上添加 @Mapper 注解(如上述示例),适合 Mapper 数量较少的情况。

  2. 在启动类上添加 @MapperScan 注解(批量扫描),适合 Mapper 数量较多的情况:批量扫描Mapper接口

java 复制代码
package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// 扫描指定包下的所有Mapper接口(无需在每个接口加@Mapper)
@MapperScan("com.example.mapper") 
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

五、事务管理

与 JDBC 一样,MyBatis 整合 Spring Boot 时可通过 @Transactional 注解实现事务管理:

java 复制代码
package com.example.service;

import com.example.entity.Order;
import com.example.entity.OrderItem;
import com.example.mapper.OrderItemMapper;
import com.example.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;

    // 声明事务:发生异常时自动回滚
    @Transactional
    public void createOrder(Order order, List<OrderItem> items) {
        // 插入订单
        orderMapper.insert(order);
        // 插入订单项(若抛出异常,订单插入会回滚)
        for (OrderItem item : items) {
            item.setOrderId(order.getId());
            orderItemMapper.insert(item);
        }
    }
}

六、高级特性:分页查询

MyBatis 本身不支持分页,需整合 pagehelper-spring-boot-starter 实现分页功能:

1. 引入分页插件依赖
xml 复制代码
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
</dependency>
2. 使用分页查询
java 复制代码
package com.example.service;

import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    // 分页查询用户(页码从1开始)
    public PageInfo<User> getUsersByPage(int pageNum, int pageSize) {
        // 在查询前调用PageHelper.startPage,自动拦截SQL添加分页
        PageHelper.startPage(pageNum, pageSize);
        List<User> users = userMapper.selectAll();
        // 包装查询结果为PageInfo,包含分页信息(总条数、总页数等)
        return new PageInfo<>(users);
    }
}

七、整合优势与最佳实践

  1. 零 XML 配置:MyBatis 的核心配置(如数据源、映射器)由 Spring Boot 自动完成。
  2. 两种 SQL 编写方式:简单 SQL 用注解,复杂 SQL 用 XML,灵活适配不同场景。
  3. 动态 SQL 支持 :通过 XML 中的 <if><where><foreach> 等标签编写动态 SQL,无需拼接字符串。
  4. 最佳实践
    • 保持 Mapper 接口与 XML 文件的目录结构一致(便于维护)。
    • 开发环境开启 SQL 日志(log-impl: StdOutImpl),方便调试。
    • 复杂业务逻辑使用事务管理,确保数据一致性。

Spring Boot 整合 MyBatis 简化了传统 MyBatis 的繁琐配置,通过自动扫描 Mapper 接口、内置数据源等特性,让开发者专注于 SQL 编写和业务逻辑。无论是简单的 CRUD 操作还是复杂的动态 SQL 场景,这种整合方式都能提供高效、灵活的解决方案,是企业级应用开发的常用选择。

6.1、Spring Boot整合JPA

Spring Boot 整合 JPA(Java Persistence API)可以极大简化数据访问层开发,通过面向对象的方式操作数据库,无需编写大量 SQL 语句。以下是详细的整合流程和使用方式:

一、核心概念

JPA 是 Java 持久化规范,Hibernate 是其最流行的实现。Spring Boot 通过 spring-boot-starter-data-jpa 实现自动配置,底层默认使用 Hibernate 作为 JPA 实现。

二、准备工作:引入依赖

pom.xml 中添加 JPA Starter 和数据库驱动(以 MySQL 为例):

xml 复制代码
<!-- Spring Data JPA Starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

三、配置数据库和 JPA 参数

application.yml 中配置数据库连接和 JPA 相关属性:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    # 数据库方言(根据使用的数据库选择)
    database-platform: org.hibernate.dialect.MySQL8Dialect
    # 是否自动生成数据库表结构
    hibernate:
      ddl-auto: update  # 可选值:create(每次重建)/create-drop(退出时删除)/update(更新)/validate(校验)/none(不操作)
    # 显示 SQL 语句(开发环境)
    show-sql: true
    # 格式化 SQL 输出
    properties:
      hibernate:
        format_sql: true
    # 指定实体类包扫描(可选,默认扫描所有)
    packages-to-scan: com.example.entity
  • ddl-auto: update:最常用配置,会根据实体类自动更新表结构(不会删除已有数据)。
  • show-sql: true:开发时开启,便于调试生成的 SQL 语句。

四、核心实现步骤

1. 定义实体类(Entity)

使用 JPA 注解映射实体类与数据库表:

java 复制代码
package com.example.entity;

import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;

@Data  // Lombok注解,自动生成getter/setter
@Entity  // 标识为JPA实体
@Table(name = "t_user")  // 指定映射的表名(默认类名小写)
public class User {

    @Id  // 主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // 自增策略(依赖数据库)
    private Long id;

    @Column(name = "username", length = 50, nullable = false)  // 映射表字段
    private String name;

    @Column(nullable = false)
    private Integer age;

    @Column(unique = true)  // 唯一约束
    private String email;

    @Column(name = "create_time")
    private LocalDateTime createTime;

    // 无参构造器(JPA必需)
    public User() {}

    // 带参构造器(可选)
    public User(String name, Integer age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
        this.createTime = LocalDateTime.now();
    }
}

常用注解说明:

  • @Entity:声明类为实体类。
  • @Table:指定数据库表名,不指定则默认使用类名小写。
  • @Id:标识主键字段。
  • @GeneratedValue:指定主键生成策略(IDENTITY 对应自增主键)。
  • @Column:配置字段属性(名称、长度、是否可为空、唯一约束等)。
2. 创建 Repository 接口

通过继承 JpaRepository 接口,无需实现即可获得基本 CRUD 方法:

java 复制代码
package com.example.repository;

import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;

// 泛型参数:<实体类, 主键类型>
public interface UserRepository extends JpaRepository<User, Long> {

    // 1. 方法名查询(无需写SQL,根据方法名自动生成)
    // 根据姓名查询(模糊匹配)
    List<User> findByNameLike(String name);
    
    // 根据年龄范围查询
    List<User> findByAgeBetween(Integer minAge, Integer maxAge);
    
    // 根据邮箱查询(返回Optional,避免空指针)
    Optional<User> findByEmail(String email);

    // 2. JPQL查询(类似SQL,操作实体类而非表)
    @Query("SELECT u FROM User u WHERE u.age > :age ORDER BY u.name ASC")
    List<User> findUsersOlderThan(@Param("age") Integer age);

    // 3. 原生SQL查询(直接操作数据库表)
    @Query(value = "SELECT * FROM t_user WHERE email LIKE %:domain%", nativeQuery = true)
    List<User> findByEmailDomain(@Param("domain") String domain);
}

Repository 接口特性:

  • 继承 JpaRepository 后,自动拥有 save()findById()findAll()deleteById() 等基础方法。
  • 方法名查询 :遵循特定命名规则(如 findByXxxfindByXxxLike),JPA 自动生成 SQL。
  • 自定义查询 :通过 @Query 注解编写 JPQL 或原生 SQL,@Param 绑定参数。
3. Service 层调用 Repository
java 复制代码
package com.example.service;

import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 查询所有用户
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    // 根据ID查询(返回Optional)
    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }

    // 新增/更新用户(save()方法兼具两种功能)
    public User saveUser(User user) {
        return userRepository.save(user);
    }

    // 删除用户
    @Transactional  // 事务支持
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }

    // 自定义查询:查询年龄大于指定值的用户
    public List<User> getUsersOlderThan(Integer age) {
        return userRepository.findUsersOlderThan(age);
    }
}
4. Controller 层提供 API
java 复制代码
package com.example.controller;

import com.example.entity.User;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        Optional<User> user = userService.getUserById(id);
        return user.map(ResponseEntity::ok)
                   .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User createUser(@RequestBody User user) {
        return userService.saveUser(user);
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id, 
            @RequestBody User user) {
        return userService.getUserById(id)
                .map(existingUser -> {
                    user.setId(id); // 确保ID一致
                    return ResponseEntity.ok(userService.saveUser(user));
                })
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        if (userService.getUserById(id).isPresent()) {
            userService.deleteUser(id);
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

五、高级特性

1. 分页与排序

通过 Pageable 实现分页和排序,无需手动编写分页 SQL:

java 复制代码
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

// 在UserService中添加分页方法
public Page<User> getUsersByPage(int pageNum, int pageSize) {
    // 排序规则:按年龄降序,姓名升序
    Sort sort = Sort.by(
        Sort.Order.desc("age"),
        Sort.Order.asc("name")
    );
    // 分页参数(页码从0开始)
    Pageable pageable = PageRequest.of(pageNum - 1, pageSize, sort);
    return userRepository.findAll(pageable);
}

Controller 调用:

java 复制代码
@GetMapping("/page")
public Page<User> getUsersByPage(
        @RequestParam(defaultValue = "1") int pageNum,
        @RequestParam(defaultValue = "10") int pageSize) {
    return userService.getUsersByPage(pageNum, pageSize);
}
2. 事务管理

通过 @Transactional 注解实现事务控制,与 Spring 无缝集成:

java 复制代码
@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private OrderItemRepository itemRepository;

    // 声明事务:发生异常自动回滚
    @Transactional
    public void createOrder(Order order, List<OrderItem> items) {
        orderRepository.save(order);
        items.forEach(item -> {
            item.setOrderId(order.getId());
            itemRepository.save(item);
        });
    }
}
3. 复杂查询(Specification)

对于动态条件查询,可使用 Specification 构建查询条件:

java 复制代码
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

// 动态条件查询
public List<User> findUsersByConditions(Map<String, Object> params) {
    Specification<User> spec = (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        
        // 按姓名模糊查询
        if (params.containsKey("name")) {
            String name = (String) params.get("name");
            predicates.add(cb.like(root.get("name"), "%" + name + "%"));
        }
        
        // 按年龄范围查询
        if (params.containsKey("minAge") && params.containsKey("maxAge")) {
            Integer minAge = (Integer) params.get("minAge");
            Integer maxAge = (Integer) params.get("maxAge");
            predicates.add(cb.between(root.get("age"), minAge, maxAge));
        }
        
        return cb.and(predicates.toArray(new Predicate[0]));
    };
    
    return userRepository.findAll(spec);
}

六、整合优势与适用场景

  1. 优势

    • 完全面向对象,无需编写 SQL 即可完成大部分数据库操作。
    • 自动生成表结构(ddl-auto 配置),简化开发流程。
    • 内置分页、排序、事务等功能,无需手动实现。
    • 支持复杂查询(JPQL、原生 SQL、Specification)。
  2. 适用场景

    • 快速开发中小型项目,减少重复 SQL 编写。
    • 数据库表结构频繁变动的场景(ddl-auto: update 自动同步)。
    • 更关注业务逻辑而非 SQL 优化的场景。

Spring Boot 整合 JPA 是一种高效的 ORM 解决方案,通过注解映射实体与表、Repository 接口提供 CRUD 方法、内置分页排序等特性,极大简化了数据访问层开发。对于需要快速开发且 SQL 逻辑不复杂的项目,JPA 是理想选择;若需极致的 SQL 优化或复杂动态 SQL,可考虑 MyBatis。

7.1、Spring Boot的数据缓存Cache

Spring Boot 提供了强大的数据缓存支持,通过抽象层整合多种统一缓存操作,可轻松集成多种缓存实现(如 Caffeine、EhCache、Redis 等)。以下是详细的缓存使用流程和核心特性:

一、缓存核心概念

  • 缓存抽象 :Spring 定义了 CacheCacheManager 接口,屏蔽不同缓存产品的实现差异。
  • 注解驱动 :通过 @Cacheable@CachePut@CacheEvict 等注解实现缓存操作,无需手动编写缓存逻辑。
  • 自动配置 :Spring Boot 根据类路径下的缓存依赖自动配置缓存管理器(如引入 Redis 则默认配置 RedisCacheManager)。

二、快速集成:使用默认缓存(Caffeine)

1. 引入依赖

Spring Boot 2.x 后默认使用 Caffeine 作为缓存实现(轻量级、高性能):

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine 缓存依赖(spring-boot-starter-cache 已包含) -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
2. 启用缓存

在启动类添加 @EnableCaching 注解开启缓存功能:

java 复制代码
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching // 启用缓存功能
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
3. 配置缓存参数

application.yml 中配置 Caffeine 缓存策略:

yaml 复制代码
spring:
  cache:
    type: caffeine # 明确指定缓存类型(可选,自动检测)
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=60s # 最大缓存1000条,写入后60秒过期
    cache-names: users,products # 预定义缓存名称(可选)
  • maximumSize=1000:缓存最大条目数。
  • expireAfterWrite=60s:写入后 60 秒自动过期。
  • 其他策略:expireAfterAccess=5m(访问后 5 分钟过期)、initialCapacity=100(初始容量)等。

三、核心缓存注解使用

1. @Cacheable:查询缓存

方法执行前先检查缓存,若存在则返回缓存数据,否则执行方法并将结果存入缓存。

java 复制代码
package com.example.service;

import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 缓存key为"user::id",缓存名称为"users"
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        // 首次调用会执行查询,后续从缓存获取
        return userRepository.findById(id).orElse(null);
    }

    // 复杂key:组合参数作为key
    @Cacheable(value = "users", key = "#name + '-' + #age")
    public User getUserByNameAndAge(String name, Integer age) {
        return userRepository.findByNameAndAge(name, age);
    }
}

注解参数说明

  • value/cacheNames:缓存名称(必填,可理解为缓存分区)。
  • key:缓存键(支持 SpEL 表达式,默认是所有参数的组合)。
  • unless:条件表达式,为 true 时不缓存(如 unless = "#result == null" 表示不缓存 null 结果)。
  • condition:条件表达式,为 false 时不执行缓存逻辑。
2. @CachePut:更新缓存

执行方法后将结果存入缓存,常用于更新操作(确保缓存与数据库同步)。

java 复制代码
import org.springframework.cache.annotation.CachePut;

// 在UserService中添加
@CachePut(value = "users", key = "#user.id") // 缓存key与查询时保持一致
public User updateUser(User user) {
    User updated = userRepository.save(user);
    return updated; // 方法返回值会被存入缓存
}
3. @CacheEvict:删除缓存

移除缓存中的数据,常用于删除操作或数据更新后清理旧缓存。

java 复制代码
import org.springframework.cache.annotation.CacheEvict;

// 在UserService中添加
// 1. 删除指定key的缓存
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
    userRepository.deleteById(id);
}

// 2. 清空整个"users"缓存(如批量删除场景)
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() {
    userRepository.deleteAll();
}

注解参数说明

  • allEntries = true:清空该缓存名称下的所有条目(默认 false)。
  • beforeInvocation = true:在方法执行前删除缓存(默认 false,方法执行后删除)。
4. @Caching:组合缓存操作

当一个方法需要同时执行多种缓存操作(如既更新又删除)时使用。

java 复制代码
import org.springframework.cache.annotation.Caching;

// 在UserService中添加
@Caching(
    put = {@CachePut(value = "users", key = "#user.id")}, // 更新缓存
    evict = {
        @CacheEvict(value = "userCounts", key = "#user.departmentId"), // 清除关联缓存
        @CacheEvict(value = "allUsers", allEntries = true) // 清除列表缓存
    }
)
public User saveOrUpdate(User user) {
    return userRepository.save(user);
}

四、集成 Redis 缓存(分布式场景)

对于分布式系统,需使用 Redis 等集中式缓存。Spring Boot 对 Redis 缓存提供自动配置支持。

1. 引入 Redis 依赖
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存抽象依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2. 配置 Redis 缓存
yaml 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    password: 123456 # 若有密码
    timeout: 2000ms
  cache:
    type: redis # 指定使用Redis缓存
    redis:
      time-to-live: 600000ms # 全局过期时间(10分钟)
      cache-null-values: false # 是否缓存null值(默认false)
      key-prefix: "app:" # 缓存key前缀(避免key冲突)
      use-key-prefix: true # 启用key前缀
3. 自定义 Redis 缓存配置(可选)

默认情况下,Redis 缓存会将对象序列化为 JSON。若需自定义序列化方式(如使用 FastJSON),可配置 RedisCacheConfiguration

Redis缓存自定义配置:

java 复制代码
package com.example.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        // 配置序列化(解决乱码问题)
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSerializer.setObjectMapper(om);

        // 配置缓存过期时间
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)) // 全局过期时间
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer())) // key序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(jacksonSerializer)) // value序列化
                .disableCachingNullValues(); // 不缓存null值

        // 对不同缓存名称设置不同过期时间
        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .withCacheConfiguration("users", 
                        config.entryTtl(Duration.ofMinutes(5))) // users缓存5分钟过期
                .withCacheConfiguration("products", 
                        config.entryTtl(Duration.ofHours(1))) // products缓存1小时过期
                .build();
    }
}

五、缓存使用最佳实践

  1. 缓存 key 设计

    • 使用有意义的前缀(如 user::123product::456),避免 key 冲突。
    • 复杂参数组合作为 key 时,确保唯一性(可通过 SpEL 表达式 #p0 + '-' + #p1 组合参数)。
  2. 缓存粒度控制

    • 避免缓存过大的集合(如全表数据),可分页缓存或按需缓存单条数据。
    • 高频访问且变化少的数据(如字典表)优先缓存。
  3. 缓存一致性

    • 数据更新时必须同步更新或删除缓存(使用 @CachePut@CacheEvict)。
    • 并发场景下可使用「更新数据库 + 删除缓存」策略,避免脏读。
  4. 缓存穿透防护

    • 对查询结果为 null 的请求,可短暂缓存(cache-null-values: true),避免缓存穿透。
    • 结合布隆过滤器过滤不存在的 key。
  5. 缓存雪崩防护

    • 不同缓存设置随机过期时间,避免同时失效。
    • 核心缓存添加过期时间兜底策略。

六、缓存注解工作原理

  1. Spring 通过 AOP 对标注缓存注解的方法进行拦截。
  2. 调用方法前,先检查缓存中是否存在符合条件的 key。
  3. 若存在则直接返回缓存值,跳过方法执行;若不存在则执行方法,并将结果存入缓存。
  4. 缓存操作由 CacheManager 管理,不同缓存实现(Caffeine/Redis)通过 CacheManager 接口适配。

Spring Boot 缓存机制通过注解驱动简化了缓存开发,核心优势:

  • 零侵入:无需修改业务逻辑,通过注解即可实现缓存。
  • 灵活性:支持多种缓存实现,可根据环境(单机/分布式)切换。
  • 高性能:减少数据库访问,提升系统响应速度。

实际开发中,需根据业务场景选择合适的缓存实现(单机用 Caffeine,分布式用 Redis),并注意缓存一致性和异常防护,避免缓存引发的数据问题。

8.1、Spring Security

Spring Security 是 Spring 生态中用于身份认证和授权的安全框架,提供了全面的安全解决方案,包括用户认证、授权管理、 CSRF 防护、会话管理等功能。以下是 Spring Security 的核心概念和使用详解:

一、核心概念

  1. 认证(Authentication):验证用户身份(如用户名密码登录、OAuth2 登录等)。
  2. 授权(Authorization):验证用户是否有权限访问某个资源(如角色、权限控制)。
  3. 安全过滤器链(Security Filter Chain):Spring Security 的核心,通过一系列过滤器拦截请求,实现安全控制。
  4. 用户详情服务(UserDetailsService):提供用户信息(用户名、密码、权限等),用于认证。
  5. 密码编码器(PasswordEncoder):对密码进行加密存储(如 BCrypt、SHA 等)。

二、快速集成 Spring Security

1. 引入依赖

pom.xml 中添加 Spring Security Starter:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

引入后,Spring Security 会自动生效,默认拦截所有请求,生成随机密码(控制台输出),默认用户名为 user

2. 基本配置类

通过配置类自定义安全规则:

java 复制代码
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // 启用 Web 安全支持
public class SecurityConfig {

    // 配置安全过滤器链(核心)
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 关闭 CSRF(前后端分离场景常用)
            .csrf(csrf -> csrf.disable())
            // 配置请求授权规则
            .authorizeHttpRequests(auth -> auth
                .antMatchers("/public/**").permitAll() // 公开接口,无需认证
                .antMatchers("/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 需要 USER 或 ADMIN 角色
                .anyRequest().authenticated() // 其他请求需要认证
            )
            // 配置表单登录(默认提供登录页)
            .formLogin(form -> form
                .loginPage("/login") // 自定义登录页路径
                .defaultSuccessUrl("/home", true) // 登录成功跳转页
                .permitAll() // 登录页允许匿名访问
            )
            // 配置退出登录
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            );
        
        return http.build();
    }

    // 配置用户信息(内存模式,仅用于演示)
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder().encode("123456")) // 密码需加密
            .roles("USER") // 角色(前缀 ROLE_ 会自动添加)
            .build();

        UserDetails admin = User.builder()
            .username("admin")
            .password(passwordEncoder().encode("admin123"))
            .roles("ADMIN", "USER") // 拥有多个角色
            .build();

        return new InMemoryUserDetailsManager(user, admin);
    }

    // 配置密码编码器(必须配置,否则报错)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCrypt 加密算法
    }
}

三、核心功能详解

1. 认证机制

Spring Security 支持多种认证方式,常用的有:

(1)基于表单的认证(默认)

通过用户名/密码登录,如上述配置中的 formLogin()

(2)基于数据库的认证

实际项目中,用户信息通常存储在数据库,需自定义 UserDetailsService

数据库用户详情服务:

java 复制代码
package com.example.service;

import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查询用户
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));

        // 将自定义 User 转换为 Spring Security 的 UserDetails
        return org.springframework.security.core.userdetails.User.builder()
                .username(user.getUsername())
                .password(user.getPassword()) // 数据库中存储的是加密后的密码
                .roles(user.getRoles().toArray(new String[0])) // 角色列表
                .build();
    }
}
(3)JWT 认证(前后端分离常用)
  1. 引入 JWT 依赖
xml 复制代码
<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>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
  1. JWT 工具类
java 复制代码
package com.example.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil {

    // 密钥(实际项目中应从配置文件读取,且足够长)
    private final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    // 令牌过期时间(24小时)
    private static final long EXPIRATION_TIME = 86400000;

    // 生成令牌
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(secretKey)
                .compact();
    }

    // 从令牌中获取用户名
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    // 验证令牌是否有效
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    // 提取令牌中的声明
    private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    // 检查令牌是否过期
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
}
  1. JWT 认证过滤器
java 复制代码
package com.example.security;

import com.example.service.CustomUserDetailsService;
import com.example.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        // 从请求头获取令牌
        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String username;

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        // 提取 JWT 令牌(去除 "Bearer " 前缀)
        jwt = authHeader.substring(7);
        try {
            username = jwtUtil.extractUsername(jwt);
            // 如果用户名不为空且未认证
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                // 验证令牌
                if (jwtUtil.validateToken(jwt, userDetails)) {
                    // 设置认证信息到上下文
                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
        } catch (Exception e) {
            logger.error("无法验证令牌: {}", e.getMessage());
        }

        filterChain.doFilter(request, response);
    }
}
  1. 更新 Security 配置
java 复制代码
package com.example.config;

import com.example.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class JwtSecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .antMatchers("/api/auth/**").permitAll() // 登录接口公开
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            // 无状态会话(JWT 不需要会话)
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            // 添加 JWT 过滤器(在用户名密码过滤器之前)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
2. 授权机制

Spring Security 支持基于角色(Role)和权限(Permission)的授权:

(1)基于注解的授权

在控制器或方法上使用注解控制访问权限:

java 复制代码
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    // 允许所有认证用户访问
    @GetMapping("/home")
    public String home() {
        return "首页";
    }

    // 仅允许 ADMIN 角色访问
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/dashboard")
    public String adminDashboard() {
        return "管理员面板";
    }

    // 允许 USER 或 ADMIN 角色访问
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    @GetMapping("/user/profile")
    public String userProfile() {
        return "用户资料";
    }

    // 基于权限的控制(需在用户详情中配置权限)
    @PreAuthorize("hasPermission('', 'product:delete')")
    @GetMapping("/products/delete")
    public String deleteProduct() {
        return "删除产品";
    }
}

需在启动类添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启注解支持(Spring Security 5.6+ 可用 @EnableMethodSecurity)。

(2)基于 URL 的授权

SecurityFilterChain 中配置 URL 与角色/权限的映射:

java 复制代码
http.authorizeHttpRequests(auth -> auth
    .antMatchers("/api/products/**").hasAnyRole("USER", "ADMIN")
    .antMatchers("/api/orders/**").hasPermission("", "order:view")
    .anyRequest().authenticated()
);

四、用户信息与权限获取

在控制器中获取当前登录用户信息:

java 复制代码
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/current-user")
    public String getCurrentUser() {
        // 获取认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 用户名
        String username = authentication.getName();
        // 权限列表
        String authorities = authentication.getAuthorities().toString();
        return "当前用户: " + username + ", 权限: " + authorities;
    }
}

五、常用安全配置

  1. CSRF 防护 :默认开启,前后端分离项目通常关闭(csrf.disable())。
  2. 会话管理
    • sessionCreationPolicy(SessionCreationPolicy.STATELESS):无状态会话(JWT 常用)。
    • maximumSessions(1):限制单用户同时登录次数。
  3. 密码策略
    • 使用 BCryptPasswordEncoder 加密密码(不可逆)。
    • 自定义密码强度(如长度、包含特殊字符等)。
  4. CORS 配置:允许跨域请求:
java 复制代码
http.cors(cors -> cors.configurationSource(request -> {
    org.springframework.web.cors.CorsConfiguration config = new org.springframework.web.cors.CorsConfiguration();
    config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
    config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(Arrays.asList("*"));
    return config;
}));

Spring Security 是一个功能强大的安全框架,核心优势:

  • 全面的安全功能:覆盖认证、授权、防护等各环节。
  • 灵活的扩展机制:支持自定义认证方式、权限控制等。
  • 与 Spring 生态无缝集成:易于在 Spring Boot 项目中使用。

实际开发中,需根据项目类型(传统 Web/前后端分离)选择合适的认证方式(表单登录/JWT/OAuth2),并遵循最小权限原则设计授权规则,确保系统安全。

9.1、Spring Boot整合RabbitMQ

Spring Boot 整合 RabbitMQ 可以轻松实现消息队列的功能,用于异步通信、服务解耦、流量削峰等场景。以下是详细的整合流程和使用方式:

一、核心概念

  • 消息代理(Broker):RabbitMQ 服务器,负责接收和转发消息。
  • 交换机(Exchange):接收生产者发送的消息,根据路由规则转发到队列。
  • 队列(Queue):存储消息,等待消费者消费。
  • 绑定(Binding):将交换机和队列按路由规则关联起来。
  • 路由键(Routing Key):消息发送时指定的键,交换机根据该键匹配路由规则。

二、准备工作

1. 安装 RabbitMQ
  • 本地安装:参考 RabbitMQ 官方文档 安装并启动服务(默认端口 5672,管理界面端口 15672)。
  • 访问管理界面:http://localhost:15672,默认账号密码 guest/guest
2. 引入依赖

pom.xml 中添加 RabbitMQ Starter:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3. 配置 RabbitMQ 连接

application.yml 中配置连接信息:

yaml 复制代码
spring:
  rabbitmq:
    host: localhost  # 服务器地址
    port: 5672       # 端口
    username: guest  # 用户名
    password: guest  # 密码
    virtual-host: /  # 虚拟主机(默认 /)
    # 生产者确认配置(可选)
    publisher-confirm-type: correlated  # 开启发布确认
    publisher-returns: true             # 开启发布返回

三、核心组件与实现

1. 声明交换机、队列和绑定关系

通过配置类声明消息队列相关组件:

RabbitMQ配置类:

java 复制代码
package com.example.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    // 1. 声明队列
    @Bean
    public Queue directQueue() {
        // 参数:队列名、是否持久化、是否排他、是否自动删除、额外参数
        return QueueBuilder.durable("direct_queue")
                .autoDelete(false)
                .build();
    }

    // 2. 声明交换机(Direct类型,精确匹配路由键)
    @Bean
    public DirectExchange directExchange() {
        // 参数:交换机名、是否持久化、是否自动删除、额外参数
        return ExchangeBuilder.directExchange("direct_exchange")
                .durable(true)
                .build();
    }

    // 3. 绑定队列和交换机(指定路由键)
    @Bean
    public Binding directBinding(Queue directQueue, DirectExchange directExchange) {
        // 将队列通过路由键 "direct_routing" 绑定到交换机
        return BindingBuilder.bind(directQueue)
                .to(directExchange)
                .with("direct_routing");
    }

    // -------------- 以下是其他类型的交换机示例 --------------

    // 扇形交换机(Fanout):广播消息到所有绑定的队列,忽略路由键
    @Bean
    public FanoutExchange fanoutExchange() {
        return ExchangeBuilder.fanoutExchange("fanout_exchange").build();
    }

    // 主题交换机(Topic):通过通配符匹配路由键(* 匹配一个单词,# 匹配多个单词)
    @Bean
    public TopicExchange topicExchange() {
        return ExchangeBuilder.topicExchange("topic_exchange").build();
    }
}
2. 消息生产者(发送消息)

使用 RabbitTemplate 发送消息到交换机:消息生产者

java 复制代码
package com.example.service;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MessageProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 发送消息到Direct交换机
    public void sendDirectMessage(String message) {
        String exchange = "direct_exchange";
        String routingKey = "direct_routing";
        // 参数:交换机、路由键、消息内容
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
        System.out.println("发送Direct消息:" + message);
    }

    // 发送对象消息(自动序列化)
    public void sendObjectMessage(User user) {
        String exchange = "direct_exchange";
        String routingKey = "direct_routing";
        rabbitTemplate.convertAndSend(exchange, routingKey, user);
        System.out.println("发送对象消息:" + user);
    }

    // 发送消息到Topic交换机(示例:路由键匹配 "user.#")
    public void sendTopicMessage(String message) {
        String exchange = "topic_exchange";
        String routingKey = "user.update"; // 匹配 "user.#"
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
        System.out.println("发送Topic消息:" + message);
    }
}

// 消息对象示例
class User {
    private Long id;
    private String name;
    // getter/setter、toString
}
3. 消息消费者(接收消息)

使用 @RabbitListener 注解监听队列:消息消费者

java 复制代码
package com.example.service;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class MessageConsumer {

    // 监听Direct队列
    @RabbitListener(queues = "direct_queue") // 监听指定队列
    public void receiveDirectMessage(String message) {
        System.out.println("收到Direct消息:" + message);
        // 处理消息逻辑...
    }

    // 监听对象消息(自动反序列化)
    @RabbitListener(queues = "direct_queue")
    public void receiveObjectMessage(User user) {
        System.out.println("收到对象消息:" + user);
    }

    // 监听Topic队列(动态声明队列和绑定)
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "topic_queue", durable = "true"), // 声明队列
            exchange = @Exchange(value = "topic_exchange", type = "topic"), // 声明交换机
            key = "user.#" // 路由键规则(匹配 user.xxx)
    ))
    public void receiveTopicMessage(String message) {
        System.out.println("收到Topic消息:" + message);
    }
}

四、消息确认与可靠性

1. 生产者确认机制

确保消息成功发送到交换机:生产者确认配置

java 复制代码
package com.example.config;

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

@Configuration
public class RabbitMQConfirmConfig {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        // 1. 消息发送到交换机确认回调
        rabbitTemplate.setConfirmCallback((CorrelationData correlationData, boolean ack, String cause) -> {
            if (ack) {
                System.out.println("消息成功发送到交换机:" + correlationData.getId());
            } else {
                System.err.println("消息发送到交换机失败:" + cause);
                // 失败处理:重试、记录日志等
            }
        });

        // 2. 消息无法路由到队列的返回回调
        rabbitTemplate.setReturnsCallback(returned -> {
            System.err.println("消息无法路由到队列:" +
                    "交换机=" + returned.getExchange() +
                    ", 路由键=" + returned.getRoutingKey() +
                    ", 消息=" + new String(returned.getMessage().getBody()));
        });
    }
}

发送消息时指定 CorrelationData(消息唯一标识):

java 复制代码
// 发送消息时添加唯一标识
public void sendWithConfirm(String message) {
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    rabbitTemplate.convertAndSend("direct_exchange", "direct_routing", message, correlationData);
}
2. 消费者确认机制

确保消息被成功消费(默认自动确认,可配置手动确认):

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual  # 手动确认模式
        prefetch: 1  # 每次从队列获取1条消息,处理完再获取下一条

手动确认消息:

java 复制代码
@RabbitListener(queues = "direct_queue")
public void receiveWithAck(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
    try {
        System.out.println("收到消息:" + message);
        // 处理消息...
        channel.basicAck(tag, false); // 手动确认消息已消费
    } catch (Exception e) {
        // 消息处理失败,拒绝并重回队列(或丢弃)
        channel.basicNack(tag, false, true); // requeue=true 重回队列
    }
}

五、消息持久化

确保 RabbitMQ 重启后消息不丢失:

  1. 队列持久化 :声明队列时设置 durable = true(如 QueueBuilder.durable("queue_name"))。
  2. 交换机持久化 :声明交换机时设置 durable = true
  3. 消息持久化 :发送消息时设置 deliveryMode = 2
java 复制代码
// 发送持久化消息
rabbitTemplate.convertAndSend(exchange, routingKey, message, msg -> {
    msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
    return msg;
});

六、高级特性

1. 死信队列

处理无法消费的消息(如过期、被拒绝、队列满):死信队列配置

java 复制代码
package com.example.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DeadLetterConfig {

    // 普通队列(设置死信参数)
    @Bean
    public Queue normalQueue() {
        return QueueBuilder.durable("normal_queue")
                .deadLetterExchange("dead_letter_exchange") // 死信交换机
                .deadLetterRoutingKey("dead_routing") // 死信路由键
                .ttl(10000) // 消息过期时间(10秒)
                .maxLength(10) // 队列最大长度
                .build();
    }

    // 死信交换机
    @Bean
    public DirectExchange deadLetterExchange() {
        return ExchangeBuilder.directExchange("dead_letter_exchange").durable(true).build();
    }

    // 死信队列
    @Bean
    public Queue deadLetterQueue() {
        return QueueBuilder.durable("dead_letter_queue").build();
    }

    // 绑定死信队列和交换机
    @Bean
    public Binding deadLetterBinding() {
        return BindingBuilder.bind(deadLetterQueue())
                .to(deadLetterExchange())
                .with("dead_routing");
    }
}
2. 延迟队列

实现消息延迟投递(基于死信队列的 TTL 特性):

java 复制代码
// 发送延迟消息(10秒后处理)
public void sendDelayMessage(String message) {
    rabbitTemplate.convertAndSend("normal_exchange", "normal_routing", message, msg -> {
        msg.getMessageProperties().setExpiration("10000"); // 延迟10秒
        return msg;
    });
}

// 死信队列消费者(处理延迟消息)
@RabbitListener(queues = "dead_letter_queue")
public void handleDelayMessage(String message) {
    System.out.println("处理延迟消息:" + message);
}

七、测试与监控

  1. 测试接口:创建控制器调用生产者发送消息:
java 复制代码
@RestController
@RequestMapping("/mq")
public class MQController {
    @Autowired
    private MessageProducer producer;

    @GetMapping("/send")
    public String sendMessage(String msg) {
        producer.sendDirectMessage(msg);
        return "消息发送成功";
    }
}
  1. 监控工具 :通过 RabbitMQ 管理界面(http://localhost:15672)查看队列状态、消息数量等。

Spring Boot 整合 RabbitMQ 的核心优势:

  • 自动配置 :引入 starter 后自动配置 ConnectionFactoryRabbitTemplate 等组件。
  • 注解驱动 :通过 @RabbitListener 快速实现消息消费。
  • 可靠性支持:提供生产者确认、消费者确认、消息持久化等机制。
  • 灵活扩展:支持多种交换机类型、死信队列、延迟队列等高级特性。

实际开发中,需根据业务场景选择合适的交换机类型,配置消息可靠性策略,并合理设计队列结构,以实现服务解耦和系统稳定性提升。

相关推荐
一个不称职的程序猿2 小时前
高并发场景下的缓存利器
java·缓存
星释2 小时前
Rust 练习册 :Proverb与字符串处理
开发语言·后端·rust
Q_Q5110082852 小时前
python+django/flask的校园活动中心场地预约系统
spring boot·python·django·flask·node.js·php
2301_801252222 小时前
Tomcat的基本使用作用
java·tomcat
lkbhua莱克瓦242 小时前
Java基础——常用算法3
java·数据结构·笔记·算法·github·排序算法·学习方法
麦麦鸡腿堡2 小时前
Java_TreeSet与TreeMap源码解读
java·开发语言
水冗水孚2 小时前
类比前端知识来学习Java的Spring Boot实现MySql的全栈CRUD功能——搭配Svelte+Vite
spring boot·svelte
教练、我想打篮球2 小时前
05 kafka 如何存储较大数据记录
java·kafka·record
uesowys2 小时前
华为OD算法开发指导-简易内存池
java·算法·华为od