Spring框架起步——IOC容器,第二篇

350 阅读12分钟

第二篇开始啦!因为字数限制,本来想把第一章写在一篇文章里面的(*~*)

出现的晦涩词汇的理解

Aware:Aware系的接口,带有入侵性,可以让实现它的Bean拥有从容器获取Bean基本信息(比如,名字,所属类)的能力,类似于给这个Bean开了VIP会员特权。

基于Java的容器配置

本节主要介绍如何在你的Java代码里面使用注解来配置Spring容器。

基本概念:@Bean和@Configuration

Spring的新的Java配置的支持的核心是基于@Configuration类和基于@Bean的方法。

@Bean注解表明这个方法会实例化,配置,并初始化一个由Spring IOC容器管理的Bean。这个注解通常和@Configuration搭配使用,虽然也可以和任意的@Component一起使用。

@Configuration标注的类表明此类的主要作用作为Bean定义的一个源。它还可以通过调用类里面的其他@Bean方法来定义Bean之间的依赖关系。最简单的用法如下:

@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

完整的@Configuration模式和“精简的”@Bean模式的对比:

当一个@Bean方法未在@Configuration类里面声明时,那么可以称这个@Bean方法的模式是精简的模式,声明在@Component组件类或普通的类里面的@Bean方法都可以被称作“精简”模式。某些服务组件可能会在每个可用的组件类上,通过一个附加的@Bean方法把管理视图暴露给容器,在这种情况下,@Bean方法更像是工厂方法。

不同于@Configuration,精简模式的@Bean没法定义Bean之间的依赖关系。取而代之的是,它们在其包含组件的内部状态上进行操作,或者是它们声明的参数。这样的@Bean方法不用当调用其他任何的@Bean方法,这样的方法,从字面上来看,应该只是特定Bean引用的工厂方法。而没有任何运行时的语义状态。负面影响就是,无法在运行时应用CGLIB代理,所以类的设计也就没有限制(比方说,定义为final)。

通常情况下,@Bean方法应该在@Configuration类里面定义,确保可以使用"完整的"模式。这样的话,跨方法引用可以被重新导向至容器的生命周期管理。这样可以防止不经意间地常规Java调用方法,对于同一@Bean方法的调用,这有助于减少在“精简”模式下运行时难以跟踪的细微bug。

使用AnnotationConfigApplicationContext实例化Spring容器

AnnotationConfigApplicationContext在Spring3.0之后引入,这个丰富的类,既可以把@Configuration作为输入源,也可以把普通的@component类作为输入,或者用JSR-330注解标注的类也可以。

当@Configuration作为输入时,@Configuration类自己会作为Bean定义注册,而且类里面的所有@Bean方法也会作为Bean定义注册。

当@Component和JSR-330标准注解标注的类作为输入时,这些类会作为Bean定义注册,并且假定类的内部会在必要的时候使用诸如@Autowired和@Inject的注解。

  1. 简单构造

正如XML那样,你可以使用@Configuration标注的类作为输入来实例化AnnotationConfigApplicationContext,比如:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

AnnotationConfigApplicationContext的部分源码:

public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
    /**
	 * Create a new AnnotationConfigApplicationContext that needs to be populated
	 * through {@link #register} calls and then manually {@linkplain #refresh refreshed}.
	 */
	public AnnotationConfigApplicationContext() {
	    // ...
	}

	/**
	 * Create a new AnnotationConfigApplicationContext with the given DefaultListableBeanFactory.
	 * @param beanFactory the DefaultListableBeanFactory instance to use for this context
	 */
	public AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory) {
	    // ...
	}

	/**
	 * Create a new AnnotationConfigApplicationContext, deriving bean definitions
	 * from the given component classes and automatically refreshing the context.
	 * @param componentClasses one or more component classes — for example,
	 * {@link Configuration @Configuration} classes
	 */
	public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
	    // ...
	}
	
	/**
	 * Create a new AnnotationConfigApplicationContext, scanning for components
	 * in the given packages, registering bean definitions for those components,
	 * and automatically refreshing the context.
	 * @param basePackages the packages to scan for component classes
	 */
	public AnnotationConfigApplicationContext(String... basePackages) {
	    // ...
	}
}
  1. 使用register(Class<?> ...)程序化地构建容器

