Springboot

Spring Boot 3 工程化实战:从数据库迁移到 Docker 部署

2026-07-07 #java#Springboot#Redis#Flyway

Flyway 管理数据库版本、SpringDoc 自动生成接口文档、多环境配置、Docker 一键部署——这些不是可选项,而是全栈项目的标配。本文以社区团购系统为例,把 Spring Boot 3 工程化的完整流程从头到尾讲透。


第一章:项目结构设计

1.1 整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
community-group-buying-system/
├── community-group-buying-system/ # 后端(Spring Boot)
│ ├── pom.xml
│ ├── src/main/java/com/
│ │ ├── SpringbootSchemaApplication.java
│ │ ├── controller/ # 15个控制器
│ │ ├── service/ # Service接口 + impl
│ │ ├── dao/ # MyBatis Plus Mapper
│ │ ├── entity/ # 实体类
│ │ ├── config/ # 7个配置类
│ │ ├── interceptor/ # 认证拦截器
│ │ ├── annotation/ # 自定义注解
│ │ ├── db/migration/ # Flyway Java迁移
│ │ └── utils/ # 工具类
│ └── src/main/resources/
│ ├── application.yml # 公共配置
│ ├── application-dev.yml # 开发环境
│ ├── application-test.yml # 测试环境
│ └── db/migration/ # Flyway SQL迁移

├── admin-vue3/ # 管理后台前端(Vue 3,端口8081)
├── front-vue3/ # 用户端前端(Vue 3,端口8084)
├── Dockerfile
└── docker-compose.yml

双前端设计:管理后台和用户端分离,共享同一套后端 API。

1.2 主启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootApplication(exclude = {
SecurityAutoConfiguration.class,
SecurityFilterAutoConfiguration.class
})
@MapperScan(basePackages = {"com.dao"})
@EnableAsync
public class SpringbootSchemaApplication extends SpringBootServletInitializer {

public static void main(String[] args) {
SpringApplication.run(SpringbootSchemaApplication.class, args);
}

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SpringbootSchemaApplication.class);
}
}

设计要点

  • 排除 Security 自动配置——只用 BCryptPasswordEncoder,不需要 Security 的过滤器链
  • @EnableAsync 开启异步支持——延迟双删的 @Async 依赖这个
  • @MapperScan("com.dao") 扫描 MyBatis Plus Mapper
  • extends SpringBootServletInitializer 支持 WAR 部署

第二章:Maven 依赖管理

2.1 核心 POM

1
2
3
4
5
6
7
8
9
10
11
12
13
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
</parent>

<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<jjwt.version>0.12.6</jjwt.version>
<springdoc.version>2.8.15</springdoc.version>
<hutool.version>5.8.25</hutool.version>
</properties>

2.2 关键依赖说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- MyBatis Plus(Spring Boot 3专用starter) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>

<!-- Druid 连接池(Spring Boot 3专用) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>

<!-- Spring Security(仅用BCrypt,禁用自动配置) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Flyway + MySQL方言 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>

注意:Spring Boot 3 用 mybatis-plus-spring-boot3-starter(注意 boot3),不是普通的 mybatis-plus-boot-starter。Druid 也需要 druid-spring-boot-3-starter


第三章:多环境配置

3.1 公共配置 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
profiles:
active: dev # 默认开发环境
flyway:
enabled: true
baseline-on-migrate: true
baseline-version: 0
locations: classpath:db/migration,classpath:com/db/migration

server:
port: 8080
servlet:
context-path: /springboot2c1hu

springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html

3.2 开发环境 application-dev.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/springboot2c1hu?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
druid:
initial-size: 5
max-active: 20
min-idle: 5
data:
redis:
host: 127.0.0.1
port: 6379
lettuce:
pool:
max-active: 16
max-idle: 8

jwt:
secret: CommunityGroupBuying2024SecretKeyForJWTTokenGenerationMustBeLongEnough
expiration: 3600000

