Spring5中文文档【11】IOC容器之ApplicationContext的附加功能

1. 前言

本系列基于最新5.3.10版本,大部分内容copy于官方文档…
官方文档地址

org.springframework.beans.factory 包提供了管理和操作 bean 的基本功能,org.springframework.context包添加了 ApplicationContext 接口,该接口扩展了BeanFactory接口,此外还扩展了其他接口,以更面向应用程序框架的风格提供附加功能。

功能:

  • 通过MessageSource界面访问 i18n 风格的消息。
  • 通过Resource Loader界面访问资源,例如 URL 和文件。
  • 事件发布,即ApplicationListener通过使用接口发布到实现接口的bean ApplicationEventPublisher。
  • 加载多个(分层)上下文,让每个上下文都通过HierarchicalBeanFactory界面专注于一个特定的层,例如应用程序的 Web 层 。

1.1 BeanFactory接口

BeanFactory接口

BeanFactory用于访问Spring bean容器的根接口,体系庞大:
 
此接口由包含许多Bean定义的对象实现,每个定义均由String名称唯一标识。根据bean的定义,工厂将返回一个包含对象的独立实例(原型设计模式),或一个共享实例(对于Singleton设计模式的替代实例)。将返回哪种类型的实例取决于bean工厂的配置,API是相同的。

核心API如下

public interface BeanFactory {
   
     
    String FACTORY_BEAN_PREFIX = "&";
  // 返回一个实例
    Object getBean(String var1) throws BeansException;

    <T> T getBean(String var1, Class<T> var2) throws BeansException;

    Object getBean(String var1, Object... var2) throws BeansException;
  // 返回与给定对象类型唯一匹配的bean实例(如果有)
    <T> T getBean(Class<T> var1) throws BeansException;

    <T> T getBean(Class<T> var1, Object... var2) throws BeansException;
  // 返回指定bean的提供程序,允许对实例进行延迟的按需检索,包括可用性和唯一性选项
    <T> ObjectProvider<T> getBeanProvider(Class<T> var1);
  // 返回指定bean的提供程序,允许对实例进行延迟的按需检索,包括可用性和唯一性选项,参数为必须匹配的类型
    <T> ObjectProvider<T> getBeanProvider(ResolvableType var1);
  // Bean是否存在
    boolean containsBean(String var1);
  // Bean是否单例模式
    boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;
  // Bean是否多例模式
    boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;
  // 检查具有给定名称的bean是否与指定的类型匹配
    boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;
  // 确定具有给定名称的bean的类型
    @Nullable
    Class<?> getType(String var1) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String var1, boolean var2) throws NoSuchBeanDefinitionException;
  // 返回给定bean名称的别名(如果有)
    String[] getAliases(String var1);
}

1.2 ApplicationContext接口

BeanFactory 是Spring最重要的最简单的Bean容器,但是由于BeanFactory 过于简单,所以它并不适合实际应用的企业级开发,因此,Spring提供了另外的一套Bean容器管理的体系:ApplicationContext体系。

ApplicationContext接口代表Spring IoC容器,并负责实例化,配置和组装Bean。容器通过读取配置元数据来获取有关要实例化,配置和组装哪些对象的指令。配置元数据以XML,Java批注或Java代码表示。它使您能够表达组成应用程序的对象以及这些对象之间的丰富相互依赖关系。

ApplicationContext提供:

  • 用于访问应用程序组件的Bean工厂方法。继承自ListableBeanFactory。
  • 以通用方式加载文件资源的能力。从ResourceLoader接口继承。
  • 将事件发布到注册的侦听器的能力。从ApplicationEventPublisher接口继承。
  • 解决消息的能力,支持国际化。从MessageSource接口继承。
  • 从父上下文继承。在后代上下文中的定义将始终优先。例如,这意味着整个Web应用程序都可以使用单个父上下文,而每个servlet都有其自己的子上下文,该子上下文独立于任何其他servlet的子上下文。
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
   
     
  // 返回此应用程序上下文的唯一ID
    @Nullable
    String getId();
  // 返回此上下文所属的已部署应用程序的名称
    String getApplicationName();
  // 返回此上下文的友好名称
    String getDisplayName();
  // 返回第一次加载此上下文时的时间戳
    long getStartupDate();
  // 回父上下文,如果没有父上下文,则返回{@code null}
    @Nullable
    ApplicationContext getParent();
  // 通过getAutowireCapableBeanFactory这个方法将 AutowireCapableBeanFactory这个接口暴露给外部使用, AutowireCapableBeanFactory这个接口一般在applicationContext的内部是较少使用的,它的功能主要是为了装配applicationContext管理之外的Bean。
    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}

2. 国际化支持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 resolvable, Locale locale):上述方法中使用的所有属性也包装在名为MessageSourceResolvable 的类中,您可以在此方法中使用该类 。

