说到 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 从哪里注册,避免显式配置和组件扫描重复定义它。

最后修改:2016 年 08 月 24 日
如果觉得我的文章对你有用,请随意赞赏