你还可以使用AnnotationConfigApplicationContext的无参构造器,构造一个实例,然后调用其register(class<?> ...)方法,如下所示:

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();
}
  1. 使用scan(String ...)启用组件扫描

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方法都会被注册到容器里面去。

  1. AnnotationConfigApplicationContext对Web应用的支持

AnnotationConfigApplicationContext是WebApplicationContext的一个变种,它的实现类——AnnotationConfigWebApplicationContext可以用于操作Web应用,比如配置SpringContextLoaderListener和Spring MVC DispatcherServlet等。

使用@Bean注解

废话不多说,看看源码先:

public @interface Bean {

    @AliasFor("name")
    String[] value() default {};
    
    @AliasFor("value")
    String[] name() default {};
    
    @Deprecated
    Autowire autowire() default Autowire.NO;
    
    boolean autowireCandidate() default true;
    
    String initMethod() default "";
    
    String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}
  1. 声明一个Bean

通过在一个方法体前面注上@Bean注解,你可以使这个方法向ApplicationContext里面注入一个Bean定义,Bean类型为方法返回值类型,Bean名称默认为方法名。如下所示:

@Configuration
public class AppConfig {

    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}

也可以把返回类型设置为接口类型,而返回的实际值是接口的实现类:

@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}

这么做的话,结果就是隐藏了实际类型,可能会在自动织入或获取Bean时产生一些冲突。如果实现类实现了不止一个接口,那么可能会产生不安全隐患,所以最好还是指出特定的返回类型。

  1. Bean依赖

对于@Bean注解的方法,可以通过方法参数的形式指明构建此Bean所需要的依赖。类似于基于构造器的依赖注入。

  1. 接收生命周期回调

所有使用@Bean定义的类都可以使用普通的生命周期回调,也可以使用,JSR-250注解——@PostContruct和@PreDestroy。

同时也支持标准的Spring生命周期回调,比如:InitializingBean,DisposableBean,Lifecycle。

标准的*Aware接口(比如:BeanFactoryAware,BeanNameAware,MessageSourceAware,ApplicationContextAware等)也被很好地支持。

@Bean也支持指定任意方法为初始化方法或销毁方法:

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,会自动导入一个销毁方法回调,如果你不希望在容器关闭时,这个销毁方法被调用的话,请这样设置:@Bean(destroyMethod="")。这种措施对于数据库数据源很有用。

上面例子的初始化方法可以这么使用:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        BeanOne beanOne = new BeanOne();
        beanOne.init();
        return beanOne;
    }

    // ...
}

当你直接使用Java形式来设计你的代码时,你可以在不依赖于容器生命周期的情况下编写任意你想要的代码。

  1. 限定Bean作用域

直接使用@Scope注解就行。比如指定是单例还是原型:

@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
}

使用代理的形式:

// 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;
}
  1. 自定义Bean名称

使用@Bean属性的name属性,就能替换掉默认为方法名的Bean名字:

@Configuration
public class AppConfig {

    @Bean(name = "myThing")
    public Thing thing() {
        return new Thing();
    }
}
  1. Bean别名

@Bean的name属性接收一个字符数组的时候,就算设置了Bean的别名了。

@Configuration
public class AppConfig {

    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}
  1. Bean描述

有时,为一个Bean添加描述是很有意义的,比方说对于具有监视意义的Bean,仅需简单地添加@Description注解:

@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Thing thing() {
        return new Thing();
    }
}

使用@Configuration注解

@Configuration定义了它所标注的类为一个Bean定义源。里面的@Bean方法定义了具体的Bean定义,同时,不同@Bean方法之间的引用也定义了Bean之间的依赖关系。

  1. 注入内部Bean依赖

当Bean依赖于其他的某个Bean时,表达依赖关系仅需简单地引用其他@Bean方法就行:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

注意:

这种定义依赖关系的方式,仅适用于在@Configuration定义的类内部使用,而不适用于@Component标注的普通类。

  1. 查找方法注入

查找方法注入是一种高级特性,我们建议尽可能少的使用。在单例Bean使用原型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();
}

然后某个类继承了CommandManager,重写了createCommand()方法,比方说这个方法会查找一个新的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();
        }
    }
}
  1. 更多的关于基于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();
    }
}

Spring容器管理的Bean默认为单例的,所以这里可能有问题,因为任何@Configuration类都会被CGLIB子类化,子类在构造Bean之前,会先检查容器有没有需要的Bean缓存(作用域),然后创建一个新的实例。

当然,如果你的Bean作用域不同的话,这又是另一种结果了。

组合基于Java的配置

  1. 使用@Import注解

@Import允许导入其他的含有@Bean方法的@Configuration类。如下所示:

@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();
    }
}

这样,在实例化ApplicationContext时,仅需ConfigB.class就行了:

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);
}

在Spring Framework4.2之后,可以直接Import普通组件类,这个作用类似于AnnotationConfigApplicationContext.register()方法,这样就不用扫描基础包了,还是挺有用的。

P.s. 个人建议,不妨试着写一个总配置类,导入所有其他的配置类,然后用这个总配置类实例化ApplicationContext就行。

把依赖注入到被导入的@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作为组件@Autowired注入到另一个@Configuration类里面去。

警告:

尽可能的以简单化地方法进行注入依赖,因为@Configuration会在上下文初始化时尽可能早地被处理。所以,就像上面的例子那样,尽可能使用基于参数的注入(因为基于参数不会那么早地就要求注入依赖)。

同样,在通过@Bean处理BeanPostProcessor和BeanFactoryPostProcessor时,你应当相当小心。这些@Bean方法应当被声明为static @Bean方法,这样就不会触发他们所在的@Configuration类的实例化。否则的话,@Autowired和@Value可能会不顶用,因为@Configuration类自身可能会比AutowiredAnnotationBeanPostProcessor更早地被创建为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");
}

小小的附加的知识

  1. 有条件地导入@Configuration类和@Bean方法

有时基于系统状态,启用或禁用某一个配置类或@Bean方法是很有用的,一个很常见的例子就是@Profile注解,用来指定需要激活的环境。

@Profile注解是通过更加灵活的@Conditional注解实现的,@Conditional注解指明,在注册一个Bean之前,应该参考org.springframework.context.annotation.Condition的实现。

然后org.springframework.context.annotation.Condition提供了matches()方法,以供进行匹配项处理,看一个例子:

@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;
}
  1. 组合Java配置和XML配置

有两种方式,以XML为中心的和以配置类为中心的,直接看示例趴!

  1. 以XML为中心

配置类:

@Configuration
public class AppConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

    @Bean
    public TransferService transferService() {
        return new TransferService(accountRepository());
    }
}

XML文件

<!--使用普通的Bean注入来引入配置类-->
<beans>
    <!-- enable processing of annotations such as @Autowired and @Configuration -->
    <context:annotation-config/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="com.acme.AppConfig"/>

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

<!--使用包扫描来引入配置类-->
<beans>
    <!-- picks up and registers AppConfig as a bean definition -->
    <context:component-scan base-package="com.acme"/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

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

使用:

public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
  1. 以配置类为中心

XML文件:

<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>

Java配置类:

@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource(url, username, password);
    }
}

用法:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

环境抽象

Environment接口是容器内的一个集成抽象,它对应用环境的两个关键层面建立模型:profile和property(配置描述和配置属性)。

profiles是一组逻辑化的,命名了的Bean定义,如果所给的profile被激活(或处于活跃状态),那么这个profile所包含的Bean定义会注册到容器里面。profile在XML和注解里均可设置,Environment的作为profile的决定者出现的,它会决定哪个profile会被使用,默认的profile是哪个。

