从零到上线:Spring Boot项目案例的完整实战指南
这不是教科书式的理论堆砌,而是源于真实开发场景的深度复盘。我们聚焦Spring Boot项目实战案例中最具代表性的开发路径,从启动报错、配置混乱到性能调优,手把手带你走完一个完整项目的生命周期。无论你是刚入门的新人,还是遇到瓶颈的资深开发者,这里都有你真正需要的实战经验。
开始学习实战经验开发者最关心的Spring Boot项目案例问题
? 项目启动失败:无从下手的“红色海洋”
%的新手在首次启动项目时都会面对满屏红色错误信息。这不是能力问题,而是对Spring Boot项目实战案例中关键环节的不了解。
- 依赖冲突:不同版本jar包的兼容性问题
- 配置缺失:application.yml中关键参数遗漏
- 类路径问题:主类路径或注解配置错误
⚙️ 配置混乱:从“约定优于配置”到“约定太模糊”
Spring Boot的“约定优于配置”本意是减少工作量,但实际开发中,当项目规模扩大,配置碎片化成了最大痛点。
- 配置项散落在多个文件中
- 同名配置项大小写不一致
- 环境变量与配置文件冲突
? 部署难题:本地能跑,上线就崩
“开发环境一切正常,测试环境启动失败,生产环境直接卡死”——这是很多Spring Boot项目案例中常见的部署困境。
- 不同JDK版本的兼容性
- 内嵌Tomcat版本差异
- 端口冲突与资源限制
? 性能瓶颈:从“能跑”到“跑得快”
项目上线后发现响应慢、内存高、请求堆积——这正是性能优化的黄金切入点。
- 数据库连接池配置优化
- Redis缓存策略设计
- 异步处理与线程池配置
?️ 日志与监控:看不见的隐患最致命
“系统突然挂了,但没有任何告警”——这往往是因为日志体系不完善。在Spring Boot项目实战案例中,完善的日志监控是生产稳定的第一道防线。
- 分布式链路追踪实现
- 异常堆栈信息采集
- 关键业务指标埋点
? 文档与交接:技术债务的终结者
个没有文档的项目,就是一座技术债务的垃圾场。好的Spring Boot项目案例必须包含完整的交付文档体系。
- Swagger接口文档自动化生成
- 部署手册与常见问题清单
- 项目架构图与数据流向图
核心问题深度解析
启动流程详解:从`mvn spring-boot:run`到应用就绪
当执行`mvn spring-boot:run`命令时,背后发生了什么?这并非简单的“运行项目”,而是一系列精密的初始化过程。我们以一个典型的Spring Boot项目案例为例,逐步拆解启动流程。
启动入口:SpringApplication.run()
所有Spring Boot项目实战案例的起点都是这个方法。它做了三件事:
- 创建并初始化ApplicationContext(应用上下文)
- 加载所有自动配置类(Auto-configuration)
- 启动内嵌Web服务器(如Tomcat、Jetty)
自动配置的秘密:@SpringBootApplication
这个组合注解包含三个关键注解:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
其中@EnableAutoConfiguration是自动配置的触发器,它会读取META-INF/spring.factories文件中的配置类,这是Spring Boot“约定优于配置”的基石。
常见启动失败场景与诊断
| 错误类型 | 典型症状 | 解决方案 |
|---|---|---|
| 依赖冲突 | NoSuchMethodError、ClassNotFoundException | 使用mvn dependency:tree分析依赖树 |
| 配置缺失 | Failed to configure a DataSource | 添加spring.datasource.配置 |
| 主类错误 | Unable to find main class | 检查start-class或mainClass配置 |
启动流程时间轴(实际项目中测量)
配置管理:从碎片化到结构化
在Spring Boot项目案例开发中,配置管理是“配置乱”的重灾区。我们通过一个真实项目中的配置重构过程,展示如何从“一锅粥”走向“结构化”。核心原则:分层配置 + 环境隔离 + 配置中心。
分层配置策略
错误做法:所有配置堆在application.properties中:
# 错误示例:所有配置混在一起
spring.datasource.url=jdbc:mysql://localhost:3306/app
spring.datasource.username=root
spring.datasource.password=123456
server.port=8080
logging.level.root=INFO
my.app.cache.expire=3600
my.app.cache.max-size=1000
正确做法:按功能模块拆分配置文件
├── application.yml
├── application-dev.yml
├── application-prod.yml
├── application-datasource.yml
├── application-cache.yml
└── application-logging.yml
application.yml而非.properties,YAML格式更清晰且支持复杂数据结构
环境隔离配置
通过spring.profiles.active指定当前环境:
# application.yml
spring:
profiles:
active: dev
# application-dev.yml
server:
port: 8080
logging:
level:
root: DEBUG
# application-prod.yml
server:
port: 8080
logging:
level:
root: WARN
配置中心集成(Nacos示例)
在分布式系统中,配置中心是必备组件。以Nacos为例:
# bootstrap.yml
spring:
application:
name: user-service
cloud:
nacos:
config:
server-addr: 192.168.1.100:8848
file-extension: yaml
namespace: prod-namespace
group: DEFAULT_GROUP
配置中心优势:
- 配置实时生效(无需重启)
- 多环境配置统一管理
- 配置版本回滚与审计
- 敏感信息加密存储
配置属性绑定最佳实践
错误使用泛型绑定(运行时类型擦除问题):
@ConfigurationProperties(prefix = "my.app")
public class AppProperties<T> { // ❌ 问题:泛型在运行时丢失
private T cache;
// ...
}
正确做法:直接绑定具体类型
@ConfigurationProperties(prefix = "my.app")
public class AppProperties {
private CacheProperties cache = new CacheProperties();
// ...
}
public class CacheProperties {
private int expire = 3600;
private int maxSize = 1000;
// getters and setters
}
@ConfigurationProperties中使用泛型,直接绑定具体类型
配置验证(JSR-303)
添加校验注解确保配置有效性:
@ConfigurationProperties(prefix = "my.app.cache")
@Validated
public class CacheProperties {
@Min(100)
@Max(10000)
private int maxSize = 1000;
@Min(60)
@Max(86400)
private int expire = 3600;
@NotBlank
private String type = "redis";
}
部署实战:从本地到生产环境的完整路径
在Spring Boot项目实战案例中,部署阶段是“本地能跑,线上崩盘”的高发区。我们通过一个真实案例,展示如何系统性解决部署问题。
常见部署失败场景复盘
| 场景 | 症状 | 解决方案 |
|---|---|---|
| Tomcat版本冲突 | Port 8080 already in use | 显式指定Tomcat版本:<tomcat.version>9.0.65</tomcat.version> |
| JDK版本不兼容 | Unsupported major.minor version | 检查java -version与spring-boot-starter-parent版本 |
| 数据库驱动缺失 | No suitable driver found | 添加mysql-connector-java依赖 |
| 内存溢出 | OutOfMemoryError: Java heap space | 调整JVM参数:-Xms512m -Xmx1024m |
Docker部署实战
标准Dockerfile模板:
FROM openjdk:17-jdk-alpine
LABEL maintainer="devops@example.com"
# 创建应用目录
RUN mkdir -p /app/logs
WORKDIR /app
# 复制JAR包
COPY target/.jar app.jar
# 设置时区
RUN apk add --no-cache tzdata &&
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &&
echo "Asia/Shanghai" > /etc/timezone
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
# 启动命令
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom",
"-jar", "app.jar"]
/dev/./urandom加速JVM启动(避免阻塞等待熵池)
启动脚本优化
生产环境推荐使用启动脚本而非直接java -jar:
#!/bin/bash
# startup.sh
APP_NAME="user-service"
JAR_FILE="target/${APP_NAME}.jar"
LOG_DIR="logs"
# 创建日志目录
mkdir -p ${LOG_DIR}
# 获取可用端口
PORT=${PORT:-8080}
# JVM参数配置
JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200
-Djava.security.egd=file:/dev/./urandom
-Duser.timezone=Asia/Shanghai
-Dspring.profiles.active=prod"
# 启动应用
nohup java ${JAVA_OPTS} -jar ${JAR_FILE} --server.port=${PORT} > ${LOG_DIR}/app.log 2>&1 &
echo "${APP_NAME} started on port ${PORT}"
健康检查与就绪探针
在Kubernetes中,健康检查至关重要:
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
probes:
enabled: true
就绪探针(Readiness)确保应用完全启动后再接收流量:
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
配置文件外部化
生产环境配置不应打包进JAR:
java -jar app.jar
--spring.config.location=application-prod.yml
--spring.profiles.active=prod
或使用环境变量覆盖:
SPRING_DATASOURCE_URL=jdbc:mysql://prod-db:3306/app
SPRING_DATASOURCE_USERNAME=prod_user
SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
性能优化:从“能跑”到“跑得快”的进阶之路
在Spring Boot项目案例中,性能优化不是上线后的补救措施,而应贯穿开发全过程。我们以一个用户服务为例,展示系统性优化路径。
数据库连接池优化
错误配置(HikariCP默认值不适合高并发):
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/app
username: root
password: 123456
hikari:
maximum-pool-size: 10 # ❌ 太小
connection-timeout: 30000
idle-timeout: 600000
优化配置(根据业务调整):
spring:
datasource:
hikari:
maximum-pool-size: 50 # 根据CPU核心数和DB性能调整
minimum-idle: 10
connection-timeout: 20000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
Redis缓存策略设计
在Spring Boot项目实战案例中,缓存是提升性能的关键组件:
# application.yml
spring:
data:
redis:
host: localhost
port: 6379
timeout: 2000ms
lettuce:
pool:
max-active: 100
max-idle: 10
min-idle: 5
# 缓存配置
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
异步处理与线程池
异步任务配置:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("Async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
使用异步服务:
@Service
public class EmailService {
@Async("taskExecutor")
public void sendEmailAsync(String email, String content) {
// 发送邮件逻辑
}
}
接口性能优化实战
| 优化点 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 数据库查询 | N+1查询 | JOIN查询 + 分页 | 500ms → 80ms |
| Redis缓存 | 无缓存 | 热点数据缓存 | 300ms → 15ms |
| 异步处理 | 同步阻塞 | 异步 + 回调 | 2000ms → 50ms |
| 接口压缩 | 无压缩 | GZIP压缩 | 带宽减少60% |
监控与调优工具
使用Spring Boot Actuator + Micrometer进行监控:
management:
endpoints:
web:
exposure:
include: health,info,metrics,httptrace,threaddump
metrics:
export:
prometheus:
enabled: true
enable:
http: true
jvm: true
process: true
配合Prometheus + Grafana构建监控体系:
- 请求耗时分布(P50/P95/P99)
- 数据库连接池状态
- JVM内存使用情况
- 线程池活动状态
日志监控:构建生产级日志体系
在Spring Boot项目案例中,完善的日志体系是故障排查的生命线。我们通过真实案例,展示如何构建企业级日志监控系统。
日志配置优化
基础Logback配置(logback-spring.xml):
<configuration>
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="app"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${APP_NAME}-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${APP_NAME}-info.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
</root>
</configuration>
分布式链路追踪(Sleuth + Zipkin)
添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
配置:
spring:
sleuth:
sampler:
probability: 1.0 # 100%采样
zipkin:
base-url: http://localhost:9411
日志中添加业务标识
通过MDC(Mapped Diagnostic Context)添加自定义字段:
@Component
public class RequestIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.remove("requestId");
}
}
}
日志模板更新:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{requestId}] - %msg%n</pattern>
异常日志最佳实践
全局异常处理器:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<ErrorResult> handleException(HttpServletRequest request, Exception e) {
String requestId = MDC.get("requestId");
log.error("Request [{}] failed: {}", requestId, request.getRequestURI(), e);
ErrorResult error = new ErrorResult();
error.setRequestId(requestId);
error.setTimestamp(System.currentTimeMillis());
error.setMessage("Internal server error");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
日志安全与合规
- 敏感信息脱敏:密码、身份证、手机号等字段进行掩码处理
- 日志分级输出:生产环境不输出DEBUG日志
- 日志审计:关键操作记录用户ID和操作内容
- 日志保留策略:根据合规要求设置保留周期
脱敏示例:
public class LogSanitizer {
public static String maskPhone(String phone) {
if (phone == null) return null;
return phone.replaceAll("(\d{3})\d{4}(\d{4})", "$1$2");
}
public static String maskIdCard(String id) {
if (id == null) return null;
return id.replaceAll("(\d{6})\d{8}(\d{4})", "$1$2");
}
}
文档规范:让项目可交接、可维护
在Spring Boot项目案例中,完善的文档体系是项目成功交付的基石。我们从架构文档、接口文档到部署手册,构建完整的文档体系。
架构设计文档模板
# 项目架构设计文档
## 系统概述
- 项目名称:用户中心服务
- 版本:v1.2.0
- 开发团队:后端开发组
## 架构图
[系统架构图]
[数据流向图]
[部署拓扑图]
## 技术栈
- 核心框架:Spring Boot 2.7.5
- 数据库:MySQL 8.0 + Redis 6.2
- 消息队列:RocketMQ 4.9.3
- 监控:Prometheus + Grafana
## 模块划分
- 用户模块:用户注册、登录、信息管理
- 权限模块:RBAC权限模型实现
- 审计模块:操作日志记录
## 关键设计决策
- 为什么选择MySQL而非MongoDB?
- 为什么使用Redis缓存而非Ehcache?
- 分库分表策略:按用户ID取模
## 部署方案
- 容器化:Docker + Kubernetes
- 高可用:双机热备 + 负载均衡
- 监控告警:Prometheus + AlertManager
## 风险与应对
- 数据库主从延迟:读写分离 + 读从库延迟检测
- 缓存击穿:布隆过滤器 + 互斥锁
- 接口限流:Redis +令牌桶算法
Swagger接口文档自动化
添加依赖:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
配置类:
@Configuration
@EnableSwagger2WebMvc
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(Arrays.asList(
new ParameterBuilder()
.name("Authorization")
.description("Bearer Token")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build()
));
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("用户中心API文档")
.description("用户中心服务RESTful API接口文档")
.version("1.2.0")
.contact(new Contact("Dev Team", "", "dev@example.com"))
.build();
}
}
部署手册标准模板
# 用户中心部署手册
## 环境要求
- JDK 17+
- MySQL 8.0+
- Redis 6.2+
- 操作系统:CentOS 7.6+
## 部署步骤
### 准备配置文件
```bash
# /opt/user-service/config/application-prod.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://prod-db:3306/user_center?useSSL=false
username: prod_user
password: ${DB_PASSWORD}
redis:
host: prod-redis
port: 6379
```
### 启动服务
```bash
# 创建日志目录
mkdir -p /opt/user-service/logs
# 启动脚本
nohup java -Xms512m -Xmx1024m
-Dspring.profiles.active=prod
-Dspring.config.location=/opt/user-service/config/application-prod.yml
-jar /opt/user-service/user-service.jar
> /opt/user-service/logs/app.log 2>&1 &
# 检查启动状态
tail -f /opt/user-service/logs/app.log
```
### 验证服务
```bash
# 健康检查
curl http://localhost:8080/actuator/health
# 接口测试
curl -H "Authorization: Bearer YOUR_TOKEN"
http://localhost:8080/api/users/1
常见问题清单(FAQ)
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 服务启动后自动退出 | 端口被占用 | 使用lsof -i:8080检查端口占用 |
| 数据库连接失败 | 密码错误或网络不通 | 使用telnet db-host 3306测试连通性 |
| Redis连接超时 | 连接池配置过小 | 增加max-active配置 |
| 接口返回500 | 代码异常或配置缺失 | 查看logs/app.log中的异常堆栈 |
项目交接检查清单
- ✅ 所有配置文件已外部化(不在代码中硬编码)
- ✅ 数据库脚本已完整(DDL + DML)
- ✅ 接口文档已同步到Swagger
- ✅ 部署手册已更新并验证
- ✅ 监控告警已配置并测试
- ✅ 敏感信息已加密(数据库密码、API密钥)
- ✅ 日志体系已建立(按天分割、分级输出)
- ✅ 常见问题清单已整理
网友们还关心的Spring Boot项目案例问题
在Spring Boot项目实战案例中,大文件上传(如1GB+)是常见需求。关键在于:流式处理 + 分片上传 + 进度追踪。
配置文件大小限制
# application.yml
spring:
servlet:
multipart:
max-file-size: 2GB
max-request-size: 2GB
enabled: true
分片上传实现
前端使用Dropzone.js或自定义分片逻辑,后端接收分片并合并:
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@PostMapping("/chunk")
public ResponseEntity<String> uploadChunk(@RequestParam("file") MultipartFile file,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("chunkTotal") int chunkTotal,
@RequestParam("fileId") String fileId) {
String chunkPath = "/uploads/" + fileId + "/chunk_" + chunkIndex;
file.transferTo(new File(chunkPath));
if (chunkIndex == chunkTotal - 1) {
// 合并分片
mergeChunks(fileId, chunkTotal);
}
return ResponseEntity.ok("Chunk " + chunkIndex + " uploaded");
}
private void mergeChunks(String fileId, int totalChunks) {
String targetPath = "/uploads/" + fileId + ".zip";
try (FileOutputStream out = new FileOutputStream(targetPath)) {
for (int i = 0; i < totalChunks; i++) {
Path chunkPath = Paths.get("/uploads/" + fileId + "/chunk_" + i);
Files.copy(chunkPath, out);
}
} catch (IOException e) {
throw new RuntimeException("Failed to merge chunks", e);
}
}
}
进度追踪实现
使用Redis记录上传进度:
@Service
public class UploadProgressService {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
public void recordProgress(String fileId, int currentChunk, int totalChunks) {
double progress = (double) (currentChunk + 1) / totalChunks 100;
redisTemplate.opsForValue().set("upload:progress:" + fileId, (int) progress, 1, TimeUnit.HOURS);
}
public Integer getProgress(String fileId) {
return redisTemplate.opsForValue().get("upload:progress:" + fileId);
}
}
MultipartFile的getInputStream()进行流式处理
在Spring Boot项目案例中,灰度发布是降低上线风险的重要手段。我们介绍三种实现方式:
基于Nginx的灰度发布
配置Nginx根据请求特征分流:
upstream app_gray {
server 192.168.1.10:8080 weight=10; # 灰度版本
}
upstream app_prod {
server 192.168.1.20:8080 weight=90; # 正式版本
}
server {
listen 80;
location / {
# 根据Cookie分流
if ($cookie_gray = "true") {
proxy_pass http://app_gray;
}
proxy_pass http://app_prod;
}
}
基于Spring Cloud Gateway的灰度
使用路由断言实现灰度:
spring:
cloud:
gateway:
routes:
- id: gray-service
uri: http://gray-service:8081
predicates:
- Cookie=gray,true
filters:
- StripPrefix=1
- id: prod-service
uri: http://prod-service:8080
predicates:
- Path=/
filters:
- StripPrefix=1
基于自定义Header的灰度
在Controller中根据Header分流:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id,
@RequestHeader(value = "X-Release-Version", required = false) String version) {
if ("gray".equals(version)) {
// 使用灰度版本逻辑
return grayUserService.getUser(id);
} else {
// 使用正式版本逻辑
return userService.getUser(id);
}
}
}
在复杂的Spring Boot项目实战案例中,多数据源(如读写分离、分库分表)是常见需求。我们实现一个基于注解的动态数据源切换方案。
数据源配置
# application.yml
spring:
datasource:
primary:
url: jdbc:mysql://localhost:3306/primary
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
url: jdbc:mysql://localhost:3306/secondary
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
动态数据源实现
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
log.info("切换数据源:{}", dataSourceType);
CONTEXT.set(dataSourceType);
}
public static String getDataSourceType() {
return CONTEXT.get();
}
public static void clearDataSourceType() {
CONTEXT.remove();
}
}
注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "primary";
}
切面实现
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
@Pointcut("@annotation(com.example.annotation.DataSource)")
public void dataSourcePointcut() {}
@Before("dataSourcePointcut()")
public void before(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = signature.getMethod().getAnnotation(DataSource.class);
DataSourceContextHolder.setDataSourceType(dataSource.value());
}
@After("dataSourcePointcut()")
public void after() {
DataSourceContextHolder.clearDataSourceType();
}
}
使用示例
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@DataSource("primary")
public User getUser(Long id) {
return jdbcTemplate.queryForObject("SELECT FROM user WHERE id = ?",
new BeanPropertyRowMapper<>(User.class), id);
}
@DataSource("secondary")
public List<Order> getUserOrders(Long userId) {
return jdbcTemplate.query("SELECT FROM orders WHERE user_id = ?",
new BeanPropertyRowMapper<>(Order.class), userId);
}
}
在Spring Boot项目案例中,集成微信、QQ、GitHub等第三方登录是常见需求。我们以GitHub为例,展示完整集成流程。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
配置第三方登录信息
# application.yml
spring:
security:
oauth2:
client:
registration:
github:
client-id: YOUR_GITHUB_CLIENT_ID
client-secret: YOUR_GITHUB_CLIENT_SECRET
scope: read:user,user:email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
client-name: GitHub
provider:
github:
authorization-uri: https://github.com/login/oauth/authorize
token-uri: https://github.com/login/oauth/access_token
user-info-uri: https://api.github.com/user
user-name-attribute: id
安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login/").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error")
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
);
return http.build();
}
}
用户信息获取
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal OAuth2User principal,
Model model) {
if (principal != null) {
model.addAttribute("username", principal.getAttribute("login"));
model.addAttribute("email", principal.getAttribute("email"));
model.addAttribute("avatar", principal.getAttribute("avatar_url"));
}
return "user/profile";
}
}
自定义用户信息映射
实现自定义OAuth2UserService:
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User user = super.loadUser(userRequest);
// 自定义映射逻辑
Map<String, Object> attributes = new HashMap<>(user.getAttributes());
attributes.put("provider", "github");
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
attributes,
"id"
);
}
}