引言
在Spring Boot的开发过程中,即使是经验丰富的开发者也难免会遇到各种棘手的问题。这些问题往往隐藏在细节之中,不易被察觉,但却会对项目的稳定性和性能产生重大影响。本文将针对Spring Boot开发中十大常见的"坑"进行深度解析,结合具体的代码示例,提供详细的解决方案,帮助开发者们有效避坑。
一、配置总出错?是不是同时用了.properties和.yml?
在Spring Boot项目中,.properties
和.yml
两种配置文件都可用于设置项目参数。但同时使用且配置项冲突时,就会导致配置错误。这是因为Spring Boot加载配置文件时,二者优先级和解析方式存在差异。
问题根源
当.properties
和.yml
文件存在相同配置项,.properties
文件的配置会覆盖.yml
文件。例如,.yml
中配置端口号为8081
,.properties
中设为8080
,项目启动时最终使用8080
端口。
解决方案
- 统一配置文件类型:建议项目中仅使用一种配置文件类型,避免冲突。
- 明确配置优先级 :若必须同时使用,可在
application.properties
添加spring.config.import=optional:classpath:application.yml
,先加载.properties
,再加载.yml
,.properties
覆盖.yml
相同配置项。
示例代码
application.properties
文件内容:
properties
server.port=8080
application.yml
文件内容:
yaml
server:
port: 8081
启动项目后,访问http://localhost:8080
可正常访问。若想让.yml
配置生效,在.properties
添加上述配置后重启,访问http://localhost:8081
即可。
二、换个位置配置就失效?搞清楚加载顺序了吗?
Spring Boot加载配置文件顺序固定,不同位置优先级不同。配置文件放置位置错误或未遵循加载顺序,会导致配置失效。
问题根源
加载顺序如下:
- 当前目录下的
config
目录。 - 当前目录。
- 类路径下的
config
包。 - 类路径根目录。
若将类路径根目录下application.yml
移至当前目录config
目录,因加载顺序变化,配置可能失效或被覆盖。
解决方案
- 了解配置文件加载顺序:掌握加载机制,确保配置文件位置正确。
- 使用多环境配置 :利用
spring.profiles.active
指定环境配置文件,如开发环境用application-dev.yml
,生产环境用application-prod.yml
,避免不同环境配置干扰。
示例代码
src/main/resources/application.yml
数据库连接配置:
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: 123456
移至src/main/resources/config/application.yml
,若无其他干扰,配置仍生效。若当前目录config
目录存在同名不同内容文件,则以该文件配置为准。
多环境配置:在src/main/resources
创建application-dev.yml
和application-prod.yml
,application.yml
添加:
yaml
spring:
profiles:
active: dev
application-dev.yml
内容:
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/devdb
username: devuser
password: devpassword
application-prod.yml
内容:
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/proddb
username: produser
password: prodpassword
开发环境使用application-dev.yml
配置,生产环境修改spring.profiles.active
切换配置。
三、定时任务不定时?是不是线程调度出问题了?
使用Spring Boot定时任务时,可能出现执行时间不准甚至不执行的情况,通常是线程调度问题导致。
问题根源
- 默认线程池限制:默认使用单线程线程池执行任务,多个任务且执行时间长时,会排队等待,导致执行时间不准确。
- 任务依赖或阻塞:任务间存在依赖或执行中阻塞,影响正常调度。
解决方案
- 自定义线程池 :通过配置类使用
@EnableScheduling
开启定时任务,用@Configuration
和@Bean
定义线程池,增加线程数提升并发能力。 - 优化任务逻辑:避免任务依赖和阻塞,确保独立高效执行。
示例代码
配置类开启定时任务并定义线程池:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
@EnableScheduling
public class ScheduleConfig {
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 设置线程池大小为10
scheduler.setThreadNamePrefix("ScheduleThread-");
return scheduler;
}
}
定义定时任务:
java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class MyScheduledTask {
@Scheduled(cron = "0/5 * * * * *") // 每5秒执行一次
public void executeTask() {
System.out.println("定时任务执行中...");
}
}
自定义线程池使多个任务并行执行,提高效率和准确性。
四、线程池报错查不出原因?异步任务处理好了吗?
Spring Boot使用线程池处理异步任务时,可能出现报错且难定位问题,通常是异步任务处理不当。
问题根源
- 未正确配置线程池:线程池配置不合理,如队列容量小、线程数不足,大量任务提交时会拒绝任务报错。
- 异常处理不当:异步任务执行异常时,无正确处理机制,异常信息无法捕获,导致问题难排查。
解决方案
- 合理配置线程池:根据任务特点和系统资源,合理设置核心线程数、最大线程数、队列容量等参数。
- 添加异常处理机制:在异步任务中添加异常处理代码,或通过全局异常处理捕获异常。
示例代码
定义线程池配置类:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(200); // 队列容量
executor.setKeepAliveSeconds(60); // 空闲线程存活时间
executor.setThreadNamePrefix("AsyncThread-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
executor.initialize();
return executor;
}
}
定义异步任务:
java
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncTask {
@Async("taskExecutor")
public void executeAsyncTask() {
try {
// 模拟任务执行
Thread.sleep(2000);
System.out.println("异步任务执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制器调用异步任务:
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 AsyncTaskController {
@Autowired
private AsyncTask asyncTask;
@GetMapping("/async")
public String executeAsync() {
asyncTask.executeAsyncTask();
return "异步任务已提交";
}
}
添加全局异常处理捕获异步任务异常:
java
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
@Configuration
public class AsyncExceptionHandlerConfig implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
System.out.println("异步任务发生异常:" + throwable.getMessage());
throwable.printStackTrace();
}
}
确保异步任务稳定执行,异常及时处理。
五、接口响应慢?是不是ObjectMapper重复实例化了?
处理JSON数据接口时,可能出现响应慢的情况,通常是ObjectMapper
重复实例化导致性能问题。
问题根源
ObjectMapper
是Jackson库处理JSON数据的核心类,初始化耗时。每次处理JSON数据都重新实例化,会浪费时间和资源,导致接口响应慢。
解决方案
- 使用单例模式 :实例化一次
ObjectMapper
,通过依赖注入使用,避免重复创建。 - 自定义配置 :对
ObjectMapper
进行自定义配置,如设置日期格式、忽略未知属性等,同时保证单例性。
示例代码
定义ObjectMapper
配置类:
java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
服务类使用ObjectMapper
:
java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class JsonService {
private final ObjectMapper objectMapper;
@Autowired
public JsonService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public String convertToJson(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
将ObjectMapper
作为单例Bean管理,提升接口响应速度。
六、依赖冲突频发?版本管理策略有没有漏洞?
Spring Boot项目依赖增多时,常出现依赖冲突,通常是版本管理策略有漏洞。
问题根源
- 传递依赖冲突:引入多个依赖,它们依赖同一库不同版本,导致冲突。如A依赖需1.0版本X库,B依赖需2.0版本X库。
- 手动版本覆盖不当:手动指定依赖版本时,未正确处理版本兼容性,引发冲突。
解决方案
- 使用依赖管理插件 :利用Spring Boot依赖管理插件,通过
pom.xml
查看依赖树,分析冲突并调整版本。 - 排除冲突依赖 :对不需要的传递依赖,用
<exclusions>
标签排除,手动引入正确版本。
示例代码
假设项目引入dependencyA
和dependencyB
,都依赖commonLib
但版本不同:
xml
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>dependencyA</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>dependencyB</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
通过mvn dependency:tree
查看依赖树发现冲突:
csharp
[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT
[INFO] +- com.example:dependencyA:jar:1.0:compile
[INFO] | \- com.example:commonLib:jar:1.0:compile
[INFO] +- com.example:dependencyB:jar:1.0:compile
[INFO] | \- com.example:commonLib:jar:2.0:compile
排除dependencyA
冲突依赖并手动引入正确版本:
xml
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>dependencyA</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>com.example</groupId>
<artifactId>commonLib</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>dependencyB</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>commonLib</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
解决依赖冲突问题。
七、跨域请求总失败?是不是注解或过滤器漏了?
前后端分离项目中,跨域请求常见,但配置不当会导致请求失败。
问题根源
- 未添加跨域注解或过滤器 :Spring Boot默认不允许跨域请求,需添加
@CrossOrigin
注解或配置跨域过滤器启用支持,遗漏则请求失败。 - 配置参数错误:即使添加配置,若允许域名、请求方法、请求头信息等参数错误,跨域请求也无法正常进行。
解决方案
- 使用@CrossOrigin注解 :在控制器类或方法添加
@CrossOrigin
注解,指定允许的跨域请求信息。 - 配置跨域过滤器:自定义过滤器拦截所有请求,添加跨域响应头信息。
示例代码
使用@CrossOrigin
注解:
java
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController
@CrossOrigin(origins = "http://localhost:3000", allowedHeaders = "*", methods = {GET, POST})
public class CrossOriginController {
@GetMapping("/data")
public String getData() {
return "跨域请求成功返回的数据";
}
}
配置跨域过滤器:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return new CorsFilter(source);
}
}
两种方式任选其一,解决跨域请求失败问题。
八、AOP切面不生效?代理模式和切入点搞对了吗?
使用Spring Boot的AOP时,可能出现切面不生效的情况,通常是代理模式和切入点配置问题。
问题根源
- 代理模式选择错误:AOP默认用JDK动态代理,只能代理实现接口的类。目标类未实现接口时,需用CGLIB代理,配置错误则切面不生效。
- 切入点表达式错误:切入点表达式定义切面织入方法,表达式错误则无法匹配目标方法,切面不生效。
解决方案
- 正确配置代理模式 :目标类未实现接口,在配置类添加
@EnableAspectJAutoProxy(proxyTargetClass = true)
启用CGLIB代理。 - 准确编写切入点表达式:掌握AspectJ切入点表达式语法,确保正确匹配目标方法。
示例代码
定义服务接口和实现类:
java
public interface UserService {
void addUser();
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("添加用户");
}
}
定义切面类:
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserServiceAspect {
@Around("execution(* com.example.service.UserService.addUser(..))")
public Object aroundAddUser(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("切面前置处理");
Object result = joinPoint.proceed();
System.out.println("切面后置处理");
return result;
}
}
若UserServiceImpl
未实现接口,配置类添加:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.aop.aspectj.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
}
通过上述配置,就能确保即使目标类没有实现接口,切面也能在其方法执行前后正常生效。例如,在项目启动后调用UserService
的addUser
方法,控制台会输出切面前置处理
、添加用户
、切面后置处理
,证明切面逻辑成功织入。
九、事务不回滚?传播机制和异常类型踩坑了吗?
在Spring Boot项目中运用事务管理功能时,事务不回滚的情况时有发生,这通常是由于对事务传播机制理解不到位,以及异常类型处理不当导致的。
问题根源
- 事务传播机制错误 :事务传播机制决定了事务方法在被其他事务方法调用时的行为。例如,当使用
REQUIRES_NEW
传播机制开启嵌套事务时,如果内层事务抛出异常但没有正确向上抛出,外层事务可能会提交,导致数据不一致 ,出现事务不回滚的假象。 - 异常类型未正确处理 :Spring Boot事务默认仅针对
RuntimeException
及其子类进行回滚操作。若业务代码抛出受检异常(Checked Exception),如IOException
、SQLException
等,事务不会自动回滚,而开发者可能误以为事务功能失效。
解决方案
- 合理选择事务传播机制 :根据业务逻辑的实际需求,谨慎选择事务传播机制。如普通的业务操作可使用
REQUIRED
,它会在有事务的上下文中加入事务,没有则新建事务;对于需要独立事务的操作,使用REQUIRES_NEW
时要确保异常能正确传递和处理。 - 配置事务回滚规则 :通过
@Transactional
注解的rollbackFor
属性,显式指定需要回滚的异常类型,包括受检异常,以满足特定业务场景的回滚需求。
示例代码
假设存在用户服务和订单服务,在创建订单时需要添加用户信息,且整个操作需在一个事务中,若出现异常则全部回滚。
首先定义UserService
:
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Transactional
public void addUser() {
// 模拟数据库插入操作,这里简化为打印日志
System.out.println("添加用户");
// 模拟业务逻辑错误,抛出异常
throw new RuntimeException("添加用户失败");
}
}
接着定义OrderService
:
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final UserService userService;
public OrderService(UserService userService) {
this.userService = userService;
}
// 设置所有异常都回滚事务
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
try {
userService.addUser();
System.out.println("创建订单");
} catch (Exception e) {
// 重新抛出异常,确保事务回滚
throw new RuntimeException("创建订单失败", e);
}
}
}
在上述代码中,OrderService
的createOrder
方法通过rollbackFor = Exception.class
配置了对所有异常进行回滚。当UserService
的addUser
方法抛出RuntimeException
时,createOrder
方法捕获异常后重新抛出,保证整个创建订单的事务操作能够回滚,避免出现部分操作成功、部分失败的数据不一致问题。
十、静态资源访问404?资源映射路径正确吗?
在Spring Boot项目中访问静态资源,如HTML、CSS、JavaScript文件时,经常会遇到404错误,这主要是由于静态资源映射路径配置错误或资源文件放置位置不当导致的。
问题根源
- 默认映射路径被覆盖 :Spring Boot对静态资源有默认的映射规则,会自动映射
classpath:/static
、classpath:/public
、classpath:/resources
、classpath:/META-INF/resources
路径下的静态资源。如果在项目配置中意外覆盖了这些默认规则,或者自定义的映射路径设置错误,就会导致静态资源无法正常访问。 - 资源文件位置错误 :静态资源文件必须放置在Spring Boot能够识别的目录下。若将文件放在错误的路径,比如在
src/main/java
目录下创建静态资源文件夹,Spring Boot无法自动扫描到,进而引发404错误。
解决方案
- 正确配置资源映射路径 :若需要自定义静态资源映射路径,可通过实现
WebMvcConfigurer
接口,重写addResourceHandlers
方法进行配置。 - 确保资源文件位置正确 :将静态资源文件放置在
src/main/resources
目录下的static
、public
等默认目录中,遵循Spring Boot的静态资源加载规则;若使用自定义路径,要保证文件实际存储位置与配置一致。
示例代码
假设项目需要将classpath:/custom-static/
目录下的静态资源映射到/my-static/**
路径进行访问。
创建配置类WebMvcConfig
:
java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/my-static/**")
.addResourceLocations("classpath:/custom-static/");
}
}
在src/main/resources
目录下创建custom-static
文件夹,并放入静态资源文件,如index.html
。启动项目后,访问http://localhost:8080/my-static/index.html
,即可正确访问到对应的静态资源。如果访问出现404错误,可检查资源文件是否存在、路径配置是否准确,以及项目是否正确加载了配置类。
总结
综上所述,Spring Boot开发中的这些常见 "坑" 虽然棘手,但只要深入理解其原理,结合正确的配置和代码示例,就能有效避免和解决问题。希望本文的解析和示例能为开发者们在Spring Boot开发过程中提供有力的帮助,让项目开发更加顺利、高效。