如何使用 JPOS Q2 Server 动态路由 ISO8583 消息?
明确结论
可以实现。JPOS Q2 并不要求所有路由都由 channel.xml 里的固定 IP 决定。XML 适合声明服务器、Channel、MUX、TransactionManager 等基础组件。具体的路由判断应放在 Java 业务代码中,例如 ISORequestListener、TransactionManager 的 TransactionParticipant 或独立的路由服务。
推荐采用以下流程:
- 从报文的指定字段中提取路由标识。
- 根据标识查询数据库,得到逻辑路由。
- 从连接池或 MUX 注册表中选出对应的下游连接。
- 通过该 MUX 发送请求并等待响应。
- 使用原始
ISOSource将下游响应返回给上游。
不要在每笔交易到达时修改 channel.xml,也不要让多个线程并发修改同一个共享 Channel 的 host 和 port。
推荐架构
Source
│
│ ISO8583 request
▼
QServer / ISORequestListener
│
├─ 解析 identifier
├─ 查询或命中路由缓存
├─ 执行业务处理
└─ 选择 Destination MUX
│
▼
Destination Host
│
│ ISO8583 response
▼
Destination MUX
│
▼
原始 ISOSource.send(response)
│
▼
Source
这里要区分两个概念:
Channel负责 TCP 连接、报文打包和收发。MUX负责在同一条连接上匹配请求与响应,通常根据 MTI、字段 11、字段 37 等信息进行关联。
因此,支付网关通常不会让多个业务线程共享一个 Channel,再分别调用 receive()。更常见的做法是通过 QMUX 或其他 MUX 匹配请求和响应。
路由表设计
数据库最好返回逻辑路由名称,避免业务代码直接使用任意 IP 地址。例如:
CREATE TABLE iso_route (
identifier VARCHAR(32) PRIMARY KEY,
route_name VARCHAR(64) NOT NULL,
host VARCHAR(255) NOT NULL,
port INTEGER NOT NULL,
enabled BOOLEAN NOT NULL,
timeout_ms INTEGER NOT NULL
);
路由结果可以抽象为:
public record Route(
String name,
String host,
int port,
long timeoutMillis
) {
}
如果系统对安全性和可运维性要求较高,建议让数据库中的 identifier 只映射到预先批准的 route_name:
identifier=ACQ001 -> route_name=bank-a
identifier=ACQ002 -> route_name=bank-b
之后由 Q2 中已经配置好的 bank-a、bank-b MUX 管理连接。这样既能使用 Q2 的连接重试、日志、启停和监控能力,也能防止数据库中的错误数据把请求发往未经允许的地址。
在入口处动态选择路由
下面以 ISORequestListener 为例,假设字段 32 是路由标识。实际项目应按照接入协议选择字段,也可能使用字段 24、33、41、42、100 或复合域中的子字段。
import org.jpos.iso.ISOException;
import org.jpos.iso.ISOMsg;
import org.jpos.iso.ISORequestListener;
import org.jpos.iso.ISOSource;
import org.jpos.iso.MUX;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
public class DynamicRouter implements ISORequestListener {
private final RouteResolver routeResolver;
private final MuxRegistry muxRegistry;
private final ExecutorService executor;
public DynamicRouter(
RouteResolver routeResolver,
MuxRegistry muxRegistry,
ExecutorService executor
) {
this.routeResolver = routeResolver;
this.muxRegistry = muxRegistry;
this.executor = executor;
}
@Override
public boolean process(ISOSource source, ISOMsg incoming) {
ISOMsg request = (ISOMsg) incoming.clone();
executor.execute(() -> {
try {
String identifier = request.getString(32);
if (identifier == null || identifier.isBlank()) {
sendError(source, request, "30");
return;
}
Route route = routeResolver.resolve(identifier);
if (route == null) {
sendError(source, request, "92");
return;
}
MUX mux = muxRegistry.get(route.name());
if (mux == null || !mux.isConnected()) {
sendError(source, request, "91");
return;
}
ISOMsg response = mux.request(
request,
route.timeoutMillis()
);
if (response == null) {
sendError(source, request, "68");
return;
}
source.send(response);
} catch (Exception e) {
try {
sendError(source, request, "96");
} catch (Exception ignored) {
// 此处应记录原始异常和发送失败异常
}
}
});
return true;
}
private void sendError(
ISOSource source,
ISOMsg request,
String responseCode
) throws ISOException, IOException {
ISOMsg response = (ISOMsg) request.clone();
response.setResponseMTI();
response.set(39, responseCode);
source.send(response);
}
}
示例中的响应码只是为了说明控制流程。字段 39 的实际取值必须遵循接入方协议,不能在所有网络中照搬 91、92、96 或 68。
process() 返回 true,表示当前监听器已经接管这条消息。示例将下游请求交给线程池处理,避免长时间占用 QServer 的接收线程。线程池必须配置容量、队列长度和拒绝策略,不能直接使用无限队列。
查询数据库时不要为每笔交易重建连接
路由查询可以使用连接池,并配合短时间缓存:
public interface RouteResolver {
Route resolve(String identifier) throws Exception;
}
下面是一个简化的 JDBC 实现:
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class JdbcRouteResolver implements RouteResolver {
private static final String SQL = """
SELECT route_name, host, port, timeout_ms
FROM iso_route
WHERE identifier = ?
AND enabled = TRUE
""";
private final DataSource dataSource;
public JdbcRouteResolver(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Route resolve(String identifier) throws Exception {
try (
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(SQL)
) {
statement.setString(1, identifier);
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
return null;
}
return new Route(
resultSet.getString("route_name"),
resultSet.getString("host"),
resultSet.getInt("port"),
resultSet.getLong("timeout_ms")
);
}
}
}
}
实际系统应使用带过期时间的本地缓存,并提前确定数据库不可用时采取哪种策略:
- 使用最近一次有效路由;
- 直接拒绝交易;
- 切换到备用路由;
- 让部分交易进入 store-and-forward 流程。
数据库异常时,金融交易不能静默选择默认目的地,否则可能造成错误清算或资金风险。
下游连接的两种实现方式
方案一:预建多个 QMUX,由数据库结果动态选择
这是相对稳妥的方案。为每个已知目的机构配置独立的 Channel 和 QMUX:
route_name=bank-a -> MUX bank-a
route_name=bank-b -> MUX bank-b
route_name=bank-c -> MUX bank-c
代码中的注册表只需按名称返回 MUX:
import org.jpos.iso.MUX;
import java.util.Map;
public class MuxRegistry {
private final Map<String, MUX> muxes;
public MuxRegistry(Map<String, MUX> muxes) {
this.muxes = Map.copyOf(muxes);
}
public MUX get(String routeName) {
return muxes.get(routeName);
}
}
项目也可以通过 Q2/NameRegistrar 获取已部署的 QMUX。不同 jPOS 版本的组件获取和注入方式可能不同,应以项目所用版本的 API 为准。原则不变:数据库返回逻辑名称,业务代码选择已经存在的 MUX。
这种方式有以下优点:
- 可以复用长连接;
- 请求与响应由 QMUX 负责关联;
- 连接断开后可以自动重连;
- 各目的端可以使用不同的 packager、header 和超时;
- 配置、日志和运维的边界比较清楚。
方案二:在运行时创建并缓存目的端连接
如果目的地址确实无法提前枚举,可以开发一个 DestinationManager:
Route(host, port, channelType, packager, timeout)
│
▼
DestinationManager.getOrCreate(route)
│
▼
Channel + MUX + lifecycle
它至少要处理以下事项:
- 按
host + port + channelType + packager缓存连接; - 限制动态目的端的数量;
- 检查地址白名单;
- 管理连接的建立、重连和关闭;
- 防止多个线程重复创建同一连接;
- 清理长期空闲的连接;
- 为每个连接提供请求与响应的关联机制;
- 配置更新后安全替换旧连接。
只执行下面这些操作,还不足以构成可靠的支付路由器:
channel.setHost(route.host(), route.port());
channel.connect();
channel.send(message);
ISOMsg response = channel.receive();
共享 Channel 在并发交易中会产生竞态。一个线程可能在另一个线程收到响应之前修改目标地址,多个线程也无法可靠判断响应属于哪个请求。如果每笔交易都新建 TCP 连接,性能、连接数量、登录握手和故障恢复又会成为问题。
如何让响应沿原路径返回
入口收到请求时,ISOSource source 代表该请求所使用的上游连接。下游响应到达后,只需调用:
source.send(response);
响应便会通过原始上游连接返回,无须再根据 IP 查询一次反向路由。
这需要满足以下条件:
- 下游请求必须通过 MUX 发送,以便把响应匹配到正确的请求。
- 处理完成前,上游连接必须保持有效。
- 总处理时间不能超过上游规定的超时时间。
- 不能只把
ISOSource、请求报文或关联信息保存在无界的内存集合中。 - 如果处理过程会跨线程、跨进程或持续较长时间,应保存明确的关联信息,不能只依赖 Java 对象引用。
在 TransactionManager 架构中,这些内容通常会放入 Context:
SOURCE
REQUEST
ROUTE
DESTINATION_REQUEST
DESTINATION_RESPONSE
随后由多个 TransactionParticipant 依次完成校验、查询路由、报文转换、发送下游、处理响应和回送上游。对于需要审计、超时控制、幂等处理、冲正和 store-and-forward 的支付系统,这种结构通常比把所有逻辑集中在一个监听器中更容易维护。
必须处理好报文关联
动态路由不只是选择 IP,还要保证每笔交易都能正确关联。至少需要检查:
- 字段 11(STAN)在各目的端是否唯一;
- 字段 37(Retrieval Reference Number)是保留还是重新生成;
- 请求 MTI 与响应 MTI 是否正确对应;
- 下游是否会修改重要的关联字段;
- 相同 STAN 是否可能同时发送到不同目的端;
- 如何处理超时后到达的响应;
- 冲正报文是否必须发送到原交易所使用的目的端。
冲正尤其不能只根据当前路由表重新计算目的地。路由配置可能在原交易完成后发生变化,因此要在交易日志中保存原始目的路由,让冲正、撤销或查询继续使用该路由。
实施顺序
可以按以下顺序推进:
- 确定用于路由的 ISO8583 字段及其合法值。
- 建立
identifier -> route_name路由表。 - 为已知目的端配置独立的 Channel 和 QMUX。
- 实现
RouteResolver,加入数据库连接池和有界缓存。 - 实现入口监听器或 TransactionManager participant。
- 使用
mux.request()发送下游请求并设置超时。 - 通过原始
ISOSource.send()回送响应。 - 加入并发限制、熔断、日志、审计和迟到响应处理。
- 保存每笔交易实际选择的路由,用于冲正和问题追踪。
如果下游机构数量有限,采用”静态配置连接、动态选择 MUX”通常就能满足动态路由需求。只有当目的地址数量很大或变化频繁时,才有必要实现运行时 Channel/MUX 管理器。
备注:内容仅供参考。
