Spring 常见错误汇总

发布于 2026-05-26 09:43 👁 9 次阅读
#spring

《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 注解,绑定失败时会在启动时报错,而不是静默使用默认值。


参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论