首先要从JMX URL说起。如下是一个典型的JMX URL:
service:jmx:rmi://localhost:5000/jndi/rmi://localhost:6000/jmxrmi
如果在服务器端,我们用该URL创建一个connector server,则大概流程如下:
可以看到,如果在服务器端创建connector server时,该URL的第三部分(即localhost:5000)如果省略的话,则connector server会随机任意选择一个可用的端口。
如果在客户端,我们通过该URL创建一个connector,则大概按照如下的流程:
可以看到,如果在客户端创建connector时,该URL的第三部分可以被省略掉。事实上我们也经常这么做,比如上面这个示例,在客户端建立connector所用的JMX URL可以被省略为:
service:jmx:rmi:///jndi/rmi://localhost:6000/jmxrmi
从这个示例中我们也可以看到,如果采用rmi作为传输协议的话,客户端需要进行两个连接。首先客户端连接到rmiregistry上得到真实服务器的stub(如rmi://localhost:6000/rmxrmi),然后客户端再根据该stub连接到真实的服务器上(如rmi://localhost:5000)。
在Sun Java 5.0的实现当中,可以通过指定运行期属性来配置JMX。比如:
-Dcom.sun.management.jmxremote.port=6000
不需要验证的配置:
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.password.file=conf/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=conf/jmxremote.access
- -Dcom.sun.management.jmxremote.port=6000
- -Dcom.sun.management.jmxremote.authenticate=false
- -Dcom.sun.management.jmxremote.ssl=false
除开密码和权限控制的配置外,这段运行期相当于一个如下的JMX URL:
service:jmx:rmi://localhost:0/jndi/rmi://localhost:6000/jmxrmi (红色部分可以被省略掉)
可以看到,通过指定com.sun.management.jmxremote.port属性,相当于指定了rmiregistry的运行端口,但是真正运行的服务器的stub export端口为0,会在运行期任意选择一个未被使用的端口号。如果该程序运行在防火墙后面,这个动态分配的端口号会让我们非常难以配置防火墙规则。
由于Sun Java 5.0实现的疏忽,并没有为我们提供一个运行期属性来配置stub export端口号,那么如何自定义一个JMX连接端口呢?
假设我们不传入任何运行期参数,仅通过代码来自定义一个connector server:
LocateRegistry.createRegistry(6000);
JMXConnectorServer cs = JMXConnectorServerFactory
.newJMXConnectorServer(url, env, beanServer);
cs.start();
第一行代码指明在6000端口创建一个rmiregistry。
第二行代码创建了一个connector server。第一个URL参数传入指定的JMXServiceURL,比如new JMXServiceURL("service:jmx:rmi://localhost:5000/jndi/rmi://localhost:6000/jmxrmi"),值得注意的是红色部分标出的IP和端口号要和第一步中创建的rmiregistry一致。第二个参数指定创建时的环境,我们可以暂时传入一个空Map。第三个参数指定要创建连接的MBeanServer,比如在Java 5.0下可以是ManagementFactory.getPlatformMBeanServer()。
第三行代码开启了创建的connector server。
这三行代码创建的connector server工作的很好,除了一点:真实的服务器没有任何认证和授权机制,会允许所有人访问。
自定义权限控制可以通过刚才第二行代码的第二个参数来进行设置:
Map env = new HashMap();
env.put(JMXConnectorServer.AUTHENTICATOR, JMXAuthenticator子类的实例);
比如在MX4J中就提供了一个mx4j.tools.remote.PasswordAuthenticator的类,通过配置文件来匹配用户名和密码;Sun也提供了一个com.sun.jmx.remote.security.JMXPluggableAuthenticator的类通过JAAS来配置验证和权限。用户可以参照这两个类来提供自定义的实现。这里提供一个简单的示例(不带任何效验机制,仅用于示例)
env.put(JMXConnectorServer.AUTHENTICATOR, new JMXAuthenticator() {
public Subject authenticate(Object credentials) {
String[] sCredentials = (String[]) credentials;
String userName = sCredentials[0];
String password = sCredentials[1];
if ("test".equals(userName) && "test".equals(password)) {
Set principals = new HashSet();
principals.add(new JMXPrincipal(user));
return new Subject(true, principals, Collections.EMPTY_SET,
Collections.EMPTY_SET);
}
throw new SecurityException("Authentication failed! "
+ message);
}
});
加入这段代码后,真实的服务器必须要验证后才能访问,但又有了一个新问题:rmiregistry没有任何验证。这意味着其他人可以把一个不相干的stub绑定到同一地址,从而替换掉真实服务器stub。好在幸运的是,无法把一个服务器端类路径上不存在的stub类绑定到rmiregistry,否则真实的用户名和密码就有可能被泄漏出去。
如果直接通过rmi对外提供的接口,我们没有办法修正这个问题,除非将代码绑定于特定的Java实现,比如Sun就直接通过它自己的rmi实现类来做(所以通过运行期参数配置jmx生成的rmiregistry是只读的,没有该安全性问题)。具体可参见sun.management.Agent和sun.management.jmxremote包下相关类。
但是我们可以换一个思路,比如不绑定到rmiregistry而绑定到其他的实现了验证机制的jndi provider,例如ldap上。由于bind jndi的代码在相关connector server的实现类中,比如javax.management.remote.rmi.RMIConnectorServer中bind jndi的代码:
void bind(String jndiUrl, Hashtable attributes,
RMIServer rmiServer, boolean rebind)
throws NamingException, MalformedURLException {
// if jndiURL is not null, we nust bind the stub to a
// directory.
InitialContext ctx =
new InitialContext(attributes);
if (rebind)
ctx.rebind(jndiUrl, rmiServer);
else
ctx.bind(jndiUrl, rmiServer);
ctx.close();
}
它在传入attributes时并没有特定的jndi的INITIAL_CONTEXT_FACTORY,所以我们可以通过两种方法来指定特定的INITIAL_CONTEXT_FACTORY:
其中第一种方式不需要额外的代码,但是可能会对系统中其他jndi的InitialContext产生影响;第二种方式稍微麻烦点,但是在实现上比较完美。
值得提到的是,如果采用rmiregistry,并且rmiregistry和server在同一个JVM内,stub和rmiregistry可以共享一个端口。比如可以使用如下的JMX URL:
service:jmx:rmi://localhost:6000/jndi/rmi://localhost:6000/jmxrmi
但注意一定要在同一JVM内,比如先用LocateRegistry.createRegistry(6000)创建本地rmiregistry,再通过该URL开启connector server,则可以共享6000端口。