更多关于 Java 配置

工程 | Costin Leau | 2007年6月5日 | ...

就像你们大多数人现在已经知道的那样,Spring 不仅仅是关于 XML 的,因为最近核心的一些“官方”扩展提供了配置容器的替代方式。

Spring Java Configuration 1.0 M2 是 在 JavaOne 期间 发布的产品之一,虽然仍标记为里程碑版本,但包含了大量重要的更新和错误修复。

  • 根包已更改为 org.springframework.config.java
  • <li>scoped beans are fully supported</li>
    
    <li>the bean name generation can be customized</li>
    
    <li>the distribution contains a 'transformed' sample (petclinic) which uses XML, JavaConfig and Groovy.</li>
    

事实上,1.0 M2 版本的大部分工作都是整合收到的关于最初公告的反馈;非常感谢所有参与者!

在本文中,我想提供一些 Java 配置的示例,作为一种真正的基于注解的 IoC 配置。让我们从 Mark 在他的关于 Spring 2.1 注解驱动依赖注入的文章中使用的示例开始。

回顾一下,下面是 Mark 使用的接口和类的图示

diagram

装配通过 @Autowired 完成,同时一些方法通过 @PostConstruct 和 @PreDestroy 标记为生命周期的一部分。

将注解驱动的配置转换为 Java 配置非常直接


@Configuration
public abstract class JavaCfg {

	@Bean (destroyMethodName = "tearDownDatabase")
	public JdbcMessageRepository messageRepo() {
		JdbcMessageRepository repo = new JdbcMessageRepository();
		repo.createTemplate(dataSource());
		// call custom init method
		repo.setUpDatabase();

		return repo;
	}
	
	@Bean
	public GreetingService greetService() {
		GreetingServiceImpl impl = new GreetingServiceImpl();
		impl.setMessageRepository(messageRepo());
		return impl;
	}


	@ExternalBean
	public abstract DataSource dataSource();
}

首先,使用标记有 @Configuration 的 Java 类创建配置。在该类中,声明了 2 个 Bean 并引用了一个外部 Bean。

声明的第一个 Bean 是 messageRepo(与方法名相同),它也定义了一个销毁方法。注意,自定义初始化方法是通过代码调用的,因此不需要任何注解或声明。尽管我建议不要那样做,你仍然可以使用 Spring InitializingBean 接口或 @Bean initMethodName 参数。上面的代码更加清晰简洁,更不用说你可以传入参数,这在使用声明式初始化方法时是不可用的。

定义的第二个 Bean 是 greetService,它使用 messageRepo 作为依赖。这就是 Java 配置的“魔力”所在,因为每次创建 greetService 时,Spring 容器都会提供 messageRepo 后面的 Bean 实例。也就是说,如果 messageRepo 是一个单例,每次都会返回同一个实例。但是,如果指定了不同的作用域,那么当需要创建新实例时,你的代码将被调用。Rod 已经解释过这一点,请参阅他的博客文章了解更多信息。

1.0 M2 版本新增的一项功能是 @ExternalBean 注解,它引用在当前配置之外声明的 Bean,同时仍然依赖于 Java 的强类型特性,从而利用你的 IDE 验证。@ExternalBean 在运行时用 getBean() 查找重写它声明的方法,像这样:


   public DataSource dataSource() {
       return (DataSource) context.getBean("dataSource");
   }

当然,特别是在使用ConfigurationSupport 类时,也可以手动完成同样的事情,但是 @ExternalBean 让事情变得容易得多。请注意,在最初的示例中,我使用了抽象方法来强调外部化,但是可以使用任何类型的非 final 方法。


  @ExternalBean
  public DataSource dataSource() {
      throw new UnsupportedOperationException("this line will NEVER execute since the method will be overridden");
  }

配置创建完成后,将其声明为一个普通 Bean 以及 JavaConfiguration 后置处理器:



<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

	<bean id="config" class="blog.javaconfig.JavaCfg" />

	<bean id="processor"
		class="org.springframework.config.java.process.ConfigurationPostProcessor" />

	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="${jdbc.driver}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
	</bean>

</beans>

这样你就准备好了(如果你运行 Mark 的测试,请确保使用 Java Configuration 的 xml 文件)。

既然一图胜千言,请看下面通过SpringIDE 展示的相同设置: springIDE view

我使用了最新的 SpringIDE 快照版本,它提供了可视化、导航以及对 Java Configuration 注解的验证(例如,插件会检查 destroyMethodName 是否指向 Bean 创建方法返回类型上的正确方法)。

捉迷藏

Java Configuration 支持大多数 XML 声明特性,也就是说,你可以在 Bean 级别(通过 @Bean)和作为默认值(通过 @Configuration)指定作用域、自动装配策略、延迟加载、depends-on 以及自定义元数据。在 1.0 M2 中,你甚至可以使用 @ScopedProxy 注解,它是 <aop:scoped-proxy/> 的直接替代。

然而,Java Configuration 相较于传统 XML 容器提供了一个新特性——“Bean 可见性”,即定义不能在其配置外部使用的 Bean 的能力。再次,让我们看一些代码:


@Configuration
public class VisibilityConfiguration {

	@Bean(scope = DefaultScopes.PROTOTYPE)
	public Object publicBean() {
		List list = new ArrayList();
		list.add(hiddenBean());
		list.add(secretBean());

		System.out.println("creating public bean");
		return list;
	}

	@Bean(scope = DefaultScopes.SINGLETON)
	protected Object hiddenBean() {
		System.out.println("creating hidden bean");
		return new String("hidden bean");
	}

