一起来读官方文档-----SpringIOC(11)

1.13。Environment

Environment接口是集成在容器中的抽象存在,它表现为应用程序环境的两个关键方面:profiles和properties。

1.13.1。Bean Definition Profiles

Bean Definition Profiles在核心容器中提供了一种机制,该机制允许在不同environment中注册不同的Bean。

说白了其实就是判断 spring.profiles.active 的值
这个值可以有多个中间用 , 隔开就可以

“environment”一词对不同的用户而言可能意味着不同的含义,并且此功能可以在许多用例中提供帮助,包括:

  • 在开发中针对内存中的数据源进行工作,而不是在进行QA或生产时从JNDI查找相同的数据源。
  • 仅在将应用程序部署到性能环境中时注册监视基础结构。
  • 为客户A和客户B部署注册bean的自定义实现。

考虑实际应用中需要使用的第一个用例DataSource。
在测试环境中,配置可能类似于以下内容:

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

现在,假设该应用程序的数据源已在生产应用程序服务器的JNDI目录中注册,请考虑如何将该应用程序部署到QA或生产环境中。
现在,我们的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语句的组合,这些${placeholder}根据环境变量的值解析为正确的配置文件路径。

Bean Definition Profiles是一项核心容器功能,可提供此问题的解决方案。

使用 @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");
    }
}
如前所述,对于@Bean方法,您通常选择使用编程式JNDI查找,方法是使用Spring的JNDIMplate/JNDilocatorDeleteGate帮助器,
或者使用前面显示的直接JNDIInitialContext用法,
而不是JndiObjectFactoryBean变量,因为factoryBean方法返回的是FactoryBean类型,而不是DataSource类型。
原理解释:
1.@Profile注解中指定了@Conditional注解中的ProfileCondition.class

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

}

2.首先在加载bean的时候发现有方法判断是否应该调过当前bean
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
            configClass.skippedBeanMethods.add(methodName);
            return;
}

在shouldSkip中会查询当前bean的所有的condition
并循环执行每个condition的matches
而@Profile的condition的matches如下所示

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")) {
                //此处 调用了environment的propertySources
                //判断当前配置中的所有的propertySources是否含有spring.profiles.active属性
                //有值的话就将它设置到environment的activeProfiles属性中
                //再判断当前类的@Profile注解中的值是否被包含在activeProfiles属性内
                //如果被包含则返回true
                if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

}

配置文件字符串可以包含简单的配置文件名称(例如production)或配置文件表达式。
配置文件表达式允许表达更复杂的配置文件逻辑(例如production & us-east)。
概要文件表达式中支持以下运算符:

  • !:配置文件的逻辑“非”
  • &:配置文件的逻辑“与”
  • |:配置文件的逻辑“或”
您不能在不使用括号的情况下混合使用 & 和 | 运算符。
例如, 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注解。  

如果一个@Component或@Configuration类标记有@Profile({"p1", "p2"}),
则除非已激活配置文件“ p1”或“p2”,否则不会注册或处理该类。  

如果给定的配置文件以NOT运算符(!)为前缀,
则仅在该配置文件未激活时才注册带注解的元素。  
例如,给定@Profile({"p1", "!p2"}),
如果配置文件“ p1”处于活动状态或配置文件“p2”未处于活动状态,
则会进行注册。

@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");
    }
}
  • 该standaloneDataSource方法仅在development配置文件中可用。
  • 该jndiDataSource方法仅在production配置文件中可用。

对于@Bean方法上的@Profile,可能会应用一个特殊的场景(在同一个配置类中):
-----对于具有相同Java方法名的重载@Bean方法(类似于构造函数重载),需要在所有重载方法上声明相同的@Profile条件,声明相同的条件并不是因为可以自动选择重载方法,是因为这一批重载方法都会因为第一个方法的校验不合格就全部不通过,如果第一个合格才会往下继续判断是否可以用其他的重载方法进行bean的注册。
-----如果条件不一致,则只有重载方法中第一个声明的条件才生效。
因此,@Profile不能用于选择具有特定参数签名的重载方法。
同一bean的所有工厂方法之间的解析在创建时遵循Spring的构造函数解析算法。

如果您想定义具有不同配置文件条件的替代bean,请使用指向相同bean名称的不同@Bean方法名,方法是使用@Bean的name属性,如前面的示例所示。

如果参数签名都相同(例如,所有变量都没有arg工厂方法),那么这是在一个有效的Java类中首先表示这种安排的唯一方法(因为只能有一个特定名称和参数签名的方法)。

