Spring深入浅出IoC(下)

简介: Environment、ApplicationContext扩展、


5.Environment



5.1 @Profile


Environment接口是集成在容器中抽象,它包含了应用程序环境两个关键的点:profiles和properties。

profiles是在bean定义时的命名、逻辑组,只有在profile激活状态下才能将bean注册到容器中。beans被分配到profile中,不管是基于XML定义还是基于注解定义的。Environment与profiles有关的作用是决定哪些profile(如果有)被激活,或者哪些profile(如果有)应该要默认激活。

properties几乎在所有应用程序中扮演着重要的角色,它有多个来源:properties文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、特殊设置的Properties对象和Map对象等等。Environment与properties有关的作用是给用户提供一个方便的服务接口来配置属性和解析属性。

@Profile注解是基于@Conditional注解实现的,源码如下:

// 注解源码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
    /**
     * The set of profiles for which the annotated component should be registered.
     */
    String[] value();
}
// 接口实现源码
class ProfileCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
}

Bean定义profile在核心容器中提供了一种在不同环境中注册不同bean的机制。environment这个词对于不同的用户意味着不同的东西,这个特性有很多使用场景,包括:

  • 在QA或生产环境中注册不同的数据源
  • 应用程序部署到性能环境时注册监控基础框架
  • 为消费者A与消费者B部署注册不同的实现

拿第一个场景举例,在实际应用中需要配置一个数据源DataSource,在开发环境配置如下:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("my-schema.sql")
            .addScript("my-test-data.sql")
            .build();
}

假设在QA或者生产环境,数据源配置如下:

@Bean
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是怎样基于当前环境来选择两个配置。现在,Spring用户有很多种解决方案,比如使用一组环境变量和XML配置

${placeholder}占位符来实现,这样当前配置就可以根据环境变量来区分。bean定义profile这个核心容器的特性也提供了一种解决方案。使用这个特性,我们可以定义一个环境专有的bean。


5.1.1 基于Java定义Profile


使用@Profile注解表示当一个或多个profile激活的时候注册某个组件。使用前面的例子:

类级配置

// 开发环境
@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时可以使用一些简单的逻辑运算符:

  • !:非,例如:!prod表示在prod没激活的时候注册
  • &:与,例如:local & dev表示在local和dev同时激活的时候注册
  • |:或,例如:local | dev表示在local或dev激活的时候注册

建议将环境的profile定义使用自定义注解封装起来:

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

方法级配置

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


5.1.2 基于XML定义Profile


配置文件级别配置

<!-- 开发环境配置文件 -->
<beans profile="development"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xsi:schemaLocation="...">
    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
<!-- 生产环境配置文件 -->
<beans profile="production"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="...">
    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>


标签级别配置

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="...">
    <!-- other bean definitions -->
    <!-- 开发环境配置 -->
    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>
    <!-- 生产环境配置 -->
    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>


5.1.3 激活Profile和默认Profile

如果我们配置好上面的示例后启动项目,肯定会报NoSuchBeanDefinitionException异常,因为容器找不到命名为dataSource的bean。需要激活profile才能注册相应环境配置的bean,在主方法类里面激活:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

另外也可以通过声明spring.profiles.active属性来激活,可以在系统环境变量、JVM系统属性、在web.xml中的Servlet上下文参数等等(见4.8),在测试模块中也可以使用@ActiveProfiles注解来配置激活。也可以同时激活多个profile:

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

也可以配置默认就激活的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被激活,则会使用默认值,只要有一个profile被激活,默认值就会失效。你也可以用EnvironmentsetDefaultProfiles()方法来配置,或者声明spring.profiles.default属性。


5.2 @PropertySource


在了解这个注解之前讲讲Spring的环境属性机制,前面说到Environment包含profile和properties,现在就讲讲properties。它提供了层级属性源的搜索功能,如下:

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

在上面的例子中,我们使用一种高级的方式来判断当前环境是否定义了my-property属性。Environment对象会从一组PropertySource对象中搜索,一个PropertySource是一些简单的键值对属性,Spring中StandardEnvironment包含两个PropertySource对象,JVM系统属性(System.getProperties())和系统环境变量(System.getenv())。上面例子中,当Java系统属性或环境变量中配置了my-property属性,env.containsProperty("my-property")就会返回true

属性的搜索存在优先级,比如在多个地方存在同样的属性时,Spring会优先选择某个属性。在一个普通的StandardServletEnvironment中,优先级从高到低如下:

  • ServletConfig参数(如果有,例如DispatcherServlet上下文)
  • ServletContext参数(web.xml上下文入口)
  • JNDI环境变量(java:comp/env/入口)
  • JVM系统属性(-D指定的命令行参数)
  • JVM系统环境(包括操作系统环境变量)

最重要的是,这个机制是完全可配置的。你可能需要集成自己的属性源,为此,只要实现PropertySource并实例化,然后添加到Environment中的PropertySources中,例如:

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

在上面的示例中,MyPropertySource拥有最高的优先级,MutablePropertySources提供了一些操作数据源有用的方法。

@PropertySource注解给Spring的Environment添加PropertySource提供了一个简单的的声明机制。创建一个文件app.properties添加键值对testbean.name=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;
    }
}

除了使用Environment来获取属性,也可以使用@Value${}占位符来获取。


6. ApplicationContext扩展



