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 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-spring-boot3-starter</artifactId > <version > ${mybatis-plus.version}</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-3-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <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 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 public class V2__encrypt_passwords extends BaseJavaMigration { @Override public void migrate (Context context) throws Exception { migrateUsersTable(context); migrateYonghuTable(context); } 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" ); if (plainPassword != null && !plainPassword.startsWith("$2a$" ) && !plainPassword.startsWith("$2b$" )) { String hashed = BCrypt.hashpw(plainPassword, BCrypt.gensalt()); 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.hashpw(cn.hutool.crypto.digest.BCrypt)
应用层用 Spring Security 的 BCryptPasswordEncoder
两者都遵循标准 BCrypt 算法,哈希互相兼容——Hutool 加密的密码可以用 Spring Security 的 matches() 验证。
4.5 V3:删除废弃表 1 2 3 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 ALTER TABLE `obtain_information` ADD FULLTEXT INDEX `ft_title_content` (`title`, `content`) WITH PARSER ngram;
1 2 3 4 5 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" ))) .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/**" ) .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 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-alpineWORKDIR /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 FROM node:22 -alpine AS buildWORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:alpineCOPY --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' , timeout : 10000 }) service.interceptors .request .use (config => { const token = localStorage .getItem ('Token' ) if (token) { config.headers ['Token' ] = token } 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 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 编排、前端代理联调——这些是全栈项目的标配,不是可选项。