# XMPP 支持

## XMPP 支持

Spring 集成为[XMPP](https://www.xmpp.org)提供通道适配器。XMPP 代表“可扩展消息传递和存在协议”。

XMPP 描述了在分布式系统中多个代理相互通信的一种方式。典型的用例是发送和接收聊天消息，尽管 XMPP 可以（并且正在）用于其他类型的应用程序。XMPP 描述了参与者的网络。在该网络中，演员可以直接相互联系，并改变广播状态（例如“存在”）。

XMPP 提供的消息架构是世界上一些最大的即时消息网络的基础，包括 Google Talk（Gtalk，也可在 Gmail 中使用）和 Facebook Chat。许多好的开源 XMPP 服务器都是可用的。两种流行的实现方式是[Openfire](https://www.igniterealtime.org/projects/openfire/)和[ejabberd](https://www.ejabberd.im)。

Spring 集成通过提供 XMPP 适配器来提供对 XMPP 的支持，该适配器支持发送和接收 XMPP 聊天消息以及来自客户端花名册中其他条目的存在变化。

你需要在项目中包含此依赖项：

Maven

```
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-xmpp</artifactId>
    <version>5.5.9</version>
</dependency>
```

Gradle

```
compile "org.springframework.integration:spring-integration-xmpp:5.5.9"
```

与其他适配器一样，XMPP 适配器支持方便的基于名称空间的配置。要配置 XMPP 名称空间，在 XML 配置文件的头中包含以下元素：

```
xmlns:int-xmpp="http://www.springframework.org/schema/integration/xmpp"
xsi:schemaLocation="http://www.springframework.org/schema/integration/xmpp
	https://www.springframework.org/schema/integration/xmpp/spring-integration-xmpp.xsd"
```

### XMPP 连接

在使用入站或出站 XMPP 适配器参与 XMPP 网络之前，参与者必须建立其 XMPP 连接。连接到特定帐户的所有 XMPP 适配器都可以共享此连接对象。通常这需要（至少）`user`、`password`和`host`。要创建基本的 XMPP 连接，你可以使用名称空间的便利，如下例所示：

```
<int-xmpp:xmpp-connection
    id="myConnection"
    user="user"
    password="password"
    host="host"
    port="port"
    resource="theNameOfTheResource"
    subscription-mode="accept_all"/>
```

|   |为了更方便起见，你可以依赖默认的命名约定，并省略`id`属性。<br/>此连接使用的默认名称（`xmppConnection`） Bean。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

如果 XMPP 连接失效，只要上一个连接状态被记录（经过身份验证），就会使用自动登录进行重新连接尝试。我们还注册了`ConnectionListener`，如果启用了`DEBUG`日志级别，它将记录连接事件。

`subscription-mode`属性启动 Rose Listener 来处理来自其他用户的订阅。此功能并不总是适用于目标 XMPP 服务器。例如，Google Cloud Messaging 和 Firebase Cloud Messaging 完全禁用了它。要关闭订阅的花名册侦听器，你可以在使用 XML 配置（`subscription-mode=""`）时使用空字符串进行配置，或者在使用 Java 配置时使用`XmppConnectionFactoryBean.setSubscriptionMode(null)`进行配置。这样做也会在登录阶段禁用花名册。有关更多信息，请参见[`Roster.setRosterLoadedAtLogin(boolean)`]（https://download.igniterealtime.org/smack/DOCS/latest/javadoc/org/jivesoftware/smack/roster/roster.html#setrosterloadedatlogin-boolean-）。

### XMPP 消息

Spring 集成为发送和接收 XMPP 消息提供了支持。为了接收它们，它提供了一个入站消息通道适配器。对于发送它们，它提供了一个出站消息通道适配器。

#### 入站消息通道适配器

Spring 集成适配器支持从系统中的其他用户接收聊天消息。为此，入站消息通道适配器代表用户“登录”，并接收发送给该用户的消息。然后将这些消息转发到你的 Spring 集成客户端。`inbound-channel-adapter`元素为 XMPP 入站消息通道适配器提供配置支持。下面的示例展示了如何配置它：

```
<int-xmpp:inbound-channel-adapter id="xmppInboundAdapter"
	channel="xmppInbound"
	xmpp-connection="testConnection"
	payload-expression="getExtension('google:mobile:data').json"
	stanza-filter="stanzaFilter"
	auto-startup="true"/>
```

除了通常的属性（对于消息通道适配器），这个适配器还需要对 XMPP 连接的引用。

XMPP 入站适配器是事件驱动的，并且是`Lifecycle`实现。启动时，它会注册一个`PacketListener`，用于侦听传入的 XMPP 聊天消息。它将接收到的任何消息转发给底层适配器，后者将它们转换为 Spring 集成消息，并将它们发送到指定的`channel`。当停止时，它将注销`PacketListener`。

从版本 4.3 开始，`ChatMessageListeningEndpoint`（及其`<int-xmpp:inbound-channel-adapter>`）支持在提供的`org.jivesoftware.smack.filter.StanzaFilter`上注册一个`org.jivesoftware.smack.filter.StanzaFilter`的注入，以及一个内部`StanzaListener`实现。有关更多信息，请参见[Javadoc](https://www.igniterealtime.org/builds/smack/docs/latest/javadoc/org/jivesoftware/smack/XMPPConnection.html#addAsyncStanzaListener%28org.jivesoftware.smack.StanzaListener,%20org.jivesoftware.smack.filter.StanzaFilter%29)。

版本 4.3 为`ChatMessageListeningEndpoint`引入了`payload-expression`属性。传入的`org.jivesoftware.smack.packet.Message`表示求值上下文的根对象。当你使用[XMPP 扩展](#xmpp-extensions)时，此选项很有用。例如，对于 GCM 协议，我们可以通过使用以下表达式来提取主体：

```
payload-expression="getExtension('google:mobile:data').json"
```

下面的示例提取了 XHTML 协议的主体：

```
payload-expression="getExtension(T(org.jivesoftware.smackx.xhtmlim.packet.XHTMLExtension).NAMESPACE).bodies[0]"
```

为了简化对 XMPP 消息中扩展的访问，将`extension`变量添加到`EvaluationContext`中。请注意，它是在消息中仅存在一个扩展名时添加的。上面显示`namespace`操作的示例可以简化为以下示例：

```
payload-expression="#extension.json"
payload-expression="#extension.bodies[0]"
```

#### 出站消息通道适配器

你还可以通过使用出站消息通道适配器向 XMPP 上的其他用户发送聊天消息。`outbound-channel-adapter`元素为 XMPP 出站消息通道适配器提供配置支持。

```
<int-xmpp:outbound-channel-adapter id="outboundEventAdapter"
						channel="outboundEventChannel"
						xmpp-connection="testConnection"/>
```

适配器期望其输入（至少）是类型`java.lang.String`的有效负载和`XmppHeaders.CHAT_TO`的标头值，该值指定消息应发送给哪个用户。要创建消息，你可以使用类似于以下内容的 Java 代码：

```
Message<String> xmppOutboundMsg = MessageBuilder.withPayload("Hello, XMPP!" )
						.setHeader(XmppHeaders.CHAT_TO, "userhandle")
						.build();
```

你还可以通过使用 XMPP Header-Enricher 支持来设置 header，如下例所示：

```
<int-xmpp:header-enricher input-channel="input" output-channel="output">
	<int-xmpp:chat-to value="[email protected]"/>
</int-xmpp:header-enricher>
```

从版本 4.3 开始，包扩展支持已添加到`ChatMessageSendingMessageHandler`（在 XML 配置中是`<int-xmpp:outbound-channel-adapter>`）。随着常规的`String`和`org.jivesoftware.smack.packet.Message`有效负载，现在可以发送有效负载为`org.jivesoftware.smack.packet.ExtensionElement`（填充到`org.jivesoftware.smack.packet.Message.addExtension()`）而不是`setBody()`的消息。为了方便起见，我们为`ChatMessageSendingMessageHandler`添加了一个`extension-provider`选项。它允许你注入`org.jivesoftware.smack.provider.ExtensionElementProvider`，从而在运行时针对有效负载构建`ExtensionElement`。在这种情况下，有效负载必须是 JSON 或 XML 格式的字符串，这取决于 XEP 协议。

### XMPP 存在

XMPP 还支持广播状态。你可以利用这种能力，让那些把你列入名册的人看到你的状态变化。这种情况经常发生在你的即时通讯客户身上。你改变你的离开状态并设置一个离开消息，每个在他们的花名册上有你的人看到你的图标或用户名的变化，以反映这个新的状态，并可能看到你的新的“离开”消息。如果你希望接收通知或通知他人状态更改，可以使用 Spring Integration 的“存在”适配器。

#### 入站存在消息通道适配器

Spring 集成提供了一种入站存在消息通道适配器，其支持从系统中在你的花名册上的其他用户接收存在事件。为此，适配器代表用户“登录”，注册`RosterListener`，并将接收到的存在更新事件作为消息转发到由`channel`属性标识的通道。消息的有效负载是`org.jivesoftware.smack.packet.Presence`对象（参见[https://www.igniterealtime.org/builds/smack/docs/latest/javadoc/org/jivesoftware/smack/packet/Presence.html](https://www.igniterealtime.org/builds/smack/docs/latest/javadoc/org/jivesoftware/smack/packet/Presence.html)）。

`presence-inbound-channel-adapter`元素为 XMPP 入站消息通道适配器提供配置支持。以下示例配置了入站存在消息通道适配器：

```
<int-xmpp:presence-inbound-channel-adapter channel="outChannel"
		xmpp-connection="testConnection" auto-startup="false"/>
```

除了通常的属性外，此适配器还需要对 XMPP 连接的引用。这个适配器是事件驱动的，并且是`Lifecycle`实现。它在启动时注册`RosterListener`，在停止时取消注册`RosterListener`。

#### 出站存在消息通道适配器

Spring 集成还支持发送存在事件，以被网络中碰巧在他们的花名册上有你的其他用户看到。当你向出站存在消息通道适配器发送消息时，它会提取有效负载（预计类型为`org.jivesoftware.smack.packet.Presence`）并将其发送到 XMPP 连接，从而将你的存在事件广告到网络的其余部分。

`presence-outbound-channel-adapter`元素为 XMPP 出站消息通道适配器提供配置支持。下面的示例展示了如何配置出站消息通道适配器：

```
<int-xmpp:presence-outbound-channel-adapter id="eventOutboundPresenceChannel"
	xmpp-connection="testConnection"/>
```

它也可以是一个轮询消费者（如果它从一个可轮询通道接收消息），在这种情况下，你将需要注册一个 Poller。下面的示例展示了如何做到这一点：

```
<int-xmpp:presence-outbound-channel-adapter id="pollingOutboundPresenceAdapter"
		xmpp-connection="testConnection"
		channel="pollingChannel">
	<int:poller fixed-rate="1000" max-messages-per-poll="1"/>
</int-xmpp:presence-outbound-channel-adapter>
```

与它的入站对应物一样，它需要对 XMPP 连接的引用。

|   |如果你依赖 XMPP 连接 Bean（[前面描述的](#xmpp-connection)）的默认命名约定，并且在你的应用程序上下文中只配置了一个 XMPP 连接 Bean，那么你可以省略`xmpp-connection`属性。，在这种情况下，<br/>，找到名为`xmppConnection`的 Bean 并将其注入适配器。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 高级配置

Spring Integration 的 XMPP 支持基于 Smack4.0API（[https://www.igniterealtime.org/projects/smack/](https://www.igniterealtime.org/projects/smack/)），它允许对 XMPP 连接对象进行更复杂的配置。

由于[更早的陈述](#xmpp-connection)，`xmpp-connection`名称空间支持旨在简化基本的连接配置，并且仅支持几个常见的配置属性。然而，`org.jivesoftware.smack.ConnectionConfiguration`对象定义了大约 20 个属性，而为所有这些属性添加名称空间支持并不提供真正的价值。因此，对于更复杂的连接配置，可以将我们的`XmppConnectionFactoryBean`实例配置为常规 Bean，并将`org.jivesoftware.smack.ConnectionConfiguration`作为构造函数参数注入到`FactoryBean`。你可以在`ConnectionConfiguration`实例上直接指定所需的每个属性。（ Bean 带有“P”名称空间的定义将很好地工作。）这样，你可以直接设置 SSL（或任何其他属性）。下面的示例展示了如何做到这一点：

```
<bean id="xmppConnection" class="o.s.i.xmpp.XmppConnectionFactoryBean">
    <constructor-arg>
        <bean class="org.jivesoftware.smack.ConnectionConfiguration">
            <constructor-arg value="myServiceName"/>
            <property name="socketFactory" ref="..."/>
        </bean>
    </constructor-arg>
</bean>

<int:channel id="outboundEventChannel"/>

<int-xmpp:outbound-channel-adapter id="outboundEventAdapter"
    channel="outboundEventChannel"
    xmpp-connection="xmppConnection"/>
```

Smack API 还提供了静态初始化器，这可能会很有帮助。对于更复杂的情况（例如注册 SASL 机制），你可能需要执行某些静态初始化器。其中一个静态初始化器是`SASL认证`，它允许你注册受支持的 SASL 机制。对于这种复杂程度，我们建议在 XMPP 连接配置中使用 Spring Java 配置。这样，你就可以通过 Java 代码配置整个组件，并在适当的时候执行所有其他必要的 Java 代码，包括静态初始化器。下面的示例展示了如何在 Java 中配置带有 SASL（简单身份验证和安全层）的 XMPP 连接：

```
@Configuration
public class CustomConnectionConfiguration {
  @Bean
  public XMPPConnection xmppConnection() {
	SASLAuthentication.supportSASLMechanism("EXTERNAL", 0); // static initializer

	ConnectionConfiguration config = new ConnectionConfiguration("localhost", 5223);
	config.setKeystorePath("path_to_truststore.jks");
	config.setSecurityEnabled(true);
	config.setSocketFactory(SSLSocketFactory.getDefault());
	return new XMPPConnection(config);
  }
}
```

有关使用 Java 进行应用程序上下文配置的更多信息，请参见[Spring Reference Manual](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-java)中的以下部分。

### XMPP 消息头

Spring 集成 XMPP 适配器自动映射标准 XMPP 属性。默认情况下，这些属性通过使用[`DefaultXmppHeaderMapper`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/xmpp/support/defaultxmppheadermapper.html）从 Spring integration`MessageHeaders`复制到 Spring integration`MessageHeaders`。

除非`DefaultXmppHeaderMapper`中的`requestHeaderNames`或`replyHeaderNames`属性明确指定，否则不会将任何用户定义的标题复制到或复制到 XMPP 消息。

|   |在映射用户定义的标头时，这些值还可以包含简单的通配符模式（例如“thing\*”或“\*thing”）。|
|---|----------------------------------------------------------------------------------------------------------------------|

从版本 4.1 开始，`AbstractHeaderMapper`（`DefaultXmppHeaderMapper`的超类）允许你为`requestHeaderNames`属性（除了`STANDARD_REQUEST_HEADERS`）配置`NON_STANDARD_HEADERS`令牌，以映射所有用户定义的标头。

`org.springframework.xmpp.XmppHeaders`类标识了`DefaultXmppHeaderMapper`要使用的默认标头：

* `xmpp_from`

* `xmpp_subject`

* `xmpp_thread`

* `xmpp_to`

* `xmpp_type`

从版本 4.3 开始，你可以通过在模式前面加上`!`来否定头映射中的模式。被否定的模式获得优先权，所以列表（如`STANDARD_REQUEST_HEADERS,thing1,thing*,!thing2,!thing3,qux,!thing1`）不映射`thing1`、`thing2`或`thing3`。该列表确实映射了标准标题加上`thing4`和`qux`。

|   |如果你有一个希望映射的以`!`开头的用户定义的头文件，则可以使用`\`将其转义为：`STANDARD_REQUEST_HEADERS,\!myBangHeader`。<br/>在该示例中，标准的请求头文件和`!myBangHeader`文件将被映射。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### XMPP 扩展

Extensions 将“可扩展”放在“可扩展消息传递和存在协议”中。

XMPP 是基于 XML 的，XML 是一种支持命名空间概念的数据格式。通过名称空间，你可以向 XMPP 添加原始规范中未定义的位。XMPP 规范特意只描述了一组核心特性：

* 客户机如何连接到服务器

* 加密（SSL/TLS）

* Authentication

* 服务器如何相互通信以中继消息

* 其他几个基本的构建模块

一旦实现了这一点，就有了一个 XMPP 客户机，可以发送任何类型的数据。然而，你可能需要做的不只是基础工作。例如，你可能需要在消息中包含格式（粗体、斜体等），而核心 XMPP 规范中并未对此进行定义。好吧，你可以想出一种方法来做到这一点，但是，除非其他所有人都像你一样做这件事，否则没有其他软件可以解释它（它们忽略它们无法理解的名称空间）。

为了解决这个问题，XMPP 标准基金会发布了一系列额外的文档，即[XMPP 扩展协议](https://xmpp.org/extensions/xep-0001.html)。通常，每个 XEP 描述一个特定的活动（从消息格式到文件传输、多用户聊天等等）。它们还提供了一种标准格式，供每个人在该活动中使用。

Smack API 通过其`extensions`和`experimental`[projects](https://www.igniterealtime.org/builds/smack/docs/latest/documentation/extensions/index.html)提供了许多 XEP 实现。从 Spring 集成版本 4.3 开始，你可以使用现有 XMPP 通道适配器的任何 XEP。

为了能够处理 XEPS 或任何其他定制的 XMPP 扩展，你必须提供 Smack 的`ProviderManager`预配置。你可以使用`static`Java 代码来实现这一点，如下例所示：

```
ProviderManager.addIQProvider("element", "namespace", new MyIQProvider());
ProviderManager.addExtensionProvider("element", "namespace", new MyExtProvider());
```

你还可以在特定实例中使用`.providers`配置文件，并使用 JVM 参数访问它，如下例所示：

```
-Dsmack.provider.file=file:///c:/my/provider/mycustom.providers
```

`mycustom.providers`文件可能如下：

```
<?xml version="1.0"?>
<smackProviders>
<iqProvider>
    <elementName>query</elementName>
    <namespace>jabber:iq:time</namespace>
    <className>org.jivesoftware.smack.packet.Time</className>
</iqProvider>

<iqProvider>
    <elementName>query</elementName>
    <namespace>https://jabber.org/protocol/disco#items</namespace>
    <className>org.jivesoftware.smackx.provider.DiscoverItemsProvider</className>
</iqProvider>

<extensionProvider>
    <elementName>subscription</elementName>
    <namespace>https://jabber.org/protocol/pubsub</namespace>
    <className>org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider</className>
</extensionProvider>
</smackProviders>
```

例如，最流行的 XMPP 消息传递扩展是[谷歌云消息](https://developers.google.com/cloud-messaging/)。Smack 库为此目的提供了`org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider`。默认情况下，它使用`experimental.providers`资源在 Classpath 中用`smack-experimental`jar 注册该类，如下 Maven 示例所示：

```
<!-- GCM JSON payload -->
<extensionProvider>
    <elementName>gcm</elementName>
    <namespace>google:mobile:data</namespace>
    <className>org.jivesoftware.smackx.gcm.provider.GcmExtensionProvider</className>
</extensionProvider>
```

此外，`GcmPacketExtension`允许目标消息传递协议解析传入的数据包并构建传出的数据包，如下例所示：

```
GcmPacketExtension gcmExtension = (GcmPacketExtension) xmppMessage.getExtension(GcmPacketExtension.NAMESPACE);
String message = gcmExtension.getJson());
```

```
GcmPacketExtension packetExtension = new GcmPacketExtension(gcmJson);
Message smackMessage = new Message();
smackMessage.addExtension(packetExtension);
```

有关更多信息，请参见本章前面的[入站消息通道适配器](#xmpp-message-inbound-channel-adapter)和[出站消息通道适配器](#xmpp-message-outbound-channel-adapter)。