	@Bean(scope = DefaultScopes.PROTOTYPE)
	private Object secretBean() {
		List list = new ArrayList();
		// hidden beans can access beans defined in the 'owning' context
		list.add(hiddenBean());
		System.out.println("creating secret bean");
		return list;
	}
}

Java Configuration 将使用方法的可见性来确定某个 Bean 是公共的(即它可以在声明其配置的外部使用)还是私有的(非公共的)。因此,任何非公共的、带有 @Bean 注解的方法都将创建一个隐藏的 Bean。这使得你可以提供 Bean 定义的封装,阻止意外或非意外的访问。非常重要的一点是,隐藏 Bean 不会转化为嵌套 Bean——它们是功能齐全的、顶级的 Bean:它们有自己的生命周期并支持自定义作用域,这与依赖于父 Bean 的内部 Bean 不同。

为了证明这一点,我将 hiddenBean 标记为 singleton,将 secretBean 标记为 prototype。

让我们用下面的测试来验证其行为:


public class VisibilityTest extends TestCase {
	private ConfigurableApplicationContext context;

	@Override
	protected void setUp() throws Exception {
		context = new AnnotationApplicationContext("**/VisibilityConfiguration.class");
	}

	@Override
	protected void tearDown() throws Exception {
		context.close();
	}

	public void testApplicationContext() {
		assertNotNull(context);
		System.out.println(Arrays.toString(context.getBeanDefinitionNames()));
		// I don't belive you container! I know you are hidding something 
		context.getBean("hiddenBean");
	}
}

测试应该打印:

[blog.javaconfig.VisibilityConfiguration, publicBean]
creating hidden bean
creating secret bean
creating public bean
creating secret bean
creating public bean

之后应该会失败,出现类似如下内容:


org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'hiddenBean' is defined
...

控制台的第一行显示,在我们持有的上下文中没有定义 secretBean 和 hiddenBean。然而,接下来的几行显示,隐藏的 Bean 只创建了一次(因为它是一个单例),而 secretBean 为每个 publicBean 创建了两次,因为它是一个原型。

那么隐藏的 Bean 在哪里呢?在子容器内部。

父容器(在我们的例子中是 context)完全不知道子容器及其内部声明的任何 Bean。尽管如此,子容器内部声明的 Bean 可以访问父容器内部声明的任何 Bean,反之则不行。另一方面,公共 Bean(如 publicBean)由 Java Configuration “推”到父容器内部,但由于它们与隐藏 Bean 在同一配置中声明,它们在实例化期间可以引用这些“秘密” Bean。

看,妈妈,没有 XML!

对于那些想要完全放弃 XML 的人,Spring Java Configuration 提供了AnnotationApplicationContext,它使用类而不是 XML 文件,正如你在上面的测试用例中看到的那样。虽然我的示例可行,但并不理想,因为没有任何缓存的情况下,每次测试都会创建和销毁应用程序上下文。另一种方法是重用现有的AbstractDependencyInjectionSpringContextTests 并适当地覆盖上下文创建方法:


public class NoXMLTest extends AbstractDependencyInjectionSpringContextTests {

	@Override
	protected ConfigurableApplicationContext createApplicationContext(String[] locations) {
		GenericApplicationContext context = new GenericApplicationContext();
		customizeBeanFactory(context.getDefaultListableBeanFactory());
                // use Java Configuration annotation-based bean definition reader
		new ConfigurationClassScanningBeanDefinitionReader(context).loadBeanDefinitions(locations);
		context.refresh();
		return context;
	}

	@Override
	protected String[] getConfigLocations() {
		return new String[] { "**/*.class" };
	}

	public void testAppCtx() {
		assertNotNull(applicationContext);
	}
}

(这可以通过 SPR-3550 进一步简化)。

哪种方法更好?

你们中的一些人可能想知道哪种注解配置方法最好:注解驱动注入还是 Java Configuration?我的回答是:“视情况而定”。

Java Configuration 遵循 IoC 原则,因为配置位于代码之外,这意味着你拥有真正的 POJO(即代码内部没有配置注解)。

本博客之前介绍的注解驱动注入允许对象对其配置有更多了解。它们可以请求依赖、进行自动装配,甚至可以指定其作用域。注入仍然发生(也就是说对象仍然由容器管理),但现在部分配置包含在对象中。

使用 JavaConfig,你可以毫无限制地配置对象,因为你使用的是纯 Java。你可以使用任意数量、任意类型的参数,并且可以调用任意数量的方法。由于它是 Java,你的配置对重构友好,并且你可以从 IDE 的自动补全中受益。这非常灵活且强大!

另一方面,使用注解驱动注入,你可以对对象进行细粒度(类、方法甚至字段级别)的控制,并且获得更多上下文信息。

考虑 @Autowire 方法:


        @Autowired
        public void createTemplate(DataSource dataSource) {
                this.jdbcTemplate = new SimpleJdbcTemplate(dataSource);
        }

Spring 使用注解不仅是为了确定自动装配发生的方法,还为了确定所需的类型。此外,可以使用多参数方法,这是采用 JavaBeans 约定(即 setter)的“传统”自动装配不支持的功能。

归根结底,这两种方法都服务于同一个目的:配置 Spring 容器。你可以选择其中一种或两种都使用,如果愿意,还可以加上一些 XML 和属性文件。事实上,Java Configuration 分发包将 Petclinic 的“传统”XML 配置替换为基于 XML、注解和Groovy 的配置。考虑到这篇博客文章,很快就会包含 JRuby

总之,你可以选择最适合你开发风格的方法。

附注:如果你对此话题感兴趣,可以参加以下 SpringOne会议进行深入讨论 :)

祝好,Costin

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

超越他人

VMware 提供培训和认证,助你加速进步。

了解更多

获取支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一次简单订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部