引言
你好呀,我是小邹。
SpringBoot作为当今Java领域最流行的开发框架,极大地简化了Spring应用的初始搭建和开发过程。然而在实际开发中,开发者仍然会遇到各种问题。本文将通过具体代码示例,详细分析SpringBoot开发中的常见问题及其解决方案。
1. 配置文件与多环境配置问题
问题描述
配置文件加载顺序混乱,多环境切换不生效
解决方案
# application.yml - 主配置文件
spring:
profiles:
active: @activatedProperties@ # Maven属性占位符
# application-dev.yml - 开发环境
server:
port: 8080
servlet:
context-path: /api
datasource:
url: jdbc:mysql://localhost:3306/dev_db
username: dev_user
password: dev_pass
# application-prod.yml - 生产环境
server:
port: 8081
datasource:
url: jdbc:mysql://prod-server:3306/prod_db
username: ${DB_USERNAME:prod_user} # 环境变量优先
password: ${DB_PASSWORD}
// 自定义配置类示例
@Configuration
@ConfigurationProperties(prefix = "custom")
@Data
public class CustomConfig {
private String endpoint;
private Integer timeout = 5000; // 默认值
private List<String> servers = new ArrayList<>();
}
// 使用@Value注入
@Component
public class AppConfig {
@Value("${server.port:8080}") // 默认值8080
private Integer port;
@Value("${app.feature.enabled:false}")
private Boolean featureEnabled;
}
2. 自动装配与依赖注入问题
问题描述
Bean创建失败、循环依赖、多实现类注入冲突
解决方案
// 1. 解决循环依赖 - 使用@Lazy
@Component
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
}
// 2. 多实现类注入 - 使用@Primary或@Qualifier
public interface MessageService {
void send(String message);
}
@Component
@Primary // 默认使用此实现
public class EmailService implements MessageService {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
@Component
@Qualifier("sms")
public class SmsService implements MessageService {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
@Component
public class NotificationService {
@Autowired
private MessageService emailService; // 自动注入EmailService
@Autowired
@Qualifier("sms")
private MessageService smsService;
}
// 3. 条件化Bean创建
@Configuration
public class FeatureConfig {
@Bean
@ConditionalOnProperty(name = "feature.enabled", havingValue = "true")
public FeatureService featureService() {
return new FeatureService();
}
@Bean
@ConditionalOnClass(name = "com.example.ExternalClass")
public ExternalIntegration externalIntegration() {
return new ExternalIntegration();
}
}
3. 事务管理失效问题
问题描述
@Transactional注解不生效,事务回滚失败
解决方案
@Service
@Slf4j
public class OrderService {
// 1. 事务方法必须public,且自调用失效
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO orderDTO) {
try {
orderRepository.save(convertToEntity(orderDTO));
// 业务逻辑...
} catch (Exception e) {
log.error("创建订单失败", e);
throw new BusinessException("订单创建失败"); // 抛出异常才会回滚
}
}
// 2. 不同传播行为示例
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 默认:存在事务则加入,否则新建
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 总是新建事务,挂起当前事务
}
@Transactional(propagation = Propagation.NESTED)
public void methodC() {
// 嵌套事务,部分数据库支持
}
}
// 3. 配置类确保事务管理器正确配置
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager manager) {
return new TransactionTemplate(manager);
}
}
4. 数据库连接池配置问题
问题描述
连接泄露、连接数不足、性能问题
解决方案
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: HikariPool
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 30000
connection-timeout: 30000
max-lifetime: 1800000
connection-test-query: SELECT 1
leak-detection-threshold: 60000 # 连接泄露检测(毫秒)
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
show_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
// 连接池监控端点
@RestController
@RequestMapping("/monitor")
public class ConnectionPoolMonitor {
@Autowired
private DataSource dataSource;
@GetMapping("/pool/status")
public Map<String, Object> getPoolStatus() {
Map<String, Object> status = new HashMap<>();
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikari = (HikariDataSource) dataSource;
status.put("activeConnections", hikari.getHikariPoolMXBean().getActiveConnections());
status.put("idleConnections", hikari.getHikariPoolMXBean().getIdleConnections());
status.put("totalConnections", hikari.getHikariPoolMXBean().getTotalConnections());
status.put("threadsAwaiting", hikari.getHikariPoolMXBean().getThreadsAwaitingConnection());
}
return status;
}
}
// 连接泄露检测
@Component
@Slf4j
public class ConnectionLeakDetector {
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void checkConnectionLeak() {
// 实现连接泄露检测逻辑
log.info("Connection leak detection running...");
}
}
5. 缓存使用问题
问题描述
缓存穿透、缓存雪崩、缓存击穿、缓存一致性
解决方案
@Service
@Slf4j
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1. 解决缓存穿透 - 缓存空值
@Cacheable(value = "products", key = "#id",
unless = "#result == null",
cacheManager = "redisCacheManager")
public Product getProductById(Long id) {
// 先查缓存,缓存中查不到再查数据库
Product product = productRepository.findById(id).orElse(null);
// 如果数据库不存在,缓存空值(设置较短过期时间)
if (product == null) {
redisTemplate.opsForValue()
.set("product:null:" + id, "", 5, TimeUnit.MINUTES);
}
return product;
}
// 2. 解决缓存击穿 - 互斥锁
public Product getProductWithLock(Long id) {
String cacheKey = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product == null) {
String lockKey = "lock:product:" + id;
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (lockAcquired != null && lockAcquired) {
try {
// 双重检查
product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product == null) {
product = productRepository.findById(id).orElse(null);
if (product != null) {
redisTemplate.opsForValue()
.set(cacheKey, product, 30, TimeUnit.MINUTES);
}
}
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 等待重试
try {
Thread.sleep(100);
return getProductWithLock(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
return product;
}
// 3. 缓存一致性 - 使用@CacheEvict
@CacheEvict(value = "products", key = "#id")
@Transactional
public void updateProduct(Long id, Product product) {
productRepository.update(product);
// 异步更新缓存
CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue()
.set("product:" + id, product, 30, TimeUnit.MINUTES);
});
}
}
// 缓存配置类
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(getCacheConfigurations())
.transactionAware()
.build();
}
private Map<String, RedisCacheConfiguration> getCacheConfigurations() {
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("products",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)));
configMap.put("users",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)));
return configMap;
}
}
6. 全局异常处理问题
问题描述
异常处理分散、HTTP状态码不正确、错误信息不统一
解决方案
// 自定义异常类
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {
private final String code;
private final String message;
public BusinessException(String code, String message) {
super(message);
this.code = code;
this.message = message;
}
}
// 统一响应封装
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private String code;
private String message;
private T data;
private Long timestamp;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.code("200")
.message("success")
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
}
// 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 处理业务异常
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Object> handleBusinessException(BusinessException e) {
log.error("业务异常: {}", e.getMessage(), e);
return ApiResponse.builder()
.code(e.getCode())
.message(e.getMessage())
.timestamp(System.currentTimeMillis())
.build();
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Object> handleValidationException(
MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
return ApiResponse.builder()
.code("400")
.message("参数校验失败")
.data(errors)
.timestamp(System.currentTimeMillis())
.build();
}
// 处理其他所有异常
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResponse<Object> handleGlobalException(Exception e) {
log.error("系统异常: ", e);
return ApiResponse.builder()
.code("500")
.message("系统内部错误")
.timestamp(System.currentTimeMillis())
.build();
}
}
// 参数校验使用示例
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
@RestController
@RequestMapping("/users")
@Validated // 类级别开启校验
public class UserController {
@PostMapping
public ApiResponse<User> createUser(@RequestBody @Valid UserDTO userDTO) {
// 参数会自动校验
return ApiResponse.success(userService.create(userDTO));
}
@GetMapping("/{id}")
public ApiResponse<User> getUser(@PathVariable @Min(1) Long id) {
// 路径参数校验
return ApiResponse.success(userService.getById(id));
}
}
7. 跨域问题
问题描述
前端访问API时出现CORS错误
解决方案
// 配置类方式
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
// 过滤器方式(更灵活)
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
String origin = request.getHeader("Origin");
if (origin != null) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods",
"POST, GET, OPTIONS, DELETE, PUT, PATCH");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, " +
"Authorization, X-CSRF-Token");
}
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
}
8. 文件上传问题
问题描述
文件大小限制、文件类型限制、文件名重复
解决方案
# application.yml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
enabled: true
file-size-threshold: 2MB
@RestController
@RequestMapping("/files")
@Slf4j
public class FileUploadController {
@Value("${file.upload-dir:./uploads}")
private String uploadDir;
@PostMapping("/upload")
public ApiResponse<String> uploadFile(@RequestParam("file") MultipartFile file) {
try {
// 1. 校验文件类型
String contentType = file.getContentType();
if (!isAllowedFileType(contentType)) {
throw new BusinessException("FILE_TYPE_ERROR", "不支持的文件类型");
}
// 2. 校验文件大小
if (file.getSize() > 10 * 1024 * 1024) { // 10MB
throw new BusinessException("FILE_SIZE_ERROR", "文件大小不能超过10MB");
}
// 3. 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = getFileExtension(originalFilename);
String newFilename = UUID.randomUUID().toString() + fileExtension;
// 4. 保存文件
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
Path filePath = uploadPath.resolve(newFilename);
Files.copy(file.getInputStream(), filePath,
StandardCopyOption.REPLACE_EXISTING);
// 5. 返回文件信息
return ApiResponse.success(newFilename);
} catch (IOException e) {
log.error("文件上传失败", e);
throw new BusinessException("UPLOAD_ERROR", "文件上传失败");
}
}
private boolean isAllowedFileType(String contentType) {
Set<String> allowedTypes = Set.of(
"image/jpeg", "image/png", "image/gif",
"application/pdf", "text/plain"
);
return allowedTypes.contains(contentType);
}
private String getFileExtension(String filename) {
if (filename == null || filename.lastIndexOf(".") == -1) {
return "";
}
return filename.substring(filename.lastIndexOf("."));
}
}
// 文件下载示例
@GetMapping("/download/{filename:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
try {
Path filePath = Paths.get(uploadDir).resolve(filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (resource.exists() && resource.isReadable()) {
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.body(resource);
} else {
throw new FileNotFoundException("文件不存在: " + filename);
}
} catch (IOException e) {
throw new BusinessException("DOWNLOAD_ERROR", "文件下载失败");
}
}
9. 接口幂等性问题
问题描述
重复提交导致数据不一致
解决方案
// 幂等性注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String key() default ""; // 幂等键
long expire() default 3600; // 过期时间(秒)
}
// 幂等性切面
@Aspect
@Component
@Slf4j
public class IdempotentAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent)
throws Throwable {
// 1. 生成幂等键
String idempotentKey = generateIdempotentKey(joinPoint, idempotent);
// 2. 检查是否已处理
Boolean isProcessed = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", idempotent.expire(), TimeUnit.SECONDS);
if (isProcessed != null && isProcessed) {
// 第一次请求,执行业务逻辑
try {
return joinPoint.proceed();
} catch (Exception e) {
// 业务执行失败,删除幂等键
redisTemplate.delete(idempotentKey);
throw e;
}
} else {
// 重复请求,返回之前的处理结果
log.warn("重复请求,幂等键: {}", idempotentKey);
throw new BusinessException("REPEAT_REQUEST", "请不要重复提交");
}
}
private String generateIdempotentKey(ProceedingJoinPoint joinPoint,
Idempotent idempotent) {
// 组合生成唯一键:用户ID + 方法名 + 参数
String userId = getCurrentUserId(); // 获取当前用户ID
String methodName = joinPoint.getSignature().toShortString();
String argsHash = DigestUtils.md5DigestAsHex(
Arrays.toString(joinPoint.getArgs()).getBytes());
return String.format("idempotent:%s:%s:%s",
userId, methodName, argsHash);
}
private String getCurrentUserId() {
// 从SecurityContext获取用户ID
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication.getName();
}
return "anonymous";
}
}
// 使用示例
@RestController
@RequestMapping("/orders")
public class OrderController {
@PostMapping
@Idempotent(expire = 300) // 5分钟内防重复提交
public ApiResponse<Order> createOrder(@RequestBody OrderDTO orderDTO) {
// 业务逻辑
return ApiResponse.success(orderService.create(orderDTO));
}
}
10. 性能监控与优化
问题描述
应用性能瓶颈难以定位,内存泄漏
解决方案
// 1. 性能监控切面
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
@Around("@within(org.springframework.web.bind.annotation.RestController)")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 记录慢查询
if (duration > 1000) { // 超过1秒
log.warn("慢接口: {}.{} 耗时 {}ms",
className, methodName, duration);
// 发送告警或记录到数据库
recordSlowQuery(className, methodName, duration, joinPoint.getArgs());
}
return result;
} catch (Throwable e) {
long endTime = System.currentTimeMillis();
log.error("接口异常: {}.{} 耗时 {}ms, 异常: {}",
className, methodName, endTime - startTime, e.getMessage());
throw e;
}
}
private void recordSlowQuery(String className, String methodName,
long duration, Object[] args) {
// 实现慢查询记录逻辑
}
}
// 2. 数据库性能监控
@Component
@Slf4j
public class SqlMonitorInterceptor {
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
// 设置慢SQL阈值
jdbcTemplate.setQueryTimeout(30); // 30秒超时
// 监控SQL执行
jdbcTemplate.execute("SET GLOBAL slow_query_log = 'ON'");
jdbcTemplate.execute("SET GLOBAL long_query_time = 2"); // 2秒
}
public void logSlowQuery(String sql, long duration) {
if (duration > 2000) { // 2秒
log.warn("慢SQL查询: {} 耗时 {}ms", sql, duration);
}
}
}
// 3. 内存使用监控
@Component
@Slf4j
public class MemoryMonitor {
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void checkMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
double usagePercentage = (double) usedMemory / maxMemory * 100;
if (usagePercentage > 80) {
log.error("内存使用率过高: {}%, 已使用: {}MB, 最大: {}MB",
String.format("%.2f", usagePercentage),
usedMemory / (1024 * 1024),
maxMemory / (1024 * 1024));
// 触发GC
System.gc();
}
log.debug("内存使用情况: 使用率={}%, 已使用={}MB, 空闲={}MB, 最大={}MB",
String.format("%.2f", usagePercentage),
usedMemory / (1024 * 1024),
freeMemory / (1024 * 1024),
maxMemory / (1024 * 1024));
}
}
// 4. 健康检查端点
@RestController
@RequestMapping("/health")
public class HealthCheckController {
@Autowired
private DataSource dataSource;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/detailed")
public Map<String, Object> detailedHealth() {
Map<String, Object> health = new HashMap<>();
// 数据库健康检查
try {
dataSource.getConnection().close();
health.put("database", "UP");
} catch (SQLException e) {
health.put("database", "DOWN");
health.put("database_error", e.getMessage());
}
// Redis健康检查
try {
redisTemplate.opsForValue().get("health_check");
health.put("redis", "UP");
} catch (Exception e) {
health.put("redis", "DOWN");
health.put("redis_error", e.getMessage());
}
// 系统信息
health.put("timestamp", System.currentTimeMillis());
health.put("uptime", ManagementFactory.getRuntimeMXBean().getUptime());
health.put("active_threads", Thread.activeCount());
return health;
}
}
总结
SpringBoot开发中遇到的问题多种多样,但通过合理的配置、良好的编码习惯和适当的监控手段,大多数问题都可以得到有效解决。本文涵盖了从配置管理到性能优化的十个关键问题领域,并提供了实用的代码示例。实际开发中,还需要结合具体业务场景不断积累经验,形成适合自己项目的解决方案。
最佳实践建议:
- 保持配置外部化,便于不同环境部署
- 合理设计事务边界,避免长事务
- 使用缓存时要考虑一致性问题
- 统一异常处理和日志记录
- 重要接口实现幂等性
- 定期监控系统性能指标
- 编写完善的单元测试和集成测试
通过持续学习和实践,掌握这些问题的解决方案,将能大大提升SpringBoot应用的稳定性和可维护性。

评论区 0