分析:
// 判断当前@Bean是否需要跳过 
// 这里的判断顺序就是 Config类中的@Bean代码的先后顺序跟@Order无关
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
    configClass.skippedBeanMethods.add(methodName);
    return;
}
//当同名方法出现并在之前被跳过之后 这里会判断skippedBeanMethods属性是否包含并直接跳过
//所以不管同一个配置类中后续的同名方法是否带有注解都将不再处理
if (configClass.skippedBeanMethods.contains(methodName)) {
    return;
}

XML Bean定义配置文件XML对应项是元素的profile属性 。 我们前面的示例配置可以用两个XML文件重写,如下所示:

<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>
</beans>

spring-bean.xsd已经做了限制,只允许这样的元素作为文件中的最后一个元素。这将有助于提供灵活性,并且不会导致XML文件的混乱。

XML对应项不支持前面描述的配置文件表达式
-----例如:(production & (us-east | eu-central))。
但是,可以通过使用!运算符来取消配置文件。
也可以通过嵌套配置文件来应用逻辑“与”,如以下示例所示:

<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="...">

    <!---如果production和 us-east配置文件都处于活动状态,则dataSource会被注册-->

    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>
Default 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();
    }
}

如果没有配置文件被激活,dataSource被创建。
您可以看到这是为一个或多个bean提供默认定义的一种方法。

如果启用了任何配置文件,则默认配置文件不适用。

您可以通过setDefaultProfiles() 或者使用声明性地使用spring.profiles.default属性来更改默认配置文件的名称。

1.13.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是对任何键-值对源的简单抽象,Spring的StandardEnvironment配置有两个PropertySource对象

  • 一个代表JVM系统属性集(System.getProperties())
  • 一个代表系统环境变量集(System.getenv())。
这些默认属性源是为StandardEnvironment提供的,供独立应用程序使用。
StandardServletEnvironment使用了附加的默认属性源,包括servlet配置和servlet上下文参数。
它可以选择启用JndiPropertySource。
所执行的搜索是分层的。
默认情况下,系统属性优先于环境变量。
因此,如果在调用env.getProperty(“my-property”)期间,恰好在两个位置都设置了my-property属性,则系统属性值“胜出”并被返回。
注意,属性值没有被合并,而是被前面的条目完全覆盖。

对于common StandardServletEnvironment,完整的层次结构如下所示,最高优先级的条目位于顶部:

    ServletConfig参数(如果适用——例如,在DispatcherServlet上下文的情况下)
    ServletContext参数(web.xml上下文参数项)
    JNDI环境变量(java:comp/env/ entries)
    JVM系统属性(-D命令行参数)
    JVM系统环境(操作系统环境变量)

最重要的是,整个机制是可配置的。
也许您具有要集成到此搜索中的自定义属性源。
为此,请实现并实例化自己的实例PropertySource并将其添加到PropertySourcescurrent的集合中Environment。
以下示例显示了如何执行此操作:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
//MyPropertySource被添加了最高优先级。  
sources.addFirst(new MyPropertySource());

该MutablePropertySources API公开了许多方法, 这些方法允许对属性源集进行精确操作。

1.13.3。使用@PropertySource

@PropertySource注解提供了一种方便的声明机制,可以将PropertySource添加到Spring的环境中。

给定一个名为app.properties的文件,
其中包含键值对testbean.name=myTestBean,
下面的@Configuration类使用@PropertySource,
调用env.getProperty("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;
    }
}

任何出现在@PropertySource资源位置的${…}占位符都会根据已经在环境中注册的属性源进行解析,如下面的示例所示:

//假定my.placeholder存在于已注册的属性源之一(例如,系统属性或环境变量)中,则占位符将解析为相应的值。  
//如果不是,则default/path用作默认值。  
//如果未指定默认值并且无法解析属性, IllegalArgumentException则抛出。

@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;
    }
}
根据Java 8的约定,@PropertySource注解是可重复的。
但是,所有这样的@PropertySource注解都需要在同一级别声明,要么直接在配置类上声明,要么作为同一自定义注解中的元注解声明。
不推荐混合使用直接注解和元注解,因为直接注解有效地覆盖了元注解。