当ApplicationContext被加载时,它自动搜索在上下文中定义的MessageSource bean。bean 必须具有 name名称为messageSource 。如果找到了这样的 bean,则对上述方法的所有调用都将委托给消息源。如果未找到消息源,则ApplicationContext尝试查找包含同名 bean 的父级。如果是,它将使用该 bean 作为MessageSource. 如果 ApplicationContext找不到任何消息来源,DelegatingMessageSource则会实例化一个空 对象,以便能够接受对上面定义的方法的调用。

Spring 提供了三种MessageSource实现ResourceBundleMessageSource,ReloadableResourceBundleMessageSource 和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,exceptions并windows 的资源包。任何解析消息的请求都以 JDK 标准的通过ResourceBundle对象解析消息的方式处理。出于示例的目的,假设上述两个资源包文件的内容如下:

    # in format.properties
    message=Alligators rock!
    # in 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!

3. 事件发布和监听

ApplicationContext中的事件处理是通过ApplicationEvent 类和ApplicationListener接口提供的。如果将实现ApplicationListener接口的 bean注册到IOC中,则每次 ApplicationEvent将 事件发布到ApplicationContext 时,都会通知该 bean。这是标准的观察者设计模式。

从Spring 4.2 开始,事件发布架构进行了改进,并提供了基于注解的模型以及发布任意事件(即不一定从 扩展的ApplicationEvent对象)的能力。当此类对象发布时,我们会为您将其包装在一个事件中。

下表描述了 Spring 提供的标准事件:

事件 概述
ContextRefreshedEvent 在ApplicationContext初始化或刷新时发布(例如,通过使用ConfigurableApplicationContext接口refresh()上的方法)。在这里,“初始化”意味着所有 bean 都被加载,后处理器 bean 被检测并激活,单例被预实例化,并且ApplicationContext对象准备好使用。只要上下文尚未关闭,就可以多次触发刷新,前提是所选对象ApplicationContext实际上支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持 。
ContextStartedEvent ApplicationContext使用ConfigurableApplicationContext的start()的方法启动时发布。在这里,“已启动”意味着所有Lifecycle bean 都收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 bean,但它也可用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。
ContextStoppedEvent
ApplicationContext使用ConfigurableApplicationContext.stop()上的方法 停止时发布。这里,“停止”意味着所有Lifecycle bean 都收到一个明确的停止信号。停止的上下文可以通过start()调用重新启动 。
ContextClosedEvent 在ApplicationContext使用接口ConfigurableApplicationContext.close()上的方法或通过 JVM 关闭挂钩关闭时发布。在这里,“关闭”意味着所有的单例 bean 都将被销毁。一旦上下文关闭,它就会到达生命的尽头,无法刷新或重新启动。
RequestHandledEvent 一个特定于 Web 的事件,告诉所有 bean 已为 HTTP 请求提供服务。此事件在请求完成后发布。此事件仅适用于使用 Spring 的DispatcherServlet.
ServletRequestHandledEvent 它的一个子类RequestHandledEvent添加了特定于 Servlet 的上下文信息。

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

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接口的实现类,并将其注入到IOC中。以下示例显示了这样一个类:

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容器本身。

要接收自定义的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...
    }
}

3.1 基于注解的事件监听器

可以使用@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表达式的注解的属性condition添加额外的运行时过滤,该属性应该匹配以实际调用特定事件的方法。

以下示例显示了如何重写我们的通知程序,使其仅在事件的content属性等于时my-event才被调用 :

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

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

名称 SPEL位置 描述 例子
事件 root object 实际的ApplicationEvent #root.event 或者 event
参数数组 root object 用于调用方法的参数(作为对象数组)。 #root.args或args; args[0]访问第一个参数等。
参数名称 evaluation context 任何方法参数的名称。如果由于某种原因,名称不可用(例如,因为编译的字节码中没有调试信息),也可以使用表示参数索引(从 0 开始)的#a<#arg>语法 where使用各个参数<#arg>。 #blEvent或#a0(您也可以使用#p0或#p<#arg>参数表示法作为别名)

如果您需要发布一个事件作为处理另一个事件的结果,您可以更改方法以返回应该发布的事件, 异步监听器不支持此功能 ,如以下示例所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
   
     
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

该handleBlockedListEvent()方法为监听BlockedListEvent事件并发布新的ListUpdateEvent 事件。如果需要发布多个事件,则可以返回一个Collection或一组事件。

3.2 异步监听器

监听器支持异步处理事件,则可以使用@Async注解支持。以下示例显示了如何执行此操作:

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

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

  • 如果异步事件监听器抛出Exception,则不会将其传播给调用者。
  • 异步事件监听器方法无法通过返回值来发布后续事件。

3.3 监听器执行顺序

如果需要在监听器之前调用另一个监听器,则可以在方法声明中添加@Order 注解,如以下示例所示:

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

3.4 通用事件

