RMI 存根:在客户端强制主机值
RMI Stubs: Force host-value on client side
我们想从我们网络中的不同主机访问同一个 RMI 服务器(dev-pc 通过 ssh-tunnel,jenkins-server 通过直接连接)。问题是 RMI 主机在不同的客户端主机上以不同的名称为人所知。
当我们连接到注册表时这不是问题,因为我们可以这样设置目标主机名:
Registry registry = LocateRegistry.getRegistry("hostname", 10099, new CustomSslRMIClientSocketFactory());
但是当我们像下面这样查找远程对象时,它包含错误的主机名。
HelloRemote hello = (HelloRemote) registry.lookup(HelloRemote.class.getSimpleName());
在调试器中,我可以观察到注册表对象需要主机,但存根不需要:
我们一调用 Stub 上的方法就会出现连接超时。如果我在调试器中手动将主机值更改为 localhost
,则方法调用成功。
我知道我可以在服务器端设置 java.rmi.server.hostname
,但是来自 jenkins 的连接不再有效。
最简单的解决方案是我强制 RMI 使用与注册表相同的主机,用于从该注册表检索的所有存根。有没有比通过反射替换 Stub 中的主机值更好的方法?
不幸的是,RMI 有一个很深的内置假设,即服务器主机有一个 'most public' IP 地址或主机名。这解释了 java.rmi.server.hostname
惨败。如果你的系统不符合你的运气。
正如 EJP 所指出的,似乎没有优雅的开箱即用解决方案。
我能想到两个不雅的:
- 更改每个客户端主机上的网络配置,以便将流量重定向到无法访问的 ip 到本地主机。
- 通过反射更改 "hello"-对象上的主机值。
我选择了第二个选项,因为我在测试环境中,而且有问题的代码无论如何都不会产生效果。否则我不建议这样做,因为此代码可能会与 java 的未来版本中断,并且如果安全管理器到位则无法工作。
但是,这是我的工作代码:
private static void forceRegistryHostNameOnStub(Object registry, Object stub) {
try {
String regHost = getReferenceToInnerObject(registry, "ref", "ref", "ep", "host").toString();
Object stubEp = getReferenceToInnerObject(stub, "h", "ref", "ref", "ep");
Field fStubHost = getInheritedPrivateField(stubEp, "host");
fStubHost.setAccessible(true);
fStubHost.set(stubEp, regHost);
} catch (Exception e) {
LOG.error("Applying the registry host to the Stub failed.", e);
}
}
private static Object getReferenceToInnerObject(Object from, String... objectHierarchy) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Object ref = from;
for (String fieldname : objectHierarchy) {
Field f = getInheritedPrivateField(ref, fieldname);
f.setAccessible(true);
ref = f.get(ref);
}
return ref;
}
private static Field getInheritedPrivateField(Object from, String fieldname) throws NoSuchFieldException {
Class<?> i = from.getClass();
while (i != null && i != Object.class) {
try {
return i.getDeclaredField(fieldname);
} catch (NoSuchFieldException e) {
// ignore
}
i = i.getSuperclass();
}
return from.getClass().getDeclaredField(fieldname);
}
Stub 上的方法调用现在成功了:
Registry registry = LocateRegistry.getRegistry("hostname", 10099, new CustomSslRMIClientSocketFactory());
HelloRemote hello = (HelloRemote) registry.lookup(HelloRemote.class.getSimpleName());
forceRegistryHostNameOnStub(registry, hello); // manipulate the stub
hello.doSomething(); // succeeds
我们想从我们网络中的不同主机访问同一个 RMI 服务器(dev-pc 通过 ssh-tunnel,jenkins-server 通过直接连接)。问题是 RMI 主机在不同的客户端主机上以不同的名称为人所知。
当我们连接到注册表时这不是问题,因为我们可以这样设置目标主机名:
Registry registry = LocateRegistry.getRegistry("hostname", 10099, new CustomSslRMIClientSocketFactory());
但是当我们像下面这样查找远程对象时,它包含错误的主机名。
HelloRemote hello = (HelloRemote) registry.lookup(HelloRemote.class.getSimpleName());
在调试器中,我可以观察到注册表对象需要主机,但存根不需要:
我们一调用 Stub 上的方法就会出现连接超时。如果我在调试器中手动将主机值更改为 localhost
,则方法调用成功。
我知道我可以在服务器端设置 java.rmi.server.hostname
,但是来自 jenkins 的连接不再有效。
最简单的解决方案是我强制 RMI 使用与注册表相同的主机,用于从该注册表检索的所有存根。有没有比通过反射替换 Stub 中的主机值更好的方法?
不幸的是,RMI 有一个很深的内置假设,即服务器主机有一个 'most public' IP 地址或主机名。这解释了 java.rmi.server.hostname
惨败。如果你的系统不符合你的运气。
正如 EJP 所指出的,似乎没有优雅的开箱即用解决方案。 我能想到两个不雅的:
- 更改每个客户端主机上的网络配置,以便将流量重定向到无法访问的 ip 到本地主机。
- 通过反射更改 "hello"-对象上的主机值。
我选择了第二个选项,因为我在测试环境中,而且有问题的代码无论如何都不会产生效果。否则我不建议这样做,因为此代码可能会与 java 的未来版本中断,并且如果安全管理器到位则无法工作。
但是,这是我的工作代码:
private static void forceRegistryHostNameOnStub(Object registry, Object stub) {
try {
String regHost = getReferenceToInnerObject(registry, "ref", "ref", "ep", "host").toString();
Object stubEp = getReferenceToInnerObject(stub, "h", "ref", "ref", "ep");
Field fStubHost = getInheritedPrivateField(stubEp, "host");
fStubHost.setAccessible(true);
fStubHost.set(stubEp, regHost);
} catch (Exception e) {
LOG.error("Applying the registry host to the Stub failed.", e);
}
}
private static Object getReferenceToInnerObject(Object from, String... objectHierarchy) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Object ref = from;
for (String fieldname : objectHierarchy) {
Field f = getInheritedPrivateField(ref, fieldname);
f.setAccessible(true);
ref = f.get(ref);
}
return ref;
}
private static Field getInheritedPrivateField(Object from, String fieldname) throws NoSuchFieldException {
Class<?> i = from.getClass();
while (i != null && i != Object.class) {
try {
return i.getDeclaredField(fieldname);
} catch (NoSuchFieldException e) {
// ignore
}
i = i.getSuperclass();
}
return from.getClass().getDeclaredField(fieldname);
}
Stub 上的方法调用现在成功了:
Registry registry = LocateRegistry.getRegistry("hostname", 10099, new CustomSslRMIClientSocketFactory());
HelloRemote hello = (HelloRemote) registry.lookup(HelloRemote.class.getSimpleName());
forceRegistryHostNameOnStub(registry, hello); // manipulate the stub
hello.doSomething(); // succeeds