《Spring 编程常见错误 50 例》精华提炼。每个问题以 ❌ 反例 + ✅ 正确做法对比呈现,覆盖 IoC、AOP、事务、Spring MVC、Spring Boot 自动配置五大模块的高频陷阱。
相关笔记:Spring 容器启动流程源码分析 · SpringBoot 启动流程源码分析 · Java 业务开发陷阱 · Spring 术语与缩写速查
目录
| 章节 | 说明 |
|---|---|
| IoC 常见错误 | 循环依赖、多实例、条件注入 |
| AOP 常见错误 | this 调用绕过代理、切面顺序、切点表达式 |
| 事务常见错误 | @Transactional 失效 7 种场景、异常类型、传播行为 |
| Spring MVC 常见错误 | 参数绑定、过滤器顺序 |
| Spring Boot 自动配置陷阱 | 条件装配、配置覆盖、Starter 冲突 |
IoC 常见错误
1. 循环依赖
Spring 默认只能解决单例 setter/field 注入的循环依赖,构造器注入和 prototype 作用域无法自动解决。
❌ 构造器循环依赖
@Component
public class A {
private final B b;
public A(B b) { this.b = b; } // 构造器注入 → 启动报 BeanCurrentlyInCreationException
}
@Component
public class B {
private final A a;
public B(A a) { this.a = a; }
}
✅ 重构:抽取公共依赖或改用 setter 注入
// 方案一:抽取公共服务,打破循环
@Component
public class CommonService { ... }
@Component
public class A {
private final CommonService common;
public A(CommonService common) { this.common = common; }
}
// 方案二:改用 @Lazy 延迟注入(不推荐,掩盖设计问题)
@Component
public class A {
@Autowired @Lazy
private B b;
}
根本原则:循环依赖是设计问题,优先重构职责,而非绕过。Spring 6.x 默认禁止循环依赖(
spring.main.allow-circular-references=false)。
2. 单例 Bean 注入 Prototype Bean 导致多实例失效
❌ 单例持有 prototype 引用,始终是同一个实例
@Component
@Scope("singleton")
public class SingletonService {
@Autowired
private PrototypeService prototypeService; // 只注入一次,永远是同一个实例
public void doWork() {
prototypeService.execute(); // ❌ 期望每次新实例,实际是旧实例
}
}
✅ 使用 ObjectProvider 或 ApplicationContext 每次获取
@Component
public class SingletonService {
@Autowired
private ObjectProvider<PrototypeService> provider;
public void doWork() {
PrototypeService svc = provider.getObject(); // ✅ 每次获取新实例
svc.execute();
}
}
3. @Autowired 与 @Qualifier 配合失效
❌ 存在多个同类型 Bean,未指定 @Qualifier 导致 NoUniqueBeanDefinitionException
@Bean
public DataSource primaryDataSource() { ... }
@Bean
public DataSource secondaryDataSource() { ... }
@Autowired
private DataSource dataSource; // ❌ 两个 DataSource Bean,Spring 无法选择
✅ 用 @Qualifier 或 @Primary 明确指定
@Bean
@Primary
public DataSource primaryDataSource() { ... } // ✅ 标记首选
// 或在注入处指定
@Autowired
@Qualifier("secondaryDataSource")
private DataSource dataSource; // ✅ 明确指定
4. 条件注入 @ConditionalOnProperty 条件不匹配
❌ 配置文件 key 拼写错误,Bean 静默不创建
@Bean
@ConditionalOnProperty(name = "feature.cache.enable", havingValue = "true")
public CacheService cacheService() { ... }
// application.yml 中写的是 feature.cache.enabled=true(多了个 d)
// 结果:Bean 不创建,注入处 NPE
✅ 开启 matchIfMissing 或在测试中验证条件
@Bean
@ConditionalOnProperty(
name = "feature.cache.enable",
havingValue = "true",
matchIfMissing = false // 明确语义
)
public CacheService cacheService() { ... }
排查技巧:启动时加
--debug参数,查看 Conditions Evaluation Report,可以看到哪些 Bean 因条件不满足而未创建。
AOP 常见错误
5. this 调用绕过 Spring 代理
Spring AOP 基于动态代理(JDK 代理或 CGLIB),只有通过 Spring 容器获取的代理对象调用才会触发切面。同类内部 this 调用直接调原始对象,切面不生效。
❌ 同类内部调用,@Transactional/@Async 等注解失效
@Service
public class OrderService {
public void createOrder() {
// this 调用,走的是原始对象,@Transactional 不生效
this.saveOrder(); // ❌
}
@Transactional
public void saveOrder() {
// 事务注解失效
}
}
✅ 方案一:注入自身代理
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入代理对象
public void createOrder() {
self.saveOrder(); // ✅ 通过代理调用,事务生效
}
@Transactional
public void saveOrder() { ... }
}
✅ 方案二:拆分为两个 Bean(推荐)
@Service
public class OrderService {
@Autowired
private OrderPersistService persistService;
public void createOrder() {
persistService.saveOrder(); // ✅ 跨 Bean 调用,代理生效
}
}
@Service
public class OrderPersistService {
@Transactional
public void saveOrder() { ... }
}
6. 切面执行顺序混乱
多个切面作用于同一方法时,不指定顺序会导致行为不可预期(如日志切面在事务切面外层,日志里看不到事务结果)。
❌ 未指定顺序,切面执行顺序不确定
@Aspect
@Component
public class LogAspect { ... }
@Aspect
@Component
public class TransactionAspect { ... }
// 执行顺序随机
✅ 用 @Order 明确顺序(数字越小越靠外层)
@Aspect
@Component
@Order(1) // 最外层:安全检查
public class SecurityAspect { ... }
@Aspect
@Component
@Order(2) // 中间层:日志记录
public class LogAspect { ... }
@Aspect
@Component
@Order(3) // 最内层:事务管理
public class TransactionAspect { ... }
执行顺序:
Security → Log → Transaction → 业务方法 → Transaction → Log → Security(洋葱模型)
7. 切点表达式错误导致切面不生效
❌ execution 表达式写法错误,方法未被拦截
// 常见错误:包名写错、方法签名不匹配
@Pointcut("execution(* com.example.service.*Service.*(..))") // ❌ 只匹配直接在 service 包下
// 漏掉了子包中的类
✅ 使用 .. 匹配子包,使用 + 匹配子类
// 匹配 service 包及所有子包下的 *Service 类的所有方法
@Pointcut("execution(* com.example.service..*Service.*(..))")
// 匹配 BaseService 及其所有子类的所有方法
@Pointcut("execution(* com.example.service.BaseService+.*(..))")
// 匹配标注了 @Logged 注解的方法
@Pointcut("@annotation(com.example.annotation.Logged)")
| 表达式符号 | 含义 |
|---|---|
* |
任意单个词(包名一段、方法名、单参数类型) |
.. |
任意多个词(多级包名、任意参数列表) |
+ |
匹配该类型及其子类型 |
事务常见错误
8. @Transactional 失效的 7 种场景
| # | 场景 | 原因 | 解决方案 |
|---|---|---|---|
| 1 | 方法是 private/protected/default | Spring AOP 代理无法拦截非 public 方法 | 改为 public |
| 2 | 同类内部 this 调用 | 绕过代理对象 | 注入自身或拆分 Bean |
| 3 | 方法被 final 修饰 | CGLIB 无法重写 final 方法 | 去掉 final |
| 4 | 类未被 Spring 管理 | 没有代理对象 | 加 @Service/@Component |
| 5 | 异常被 catch 吞掉 | Spring 无法感知异常,不回滚 | 重新抛出或配置 rollbackFor |
| 6 | 异常类型不匹配 | 默认只回滚 RuntimeException | 配置 rollbackFor = Exception.class |
| 7 | 多线程中调用事务方法 | 事务绑定在 ThreadLocal,子线程无法继承 | 在子线程中单独开事务 |
❌ 异常被吞,事务不回滚
@Transactional
public void transfer() {
try {
accountDao.deduct(fromId, amount);
accountDao.add(toId, amount);
} catch (Exception e) {
log.error("transfer failed", e); // ❌ 吞掉异常,事务正常提交,数据不一致
}
}
✅ 重新抛出异常
@Transactional(rollbackFor = Exception.class)
public void transfer() {
try {
accountDao.deduct(fromId, amount);
accountDao.add(toId, amount);
} catch (Exception e) {
log.error("transfer failed", e);
throw new RuntimeException("转账失败", e); // ✅ 重新抛出,触发回滚
}
}
9. 事务传播行为误用
❌ REQUIRES_NEW 在同类内部调用不生效
@Service
public class OrderService {
@Transactional
public void createOrder() {
saveOrder();
sendNotification(); // ❌ this 调用,REQUIRES_NEW 不生效,仍在同一事务中
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotification() {
// 期望独立事务,实际和 createOrder 共享事务
}
}
| 传播行为 | 含义 | 适用场景 |
|---|---|---|
REQUIRED(默认) |
有事务则加入,没有则新建 | 大多数业务方法 |
REQUIRES_NEW |
总是新建事务,挂起当前事务 | 独立日志记录、不影响主事务的操作 |
NESTED |
嵌套事务(保存点),子事务回滚不影响父事务 | 批量操作中允许部分失败 |
NOT_SUPPORTED |
以非事务方式运行,挂起当前事务 | 不需要事务的查询操作 |
NEVER |
不允许在事务中运行,有事务则抛异常 | 强制要求无事务的操作 |
MANDATORY |
必须在事务中运行,没有事务则抛异常 | 必须由调用方管理事务的方法 |
SUPPORTS |
有事务则加入,没有则以非事务运行 | 可选事务的查询 |
10. 只读事务误用
❌ 查询方法未标注 readOnly,占用写锁资源
@Transactional // ❌ 默认非只读,数据库可能加锁
public List<Order> queryOrders(Long userId) {
return orderDao.findByUserId(userId);
}
✅ 查询方法标注 readOnly=true
@Transactional(readOnly = true) // ✅ 数据库优化只读操作,MySQL 可路由到从库
public List<Order> queryOrders(Long userId) {
return orderDao.findByUserId(userId);
}
Spring MVC 常见错误
11. 请求参数绑定错误
❌ @RequestParam 与 @RequestBody 混用
// 前端发 JSON body,后端用 @RequestParam 接收 → 参数为 null
@PostMapping("/create")
public Result create(@RequestParam String name, @RequestParam Integer age) { ... }
// 前端:fetch('/create', { method: 'POST', body: JSON.stringify({name, age}) })
✅ JSON body 用 @RequestBody,表单参数用 @RequestParam
// JSON body
@PostMapping("/create")
public Result create(@RequestBody CreateRequest request) { ... }
// 表单或 URL 参数
@GetMapping("/query")
public Result query(@RequestParam String keyword,
@RequestParam(defaultValue = "1") Integer page) { ... }
// 路径变量
@GetMapping("/users/{id}")
public Result getUser(@PathVariable Long id) { ... }
12. 过滤器(Filter)与拦截器(Interceptor)顺序
❌ 未理解 Filter 和 Interceptor 的执行顺序,在错误的地方做认证
请求 → Filter(Servlet 容器级)→ DispatcherServlet → Interceptor(Spring 级)→ Controller
| 维度 | Filter | Interceptor |
|---|---|---|
| 所属层 | Servlet 容器(javax.servlet) | Spring MVC 框架 |
| 配置方式 | @WebFilter 或 FilterRegistrationBean | 实现 HandlerInterceptor |
| 可访问 Spring Bean | 可以(通过 ApplicationContext) | 可以(本身就是 Bean) |
| 适用场景 | 跨域、编码、全局日志、限流 | 权限校验、登录验证、参数预处理 |
| 执行时机 | DispatcherServlet 之前/之后 | Handler 方法执行前后 |
✅ 多个 Filter 用 @Order 或 FilterRegistrationBean.setOrder() 控制顺序
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter());
bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // ✅ 跨域过滤器放最前
return bean;
}
13. 全局异常处理器不生效
❌ @ExceptionHandler 只放在 Controller 内,无法处理其他 Controller 的异常
@RestController
public class UserController {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) { ... }
// ❌ 只能处理本 Controller 内的异常
}
✅ 使用 @RestControllerAdvice 全局处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusiness(BusinessException e) {
return Result.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.fail(400, msg);
}
@ExceptionHandler(Exception.class)
public Result handleUnknown(Exception e) {
log.error("未知异常", e);
return Result.fail(500, "服务器内部错误");
}
}
Spring Boot 自动配置陷阱
14. 自动配置被意外覆盖
❌ 自定义 Bean 与自动配置 Bean 同名,导致自动配置失效
// 自定义了一个 dataSource Bean,导致 DataSourceAutoConfiguration 整个失效
@Bean
public DataSource dataSource() {
return new HikariDataSource(...);
}
// 问题:自动配置中其他依赖 dataSource 的 Bean(如 JdbcTemplate)可能用了错误的数据源
✅ 用 @ConditionalOnMissingBean 保证只在没有自定义时才自动配置
// 自动配置类的正确写法
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
// 用户自定义了 DataSource → 自动配置跳过
// 用户未自定义 → 自动配置生效
15. Starter 依赖引入冲突
❌ 引入多个 Starter,自动配置互相干扰
<!-- 同时引入了 spring-boot-starter-web(内嵌 Tomcat)和 spring-boot-starter-webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 结果:Spring Boot 优先选 Spring MVC,WebFlux 部分功能不可用 -->
✅ 明确选择一个 Web 栈,或排除冲突依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
16. 配置属性绑定失败(@ConfigurationProperties)
❌ 属性类没有 setter 或没有无参构造,绑定静默失败
@ConfigurationProperties(prefix = "app.cache")
public class CacheProperties {
private final int maxSize; // ❌ final 字段,无 setter,绑定失败,值为默认值 0
public CacheProperties(int maxSize) {
this.maxSize = maxSize;
}
}
✅ 标准写法:有 setter 的 POJO,或使用 @ConstructorBinding(Spring Boot 2.2+)
// 方式一:标准 POJO(有 getter/setter)
@ConfigurationProperties(prefix = "app.cache")
@Component
public class CacheProperties {
private int maxSize = 100; // ✅ 有默认值 + setter
public int getMaxSize() { return maxSize; }
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
}
// 方式二:构造器绑定(Spring Boot 2.2+,字段可以是 final)
@ConfigurationProperties(prefix = "app.cache")
@ConstructorBinding
public class CacheProperties {
private final int maxSize;
public CacheProperties(int maxSize) {
this.maxSize = maxSize;
}
}
排查技巧:在配置类上加
@Validated+ JSR-303 注解,绑定失败时会在启动时报错,而不是静默使用默认值。
参考资料
- 《Spring 编程常见错误 50 例》— 丁雪丰
- Spring Framework 官方文档
- Spring Boot 自动配置原理
评论 (0)
发表评论