1.13.4。声明中的占位符解析 过去,元素中的占位符的值只能根据JVM系统属性或环境变量解析。
现在情况已经不一样了。
因为环境抽象集成在整个容器中,所以很容易通过它来解析占位符。
这意味着您可以以任何您喜欢的方式配置解析过程。
您可以更改搜索系统属性和环境变量的优先级,或者完全删除它们。
您还可以在适当的情况下添加您自己的属性源。

具体地说,无论客户属性定义在哪里,只要它在环境中可用,以下语句都适用:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
1.15。ApplicationContext的其他功能

正如在引言中所讨论的,
org.springframework.beans.factory包提供了管理和操作bean的基本功能,包括以编程的方式。
org.springframework.context 包添加了ApplicationContext接口, 该接口扩展了BeanFactory接口,
此外还扩展了其他接口,以更面向应用程序框架的风格提供额外的功能。

许多人以一种完全声明式的方式使用ApplicationContext,甚至不是通过编程来创建它,而是依赖于支持类(如ContextLoader)来自动实例化一个ApplicationContext,作为Java EE web应用程序的正常启动过程的一部分。

为了以更面向框架的风格增强BeanFactory的功能,上下文包还提供了以下功能:

  • 通过MessageSource接口访问i18n风格的消息。
  • 通过ResourceLoader接口访问资源,例如url和文件。
  • 事件发布,即通过使用ApplicationEventPublisher接口发布到实现ApplicationListener接口的bean。
  • 通过HierarchicalBeanFactory接口加载多个(分层的)上下文,让每个上下文都关注于一个特定的层,比如应用程序的web层。
1.15.1。国际化使用MessageSource

ApplicationContext接口扩展了一个名为MessageSource的接口,因此提供了国际化(“i18n”)功能。
Spring还提供了HierarchicalMessageSource接口,该接口可以分层解析消息。

这些接口一起提供了Spring实现消息解析的基础。

在这些接口上定义的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc):
    用于从MessageSource检索消息的基本方法。
    如果未找到指定语言环境的消息,则使用默认消息。
    通过使用标准库提供的MessageFormat功能,传入的任何参数都将成为替换值。
  • String getMessage(String code, Object[] args, Locale loc):
    本质上与前面的方法相同,但有一个区别:不能指定缺省消息。
    如果找不到消息,则抛出NoSuchMessageException。
  • String getMessage(MessageSourceResolvable, Locale Locale):前面方法中使用的所有属性也包装在一个名为MessageSourceResolvable类中,可与此方法一起使用。

加载ApplicationContext时,它会自动搜索上下文中定义的MessageSource bean。
bean的名称必须是messageSource。

  • 如果找到这样一个bean,对前面方法的所有调用都将委托给消息源。
  • 如果没有找到消息源,ApplicationContext将尝试查找包含同名bean的父消息源。如果是,则使用该bean作为消息源。
  • 如果ApplicationContext找不到任何消息源,则实例化一个空的DelegatingMessageSource,以便能够接受对上面定义的方法的调用。

Spring提供了两个消息源实现:
ResourceBundleMessageSource和StaticMessageSource。

两者都实现了HierarchicalMessageSource以执行嵌套消息传递。
很少使用StaticMessageSource,但它提供了将消息添加到源的编程方法。

下面的示例展示ResourceBundleMessageSource:

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

这个例子假设你有所谓的三个资源包format.properties,exceptions.properties,windows.properties 在类路径中定义。
解析消息的任何请求均通过JDK标准的通过ResourceBundle对象解析消息的方式来处理。
就本示例而言,假定上述两个资源束文件的内容如下:

#在format.properties中
message=Alligators rock!



#在exceptions.properties中
argument.required=The {0} argument is required.

下一个示例显示了运行该MessageSource功能的程序。
请记住,所有ApplicationContext实现也是MessageSource实现,因此可以强制转换为MessageSource接口。

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
    System.out.println(message);
}

以上程序的结果输出如下:

Alligators rock!

总之,MessageSource是在一个名为beans.xml的文件中定义的,它存在于classpath中。
MessageSource bean定义通过其basenames属性引用大量资源包。
在列表中传递给basenames属性的三个文件作为类路径的根文件存在,它们被称为format.properties,exceptions.properties,and windows.properties。

下一个示例显示了传递给消息查找的参数。

这些参数被转换为字符串对象,并插入到查找消息中的占位符中。

<beans>

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <bean id="example" class="com.something.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", Locale.ENGLISH);
        System.out.println(message);
    }
}

execute()方法调用的结果输出如下:

The userDao argument is required.

