Spring框架作为JavaEE开发的事实标准,其核心思想可以概括为"让Java开发变得更简单"。在实际项目开发中,我见过太多团队因为不理解IoC和DI的本质,导致代码越写越复杂。今天我们就来彻底拆解这两个核心概念,看看它们如何改变我们的编程方式。
传统Java开发中,对象创建和依赖管理往往是这样的:
java复制// 传统方式
UserService userService = new UserServiceImpl();
userService.setUserDao(new UserDaoImpl());
这种硬编码方式存在明显问题:每次更换实现类都需要修改源代码,单元测试难以模拟依赖对象。而Spring的解决方案是通过容器来管理这些对象(我们称之为Bean)及其依赖关系。
Spring IoC容器的启动流程远比表面看到的复杂。以ClassPathXmlApplicationContext为例,其初始化过程包含几个关键阶段:
这个过程中最值得关注的是BeanDefinition的转换。容器并非直接存储我们配置的XML信息,而是将其转换为包含完整元数据的BeanDefinition对象。这种设计使得Spring可以支持多种配置方式(注解、JavaConfig等)。
Spring设计了精妙的接口分层来支持不同场景:
实际开发中,99%的场景我们会直接使用ApplicationContext的实现类。但了解这个体系有助于理解Spring的扩展机制。
构造器注入是Spring团队推荐的首选方式:
java复制@Service
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
这种方式有几个优势:
提示:在Spring 4.3+版本中,单个构造器可以省略@Autowired注解
虽然构造器注入是首选,但Setter注入在某些场景下仍然必要:
java复制@Controller
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
适合使用Setter的场景包括:
尽管字段注入写法简洁:
java复制@Repository
public class UserDaoImpl {
@Autowired
private DataSource dataSource;
}
但存在明显缺点:
建议仅在测试代码或配置类中使用字段注入。
Spring提供了强大的条件装配机制:
java复制@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder().build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
// 生产环境数据源配置
}
}
还可以使用更灵活的@Conditional注解:
java复制@Bean
@Conditional(MySQLDatabaseCondition.class)
public DataSource mysqlDataSource() {
// MySQL数据源配置
}
虽然Spring能处理部分循环依赖,但良好的设计应该避免这种情况。如果确实需要,可以考虑:
我曾经遇到过一个典型案例:
java复制@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
这种设计会导致启动时报BeanCurrentlyInCreationException。正确的做法是重新设计服务边界,或将公共逻辑提取到第三个服务中。
Spring默认使用单例(singleton)作用域,但在Web应用中需要特别注意:
错误的作用域选择会导致严重的内存泄漏。我曾经调试过一个案例:将本应是singleton的Service配置为prototype,导致每秒创建上千个实例,最终OOM。
通过@Lazy注解可以延迟Bean初始化:
java复制@Configuration
public class AppConfig {
@Bean
@Lazy
public ExpensiveService expensiveService() {
return new ExpensiveService();
}
}
这种策略适用于:
但要注意:延迟初始化可能掩盖某些配置错误,因为问题只有在首次使用时才会暴露。
当看到"No qualifying bean of type"错误时,可按以下步骤排查:
一个容易忽略的点是:内部类默认不会被组件扫描捕获,除非显式声明为static。
当存在多个同类型Bean时,Spring会抛出NoUniqueBeanDefinitionException。解决方案包括:
java复制@Bean
@Primary
public DataSource primaryDataSource() {
// 主数据源配置
}
java复制@Autowired
@Qualifier("secondaryDataSource")
private DataSource dataSource;
虽然XML配置仍然可用,但现代Spring项目更推荐注解方式:
java复制@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:app.properties")
public class AppConfig {
@Bean
public DataSource dataSource() {
// Java配置数据源
}
}
这种方式的优势在于:
Spring允许创建自定义组合注解来简化配置:
java复制@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@EnableTransactionManagement
public @interface MyAppConfig {
String[] value() default {};
}
这样应用配置只需要:
java复制@MyAppConfig("com.example")
public class Application {
// 启动代码
}
在实际项目中,合理使用组合注解可以大幅减少样板代码。我在一个微服务项目中通过自定义注解将每个服务的配置代码减少了70%。
对于依赖Spring容器的测试,合理使用Mock是关键:
java复制@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private PaymentService paymentService;
@InjectMocks
private OrderService orderService;
@Test
void shouldProcessOrder() {
// 测试逻辑
}
}
这种方式完全不需要启动Spring容器,执行速度极快。
当确实需要测试容器行为时:
java复制@SpringJUnitConfig(TestConfig.class)
class UserRepositoryIT {
@Autowired
private UserRepository repository;
@Test
void shouldSaveUser() {
// 数据库操作测试
}
}
建议:
我在项目中发现,合理划分单元测试和集成测试可以将测试套件执行时间从15分钟缩短到2分钟。