更多关于 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 Configuration 的例子,将其作为一个真正的基于注解的 IoC 配置。让我们从 Mark 的例子开始,他在 关于 Spring 2.1 注解驱动依赖注入的文章 中使用了它。

简而言之,下面是 Mark 使用的接口和类的图示

diagram

依赖注入是通过 @Autowired 完成的,而一些方法则通过 @PostConstruct 和 @PreDestroy 标记为生命周期的一部分。

将注解驱动的配置转换为 Java Configuration 非常简单


@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 Configuration 的魔力所在,因为每次创建 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 声明的大部分功能,即您可以指定作用域、自动装配策略、懒加载、depends-on 以及 bean 级别的自定义元数据(通过 @Bean)和默认值(通过 @Configuration)。在 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 原则,因为配置位于代码之外,这意味着您拥有真正的 **P**OJO(即,代码中没有配置注解)。

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

使用 JavaConfig,您可以无限制地配置您的对象,因为您使用的是纯 Java。您可以使用任意数量的、任意类型的参数,并且可以调用任意数量的方法。由于它是 Java,因此您的配置是重构友好的,您可以利用 IDE 的自动完成功能。这非常灵活和强大!

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

考虑 @Autowire 方法


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

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

最终,这两种方法服务于一个目的:配置 Spring 容器。您可以单独使用其中一种,也可以同时使用它们,再加上一些 XML 和 属性。事实上,Java Configuration 分发版用 XML、注解和 Groovy 的配置替换了 Petclinic 的“传统” XML 配置。考虑到这篇博客 文章,不久 JRuby 也将被包含进来。

底线是,您可以选择最适合您开发风格的方法。

附注:如果您对这个话题感兴趣,您可能想参加以下 SpringOne 会议 进行深入讨论 :)

祝好,Costin

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

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

了解更多

即将举行的活动

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

查看所有