token:
redis-prefix: "token:"
expire-seconds: 3600

springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true

3.3 生产环境 application-prod.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME}?useSSL=true
username: ${DB_USER}
password: ${DB_PASSWORD}
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
password: ${REDIS_PASSWORD}

jwt:
secret: ${JWT_SECRET}

springdoc:
api-docs:
enabled: false # ★ 生产环境关闭文档
swagger-ui:
enabled: false

环境变量注入:生产配置用 ${ENV_VAR} 引用环境变量,避免在代码中硬编码密码。


第四章:Flyway 数据库迁移

4.1 Flyway 配置

1
2
3
4
5
6
7
spring:
flyway:
enabled: true
baseline-on-migrate: true # 已有数据的库也能执行迁移
baseline-version: 0 # 基线版本
locations: classpath:db/migration,classpath:com/db/migration
table: flyway_schema_history # 迁移历史表名

两个 location 的含义

  • classpath:db/migration → 放 SQL 迁移文件(.sql
  • classpath:com/db/migration → 放 Java 迁移文件(.java,继承 BaseJavaMigration

4.2 版本命名规则

1
2
3
V1__init_schema.sql        ← SQL迁移,版本号1
V2__encrypt_passwords.java ← Java迁移,版本号2
V3__drop_token_table.sql ← SQL迁移,版本号3
格式 含义
V Versioned migration(版本化迁移,只执行一次)
R Repeatable migration(可重复迁移,每次checksum变化都执行)
__ 双下划线分隔版本号和描述

4.3 V1:初始化 Schema(SQL 迁移)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- V1__init_schema.sql
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(100) NOT NULL,
`password` varchar(100) NOT NULL COMMENT '密码(明文,V2会加密)',
`role` varchar(100) DEFAULT '管理员',
PRIMARY KEY (`id`),
UNIQUE INDEX `username_index`(`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `yonghu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`zhanghao` varchar(200) NOT NULL,
`mima` varchar(200) NOT NULL COMMENT '密码(明文,V2会加密)',
PRIMARY KEY (`id`),
UNIQUE INDEX `zhanghao_index`(`zhanghao`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 初始数据(明文密码)
INSERT INTO `users` VALUES (1, 'abo', 'abo', '超级管理员');
INSERT INTO `yonghu` VALUES (1, '用户1', '123456');

注意:V1 中密码是明文——这是一个”技术债”,V2 会用 Java 迁移加密。

4.4 V2:Java 迁移——BCrypt 加密存量密码

SQL 无法做到”逐行读取、条件判断、加密更新”这种复杂逻辑。Flyway 的 Java 迁移可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// V2__encrypt_passwords.java
public class V2__encrypt_passwords extends BaseJavaMigration {

@Override
public void migrate(Context context) throws Exception {
migrateUsersTable(context); // 加密 users.password
migrateYonghuTable(context); // 加密 yonghu.mima
}

private void migrateUsersTable(Context context) throws Exception {
try (Statement select = context.getConnection().createStatement()) {
ResultSet rs = select.executeQuery("SELECT id, password FROM users");

while (rs.next()) {
Long id = rs.getLong("id");
String plainPassword = rs.getString("password");

// ★ 幂等性:跳过已加密的密码
// BCrypt哈希以 $2a$ 或 $2b$ 开头
if (plainPassword != null
&& !plainPassword.startsWith("$2a$")
&& !plainPassword.startsWith("$2b$")) {

String hashed = BCrypt.hashpw(plainPassword, BCrypt.gensalt());
// SQL注入防护:单引号转义
String escapedHash = hashed.replace("'", "''");
try (Statement update = context.getConnection().createStatement()) {
update.executeUpdate(
"UPDATE users SET password = '" + escapedHash + "' WHERE id = " + id);
}
}
}
}
}
}

幂等性设计:通过 $2a$ 前缀判断是否已加密,迁移可重复执行。如果迁移失败,修复数据后重新执行即可。

两种 BCrypt 实现

  • V2迁移用 Hutool 的 BCrypt.hashpwcn.hutool.crypto.digest.BCrypt
  • 应用层用 Spring Security 的 BCryptPasswordEncoder

两者都遵循标准 BCrypt 算法,哈希互相兼容——Hutool 加密的密码可以用 Spring Security 的 matches() 验证。

4.5 V3:删除废弃表

1
2
3
-- V3__drop_token_table.sql
-- Token 已迁移到 Redis 存储,不再需要数据库 token 表
DROP TABLE IF EXISTS `token`;

演进故事:V1 建了 token 表 → 架构升级为 JWT + Redis → V3 删除 token 表。每个迁移文件都是架构演进的快照。

4.6 Flyway 执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
应用启动


Flyway 检查 flyway_schema_history 表

├── 表不存在 → 创建表,记录 baseline (version=0)

├── 表存在 → 读取已执行的迁移记录


扫描 classpath:db/migration 和 classpath:com/db/migration

├── 找到 V1 → 未执行则执行 V1__init_schema.sql
├── 找到 V2 → 未执行则执行 V2__encrypt_passwords.java
├── 找到 V3 → 未执行则执行 V3__drop_token_table.sql


更新 flyway_schema_history 表(版本号、描述、类型、checksum、执行时间)

4.7 FULLTEXT + ngram 中文全文索引

钢铁工厂项目中,Flyway 迁移还包含中文全文索引:

1
2
3
4
5
6
7
8
-- steel-factory-fullstack 的 Flyway 迁移
ALTER TABLE `obtain_information`
ADD FULLTEXT INDEX `ft_title_content` (`title`, `content`)
WITH PARSER ngram;

-- ngram 是 MySQL 5.7.6+ 的中文分词器
-- 将中文按 n-gram(默认2字)切分
-- "钢管价格" → "钢管", "管价", "价格"
1
2
3
4
5
// 搜索时使用 MATCH ... AGAINST
List<News> search(String keyword) {
return newsMapper.search(
"MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)", keyword);
}

4.8 常见问题

迁移失败后启动报错:手动修复数据库到上一个版本的状态,删除 flyway_schema_history 中失败的记录,重启应用。

checksum 不匹配:修改了已执行的迁移文件。解法:不要修改已执行的文件,只能新增新的 V* 文件。

Java 迁移中使用 Spring Bean:Java 迁移中不能注入 Spring Bean——Flyway 在 Spring 完全初始化前执行。Context 对象只提供 JDBC Connection


第五章:SpringDoc OpenAPI 接口文档

5.1 从 Swagger 2 到 OpenAPI 3

特性 Springfox (Swagger 2) SpringDoc (OpenAPI 3)
Spring Boot 3 不兼容 完全兼容
依赖 springfox-swagger2 springdoc-openapi-starter-webmvc-ui
注解 @Api / @ApiOperation @Tag / @Operation
配置 Docket Bean OpenAPI Bean

5.2 配置类

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

@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("社区团购系统 API")
.description("社区团购系统后端接口文档")
.version("v1.0.0")
.contact(new Contact()
.name("Community Group Buying")))
// ★ 配置JWT全局认证
.addSecurityItem(new SecurityRequirement().addList("Token"))
.components(new Components()
.addSecuritySchemes("Token",
new SecurityScheme()
.name("Token")
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.description("JWT Token")));
}
}

APIKEY vs Bearer 的区别:本项目 Token 放在自定义 Header Token 中(不是标准的 Authorization: Bearer xxx),所以用 APIKEY 类型。

5.3 拦截器排除 Swagger 路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getAuthorizationInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/static/**")
// ★ 排除Swagger相关路径
.excludePathPatterns(
"/swagger-ui/**", "/swagger-ui.html",
"/v3/api-docs/**", "/swagger-resources/**",
"/webjars/**"
)
.excludePathPatterns("/actuator/**");
super.addInterceptors(registry);
}
}

5.4 接口注解

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/orders")
@Tag(name = "订单管理", description = "订单的增删改查")
public class OrdersController {

@Operation(summary = "创建订单", description = "用户下单创建新订单")
@PostMapping("/create")
public R create(
@Parameter(description = "商品ID", required = true) @RequestParam Long productId,
@Parameter(description = "购买数量") @RequestParam Integer quantity
) { ... }
}

5.5 多环境控制

1
2
3
4
5
6
# application-prod.yml
springdoc:
api-docs:
enabled: false # 生产环境关闭文档
swagger-ui:
enabled: false

第六章:跨域配置

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

注意allowed-origins: "*"allow-credentials: true 在浏览器规范中是互斥的。用 allowedOriginPatterns("*") 可以绕过这个限制。


第七章:Docker 部署

7.1 后端 Dockerfile

1
2
3
4
5
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/community-group-buying-system.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]

7.2 前端 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
# admin-vue3/Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

7.3 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: springboot2c1hu
ports: ["3306:3306"]
volumes:
- mysql_data:/var/lib/mysql

redis:
image: redis:7-alpine
ports: ["6379:6379"]
volumes:
- redis_data:/data

backend:
build: .
ports: ["8080:8080"]
depends_on: [mysql, redis]
environment:
- DB_HOST=mysql
- DB_NAME=springboot2c1hu
- DB_USER=root
- DB_PASSWORD=root
- REDIS_HOST=redis
- REDIS_PORT=6379
- SPRING_PROFILES_ACTIVE=prod

admin-frontend:
build: ./admin-vue3
ports: ["8081:80"]

front-frontend:
build: ./front-vue3
ports: ["8084:80"]

volumes:
mysql_data:
redis_data:

一键启动docker-compose up -d 启动全部服务。


第八章:前后端联调

8.1 Vue 3 请求封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import axios from 'axios'

const service = axios.create({
baseURL: '/springboot2c1hu', // 后端context-path
timeout: 10000
})

// 请求拦截器:自动携带Token
service.interceptors.request.use(config => {
const token = localStorage.getItem('Token')
if (token) {
config.headers['Token'] = token // ★ 自定义Header名
}
return config
})

// 响应拦截器:统一处理错误
service.interceptors.response.use(
response => response.data,
error => {
if (error.response.status === 401) {
localStorage.removeItem('Token')
router.push('/login')
}
return Promise.reject(error)
}
)

export default service

8.2 Vite 代理配置

1
2
3
4
5
6
7
8
9
10
11
12
// vite.config.js
export default defineConfig({
server: {
port: 8081,
proxy: {
'/springboot2c1hu': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})

第九章:Druid 连接池监控

9.1 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
datasource:
druid:
initial-size: 5
max-active: 20
min-idle: 5
max-wait: 60000
# 监控配置
stat-view-servlet:
enabled: true
login-username: admin
login-password: admin
url-pattern: /druid/*
web-stat-filter:
enabled: true
url-pattern: /*

9.2 访问 Druid 监控

1
http://localhost:8080/springboot2c1hu/druid/

可以查看 SQL 执行统计、慢查询、连接池状态等。生产环境建议关闭或加 IP 白名单。


总结

配置项 说明
Spring Boot 3.4.1 Java 17
前端 Vue 3.5.34 Vite 构建
数据库 MySQL 8.0 + Druid 连接池
缓存 Redis 7 + Lettuce 异步客户端
ORM MyBatis Plus 3.5.9 代码生成
安全 JWT + BCrypt 自定义拦截器
迁移 Flyway 版本化SQL + Java迁移
文档 SpringDoc 2.8.15 OpenAPI 3
部署 Docker Compose 一键启动

工程化要点:多环境配置(dev/test/prod)、环境变量注入、Flyway 版本化管理、Docker 编排、前端代理联调——这些是全栈项目的标配,不是可选项。

评论
分享