关于国际化(“i18n”),Spring的各种MessageSource实现遵循与标准JDK ResourceBundle相同的语言环境解析和回退规则。
简而言之,继续前面定义的示例messageSource,如果您希望根据英国(en-GB)地区解析消息,您将创建名为format_en_GB.properties, exceptions_en_GB.properties, and windows_en_GB.properties。

通常,语言环境解析由应用程序的周围环境管理。
在下面的示例中,手动指定解析(英国)消息所对应的语言环境:

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.


public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

运行上述程序的结果输出如下:

Ebagum lad, the 'userDao' argument is required, I say, required.

您还可以使用MessageSourceAware接口来获取对已定义的任何消息源的引用。
当创建和配置bean时,在ApplicationContext中定义的任何bean实现MessageSourceAware接口的都被注入应用上下文的MessageSourceAware接口。

作为ResourceBundleMessageSource的替代方案,Spring提供了一个ReloadableResourceBundleMessageSource类。
这个变体支持相同的bundle文件格式,但是比基于JDK的标准ResourceBundleMessageSource实现更加灵活。
特别是,它允许从任何Spring资源位置读取文件(不仅仅是从类路径),并支持bundle属性文件的热重新加载(同时有效地缓存它们)。
有关详细信息,请参见ReloadableResourceBundleMessageSource javadoc。
1.15.2。标准和自定义事件

ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。
如果实现ApplicationListener接口的bean被部署到上下文中,那么每当一个ApplicationEvent被发布到ApplicationContext时,该bean就会得到通知。
本质上,这就是标准的观察者设计模式。

从Spring 4.2开始,事件基础设施已经得到了显著改进,并提供了一个基于注解的模型,
以及发布任意事件的能力(也就是说,不需要从ApplicationEvent扩展的对象)。
当发布这样的对象时,我们为您将其包装在事件中。

表7.内置事件

事件说明
ContextRefreshedEvent在初始化或刷新ApplicationContext时发布
例如,ConfigurableApplicationContext.refresh()方法

这里,初始化意味着加载了所有bean
检测并激活了后处理器bean
预先实例化了singleton
并且ApplicationContext对象可以使用了。

只要上下文尚未关闭
并且所选的ApplicationContext实际上支持这种“热”刷新
就可以多次触发刷新
例如,XmlWebApplicationContext支持热刷新,
但GenericApplicationContext不支持。
ContextStartedEvent在ConfigurableApplicationContext.start()方法
启动ApplicationContext时发布。

这里,启动意味着所有生命周期bean都收到一个显式的启动信号。
通常,这个信号用于在显式停止后重新启动bean
,但是它也可以用于启动尚未配置为自动启动的组件
例如,尚未在初始化时启动的组件。
ContextStoppedEvent在ConfigurableApplicationContext.stop()方法
停止ApplicationContext时发布。

“停止”意味着所有生命周期bean都收到一个显式的停止信号。
停止的上下文可以通过start()调用重新启动。
ContextClosedEvent通过使用ConfigurableApplicationContext.close()方法
或通过JVM shutdown hook关闭ApplicationContext时发布。

,“关闭”意味着所有的单例bean将被销毁。
一旦上下文关闭,
它就会到达生命的终点,无法刷新或重新启动。
RequestHandledEvent一个特定于web的事件,
告诉所有bean一个HTTP请求已经得到服务。
此事件在请求完成后发布。
此事件仅适用于使用Spring的DispatcherServlet的web应用程序。
ServletRequestHandledEvent该类的子类RequestHandledEvent
添加了Servlet-specific的上下文信息。

您还可以创建和发布自己的自定义事件。
下面的示例显示了一个简单的类,该类扩展了Spring的ApplicationEvent基类:

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

要发布自定义ApplicationEvent,请在ApplicationEventPublisher上调用publishEvent()方法 。

通常,这是通过创建一个实现ApplicationEventPublisherAware并注册为Spring bean的类来完成的 。

以下示例显示了此类:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在配置时,Spring容器检测到该ApplicationEventPublisherAware实现EmailService 并自动调用 setApplicationEventPublisher()。

实际上,传入的参数是Spring容器本身。
您正在通过其ApplicationEventPublisher界面与应用程序上下文进行交互。

