说到 Spring 怎么配置 Bean,我最先想到的还是 XML。平时能照着项目写,真要把几种方式放在一起说明白,却容易漏掉一些细节。这次把 XML、注解和 Java 配置放到同一个例子里,看看它们分别解决什么问题。
先明确一件事:三种方式最终都是告诉 Spring,创建什么对象、对象之间怎样依赖,以及对象怎样初始化。区别主要在于这些信息写在哪里。
下面以 JDK 8、Spring 4.x 为例,需要在项目中引入 spring-context 及其依赖。三组示例分别演示同一个 Bean 的配置方式,运行时每次使用一组,避免把同一个对象重复注册。
一、用 XML 描述 Bean
先准备一个简单的类。这里不连接数据库,只用输出观察初始化和销毁过程。
package com.example.dao;
public class LoginUserDao {
public void myInit() {
System.out.println("LoginUserDao init");
}
public void myDestroy() {
System.out.println("LoginUserDao destroy");
}
}在类路径下放一个 applicationContext.xml。如果是 Maven 项目,可以放在 src/main/resources 目录:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="loginUserDao"
class="com.example.dao.LoginUserDao"
init-method="myInit"
destroy-method="myDestroy" />
</beans>id 是 Bean 的名字,class 是实现类。没有设置 scope 时,默认是 singleton,即同一容器内,同一个 Bean 定义对应一个共享实例。
有属性依赖时,可以通过 <property> 设置值或引用其他 Bean,也可以通过 <constructor-arg> 配置构造参数。配置集中在一起,看对象之间的关系比较方便,不过对象多了以后,XML 也容易变得很长。
下面的入口可以观察这个例子的生命周期:
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class XmlDemo {
public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
context.getBean("loginUserDao");
context.close();
}
}按这个配置,容器启动时创建 Bean 并执行初始化方法,关闭时执行销毁方法。若增加 lazy-init="true",它通常会推迟到第一次需要该 Bean 时创建;但如果一个启动时就创建的 Bean 依赖它,它仍然需要提前创建。
忽略 Spring 自身的日志,这个示例应依次输出:
LoginUserDao init
LoginUserDao destroy这里显式调用 close(),是为了让独立运行的例子正常关闭容器并触发销毁回调。
二、用注解声明,再让 Spring 扫描
如果类是自己写的,可以把组件信息直接标在类上。用下面的写法替换第一节的 LoginUserDao:
package com.example.dao;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Repository;
@Repository("loginUserDao")
public class LoginUserDao {
@PostConstruct
public void myInit() {
System.out.println("LoginUserDao init");
}
@PreDestroy
public void myDestroy() {
System.out.println("LoginUserDao destroy");
}
}然后用下面的扫描配置替换前面 XML 中的显式 Bean 定义:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.example.dao" />
</beans>只给类加注解还不够,容器还得发现它。扫描包写错时,即使类上有 @Repository,也不会自动出现在容器里。这个例子仍可以用前面的 XmlDemo 加载。
几种常见注解可以按职责理解:
| 注解 | 通常用于 |
|---|---|
@Component | 普通组件 |
@Repository | 数据访问层 |
@Service | 业务逻辑层 |
@Controller | 控制器层 |
这些组件注解负责声明 Bean,@Autowired 则负责注入它需要的依赖。后者通常先根据类型寻找候选对象;有多个候选对象时,可以用 @Qualifier 进一步限定。
这种方式适合项目里自己维护的类。配置靠近代码,改类名、查依赖时方便一些;第三方类不能随意加注解,就需要其他注册方式。
三、用 Java 配置类创建 Bean
Java 配置适合把对象创建过程直接写成代码。这里使用第一节中没有组件注解的 LoginUserDao,单独加载这个配置类:
package com.example.config;
import com.example.dao.LoginUserDao;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(initMethod = "myInit", destroyMethod = "myDestroy")
public LoginUserDao loginUserDao() {
return new LoginUserDao();
}
}默认情况下,方法名 loginUserDao 就是 Bean 名。与 XML 相比,这里的创建过程有 Java 编译器帮助检查,复杂的构造过程也比较容易表达。
加载入口如下:
import com.example.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class JavaConfigDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean("loginUserDao");
context.close();
}
}@Bean 方法需要其他 Bean 时,可以通过方法参数接收依赖,由 Spring 解析。在由容器管理的 @Configuration 配置类中,也可以调用另一个 @Bean 方法表达依赖,Spring 会根据 Bean 定义处理这个调用。配置类需要通过容器加载;直接 new AppConfig() 再调用方法,就只是普通的 Java 对象调用。
第三方类同样可以在 @Bean 方法里创建,例如数据源对象:在方法中完成实例化、设置连接属性,再返回给容器管理即可。这样不需要修改第三方类的源码。XML 中的一些框架配置也有相应 Java 配置支持,需要看具体功能和 Spring 版本。
四、作用域和生命周期不要混着记
如果把 prototype、延迟初始化和销毁方法放在同一个例子里,容易误以为它们总是配套生效。
prototype 表示每次向容器请求这个 Bean 时创建新实例,XML 可以写 scope="prototype",注解方式可以使用 @Scope("prototype")。需要注意两个地方:
- Spring 会执行 prototype Bean 的初始化回调,但不会自动执行它的销毁回调,资源释放需要使用方负责。
- 把 prototype Bean 注入 singleton Bean 时,依赖通常只在 singleton 创建时解析一次。之后反复调用 singleton 的业务方法,并不会自动得到新的依赖对象。
prototype Bean 通常在被请求时创建,不需要照搬 singleton 的方式设置延迟初始化。相关边界见 Spring 容器文档的作用域与生命周期说明。
这也是本文先用默认 singleton 演示生命周期的原因:把创建与销毁观察清楚,再单独调整作用域,比一次堆上很多属性容易理解。
五、放在一起比较
| 对比项 | XML 配置 | 组件注解配置 | Java 配置 |
|---|---|---|---|
| 定义位置 | XML 中的 <bean> | 类上的 @Component 等注解 | 配置类中的 @Bean 方法 |
| 指定名字 | id 或 name | 如 @Repository("loginUserDao") | 默认取方法名,也可用 @Bean("loginUserDao") 指定 |
| 依赖关系 | <property>、<constructor-arg> 等 | @Autowired 等注入注解 | @Bean 方法参数或配置方法调用 |
| 初始化与销毁 | init-method、destroy-method | @PostConstruct、@PreDestroy | @Bean 的 initMethod、destroyMethod |
| 适合的情况 | 集中描述对象关系、沿用已有 XML 配置 | 项目中自己维护的业务组件 | 第三方对象、需要明确创建步骤的对象 |
这些是常见写法,不是互斥的限制。例如 XML 注册的 Bean 也可以使用生命周期注解,只是需要启用相应的注解处理。
对我来说,选择配置方式可以先看类由谁维护、创建逻辑是否复杂,以及项目现有习惯。业务类用注解通常比较直接;数据源等第三方对象可以交给 XML 或 Java 配置;已有的 XML 配置清楚、稳定,也没有必要为了统一形式全部改写。项目中混合使用很常见,关键是弄清每个 Bean 从哪里注册,避免显式配置和组件扫描重复定义它。