本节介绍了如何在你的Java代码中使用注解来配置Spring容器。它包括以下主题。
- 基本概念:@Bean 和 @Configuration
- 通过使用 AnnotationConfigApplicationContext 实例化Spring容器
- 使用 @Bean 注解
- 使用 @Configuration 注解
- 构建基于Java的配置
- Bean定义配置
- PropertySource 抽象
- 使用 @PropertySource
- 声明中的占位符解析
一. 基本概念:@Bean
和 @Configuration
Spring的Java配置支持的核心工件是 @Configuration
注解的类和 @Bean
注解的方法。
@Bean
注解用来表示一个方法实例化、配置和初始化了一个新的对象,由Spring IoC容器管理。对于那些熟悉Spring的 <beans/>
XML配置的人来说,@Bean
注解的作用与 <bean/>
元素的作用相同。你可以在任何Spring @Component
中使用 @Bean
注解的方法。然而,它们最常被用于 @Configuration
Bean。
用 @Configuration
来注解一个类,表明它的主要目的是作为Bean定义的来源。此外, @Configuration
类允许通过调用同一个类中的其他 @Bean
方法来定义bean间的依赖关系。最简单的 @Configuration
类如下
@Configuration public class AppConfig { @Bean public MyServiceImpl myService() { return new MyServiceImpl(); } }
前面的 AppConfig
类等同于下面的 Spring <beans/>
XML:
<beans> <bean id="myService" class="com.acme.services.MyServiceImpl"/> </beans>
完整的 @Configuration 与 "精简的" @Bean 模式?
当
@Bean
方法被声明在没有@Configuration
注解的类中时,它们被称为以 "精简" 模式处理。在@Component
或甚至在一个普通的类中声明的Bean方法被认为是 "精简" 的,包含类的主要目的不同,而@Bean
方法是那里的一种奖励。例如,服务组件可以通过每个适用的组件类上的一个额外的@Bean
方法向容器暴露管理视图。在这种情况下,@Bean
方法是一种通用的工厂方法机制。与完整的
@Configuration
不同,精简的@Bean
方法不能声明bean间的依赖关系。相反,它们对其包含的组件的内部状态进行操作,也可以选择对其可能声明的参数进行操作。因此,这样的@Bean
方法不应该调用其他的@Bean
方法。每个这样的方法从字面上看只是一个特定的Bean引用的工厂方法,没有任何特殊的运行时语义。这里的正面效果是,在运行时不需要应用CGLIB子类,所以在类的设计方面没有任何限制(也就是说,包含的类可以是final
的等等)。在常见的情况下,
@Bean
方法要在@Configuration
类中声明,确保始终使用 "完整" 模式,因此跨方法引用会被重定向到容器的生命周期管理。这可以防止同一个@Bean
方法被意外地通过普通的Java调用来调用,这有助于减少在 "精简" 模式下操作时很难追踪的细微Bug。
下面几节将深入讨论 @Bean
和 @Configuration
注解。然而,首先,我们将介绍通过使用基于Java的配置来创建spring容器的各种方法。
二. 通过使用 AnnotationConfigApplicationContext
实例化Spring容器
下面的章节记录了Spring的 AnnotationConfigApplicationContext
,它在Spring 3.0中引入。这个多功能的 ApplicationContext
实现不仅能够接受 @Configuration
类作为输入,还能够接受普通的 @Component
类和用JSR-330元数据注解的类。
当 @Configuration
类被提供为输入时,@Configuration
类本身被注册为Bean定义,该类中所有声明的 @Bean
方法也被注册为Bean定义。
当 @Component
和JSR-330类被提供时,它们被注册为bean定义,并且假定DI元数据如 @Autowired
或 @Inject
在必要时被用于这些类。
1、简单构造
与实例化 ClassPathXmlApplicationContext
时使用Spring XML文件作为输入一样,你可以在实例化 AnnotationConfigApplicationContext
时使用 @Configuration
类作为输入。这使得Spring容器的使用完全不需要XML,正如下面的例子所示:
public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); }
如前所述,AnnotationConfigApplicationContext
不限于只与 @Configuration
类一起工作。任何 @Component
或JSR-330注解的类都可以作为输入提供给构造函数,正如下面的例子所示:
public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); }
前面的例子假设 MyServiceImpl
、Dependency1
和 Dependency2
使用Spring的依赖注入注解,如 @Autowired
。
2、通过使用 register(Class<?>…)
以编程方式构建容器。
你可以通过使用无参数构造函数来实例化 AnnotationConfigApplicationContext
,然后通过 register()
方法来配置它。这种方法在以编程方式构建 AnnotationConfigApplicationContext
时特别有用。下面的例子展示了如何做到这一点:
public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AppConfig.class, OtherConfig.class); ctx.register(AdditionalConfig.class); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); }
3、用 scan(String…)
启用组件扫描。
为了启用组件扫描,你可以对你的 @Configuration
类添加如下注解:
@Configuration @ComponentScan(basePackages = "com.acme") (1) public class AppConfig { // ... } (1)这个注解可以实现组件扫描。
在前面的例子中,com.acme
包被扫描以寻找任何 @Component
注解的类,这些类被注册为容器中的Spring Bean定义。AnnotationConfigApplicationContext
暴露了 scan(String…)
方法,以实现同样的组件扫描功能,如下例所示:
public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.scan("com.acme"); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); }
请记住,
@Configuration
类是用@Component
元注解的,所以它们是组件扫描的候选者。在前面的例子中,假设AppConfig
是在com.acme
包(或下面的任何包)中声明的,它在调用scan()
时被选中。在refresh()
时,它的所有@Bean
方法都被处理并注册为容器中的 bean 定义。
4、用 AnnotationConfigWebApplicationContext
支持Web应用程序
AnnotationConfigApplicationContext
的一个 WebApplicationContext
变体可以用 AnnotationConfigWebApplicationContext
。你可以在配置Spring ContextLoaderListener
servlet listener、Spring MVC DispatcherServlet
等时使用这个实现。下面的 web.xml
片段配置了一个典型的Spring MVC Web应用(注意使用 contextClass
的 context-param
和 init-param
):
<web-app> <!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext instead of the default XmlWebApplicationContext --> <context-param> <param-name>contextClass</param-name> <param-value> org.springframework.web.context.support.AnnotationConfigWebApplicationContext </param-value> </context-param> <!-- Configuration locations must consist of one or more comma- or space-delimited fully-qualified @Configuration classes. Fully-qualified packages may also be specified for component-scanning --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>com.acme.AppConfig</param-value> </context-param> <!-- Bootstrap the root application context as usual using ContextLoaderListener --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Declare a Spring MVC DispatcherServlet as usual --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext instead of the default XmlWebApplicationContext --> <init-param> <param-name>contextClass</param-name> <param-value> org.springframework.web.context.support.AnnotationConfigWebApplicationContext </param-value> </init-param> <!-- Again, config locations must consist of one or more comma- or space-delimited and fully-qualified @Configuration classes --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>com.acme.web.MvcConfig</param-value> </init-param> </servlet> <!-- map all requests for /app/* to the dispatcher servlet --> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/app/*</url-pattern> </servlet-mapping> </web-app>
对于编程用例,
GenericWebApplicationContext
可以作为AnnotationConfigWebApplicationContext
的替代
三.使用 @Bean
注解
@Bean
是一个方法级注解,是XML <bean/>
元素的直接类似物。该注解支持 <bean/>
所提供的一些属性,例如:
- init-method
- destroy-method
- autowiring
- name.
你可以在 @Configuration 或 @Component 注解的类中使用 @Bean 注解
1、声明一个 Bean
为了声明一个Bean,你可以用 @Bean
注解来注解一个方法。你可以用这个方法在 ApplicationContext
中注册一个Bean定义,该类型被指定为该方法的返回值。默认情况下,Bean的名字和方法的名字是一样的。下面的例子显示了一个 @Bean
方法声明:
@Configuration public class AppConfig { @Bean public TransferServiceImpl transferService() { return new TransferServiceImpl(); } }
前面的配置完全等同于下面的Spring XML。
<beans> <bean id="transferService" class="com.acme.TransferServiceImpl"/> </beans>
这两个声明使 ApplicationContext
中一个名为 transferService
的Bean可用,并与 TransferServiceImpl
类型的对象实例绑定,正如下面的文字图片所示:
transferService -> com.acme.TransferServiceImpl
你也可以使用 default 方法来定义Bean。这允许通过在默认方法上实现带有Bean定义的接口来组成Bean配置:
public interface BaseConfig { @Bean default TransferServiceImpl transferService() { return new TransferServiceImpl(); } } @Configuration public class AppConfig implements BaseConfig { }
你也可以用一个接口(或基类)的返回类型来声明你的 @Bean
方法,如下例所示:
@Configuration public class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); } }
然而,这将提前类型预测的可见性限制在指定的接口类型(TransferService
)。然后,只有在受影响的 singleton Bean被实例化后,容器才知道完整的类型(TransferServiceImpl
)。非lazy单体Bean根据它们的声明顺序被实例化,所以你可能会看到不同的类型匹配结果,这取决于另一个组件何时试图通过非声明的类型进行匹配(比如 @Autowired TransferServiceImpl
,它只在 transferService
Bean被实例化后才会解析)
2、Bean 依赖
一个 @Bean
注解的方法可以有任意数量的参数,描述构建该Bean所需的依赖关系。例如,如果我们的 TransferService
需要一个 AccountRepository
,我们可以用一个方法参数将这种依赖关系具体化,如下例所示:
@Configuration public class AppConfig { @Bean public TransferService transferService(AccountRepository accountRepository) { return new TransferServiceImpl(accountRepository); } }
解析机制与基于构造函数的依赖注入基本相同
3、接收生命周期的回调
任何用 @Bean 注解定义的类都支持常规的生命周期回调,并且可以使用JSR-250的 @PostConstruct 和 @PreDestroy 注解。更多细节请参见 JSR-250注解。
常规的Spring 生命周期 回调也被完全支持。如果一个bean实现了 InitializingBean、DisposableBean 或 Lifecycle,它们各自的方法就会被容器调用。
标准的 *Aware 接口集(如 BeanFactoryAware、BeanNameAware、MessageSourceAware、ApplicationContextAware 等)也被完全支持。
@Bean 注解支持指定任意的初始化和销毁回调方法,就像Spring XML在 bean 元素上的 init-method 和 destroy-method 属性一样,如下例所示:
public class BeanOne { public void init() { // initialization logic } } public class BeanTwo { public void cleanup() { // destruction logic } } @Configuration public class AppConfig { @Bean(initMethod = "init") public BeanOne beanOne() { return new BeanOne(); } @Bean(destroyMethod = "cleanup") public BeanTwo beanTwo() { return new BeanTwo(); } }
默认情况下,用Java配置定义的具有 public 的 close
或 shutdown
方法的Bean会自动被列入销毁回调。如果你有一个 public 的 close
或 shutdown
方法,并且你不希望它在容器关闭时被调用,你可以在你的Bean定义中添加 @Bean(destroyMethod = "")
来禁用默认 (inferred)
模式。
你可能想对你用JNDI获取的资源默认这样做,因为它的生命周期是在应用程序之外管理的。特别是,要确保总是对 DataSource
这样做,因为它在Jakarta EE应用服务器上已知是有问题的。
下面的例子显示了如何阻止一个 DataSource
的自动销毁回调。
@Bean(destroyMethod = "") public DataSource dataSource() throws NamingException { return (DataSource) jndiTemplate.lookup("MyDS"); }
另外,对于 @Bean
方法,你通常使用程序化的JNDI查找,要么使用Spring的 JndiTemplate
或 JndiLocatorDelegate
helper,要么直接使用JNDI InitialContext
,但不使用 JndiObjectFactoryBean
变体(这将迫使你将返回类型声明为 FactoryBean
类型,而不是实际的目标类型,使得它难以用于其他 @Bean
方法中的交叉引用调用,这些方法打算引用这里提供的资源)。
就前文例子中的 BeanOne
而言,在构造过程中直接调用 init()
方法同样有效,正如下面的例子所示:
@Configuration public class AppConfig { @Bean public BeanOne beanOne() { BeanOne beanOne = new BeanOne(); beanOne.init(); return beanOne; } // ... }
4、指定 Bean 的 Scope
Spring包括 @Scope
注解,这样你就可以指定Bean的 scope。
使用 @Scope
注解
你可以指定你用 @Bean
注解定义的 Bean 应该有一个特定的 scope。你可以使用 Bean Scopes部分中指定的任何一个标准 scope。
默认的scope是 singleton
,但你可以用 @Scope
注解来覆盖它,如下例所示:
@Configuration public class MyConfiguration { @Bean @Scope("prototype") public Encryptor encryptor() { // ... } }
@Scope 和 scoped-proxy
Spring提供了一种通过 scope 代理 来处理scope 依赖的便捷方式。使用XML配置时,创建这种代理的最简单方法是 <aop:scoped-proxy/> 元素。在Java中用 @Scope 注解配置你的Bean,提供了与 proxyMode 属性相当的支持。默认是 ScopedProxyMode.DEFAULT,这通常表示不应该创建任何 scope 代理,除非在组件扫描指令级别配置了不同的默认值。你可以指定 ScopedProxyMode.TARGET_CLASS、ScopedProxyMode.INTERFACES 或 ScopedProxyMode.NO。
如果你把XML参考文档中的 scope 代理例子(见 scope 代理)移植到我们使用Java的 @Bean 上,它类似于以下内容:
// an HTTP Session-scoped bean exposed as a proxy @Bean @SessionScope public UserPreferences userPreferences() { return new UserPreferences(); } @Bean public Service userService() { UserService service = new SimpleUserService(); // a reference to the proxied userPreferences bean service.setUserPreferences(userPreferences()); return service; }
自定义Bean的命名
默认情况下,配置类使用 @Bean
方法的名称作为结果Bean的名称。然而,这个功能可以通过 name
属性来重写,正如下面的例子所示:
@Configuration public class AppConfig { @Bean("myThing") public Thing thing() { return new Thing(); } }
Bean 别名
正如在 Bean 命名中所讨论的,有时最好给一个Bean起多个名字,也就是所谓的Bean别名。@Bean
注解的 name
属性接受一个 String 数组来实现这一目的。下面的例子展示了如何为一个Bean设置若干别名:
@Configuration public class AppConfig { @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"}) public DataSource dataSource() { // instantiate, configure and return DataSource bean... } }
Bean 描述(Description)
有时,为 Bean 提供更详细的文本描述是有帮助的。当Bean被暴露(也许是通过JMX)用于监控目的时,这可能特别有用。 为了给 @Bean 添加描述,你可以使用 @Description 注解,如下图所示:
@Configuration public class AppConfig { @Bean @Description("Provides a basic example of a bean") public Thing thing() { return new Thing(); } }
四. 使用 @Configuration
注解
@Configuration
是一个类级注解,表示一个对象是Bean定义的来源。@Configuration
类通过 @Bean
注解的方法声明bean。对 @Configuration
类上的 @Bean
方法的调用也可以用来定义bean间的依赖关系。
1、注入bean间的依赖
当Bean相互之间有依赖关系时,表达这种依赖关系就像让一个Bean方法调用另一个一样简单,正如下面的例子所示:
@Configuration public class AppConfig { @Bean public BeanOne beanOne() { return new BeanOne(beanTwo()); } @Bean public BeanTwo beanTwo() { return new BeanTwo(); } }
在前面的例子中,beanOne
通过构造函数注入收到了对 beanTwo
的引用。
这种声明bean间依赖关系的方法只有在
@Configuration
类中声明了@Bean
方法时才有效。你不能通过使用普通的@Component
类来声明bean间的依赖关系。
2、查询方法注入
如前所述, 查询方法注入是一个高级功能,你应该很少使用。在 singleton scope 的Bean对 prototype scope 的Bean有依赖性的情况下,它是很有用的。为这种类型的配置使用Java提供了实现这种模式的自然手段。下面的例子展示了如何使用查找方法注入:
public abstract class CommandManager { public Object process(Object commandState) { // grab a new instance of the appropriate Command interface Command command = createCommand(); // set the state on the (hopefully brand new) Command instance command.setState(commandState); return command.execute(); } // okay... but where is the implementation of this method? protected abstract Command createCommand(); }
通过使用Java配置,你可以创建一个 CommandManager
的子类,其中抽象的 createCommand()
方法被重载,这样它就可以查找到一个新的(prototype) command 对象。下面的例子显示了如何做到这一点:
@Bean @Scope("prototype") public AsyncCommand asyncCommand() { AsyncCommand command = new AsyncCommand(); // inject dependencies here as required return command; } @Bean public CommandManager commandManager() { // return new anonymous implementation of CommandManager with createCommand() // overridden to return a new prototype Command object return new CommandManager() { protected Command createCommand() { return asyncCommand(); } } }
3、关于基于Java的配置如何在内部工作的进一步信息
考虑一下下面的例子,它显示了一个 @Bean
注解的方法被调用了两次:
@Configuration public class AppConfig { @Bean public ClientService clientService1() { ClientServiceImpl clientService = new ClientServiceImpl(); clientService.setClientDao(clientDao()); return clientService; } @Bean public ClientService clientService2() { ClientServiceImpl clientService = new ClientServiceImpl(); clientService.setClientDao(clientDao()); return clientService; } @Bean public ClientDao clientDao() { return new ClientDaoImpl(); } }
clientDao()
在 clientService1()
和 clientService2()
中被调用了一次。由于该方法创建了一个新的 ClientDaoImpl
实例并将其返回,你通常会期望有两个实例(每个服务都有一个)。这肯定是有问题的:在Spring中,实例化的Bean默认有一个 singleton
scope。这就是神奇之处。所有的 @Configuration
类都是在启动时用 CGLIB
子类化的。在子类中,子方法首先检查容器中是否有任何缓存(scope)的Bean,然后再调用父方法并创建一个新实例。
由于CGLIB在启动时动态地添加功能,所以有一些限制。特别是,配置类不能是 如果你想避免任何CGLIB施加的限制,可以考虑在非 |
五. 构建基于Java的配置
Spring基于Java的配置功能让你可以编写注解,这可以降低配置的复杂性。
1、使用 @Import
注解
就像 <import/>
元素在Spring XML文件中被用来帮助模块化配置一样,@Import
注解允许从另一个配置类中加载 @Bean
定义,如下例所示:
@Configuration public class ConfigA { @Bean public A a() { return new A(); } } @Configuration @Import(ConfigA.class) public class ConfigB { @Bean public B b() { return new B(); } }
现在,在实例化上下文时不需要同时指定 ConfigA.class
和 ConfigB.class
,而只需要明确提供 ConfigB
,正如下面的例子所示:
public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class); // now both beans A and B will be available... A a = ctx.getBean(A.class); B b = ctx.getBean(B.class); }
这种方法简化了容器的实例化,因为只需要处理一个类,而不是要求你在构建过程中记住潜在的大量 @Configuration
类。
2、在导入的 @Bean
定义上注入依赖
前面的例子是可行的,但也是简单的。在大多数实际情况下,Bean在配置类之间有相互依赖的关系。当使用XML时,这不是一个问题,因为不涉及编译器,你可以声明 ref="someBean"
并相信Spring会在容器初始化过程中解决这个问题。当使用 @Configuration
类时,Java编译器会对配置模型进行约束,即对其他Bean的引用必须是有效的Java语法。
幸运的是,解决这个问题很简单。一个 @Bean
方法可以有任意数量的参数来描述Bean的依赖关系。考虑下面这个更真实的场景,有几个 @Configuration
类,每个类都依赖于其他类中声明的bean:
@Configuration public class ServiceConfig { @Bean public TransferService transferService(AccountRepository accountRepository) { return new TransferServiceImpl(accountRepository); } } @Configuration public class RepositoryConfig { @Bean public AccountRepository accountRepository(DataSource dataSource) { return new JdbcAccountRepository(dataSource); } } @Configuration @Import({ServiceConfig.class, RepositoryConfig.class}) public class SystemTestConfig { @Bean public DataSource dataSource() { // return new DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); // everything wires up across configuration classes... TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); }
还有一种方法可以达到同样的效果。记住,@Configuration
类最终只是容器中的另一个Bean。这意味着它们可以像其他Bean一样利用 @Autowired
和 @Value
注入以及其他功能。
下面的例子显示了一个Bean是如何被自动注入到另一个Bean的:
@Configuration public class ServiceConfig { @Autowired private AccountRepository accountRepository; @Bean public TransferService transferService() { return new TransferServiceImpl(accountRepository); } } @Configuration public class RepositoryConfig { private final DataSource dataSource; public RepositoryConfig(DataSource dataSource) { this.dataSource = dataSource; } @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } } @Configuration @Import({ServiceConfig.class, RepositoryConfig.class}) public class SystemTestConfig { @Bean public DataSource dataSource() { // return new DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); // everything wires up across configuration classes... TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); }
从Spring Framework 4.3开始,
@Configuration
类中的构造函数注入才被支持。还要注意的是,如果目标Bean只定义了一个构造函数,就不需要指定@Autowired
。
在前面的场景中,使用 @Autowired
效果很好,并提供了所需的模块化,但确定自动注入的Bean定义到底在哪里声明,还是有些模糊。例如,作为一个查看 ServiceConfig
的开发者,你怎么知道 @Autowired AccountRepository
Bean到底是在哪里声明的?它在代码中并不明确,而这可能就很好。请记住, Spring Tools for Eclipse提供的工具可以呈现图形,显示一切是如何注入的,这可能就是你所需要的。另外,你的Java IDE可以很容易地找到 AccountRepository
类型的所有声明和使用,并快速显示返回该类型的 @Bean
方法的位置。
在不能接受这种模糊性的情况下,你希望在你的IDE中从一个 @Configuration
类直接导航到另一个,可以考虑自动注入配置类本身。下面的例子展示了如何做到这一点:
@Configuration public class ServiceConfig { @Autowired private RepositoryConfig repositoryConfig; @Bean public TransferService transferService() { // navigate 'through' the config class to the @Bean method! return new TransferServiceImpl(repositoryConfig.accountRepository()); } }
在前面的情况下,AccountRepository
的定义是完全明确的。然而,ServiceConfig
现在与 RepositoryConfig
紧密耦合了。这就是权衡的结果。通过使用基于接口或基于抽象类的 @Configuration
类,这种紧密耦合可以得到一定程度的缓解。考虑一下下面的例子:
@Configuration public class ServiceConfig { @Autowired private RepositoryConfig repositoryConfig; @Bean public TransferService transferService() { return new TransferServiceImpl(repositoryConfig.accountRepository()); } } @Configuration public interface RepositoryConfig { @Bean AccountRepository accountRepository(); } @Configuration public class DefaultRepositoryConfig implements RepositoryConfig { @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(...); } } @Configuration @Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config! public class SystemTestConfig { @Bean public DataSource dataSource() { // return DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); }
现在,ServiceConfig
与具体的 DefaultRepositoryConfig
是松散耦合的,内置的IDE工具仍然有用。你可以轻松获得 RepositoryConfig
实现的类型层次。这样一来,浏览 @Configuration
类和它们的依赖关系就变得与浏览基于接口的代码的通常过程没有什么不同。
如果你想影响某些Bean的启动创建顺序,可以考虑将其中一些Bean声明为 @Lazy (在第一次访问时创建,而不是在启动时创建)或者声明为 @DependsOn 某些其他Bean(确保特定的其他Bean在当前Bean之前创建,超出后者的直接依赖关系)。 |
3、有条件地包括 @Configuration
类或 @Bean
方法
根据一些任意的系统状态,有条件地启用或禁用一个完整的 @Configuration
类,甚至是单个的 @Bean
方法,往往是很有用的。一个常见的例子是使用 @Profile
注解来激活Bean,只有在Spring Environment
中启用了特定的配置文件时。
@Profile
注解实际上是通过使用一个更灵活的注解来实现的,这个注解叫做@Conditional
。@Conditional
注解指出了特定的 org.springframework.context.annotation.Condition
实现,在注册 @Bean
之前应该参考这些实现。
Condition
接口的实现提供了一个 matches(…)
方法,它返回 true
或 false
。例如,下面的列表显示了用于 @Profile
的实际 Condition
实现:
@Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // Read the @Profile annotation attributes MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().acceptsProfiles(((String[]) value))) { return true; } } return false; } return true; }
六. Environment 抽象
Environment 接口是一个集成在容器中的抽象,它对 application environment 的两个关键方面进行建模:配置文件(profiles) 和 属性(properties)。
profile是一个命名的、逻辑上的bean定义组,只有在给定的profile处于活动状态时才会在容器中注册。无论是用 XML 定义的还是用注解定义的,Bean 都可以被分配给一个profile。Environment
对象在profile方面的作用是确定哪些profile(如果有的话)是当前活动(active)的,以及哪些profile(如果有的话)应该是默认活动的。
属性(Properties)在几乎所有的应用程序中都扮演着重要的角色,它可能来自各种来源:properties 文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、特设的 Properties
对象、Map
对象等等。与属性有关的 Environment
对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从它们那里解析属性。
1. Bean定义配置
Bean定义配置(Bean definition profiles) 在核心容器中提供了一种机制,允许在不同的环境中注册不同的bean。“环境”这个词对不同的用户来说意味着不同的东西,而这个功能可以帮助许多用例,包括。
- 在开发中针对内存中的数据源工作,而在QA或生产中从JNDI查找相同的数据源。
- 仅在将应用程序部署到 performance 环境中时才注册监控基础设施。
- 为 customer A 与 customer B 的部署注册定制的bean实现。
考虑一下实际应用中的第一个用例,它需要一个 DataSource
。在一个测试环境中,配置可能类似于以下:
@Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("my-schema.sql") .addScript("my-test-data.sql") .build(); }
现在考虑如何将这个应用程序部署到QA或production环境中,假设该应用程序的数据源是在生产应用服务器的JNDI目录下注册的。我们的 dataSource
bean现在看起来如下:
@Bean(destroyMethod = "") public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); }
问题是如何根据当前环境在使用这两种变化之间切换。随着时间的推移,Spring用户已经设计出了许多方法来完成这一工作,通常是依靠系统环境变量和包含 ${placeholder}
标记的XML <import/>
语句的组合,根据环境变量的值解析到正确的配置文件路径。Bean定义配置是一个核心的容器功能,为这个问题提供了一个解决方案。
如果我们把前面的例子中显示的环境特定的Bean定义的用例进行概括,我们最终需要在某些情况下注册某些Bean定义,但在其他情况下不需要。你可以说,你想在情况A中注册某种类型的bean定义,而在情况B中注册另一种类型。
1.1使用 @Profile
@Profile
注解让你表明当一个或多个指定的配置文件处于活动状态时,一个组件就有资格注册。使用我们前面的例子,我们可以重写 dataSource
配置如下:
@Configuration @Profile("development") public class StandaloneDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } }
@Configuration @Profile("production") public class JndiDataConfig { @Bean(destroyMethod = "") (1) public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } } (1)@Bean(destroyMethod = "") 禁用默认的销毁方法推理。
如前所述,对于
@Bean
方法,你通常会选择使用程序化的JNDI查找,通过使用Spring的JndiTemplate
/JndiLocatorDelegat
helper 或前面显示的直接使用JNDIInitialContext
,但不使用JndiObjectFactoryBean
的变体,这将迫使你将返回类型声明为FactoryBean
类型。
profile 字符串可以包含一个简单的 profile 名称(例如,production
)或一个 profile 表达式。profile 表达式允许表达更复杂的 profile 逻辑(例如,production & us-east
)。profile 表达式中支持以下运算符。
!
: profile的NOT
逻辑&
: profile的AND
的逻辑|
: profile的OR
的逻辑
你不能在不使用括号的情况下混合使用
&
和|
运算符。例如,production & us-east | eu-central
不是一个有效的表达。它必须表示为production & (us-east | eu-central)
。
你可以使用 @Profile 作为 元注解,以创建一个自定义的组成注解。下面的例子定义了一个自定义的 @Production 注解,你可以把它作为 @Profile("production") 的直接替换:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Profile("production") public @interface Production { }
如果一个 @Configuration 类被标记为 @Profile ,所有与该类相关的 @Bean 方法和 @Import 注解都会被绕过,除非一个或多个指定的 profiles 处于激活状态。如果一个 @Component 或 @Configuration 类被标记为 @Profile({"p1", "p2"}) ,该类不会被注册或处理,除非 profiles "p1" 或 "p2" 已经被激活。如果一个给定的profiles前缀为NOT操作符(! ),那么只有在该profiles没有激活的情况下,才会注册被注解的元素。例如,给定 @Profile({"p1", "!p2"}) ,如果profile 'p1' 被激活或 profile 'p2' 未被激活,注册将发生。 |
@Profile
也可以在方法层面上声明,以便只包括一个配置类的一个特定Bean(例如,对于一个特定Bean的备选变体),正如下面的例子所示:
@Configuration public class AppConfig { @Bean("dataSource") @Profile("development") (1) public DataSource standaloneDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } @Bean("dataSource") @Profile("production") (2) public DataSource jndiDataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } } (1)StandaloneDataSource 方法只在 development profile 中可用。 (2)jndiDataSource 方法只在 production profile 中可用。
对于
@Bean
方法的@Profile
,一个特殊的情况可能适用。在同一Java方法名的重载@Bean
方法的情况下(类似于构造函数重载),@Profile
条件需要在所有重载的方法上一致声明。如果条件不一致,那么在重载的方法中,只有第一个声明的条件才是重要的。因此,@Profile
不能被用来选择具有特定参数签名的重载方法而不是另一个。同一个Bean的所有工厂方法之间的解析遵循Spring在创建时的构造函数解析算法。如果你想定义具有不同概况条件的备选Bean,请使用不同的Java方法名,通过使用
@Bean
name 属性指向同一个Bean名称,如前面的例子所示。如果参数签名都是一样的(例如,所有的变体都有无参数的工厂方法),这是首先在一个有效的Java类中表示这种安排的唯一方法(因为一个特定名称和参数签名的方法只能有一个)。
1.2 默认 Profile
默认 profile 代表默认启用的 profile。考虑一下下面的例子:
@Configuration @Profile("default") public class DefaultDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .build(); } }
如果没有激活profile,就会创建 dataSource
。你可以把它看作是为一个或多个Bean提供默认定义的一种方式。如果任何profile被启用,默认的profile就不应用。
你可以通过在环境中使用 setDefaultProfiles()
来改变默认配置文件的名称,或者通过声明性地使用 spring.profiles.default
属性。
2. PropertySource
抽象
Spring的 Environment
抽象提供了对可配置的属性源层次结构的搜索操作。考虑一下下面的列表
ApplicationContext ctx = new GenericApplicationContext(); Environment env = ctx.getEnvironment(); boolean containsMyProperty = env.containsProperty("my-property"); System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
在前面的片段中,我们看到了一种询问Spring的高级方式,即询问 my-property 属性是否为当前环境所定义。为了回答这个问题,Environment 对象在一组 PropertySource 对象上执行搜索。PropertySource 是对任何key值对来源的简单抽象,Spring的 StandardEnvironment 配置了两个 PropertySource 对象—一个代表JVM系统属性集合(System.getProperties()),一个代表系统环境变量集合(System.getenv())。
具体来说,当你使用 StandardEnvironment
时,如果运行时存在 my-property
系统属性或 my-property
环境变量,调用 env.containsProperty("my-property"
) 会返回 true
。
最重要的是,整个机制是可配置的。也许你有一个自定义的属性源,你想集成到这个搜索中。要做到这一点,实现并实例化你自己的 PropertySource
,并将其添加到当前环境的 PropertySources
集合中。下面的例子展示了如何做到这一点:
ConfigurableApplicationContext ctx = new GenericApplicationContext(); MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); sources.addFirst(new MyPropertySource());
在前面的代码中,MyPropertySource 在搜索中被加入了最高优先级。如果它包含 my-property 属性,该属性将被检测并返回,而不是任何其他 PropertySource 中的任何 my-property 属性。 MutablePropertySources API暴露了许多方法,允许精确地操作属性源(property sources)的集合。
3. 使用 @PropertySource
@PropertySource
注解为向Spring的 Environment
添加 PropertySource
提供了一种方便的声明性机制。
给定一个包含键值对 testbean.name=myTestBean
的名为 app.properties
的文件,下面的 @Configuration
类以这样一种方式使用 @PropertySource
,即调用 testBean.getName()
返回 myTestBean:
@Configuration @PropertySource("classpath:/com/myco/app.properties") public class AppConfig { @Autowired Environment env; @Bean public TestBean testBean() { TestBean testBean = new TestBean(); testBean.setName(env.getProperty("testbean.name")); return testBean; } }
任何存在于 @PropertySource
资源位置的 ${…}
占位符都会根据已经针对环境(environment)注册的属性源集合进行解析,如下例所示:
@Configuration @PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties") public class AppConfig { @Autowired Environment env; @Bean public TestBean testBean() { TestBean testBean = new TestBean(); testBean.setName(env.getProperty("testbean.name")); return testBean; } }
假设 my.placeholder
存在于一个已经注册的属性源中(例如,系统属性或环境变量),那么该占位符将被解析为相应的值。如果没有,那么就使用 default/path
作为默认值。如果没有指定默认值,并且一个属性不能被解析,就会抛出一个 IllegalArgumentException
。
Doker 技术人的数码品牌!!!
文章下方有交流学习区!一起学习进步!也可以前往官网,加入官方微信交流群!!!你的支持和鼓励是我创作的动力❗❗❗
官网:Doker 多克; 官方旗舰店:首页-Doker 多克创新官方店-淘宝网全品大优惠优惠!!!