要接收自定义ApplicationEvent,您可以创建一个实现 ApplicationListener并注册为Spring bean的类。
以下示例显示了此类:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意,ApplicationListener通常是用自定义事件的类型参数化的(在前面的示例中是BlockedListEvent)。
这意味着onApplicationEvent()方法可以保持类型安全,避免向下强制转换。
您可以注册任意数量的事件监听器,但是请注意,默认情况下,事件监听器同步接收事件。 这意味着publishEvent()方法会阻塞,直到所有监听器都完成了事件的处理。
这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它将在发布程序的事务上下文内操作。

下面的例子显示了用于注册和配置上面每个类的bean定义:

<!--当调用emailService bean的sendEmail()方法时,  
    如果有任何需要阻止的电子邮件消息,
    则发布类型为BlockedListEvent的自定义事件。 -->
<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<!--blockedListNotifier         
    bean注册为一个ApplicationListener并接收BlockedListEvent, 
    此时它可以通知适当的方。-->
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="blockedlist@example.org"/>
</bean>
Spring的事件机制是为相同上下文中的Spring bean之间的简单通信而设计的。

然而,对于更复杂的企业集成需求,
单独维护的Spring integration项目提供了构建轻量级、面向模式、事件驱动架构的完整支持,
这些架构构建在众所周知的Spring编程模型之上。
基于注解的事件侦听器

从Spring 4.2开始,您可以使用@EventListener注解在托管Bean的任何公共方法上注册事件侦听器。

该BlockedListNotifier可改写如下:

public class BlockedListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

方法签名再次声明它侦听的事件类型,但是这次使用了灵活的名称,并且没有实现特定的侦听器接口。
只要实际事件类型在其实现层次结构中解析泛型参数,就可以通过泛型缩小事件类型。

如果您的方法应该侦听多个事件,或者您希望在不使用任何参数的情况下定义它,那么还可以在注解本身上指定事件类型。

下面的例子展示了如何做到这一点:

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

还可以通过使用定义SpEL表达式的注解的条件属性来添加额外的运行时过滤,该注解应该与针对特定事件实际调用方法相匹配。

下面的例子展示了我们的通知程序如何被重写,只有在content 属性等于my-event时才被调用:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blockedListEvent) {
    // notify appropriate parties via notificationAddress...
}

每个SpEL表达式针对专用上下文进行评估。 下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:

表8. Event SpEL可用的元数据

名称例子
Event#root.event or event
参数数组#root.args or args;
args[0]访问第一个参数等。
参数名称#blEvent或#a0
(您还可以使用#p0或#p<#arg>参数表示法作为别名)
请注意,root.event允许您访问基础事件,即使您的方法签名实际上引用了已发布的任意对象。

如果你需要发布一个事件作为处理另一个事件的结果,你可以改变方法签名来返回应该发布的事件,如下面的例子所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}
asynchronous listeners.不支持此功能 。

此新方法为每处理一个BlockedListEvent事件都会发布一个新事件ListUpdateEvent。
如果您需要发布多个事件,则可以返回一个Collection事件。

asynchronous listeners 异步侦听器

如果需要一个特定的侦听器异步处理事件,可以重用常规的@Async支持。
下面的例子展示了如何做到这一点:

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

使用异步事件时,请注意以下限制:

  • 如果异步事件侦听器抛出Exception,则不会传播到调用者。
  • 异步事件侦听器方法无法通过返回值来发布后续事件。如果您需要发布另一个事件作为处理的结果,请插入一个 ApplicationEventPublisher 以手动发布事件。
Ordering Listeners

如果需要先调用一个侦听器,则可以将@Order注解添加到方法声明中,
如以下示例所示:

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}
一般事件

还可以使用泛型来进一步定义事件的结构。
考虑使用EntityCreatedEvent ,其中T是所创建的实际实体的类型。
例如,您可以创建以下侦听器定义来只为一个人接收EntityCreatedEvent:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

由于类型擦除,只有在触发的事件解析了事件侦听器所基于的通用参数
(即class PersonCreatedEvent extends EntityCreatedEvent { …​ })时,此方法才起作用 。

在某些情况下,如果所有事件都遵循相同的结构(前面示例中的事件也应该如此),那么这可能会变得非常乏味。
在这种情况下,您可以实现ResolvableTypeProvider来指导运行时环境所提供的框架。
下面的事件展示了如何做到这一点:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}
这不仅适用于ApplicationEvent作为事件发送的任何对象,而且适用于该对象。
1.15.3。方便地访问低级资源

为了优化使用和理解应用程序上下文,您应该熟悉Spring的Resource类,如参考资料中所述。

