说实话,刚上手 Spring Boot 的时候,谁还没被 NoSuchBeanDefinitionException 恶心过几次呢?我记得自己第一次遇到的时候,盯着控制台那堆红色的报错看了半小时,最后发现是因为漏加了一个注解。那种“原来如此”的释然感,现在想起来还挺有趣的。今天我们就像老朋友聊天一样,把 Service 和 Mapper 注入失败的坑都挖一挖,顺便配上实打实的代码案例,让你下次再遇到时能秒杀问题。
先说个最常见的场景:你新建了一个 UserService,里面要注入 UserMapper,结果启动直接报错。别急,这通常不是你的代码逻辑错了,而是 Spring 的“视野”没覆盖到你的类。Spring 的工作原理其实挺简单的——它就像一个爱管闲事的管家,启动时会把特定包路径下的所有组件扫描一遍,把它们变成单例 Bean 存起来。等你代码里写 @Autowired 要用的时候,它再从仓库里取出来给你。如果管家没扫到某个地方,或者你拿错了钥匙,就会出岔子。
我们一个个来拆解,从最容易被忽视的包路径问题说起。假设你的项目结构是这样的:
com.example.demo
├── DemoApplication.java
├── controller
│ └── UserController.java
├── service
│ └── UserService.java
└── mapper
└── UserMapper.java
DemoApplication.java 在根包 com.example.demo 下,那么 Spring 默认会扫描它所在包及其所有子包。这样的话,controller、service、mapper 里的组件都能被扫到,注入应该没问题。但是,如果你把 UserMapper 单独放到了另一个包 com.example.mybatis.mapper,或者把 DemoApplication 放到了 com.example.app,而 UserService 还在 com.example.demo 里,那问题就来了。Spring 默认只扫描启动类所在包及其子包,所以 com.example.mybatis.mapper 这个“外人”根本进不了管家的视野。
解决这种问题,最直观的方法是在启动类上加 @ComponentScan 显式指定扫描路径,但更优雅的做法是用 @MapperScan 来处理 MyBatis 的 Mapper,用 @SpringBootApplication 的 scanBasePackages 属性来统一配置。比如:
package com.example.app;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.example") // 扫描 com.example 下所有子包
@MapperScan("com.example.mybatis.mapper") // 专门扫描 MyBatis Mapper 接口
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
这样,无论你的 Service 和 Mapper 分布在哪个子包下,只要它们在 com.example 范围内,Spring 都能找到它们。如果你用的是 MyBatis-Plus,记得把 @MapperScan 换成 @MapperScan 指向你的 Mapper 接口包,并确保 application.yml 里配置了正确的 mapper-locations 路径。
接下来聊聊注解忘加的情况。这是新手最容易踩的坑,尤其是从传统 Spring 项目转过来的开发者。在 Spring 里,一个类要成为 Bean 并被注入,必须明确告诉 Spring “我是组件”。对于 Service 层,你需要加 @Service 注解;对于 Controller 层,加 @Controller 或 @RestController;对于普通的工具类或配置类,加 @Component。MyBatis 的 Mapper 接口比较特殊,它通常用 @Mapper 注解或者通过 @MapperScan 来标记。
看看这个反面教材:
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.mapper.UserMapper;
@Service // 正确:声明这是一个 Spring 管理的 Bean
public class UserService {
@Autowired // 尝试注入 UserMapper
private UserMapper userMapper;
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
package com.example.demo.mapper;
import org.apache.ibatis.annotations.Mapper; // 正确:声明这是一个 MyBatis Mapper
import org.apache.ibatis.annotations.Select;
import com.example.demo.model.User;
@Mapper // 如果没有用 @MapperScan,这里必须加 @Mapper
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
}
如果 UserMapper 接口上没有 @Mapper 注解,且启动类上没有 @MapperScan,Spring 就不会创建它的 Bean。这时候 UserService 里的 @Autowired UserMapper 就会失败,报错信息大致是:“No qualifying bean of type ‘com.example.demo.mapper.UserMapper’ available”。
有些开发者喜欢用 XML 配置而不是注解,这也是可以的。比如在 mybatis-config.xml 里配置 Mapper 扫描:
<configuration>
<mappers>
<package name="com.example.demo.mapper"/>
</mappers>
</configuration>
然后在 application.yml 里引用它:
mybatis:
config-location: classpath:mybatis-config.xml
但要注意,XML 配置和注解配置不要混用导致冲突,尽量保持统一风格。
第三个常见问题是循环依赖。听起来很高大上,其实就是一只猫追自己的尾巴——你中有我,我中有你。比如 UserService 注入了 OrderService,而 OrderService 又注入了 UserService。Spring 在初始化 Bean 时,需要先创建依赖的 Bean,结果发现要创建 UserService 必须先有 OrderService,而要创建 OrderService 又必须先有 UserService,于是陷入了死锁,直接抛出 BeanCurrentlyInCreationException。
举个例子:
@Service
public class UserService {
@Autowired
private OrderService orderService; // 依赖 OrderService
public void doSomething() {
orderService.process();
}
}
@Service
public class OrderService {
@Autowired
private UserService userService; // 依赖 UserService
public void process() {
userService.getUserById(1L);
}
}
这种代码在逻辑上可能是合理的,但 Spring 默认不支持循环依赖。解决方式有三种:一是重构代码,打破循环依赖,比如引入一个第三方的服务来协调两者;二是使用 @Lazy 注解延迟注入,让 Spring 先创建其中一个 Bean;三是在 application.yml 里开启循环依赖支持(不推荐,因为这只是掩盖问题):
spring:
main:
allow-circular-references: true
我更喜欢第一种方法,因为循环依赖往往意味着设计上有缺陷。你可以想象一下,如果两个服务互相依赖,代码维护起来会很痛苦,测试也难以进行。
第四个坑是作用域问题。Spring 的 Bean 默认是单例的,但有些场景下你需要原型作用域(每次请求都创建新实例)。如果不小心把需要原型作用域的 Bean 标注为单例,或者反过来,可能会导致注入行为不符合预期。比如,你有一个 ThreadLocal 相关的 Service,希望每次请求都有独立的实例,但没加 @Scope("prototype"),结果所有请求共享同一个实例,数据就乱了。
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
@Service
@Scope("prototype") // 明确指定为原型作用域
public class RequestContextService {
private ThreadLocal<String> currentRequest = new ThreadLocal<>();
public void setRequest(String request) {
currentRequest.set(request);
}
public String getRequest() {
return currentRequest.get();
}
}
如果忘记加 @Scope("prototype"),默认单例下,ThreadLocal 会被所有请求共享,造成数据污染。虽然这不是严格的“注入失败”,但行为异常的表现类似,值得注意。
第五个常见原因是条件注解导致的 Bean 未被创建。Spring Boot 提供了很多条件注解,如 @ConditionalOnProperty、@ConditionalOnClass、@Profile 等,用于根据条件决定是否创建 Bean。如果你忘记配置相应的属性,或者环境不匹配,Bean 就不会被创建,注入自然失败。
比如,你有一个数据源配置类:
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(name = "app.datasource.type", havingValue = "mysql")
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
// 创建 MySQL 数据源
return new HikariDataSource();
}
}
如果在 application.yml 里没有设置 app.datasource.type=mysql,这个 DataSource Bean 就不会存在。当其他 Service 尝试注入 DataSource 时,就会报错。解决方法是确保配置文件中包含了必要的属性,或者调整条件注解的逻辑。
还有一个容易被忽视的细节:构造器注入 vs 字段注入。虽然 @Autowired 加在字段上很 convenient,但 Spring 官方更推荐构造器注入,因为它能让依赖关系更明确,也更容易测试。如果你用构造器注入,记得确保构造器只有一个,或者用 @Autowired 标注构造器。
@Service
public class UserService {
private final UserMapper userMapper;
// 构造器注入
@Autowired
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
如果构造器有多个,Spring 会尝试按类型匹配,如果匹配不到就会报错。确保所有依赖都被正确声明。
最后,聊聊调试技巧。当注入失败时,别急着改代码,先用 Spring Boot 的 Actuator 端点看看当前有哪些 Bean 被创建了。启动应用后,访问 /actuator/beans(记得在 application.yml 里开启 actuator),可以看到所有 Bean 的详细信息。如果找不到你期望的 Bean,说明它根本没被创建,问题就在前面的扫描或注解上。
另外,启用 Spring 的调试日志也很有帮助。在 application.yml 里加上:
logging:
level:
org.springframework.context: DEBUG
org.mybatis.spring: DEBUG
这样启动时你会看到每个 Bean 的创建过程,以及 MyBatis Mapper 的扫描情况,能快速定位问题所在。
总结一下,Service 和 Mapper 注入失败,绝大多数情况下逃不出这几个原因:包路径扫描不到、注解缺失、循环依赖、作用域错误、条件注解未满足。排查时,先从最简单的开始——检查启动类的包位置,确认注解齐全,然后看看配置是否正确。如果还不行,再动用 Actuator 和调试日志深入分析。
希望这篇指南能帮你少走弯路。编程路上,坑总是难免的,但每一次踩坑后的解决,都是能力的提升。加油!