可以使用泛型进一步定义事件的结构。比如使用 EntityCreatedEvent<T>,T是创建的实际实体的类型。例如,可以创建以下监听器定义只接收泛型为Person的EntityCreatedEvent :

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

4. 访问底层资源Resources

Spring 的Resource(资源)管理,下篇文档会详细介绍。

5. 应用程序启动跟踪

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

使用特定指标跟踪应用程序启动步骤有助于了解启动阶段的时间花在何处,也可以更好地了解整个上下文生命周期。

ApplicationContext(及其子类)使用 一个 ApplicationStartup进行检测,它收集StartupStep有关各个启动阶段的数据:

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

以下是AnnotationConfigApplicationContext容器中示例:

// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();

应用程序上下文已经通过多个步骤进行了检测。记录后,可以使用特定工具收集、显示和分析这些启动步骤。

默认ApplicationStartup实现是无操作变体,以实现最小开销。这意味着默认情况下不会在应用程序启动期间收集任何指标。Spring Framework 附带了一个使用 Java Flight Recorder 跟踪启动步骤的实现FlightRecorderApplicationStartup, 要使用此变体,您必须ApplicationContext在创建后立即将其配置它的实例。

如果开发人员ApplicationStartup提供他们自己的AbstractApplicationContext子类,或者如果他们希望收集更精确的数据,他们也可以使用基础设施 。

ApplicationStartup仅在应用程序启动期间和核心容器中使用;这绝不是 Java 分析器或Micrometer等度量库的替代品。

要开始收集 自定义的StartupStep,ApplicationStartup 组件可以直接从应用程序上下文中获取实例,使它们的组件实现ApplicationStartupAware,或者ApplicationStartup在任何注入点上询问类型。

开发人员在创建自定义启动步骤时不应使用命名空间"spring.*"。这个命名空间是为内部 Spring 使用保留的,可能会发生变化。

6. Web 应用程序的ApplicationContext 实例化

ApplicationContext例如,您可以通过使用以声明方式创建实例 ContextLoader。当然,您也可以ApplicationContext通过使用其中一种ApplicationContext实现以编程方式创建实例。

你可以注册一个ApplicationContext使用ContextLoaderListener,如下例所示:

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

监听器检查contextConfig Location参数。如果该参数不存在,则侦听器将/WEB-INF/applicationContext.xml用作默认值。当参数确实存在时,监听器通过使用预定义的分隔符(逗号、分号和空格)来分隔参数,并将这些值用作搜索应用程序上下文的位置。也支持 Ant 风格的路径模式。示例是/WEB-INF/*Context.xml(对于名称以 结尾 Context.xml并驻留在WEB-INF目录/WEB-INF/**/*Context.xml 中的所有文件)和(对于 的任何子目录中的所有此类文件WEB-INF)。

7. 将 Spring ApplicationContext 部署为 Java EE RAR 文件

可以将Spring ApplicationContext部署为 RAR 文件,将上下文及其所有必需的 bean 类和库 JAR 封装在 Java EE RAR 部署单元中。这相当于引导独立的ApplicationContext(仅托管在 Java EE 环境中)能够访问 Java EE 服务器设施。RAR 部署是部署无头 WAR 文件场景的更自然的替代方案——实际上,一个没有任何 HTTP 入口点的 WAR 文件仅用于ApplicationContext在 Java EE 环境中引导 Spring 。

RAR部署非常适合不需要 HTTP 入口点而仅包含消息端点和计划作业的应用程序上下文。这种上下文中的 Bean 可以使用应用服务器资源,例如 JTA 事务管理器和 JNDI 绑定的 JDBC DataSource实例和 JMSConnectionFactory实例,还可以向平台的 JMX 服务器注册——所有这些都通过 Spring 的标准事务管理和 JNDI 和 JMX 支持设施。应用程序组件还可以WorkManager通过 Spring 的TaskExecutor抽象与应用程序服务器的 JCA进行交互。

将Spring ApplicationContext 简单部署为 Java EE RAR 文件:

1、 将所有应用程序类打包成一个RAR文件(这是一个具有不同文件扩展名的标准JAR文件);
2、 将所有需要的库JAR添加到RAR存档的根目录中;
3、 添加META-INF/ra.xml部署描述符(如的javadoc中SpringContextResourceAdapter所示)和相应的SpringXMLbean定义文件(通常为META-INF/applicationContext.xml);
4、 将生成的RAR文件放到应用程序服务器的部署目录中;

这种RAR 部署单元通常是独立的。它们不会将组件暴露给外部世界,甚至不会暴露给同一应用程序的其他模块。与基于 RAR 的交互ApplicationContext通常通过它与其他模块共享的 JMS 目标发生。ApplicationContext例如,基于 RAR 的还可以调度一些作业或对文件系统中的新文件做出反应(或类似的)。如果它需要允许来自外部的同步访问,它可以(例如)导出 RMI 端点,这些端点可能被同一台机器上的其他应用程序模块使用。

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: