ApplicationContext 的附加功能

如前文所述,在 章节介绍中, infra.beans.factory 包提供了管理和操作 bean 的基本功能,包括以编程方式进行操作。 infra.context 包添加了 ApplicationContext 接口,该接口扩展了 BeanFactory 接口,并扩展了其他接口,以提供更多面向应用框架的功能。许多人以完全声明式的方式使用 ApplicationContext, 甚至不会以编程方式创建它,而是依赖于支持类(如 ContextLoader)在 Jakarta EE Web 应用程序的正常启动过程中自动实例化 ApplicationContext。 为了以更面向框架的方式增强 BeanFactory 的功能,上下文包还提供了以下功能:

  • 通过 MessageSource 接口以 i18n 样式访问消息。

  • 通过 ResourceLoader 接口访问资源,如 URL 和文件。

  • 通过 ApplicationEventPublisher 接口向实现 ApplicationListener 接口的 bean 发布事件。

  • 加载多个(分层)上下文,让每个上下文专注于一个特定的层,例如应用程序的 Web 层,通过 HierarchicalBeanFactory 接口。

MessageSource 国际化

ApplicationContext 接口扩展了一个称为 MessageSource 的接口,因此提供了国际化(“i18n”)功能。 Infra 还提供了 HierarchicalMessageSource 接口,可以按层次解析消息。这些接口共同构成了 Infra 实现消息解析的基础。这些接口定义的方法包括:

  • 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 必须命名为 messageSource。如果找到这样的 bean,则所有对上述方法的调用都将委托给消息源。 如果找不到消息源,则 ApplicationContext 尝试查找一个具有相同名称的父级包含的 bean。 如果找到,则将该 bean 用作 MessageSource。如果 ApplicationContext 找不到任何消息源, 则将实例化一个空的 DelegatingMessageSource,以便能够接受上述定义的方法的调用。 Infra 提供了三个 MessageSource 实现,ResourceBundleMessageSourceReloadableResourceBundleMessageSourceStaticMessageSource。 它们都实现了 HierarchicalMessageSource,以便进行嵌套消息传递。StaticMessageSource 很少使用,但提供了向源添加消息的程序化方法。以下示例展示了 ResourceBundleMessageSource

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

示例假设您的类路径中定义了三个资源包,分别称为 formatexceptionswindows。任何解析消息的请求都是通过 ResourceBundle 对象以 JDK 标准的方式处理消息解析。为了示例的目的,假设上述两个资源包文件的内容如下:

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

总结一下,MessageSource 在名为 beans.xml 的文件中定义,该文件存在于类路径的根目录下。 messageSource bean 定义通过其 basenames 属性引用了一些资源包。传递给 basenames 属性的列表中有三个文件,在类路径的根目录下分别称为 format.propertiesexceptions.propertieswindows.properties

下一个示例展示了传递给消息查找的参数。这些参数被转换为 String 对象,并插入到查找消息中的占位符中。

<beans>

  <!-- this MessageSource is being used in a web application -->
  <bean id="messageSource" class="infra.context.support.ResourceBundleMessageSource">
    <property name="basename" value="exceptions"/>
  </bean>

  <!-- lets inject the above MessageSource into this POJO -->
  <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”),Infra 中的各种 MessageSource 实现遵循与标准 JDK ResourceBundle 相同的区域设置解析和回退规则。简而言之,继续使用之前定义的示例 messageSource,如果您想针对英国 (en-GB)区域设置解析消息,您需要分别创建名为 format_en_GB.propertiesexceptions_en_GB.properties 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 接口来获取对已定义的任何 MessageSource 的引用。 在实现了 MessageSourceAware 接口的 ApplicationContext 中定义的任何 bean 在创建和配置时都会被注入应用程序上下文的 MessageSource

由于 Infra 的 MessageSource 是基于 Java 的 ResourceBundle, 它不会合并具有相同基本名称的包,而是仅使用找到的第一个包。具有相同基本名称的后续消息包将被忽略。
作为 ResourceBundleMessageSource 的替代方案,Infra 提供了一个 ReloadableResourceBundleMessageSource 类。 这个变种支持相同的包文件格式,但比标准的基于 JDK 的 ResourceBundleMessageSource 实现更灵活。 特别是,它允许从任何 Infra 资源位置(不仅仅是从类路径)读取文件,并支持热重新加载包属性文件(同时在它们之间高效地缓存)。有关详细信息, 请参阅 ReloadableResourceBundleMessageSource 的 javadoc。

内置的和自定义事件

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

提供了 基于注解的模型 , 以及发布任意事件的能力(即,不一定要扩展自 ApplicationEvent 的对象)。当发布这样的对象时,我们会为您封装成一个事件。

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

Table 1. 内置事件
事件 解释

ContextRefreshedEvent

ApplicationContext 被初始化或刷新时发布(例如,通过在 ConfigurableApplicationContext 接口上使用 refresh() 方法)。在这里,“initialized”意味着所有的 bean 都已加载,后处理器 bean 已被检测并激活,单例已预实例化,并且 ApplicationContext 对象已准备就绪。只要上下文没有关闭, 就可以多次触发刷新,前提是所选的 ApplicationContext 实际上支持这种“hot”刷新。例如, XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。

ContextStartedEvent

ApplicationContext 通过在 ConfigurableApplicationContext 接口上使用 start() 方法启动时发布。在这里,“started”意味着所有的 Lifecycle bean 都接收到一个明确的启动信号。 通常,此信号用于在明确停止后重新启动 bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。

ContextStoppedEvent

ApplicationContext 通过在 ConfigurableApplicationContext 接口上使用 stop() 方法停止时发布。 在这里,“stopped”意味着所有的 Lifecycle bean 都接收到一个明确的停止信号。停止的上下文可以通过调用 start() 方法重新启动。

ContextClosedEvent

ApplicationContext 通过在 ConfigurableApplicationContext 接口上使用 close() 方法或通过 JVM 关闭钩子进行关闭时发布。在这里,“closed”意味着所有的单例 bean 将被销毁。一旦上下文被关闭, 它就达到了生命周期的终点,无法刷新或重新启动。

RequestHandledEvent

这是一个 Web 特定的事件,通知所有 bean 一个 HTTP 请求已被处理。此事件在请求完成后发布。 这个事件仅适用于使用 Infra DispatcherHandler 的 Web 应用程序。

您还可以创建并发布自己的自定义事件。以下示例显示了一个简单的类,它扩展了 Infra 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 接口的类, 并将其注册为 Infra 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...
  }
}

在配置时间,Infra 容器检测到 EmailService 实现了 ApplicationEventPublisherAware, 并自动调用 setApplicationEventPublisher() 方法。实际上,传递的参数是 Infra 容器本身。 通过其 ApplicationEventPublisher 接口与应用程序上下文进行交互。

要接收自定义的 ApplicationEvent,您可以创建一个实现了 ApplicationListener 接口的类, 并将其注册为 Infra 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() 方法会阻塞,直到所有监听器完成事件处理。 这种同步和单线程方法的一个优点是,当监听器接收到事件时,如果存在事务上下文,它会在发布者的事务上下文中操作。 如果需要另一种事件发布策略,例如默认情况下进行异步事件处理,请参阅 Infra 的 javadoc 中关于 ApplicationEventMulticaster 接口 和 SimpleApplicationEventMulticaster 实现的配置选项,这些选项可以应用于自定义的 "applicationEventMulticaster" bean 定义。 在这些情况下,ThreadLocals 和日志上下文不会被传播到事件处理中。

以下示例展示了用于注册和配置上述每个类的 bean 定义:

<bean id="emailService" class="example.EmailService">
  <property name="blockedList">
    <list>
      <value>[email protected]</value>
      <value>[email protected]</value>
      <value>[email protected]</value>
    </list>
  </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
  <property name="notificationAddress" value="[email protected]"/>
</bean>

  <!-- optional: a custom ApplicationEventMulticaster definition -->
<bean id="applicationEventMulticaster" class="infra.context.event.SimpleApplicationEventMulticaster">
  <property name="taskExecutor" ref="..."/>
  <property name="errorHandler" ref="..."/>
</bean>

将所有内容整合在一起,当调用 emailService bean 的 sendEmail() 方法时,如果有任何应该被阻止的电子邮件消息, 将发布一个类型为 BlockedListEvent 的自定义事件。blockedListNotifier bean 被注册为一个 ApplicationListener 并接收 BlockedListEvent,在此时它可以通知相关方。

使用注解标注事件监听器

您可以通过使用 @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() {
  // ...
}

还可以通过使用注解的 condition 属性添加额外的运行时过滤,该属性定义了一个 SpEL 表达式,该表达式应该匹配实际为特定事件调用方法。

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

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

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

  1. SpEL 可用的元数据

名称 位置 描述 示例

Event

root object

实际的 `ApplicationEvent`对象.

#root.event 或者 event

Arguments array

root object

用于调用该方法的参数(作为对象数组)。

#root.argsargsargs[0] 用于访问第一个参数,依此类推。

Argument name

evaluation context

任何方法参数的名称。如果由于某种原因名称不可用(例如,因为编译的字节码中没有调试信息), 也可以使用 #a<#arg> 语法来获取单个参数,其中 <#arg> 代表参数索引(从 0 开始)。

#blEvent#a0(您还可以使用 #p0#p<#arg> 参数符号作为别名)。

请注意,即使您的方法签名实际上是引用发布的任意对象,#root.event 也可以访问底层事件。

如果需要在处理另一个事件的结果上发布事件,您可以更改方法签名以返回应该发布的事件,如以下示例所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
  // notify appropriate parties via notificationAddress and
  // then publish a ListUpdateEvent...
}
对于 异步监听器,不支持此功能。

handleBlockedListEvent() 方法为它处理的每个 BlockedListEvent 发布一个新的 ListUpdateEvent。 如果需要发布多个事件,您可以返回一个事件的 Collection 或数组。

异步监听器