应用程序上下文是一个ResourceLoader,可用于加载资源对象。

Resource本质上是JDK java.net.URL类的功能丰富版本。
事实上,Resource 的实现在适当的时候包装了一个java.net.URL的实例。

Resource可以以透明的方式从几乎任何位置获取底层资源,包括类路径、文件系统位置、可用标准URL描述的任何位置,以及其他一些变体。

如果Resource位置字符串是没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。

您可以配置部署到应用程序上下文中的bean,以实现特殊的回调接口ResourceLoaderAware,在初始化时自动回调,而应用程序上下文本身作为ResourceLoader传入。
您还可以公开Resource类型的属性,以便用于访问静态资源。Resource 像其他属性一样可以被注入。

您可以将这些资源属性指定为简单的字符串路径,并在部署bean时依赖于从这些文本字符串到实际资源对象的自动转换。

提供给ApplicationContext构造函数的位置路径或路径实际上是资源字符串,并且以简单的形式,根据特定的上下文实现进行适当的处理。
例如,ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。
您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL加载定义,而不管实际上下文类型是什么。

1.15.4。应用程序启动跟踪

ApplicationContext管理Spring应用程序的生命周期,并围绕组件提供丰富的编程模型。
因此,复杂的应用程序可能具有同样复杂的组件图和启动阶段。

使用特定的度量来跟踪应用程序的启动步骤可以帮助理解启动阶段的时间花费在哪里,它也可以作为一种更好地理解整个上下文生命周期的方法。

AbstractApplicationContext(及其子类)由ApplicationStartup检测,它收集关于不同启动阶段的StartupStep数据:

  • 应用程序上下文生命周期(基本包扫描,配置类管理)
  • bean生命周期(实例化、智能初始化、后处理)
  • 应用程序事件处理

下面是AnnotationConfigApplicationContext中的插装示例:

// 创建并启动记录
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// 向当前步骤添加标记信息
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行我们正在测量的实际阶段
this.scanner.scan(basePackages);
// 结束
scanPackages.end();
1.15.5。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作为默认值。

当参数确实存在时,侦听器使用预定义的分隔符(逗号、分号和空白)分隔字符串,并将这些值用作搜索应用程序上下文的位置。

也支持Ant风格的路径模式。
例如:
/WEB-INF/*Context.xml(对于在WEB-INF目录中名称以Context结尾的所有文件)

/WEB-INF/*/Context.xml(对于WEB-INF任何子目录中的所有此类文件)

1.16.1。BeanFactory or ApplicationContext?

本节解释BeanFactory和ApplicationContext容器级别之间的差异,以及引导的含义。

您应该使用ApplicationContext,除非您有很好的理由不这样做,使用GenericApplicationContext和它的子类AnnotationConfigApplicationContext作为自定义引导的通用实现。
这些是用于所有常见目的的Spring核心容器的主要入口点:加载配置文件、触发类路径扫描、以编程方式注册bean定义和带注解的类,以及(从5.0开始)注册功能性bean定义。

因为ApplicationContext包含了BeanFactory的所有功能,所以一般建议它优于普通的BeanFactory,除非需要对bean处理进行完全控制的场景除外。

对于许多扩展的容器特性,如注解处理和AOP代理,BeanPostProcessor扩展点是必不可少的。如果只使用普通的DefaultListableBeanFactory,默认情况下不会检测到这种后处理器并激活它。

下表列出了BeanFactory和ApplicationContext接口和实现提供的特性。 表9.功能矩阵

特征BeanFactoryApplicationContext
Bean实例化/布线YesYes
集成的生命周期管理NoYes
自动BeanPostProcessor登记NoYes
方便的消息源访问(用于内部化)NoYes
内置ApplicationEvent发布机制NoYes

要使用显式注册Bean后处理器DefaultListableBeanFactory,您需要以编程方式调用addBeanPostProcessor,如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

要将一个BeanFactoryPostProcessor应用到一个普通的DefaultListableBeanFactory中,你需要调用它的postProcessBeanFactory方法,如下面的例子所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

如上所见手动登记是十分不方便的,尤其是依靠BeanFactoryPostProcessor和BeanPostProcessor扩展功能的时候。

一个AnnotationConfigApplicationContext注册了所有公共注解后处理器,并可能通过配置注解(如@EnableTransactionManagement)在后台引入额外的处理器。

在Spring的基于注解的配置模型的抽象层上,bean后处理器的概念仅仅成为容器内部的细节。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值