properties在几乎所有应用里都扮演着很重要的角色,property可能从各种源里生成,比如:配置文件,JVM系统参数,系统环境变量,JNDI,servlet上下文参数,ad-hoc配置对象,Map对象,等等。Environment之于property的意义在于,提供了一个接口,通过这个接口,可以操作配置源,包括修改和读取。

Bean定义配置文件

Bean定义描述提供了一个包含在核心容器的机制,通过这个机制,允许在不同的环境注册不同的Bean,这在开发时是很有用的,考虑如下场景:

a) 在开发中针对内存中的数据源进行工作,而不是在进行QA或生产时从JNDI查找相同的数据源。
b) 仅在性能模式下注册资源监视组件。
c) 针对部署时和开发时不同Bean实现的切换。
  1. 使用@Profile

@Profile注解会在它所指定的profile被激活时向容器内注册相应的Bean:

@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="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

@Profile的值可以包含表达式,比如,'!'代表“非”;'&'代表“且”;'|'代表“或”。在组合使用时,记得加小括号"()"。当然,你也可以使用@Profile作为元注解来编写你自己的注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

如果一个配置类被@Profile标注了,那么它所包含的@Bean方法和与其关联的@Import注解都不会发生作用,直到profile处于激活状态。

当然,也可以标注在方法级别,用来指定某一特定Bean是否被激活:

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development")
    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")
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

注意:

对于使用@Profile注解的@Bean方法,如果两个方法名一样,仅根据profile的不同而返回同一类型的Bean的话,请记得,方法参数必须一致(都是无参或只有n个类型顺序均一致的参数)。否则会出错,如果你想完成profile不同,且参数也不同的话,记得使用@Bean的name属性加不一样的方法名。

  1. 在XML文件中使用profile

暂略不表,原文在这

  1. 激活一个profile

言简意赅地说,用Environment接口的setActiveProfiles(String... profiles)就能指定需要激活的profile。看看码:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
  1. 默认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();
    }
}

也可以使用setDefaultProfiles(String... profiles)方法来设置默认profile。

配置源抽象

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的StandardEnvironment由两个配置源对象组成,一个是JVM系统配置(System.getProperties()),一个是应用系统变量(System.getenv())。

对于Web应用,还有一个StandardServletEnvironment可以使用,通过它,可以获取你需要的servlet config和servlet context parameter。

对于配置源的搜索的层次化的。默认情况下,系统级配置拥有比环境配置更高的优先级,还有,同名属性不会合并,系统级会覆盖掉环境级的。对于一个普通的StandardServletEnvironment,它的搜索层次大致如下所示:

ServletConfig参数

ServletContext参数

JNDI环境变量

JVM系统参数

JVM系统环境

更重要的是,整个机制都是可配置的。如果你想添加一个自定义的配置源,可以试着向下面这么做:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

此时MyPropertySource拥有最高优先级。MutablePropertySources提供了一些方法,用来精确操纵配置源集合。

使用@PropertySource注解

@PropertySource提供了和通过上面的add()方法向Spring Environment添加数据源的等价的方法。看一个例子,其中app.properties是一个包含键值对的配置源:

@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里面的占位符都是针对环境中已经注册的配置进行解析的:

@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;
    }
}

有一个需要注意的,就是,@PropertySource是可以重复的,但是,仅限同一层面,什么意思呢?就是,要么仅作为单一注解使用,要么仅作为你自定义注解的元注解使用,不能既是元注解还是单一注解。

语句中的占位符

介于历史原因,语句中的占位符仅限JVM系统参数和环境变量,但是,现在来说,么得这个问题了!只要属性是注册在Environment里面就够了,Spring会自动搜寻并把占位符替换成属性值。

注册一个LoadTimeWeaver

LoadTimeWeaver是用于当类被加载进JVM时,Spring动态地进行类转换时使用的。为了启用它,可以在你的任何一个配置类前面加上@EnableLoadTimeWeaving:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

或XML文件:

<beans>
    <context:load-time-weaver/>
</beans>

下一篇