如果您希望特定的监听器异步处理事件,您可以重用 常规的 @Async 支持。 以下示例展示了如何实现:

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

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

  • 异常处理:如果异步事件监听器抛出异常,它不会传播给调用者。有关详细信息,请参阅 AsyncUncaughtExceptionHandler

  • 发布后续事件:异步事件监听器方法不能通过返回值发布其他事件。如果需要作为处理结果发布另一个事件,请注入 ApplicationEventPublisher 并手动发布事件。

  • ThreadLocals 和日志上下文:默认情况下,ThreadLocals 和日志上下文在事件处理期间不会传播。这可能会影响监听器中的任何上下文相关操作。

监听器的顺序

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

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

通用事件

您还可以使用泛型来进一步定义事件的结构。考虑使用 EntityCreatedEvent<T>, 其中 T 是实际创建的实体的类型。例如,您可以创建以下监听器定义,以仅接收 Person 类型的 EntityCreatedEvent

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

由于类型擦除,这仅在触发的事件解析了事件监听器过滤器的泛型参数时才起作用 (也就是说,类似于 class PersonCreatedEvent extends EntityCreatedEvent<Person> { …​ })。

在某些情况下,如果所有事件都遵循相同的结构(正如前面示例中的事件应该遵循的那样), 这可能变得相当繁琐。在这种情况下,您可以实现 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,而且适用于任何您发送的作为事件的任意对象。

最后,与经典的 ApplicationListener 实现一样,实际的多播通过运行时的全局上下文 ApplicationEventMulticaster 完成。默认情况下,这是一个具有同步事件发布的 SimpleApplicationEventMulticaster,它在调用线程中完成。可以通过 "applicationEventMulticaster" bean 定义进行替换/自定义,例如,用于异步处理所有事件和/或处理监听器异常:

@Component
static ApplicationEventMulticaster applicationEventMulticaster() {
  SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
  multicaster.setTaskExecutor(...);
  multicaster.setErrorHandler(...);
  return multicaster;
}

便捷地获取底层资源

为了最佳地使用和理解应用程序上下文,您应该熟悉 Infra Resource 抽象,如资源中所述。

应用程序上下文是一个 ResourceLoader,可用于加载 Resource 对象。Resource 本质上是 JDK java.net.URL 类的更丰富的版本。事实上,Resource 的实现包装了一个 java.net.URL 实例, 如果适用的话。Resource 可以以透明的方式从几乎任何位置获取低级资源,包括从类路径、 文件系统位置以及任何可以用标准 URL 描述的地方,以及其他一些变体。如果资源位置字符串是一个简单的路径, 没有任何特殊的前缀,那么这些资源来自于哪里是特定和适合于实际应用程序上下文类型的。

您可以配置部署到应用程序上下文中的 bean 来实现特殊的回调接口 ResourceLoaderAware, 在初始化时会自动回调,传递应用程序上下文本身作为 ResourceLoader。您还可以暴露类型为 Resource 的属性,用于访问静态资源。它们被注入其中,就像任何其他属性一样。您可以将这些 Resource 属性指定为简单的 String 路径,并在部署 bean 时依赖于从这些文本字符串自动转换为实际 Resource 对象。

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

为 Web 应用程序提供便捷的 ApplicationContext 实例化

5.0 不再支持 Servlet

您可以使用例如 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>infra.web.context.ContextLoaderListener</listener-class>
</listener>

监听器检查 contextConfigLocation 参数。如果参数不存在,监听器默认使用 /WEB-INF/applicationContext.xml。 当参数存在时,监听器使用预定义的分隔符(逗号、分号和空格)分隔 String,并将这些值用作搜索应用程序上下文的位置。 也支持 Ant 样式的路径模式。 例如 /WEB-INF/*Context.xml(用于所有以 Context.xml 结尾且位于 WEB-INF 目录中的文件) 和 /WEB-INF/**/*Context.xml(用于 WEB-INF 任何子目录中的所有此类文件)。

将 Infra ApplicationContext 部署为 Jakarta EE RAR 文件

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

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

有关 RAR 部署所涉及的配置详细信息,请参阅 SpringContextResourceAdapter 类的 javadoc。

对于将 Infra ApplicationContext 作为 Jakarta EE RAR 文件进行简单部署:

  1. 将所有应用程序类打包到一个 RAR 文件中(这是一个具有不同文件扩展名的标准 JAR 文件)。

  2. 将所有必需的库 JAR 添加到 RAR 存档的根目录中。

  3. 添加一个 META-INF/ra.xml 部署描述符(如 SpringContextResourceAdapter 的 javadoc 中所示) 和相应的 Infra XML bean 定义文件(通常是 META-INF/applicationContext.xml)。

  4. 将生成的 RAR 文件放入应用服务器的部署目录中。

此类 RAR 部署单元通常是独立的。它们不向外界公开组件,甚至不向同一应用程序的其他模块公开。 与基于 RAR 的 ApplicationContext 的交互通常通过它与其他模块共享的 JMS 目标进行。 基于 RAR 的 ApplicationContext 还可以例如安排一些作业或对文件系统中的新文件做出反应(或类似情况)。 如果它需要允许来自外部的同步访问,它可以(例如)导出 RMI 端点,这些端点可以由同一台机器上的其他应用程序模块使用。