在本节前面讲了org.springframework.beans.factory包的BeanFactory接口,后来又说了org.springframework.context包的ApplicationContext接口,它继承了BeanFactory接口,提供了更强大的功能,为了使BeanFactory功能更具面向框架风格,context包还提供了下面的功能:

  • 通过MessageSource接口以国际化风格访问消息
  • 通过ResourceLoader接口访问像URL和文件资源
  • 通过ApplicationListenerApplicationEventPublisher接口来监听和发布事件
  • 通过HierarchicalBeanFactory接口来加载多个分级context,让每个容器都专注特定的层,例如:应用程序的Web层

这些功能就不全讲,需要使用的时候完全可以去找资料文档来使用这些功能。


6.1 监听事件


ApplicationContext通过ApplicationEvent类和ApplicationListener接口提供了事件处理机制,每次ApplicationEvent事件发布到ApplicationContext时,实现了ApplicationListener接口的bean就会被通知,这运用了标准的观察者模式。


6.1.1 @EventListener

Spring提供了一些标准事件如下:

  • ContextRefreshedEvent
  • ContextStartedEvent
  • ContextStoppedEvent
  • ContextClosedEvent
  • RequestHandledEvent

它们的功能就不介绍了,自己去查询资料,我们拿ContextClosedEvent举例,新建监听类:

@Service
public class ApplicationNotifier implements ApplicationListener<ContextClosedEvent> {
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println("ApplicationNotifier.onApplicationEvent");
    }
}

运行程序,当ApplicationContext调用close()方法时就会通知该类。在Spring4.2以后,还提供了基于注解创建监听器,把上面代码修改如下:

@Service
public class ApplicationNotifier {
    @EventListener
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println("ApplicationNotifier.onApplicationEvent");
    }
}

也能达到同样的效果,使用注解配置不需要实现ApplicationListener接口,默认@EventListener注解是根据方法参数指定监听的事件,也可以使用该注解的属性来添加被监听的事件,可以配置多个,例如:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}


6.1.2 自定义事件监听


我们除了使用Spring提供的事件外,还可以自己创建和发布自定义事件,创建事件类如下:

public class BlackListEvent extends ApplicationEvent {
    private String address;
    private String content;
    public BlackListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }
    // Getter, Setter and toString

通过调用ApplicationEventPublisherpublishEvent()方法来发布一个自定义ApplicationEvent。通常会创建一个bean类来实现ApplicationEventPublisherAware接口并注册,例如:

@Service
public class EmailService implements ApplicationEventPublisherAware {
    private List<String> blackList;
    private ApplicationEventPublisher publisher;
    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
    public void sendEmail(String address, String content) {
        if (blackList.contains(address)) {
            publisher.publishEvent(new BlackListEvent(this, address, content));
            return;
        }
        System.out.println("EmailService.sendEmail");
    }
}

创建监听类:

@Service
public class BlackListNotifier {
    @EventListener
    public void onApplicationEvent(BlackListEvent event) {
        System.out.println("BlackListNotifier.onApplicationEvent:" + event.toString());
    }


6.1.3 异步监听

使用@Async配置监听器异常执行

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
    // BlackListEvent is processed in a separate thread
}

使用异步监听要注意两点:

  • 如果监听器抛出了异常,不会传播给调用者,详情看AsyncUncaughtExceptionHandler
  • 这类事件监听不能发送回复。如果需要将处理结果发送给另一个事件,注入ApplicationEventPublisher手动发送


6.1.4 顺序监听

如果一个事件被多个监听器监听,我们可以使用@Order注解来指定监听器执行的顺序

@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress...
}


6.2 Web应用实例化ApplicationContext


你可以使用ContextLoader以声明方式创建ApplicationContext实例。当然,你也可以使用ApplicationContext的任何一个实现来以编程方式创建ApplicationContext实例。

你可以使用ContextLoaderListener来注册一个ApplicationContext,如下:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听器会检查contextConfigLocation参数,如果该参数不存在,则会默认使用/WEB-INF/applicationContext.xml。当存在参数并且有多个参数时,可能使用逗号、分号或空格来隔开。也可以使用模糊路径,比如/WEB-INF/*Context.xml(在WEB-INF目录下的所有以Context.xml结尾的文件)和/WEB-INF/**/*Context.xml(在WEB-INF目录下和所有子目录下以Context.xml结尾的文件)。


目录
相关文章
|
16天前
|
XML Java API
IoC 之 Spring 统一资源加载策略
IoC 之 Spring 统一资源加载策略
29 2
|
4天前
|
XML Java 测试技术
Spring IOC 控制反转总结
Spring IOC 控制反转总结
|
2天前
|
XML Java 数据格式
Spring5系列学习文章分享---第一篇(概述+特点+IOC原理+IOC并操作之bean的XML管理操作)
Spring5系列学习文章分享---第一篇(概述+特点+IOC原理+IOC并操作之bean的XML管理操作)
7 1
|
9天前
|
存储 Java 测试技术
Java Spring IoC&DI :探索Java Spring中控制反转和依赖注入的威力,增强灵活性和可维护性
Java Spring IoC&DI :探索Java Spring中控制反转和依赖注入的威力,增强灵活性和可维护性
9 1
|
16天前
|
XML Java API
IoC 之 Spring 统一资源加载策略【Spring源码】
IoC 之 Spring 统一资源加载策略【Spring源码】
22 2
|
16天前
|
存储 Java Spring
Spring IOC 源码分析之深入理解 IOC
Spring IOC 源码分析之深入理解 IOC
32 2
|
17天前
|
XML Java 数据格式
Spring--两大核心之一--IOC
Spring--两大核心之一--IOC
|
23天前
|
XML Java 数据格式
|
23天前
|
druid Java 关系型数据库
|
23天前
|
Oracle Java 关系型数据库