Monitor Your Applications With JConsole - Part 3

来源:互联网 发布:安全设置阻止java运行 编辑:程序博客网 时间:2024/05/02 04:50

[Original Page]

  Using JConsole in a production environment usually involves having to go through a firewall. Because JConsole uses RMI to communicate withremote applications, we need to open up the firewall or use some kind of tunnel. Both scenarios require a bit of customization of the remote application to make this possible as we'll see in this last part.

In Part 2 we showed how to use JConsole to monitor and manage JBoss running on a remote machine that's not behinda firewall. In the final scenario that we're going to look at we'll see what's needed to access JBoss with JConsole through a firewall. Whendealing with a firewall located between the machine running JConsole and the machine running the remote application (e.g. JBoss) there are basically two options:

  1. Opening ports in the firewall needed for the RMI communication between JConsole and the remote application.
  2. Tunneling the RMI communication over a port that's already open.

The first option is obviously the simpler of the two, but requires the cooperation of your friendly firewall administrator. While this mayprove to be difficult depending on how friendly your firewall administrator really is, don't dismiss this option as impossible right away.Don't forget that even if you decide to use tunneling you still have to talk to your firewall administrator to make sure he approves of this solution and to check if it's allowed within the security policies of your company. It's tempting to think of tunneling as a way to not haveto deal with your firewall administrator or even as a method to circumvent security measures, but this may get you in some serious troublein the long run.

So the best way to deal with a firewall is to get in touch with your firewall administrator and explain to him why you need JConsole and whatyou intend to do. Ask him if he's willing (and allowed) to open ports for you and what kind of procedure you'll have to follow to realize this.Only if it's absolutely not possible to open ports, should you propose to use tunneling. Don't give up too easy if the first response to yourrequest to open ports is negative, since usually firewall administrators will favor this option above tunneling. The reason for this is thatopening ports gives firewall administrators more control because this gives them the ability to only allow access to these ports from specific IP-addresses or ranges, while tunneling opens up the firewall for everyone who has access to the port you're tunneling over.

Let's take a look at what we have to do after we managed to convince our firewall administrator to open up ports in the firewall. We just haveto negotiate about which port to use and then configure it using the com.sun.management.jmxremote.port system property, right?Unfortunately, things are a bit more complicated. First of all we need two ports for communicating with a remote application using RMI. The firstport, which we can configure with the system property mentioned above, is used by the RMI Registry. The second port is used by the JMX Connector Server of the JMX Agent. JConsole will access the RMI Registry to find out where it can find the JMX Connector Server to communicate with the MBean server.

The problem is that we can't configure the port that the JMX Connector Server should use, because this is chosen by the RMI stack. We canlook at the detailed logging that we enabled for JConsole to find out which port is being used to connect to the remote application.   



  However there's no guarantee that the JMX Connector Server will always be available on the same port. Most likely after restarting JBossthe RMI stack will choose another port. So before we can tell our firewall administrator which ports need to be opened, we have got to find a way to make the JMX Connector Server always available on the same port. Luckily we can use the JMX Remote API to programmatically create the JMX Connector Server and control the port it uses. All we really have to do is create a JMXServiceURL using the ports we want to use and create and start the JMX Connector Server.

The code sample below shows the most basic setup for creating a custom JMX Connector Server that can be used by JConsole to communicatewith the MBean server.

 MBeanServer mbeanServer =    ManagementFactory.getPlatformMBeanServer(); JMXServiceURL url = new JMXServiceURL(   "service:jmx:rmi://localhost:12199/jndi/rmi://localhost:11199/jmxrmi"); JMXConnectorServer connectorServer =   JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbeanServer);         connectorServer.start();

The JMXServiceURL above specifies that the JMX Connector Server will use port 12199 and register itself with the RMIregistry running on port 11199. Of course we also have to make sure the RMI registry is running and we need some extra code if we wantto use SSL and authentication, but before we do that let's first think about where we're going to put this code.

One option is to add this code to our application running in JBoss and make sure it's executed during startup of the application. We could dothis very easily by calling this code during the initialization of a servlet that's configured to load on startup. Although this will work justfine, it's better not to incorporate this functionality in our application. First of all when we're using JConsole we're not just monitoring and managing our application, but JBoss and all the applications running on it. So technically this functionality should not be part of any singleapplication, but be packaged on its own. This will also make it easier to reuse our custom JMX Connector Server.

We could create a very simple application containing just a servlet to execute the code to create the JMX Connector Server during startupand deploy it alongside our other application(s). But there's an even better way to do this. We can create a ServiceMBean to createa custom JBoss service, which will also be available in the JBoss JMX console so we can change the configuration and start and stop the service.

Creating the JConsoleService MBean for JBoss

To create a ServiceMBean we need three things: the MBean interface, the MBean implementation and a jboss-service.xml locatedin the META-INF folder. By packaging these files as a Service Archive (.sar) file and dropping it in the JBoss deploy folder our ServiceMBean will be deployed and started automatically. Since a .sar file is nothing more than a JAR file with a different extension creating it is very simple. For our JConsoleService the content of the .sar file will look something like this when we list it using the jar utility:

 META-INF/MANIFEST.MF com/componative/jboss/services/jconsole/JConsoleService.class com/componative/jboss/services/jconsole/JConsoleServiceMBean.class META-INF/jboss-service.xml

Let's start with creating the MBean interface. Because we'll be using a Standard MBean for our service the interface must takethe name of the Java class that implements it with the suffix MBean added (see Resources for moreinformation). We'll use JConsoleService for the implementation class, giving us the name JConsoleServiceMBean forthe interface.
By extending the interface from org.jboss.system.ServiceMBean JBoss will automatically call the JBoss specific lifecycle methodscreate() and start() during startup and stop() and destroy() during shutdown (see Resources for moreinformation). This will also enable us to call these methods from the JBoss JMX Console. For now we will not define any attributes or extra operations, so the interface will simply look like this:

 public interface JConsoleServiceMBean extends ServiceMBean { }

For the implementation of the MBean we create a JConsoleService class that extends from org.jboss.system.ServiceMBeanSupport. This base class will take care of implementing the ServiceMBean interface, leavingus with only three methods that need to be overridden:

  • getName() for returning the name of our service as displayed in the JBoss JMX Console.
  • startService() for starting our service.
  • stopService() for stopping our service.

We could also override the createService() and destroyService() methods, but we don't need them for this service. Thisgives us our first simple version of the JConsoleService. Don't worry about the hardcoded port numbers for now; we'll fix this shortly.

 public class JConsoleService    extends ServiceMBeanSupport implements JConsoleServiceMBean {      private JMXConnectorServer connectorServer = null;   private Registry rmiRegistry = null;      @Override   public String getName() {     return "com.componative.jboss:service=JConsoleService";   }      @Override   protected void startService() throws Exception {     getLog().info("Starting JConsoleService...");     super.startService();     MBeanServer mbeanServer =        ManagementFactory.getPlatformMBeanServer();     JMXServiceURL url = new JMXServiceURL(       "service:jmx:rmi://localhost:12199/jndi/rmi://localhost:11199/jmxrmi");     startRMIRegistry();          this.connectorServer =       JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbeanServer);     this.connectorServer.start();   }      @Override   protected void stopService() throws Exception {     getLog().info("Stopping JConsoleService...");     if (this.connectorServer != null) {       this.connectorServer.stop();       this.connectorServer = null;     }     stopRMIRegistry();          super.stopService();   }      private void startRMIRegistry() throws RemoteException {     this.rmiRegistry = null;     try {       // Ensure cryptographically strong random number generator used       // to choose the object number - see java.rmi.server.ObjID       System.setProperty("java.rmi.server.randomIDs", "true");       this.rmiRegistry = LocateRegistry.createRegistry(11199);     } catch (ExportException ex) {       // registry already exists        // => no problem, just try to use it       // => don't set rmiRegistry instance variable so we don't stop it       //    during stopService() since it may be used for other remote        //    objects       // => log a message to indicate we're using an existing registry       getLog().warn("RMIRegistry already exists: " + ex.toString());     }   }      private void stopRMIRegistry() throws NoSuchObjectException {     if (this.rmiRegistry != null) {       UnicastRemoteObject.unexportObject(this.rmiRegistry, true);       this.rmiRegistry = null;     }   } }

As we can see the code for creating and starting the JMX Connector Server ended up in the startService() method and weadded functionality to stop the JMX Connector Server to the stopService() method. But what about the startRMIRegistry() method? Couldn't we simply use the default RMI registry running on port 1099 or start one using the out-of-the-box configuration weused in Part 2? The answer to both questions is: no we can't.

The reason why we can't use the default RMI registry port 1099, is because JBoss uses it to run it's JNDI service instead of a RMI registry. If we would try to register our JMX Connector Server with the JNDI service running on port 1099 we would get an java.rmi.ConnectIOException telling us that were trying to connect to a 'non-JRMP server at remote endpoint'.
Using -Dcom.sun.management.jmxremote.port=11199 on the other hand will create a non-modifiable RMI registry containing the out-of-the-box JMX Connector Server. If we try to replace this one with our custom version we'll get the following exception:

java.rmi.AccessException: Cannot modify this registry

So we simply have to start (and stop) the registry ourselves. The only tricky part here is not to stop the registry if it was already running.The problem is that we can't test if the registry is running by simply calling LocateRegistry.getRegistry(port) because this willalways return a reference even if the registry is not running as we can read in the JavaDoc for LocateRegistry:

Note that a getRegistry call does not actually make a connection to the remote host. It simply creates a local reference to the remote registryand will succeed even if no registry is running on the remote host. Therefore, a subsequent method invocation to a remote registry returned as a result of this method may fail.

This leaves us with two options: use LocateRegistry.getRegistry(port) and call for example the list() method on thereturned registry to see if it generates an exception or simply try to create the registry and catch the exception that is thrown if it alreadyexits. Since the most common scenario will be that there is no registry running yet, we use the second option here because this will generallyresult in less exceptions being thrown and caught. As a bonus this also requires less code.

Finally we need a jboss-service.xml to tell JBoss which class implements the service and the name of the service.

 <?xml version="1.0" encoding="UTF-8"?> <server>   <mbean code="com.componative.jboss.services.jconsole.JConsoleService"      name="com.componative.jboss:service=JConsoleService">   </mbean> </server>

Now we can package these three files as a .sar file and drop it in the JBoss deploy folder (see Resources fordownloads of the .sar file). Before you do this make sure that the port configured for the out-of-the-box version of the JMX Connector Serverdoes not conflict with our custom version. If this is the case you can run both the out-of-the-box and the custom version simultaneously as longas you remember that our custom version will not use any of the system properties used by the out-of-the-box version except for the keystore andtruststore settings as we'll see later on. To avoid confusing it's probably better to remove the out-of-the-box settings from the JBoss startup script.

If we now start JConsole and connect to JBoss on port 11199 we can see in the detailed logging that port 12199 is used for theJMX Connector Server.  



Adding features to the JConsoleService

Now that we've got our first version going, let's add the same features the out-of-the-box version has and make the settings configurable viathe JBoss JMX Console. We start with adding the getters and setters for the different attributes to the MBean interface. We'll also adda readonly attribute to determine if SSL client authentication for the RMI registry is supported (i.e. if we're running on JDK 6 or later).

 public interface JConsoleServiceMBean extends ServiceMBean {   void setRMIRegistryPort(int port);   int getRMIRegistryPort();   void setJMXConnectorServerPort(int port);   int getJMXConnectorServerPort();   void setPasswordFile(String passwordFile);   String getPasswordFile();   void setAccessFile(String accessFile);   String getAccessFile();   void setSSLEnabled(boolean enabled);   boolean isSSLEnabled();   void setSSLClientAuthenticationEnabled(boolean enabled);   boolean isSSLClientAuthenticationEnabled();   void setSSLRegistryAuthenticationEnabled(boolean enabled);   boolean isSSLRegistryAuthenticationEnabled();   boolean isSSLRegistryAuthenticationSupported(); }

Next we implement the getters and setters defined on the interface.

 public class JConsoleService         extends ServiceMBeanSupport implements JConsoleServiceMBean {   private int rmiRegistryPort = 0;   private int jmxConnectorServerPort = 0;   private String passwordFile = null;   private String accessFile = null;   private boolean sslEnabled = false;   private boolean sslClientAuthenticationEnabled = false;   private boolean sslRegistryAuthenticationEnabled = false;   ...   public void setRMIRegistryPort(int port) {     this.rmiRegistryPort = port;   }   public int getRMIRegistryPort() {     return this.rmiRegistryPort;   }   public void setJMXConnectorServerPort(int port) {     this.jmxConnectorServerPort = port;   }   public int getJMXConnectorServerPort() {     return this.jmxConnectorServerPort;   }   public void setAccessFile(String accessFile) {     this.accessFile = accessFile;   }   public String getAccessFile() {     return this.accessFile;   }   public void setPasswordFile(String passwordFile) {     this.passwordFile = passwordFile;   }   public String getPasswordFile() {     return this.passwordFile;   }   public void setSSLEnabled(boolean enabled) {     this.sslEnabled = enabled;   }   public boolean isSSLEnabled() {     return this.sslEnabled;   }   public void setSSLClientAuthenticationEnabled(boolean enabled) {     this.sslClientAuthenticationEnabled = enabled;   }   public boolean isSSLClientAuthenticationEnabled() {     // client authentication is only possible if SSL is enabled     if (!isSSLEnabled()) {       this.sslClientAuthenticationEnabled = false;     }     return this.sslClientAuthenticationEnabled;   }   public void setSSLRegistryAuthenticationEnabled(boolean enabled) {     this.sslRegistryAuthenticationEnabled = enabled;   }   public boolean isSSLRegistryAuthenticationEnabled() {     // registry authentication is only possible if client authentication is      // enabled and the feature is supported     if (!isSSLEnabled() || !isSSLClientAuthenticationEnabled()              || !isSSLRegistryAuthenticationSupported()) {       this.sslRegistryAuthenticationEnabled = false;     }     return this.sslRegistryAuthenticationEnabled;   }   public boolean isSSLRegistryAuthenticationSupported() {     boolean supported = false;     String version = System.getProperty("java.version");     if (version != null && version.length() >= 3) {       supported = version.substring(0, 3).compareTo("1.5") > 0;     }     return supported;   }      ...    }

The implementation will check if the prerequisites for enabling a feature are met and automatically disable the feature if this is not the case. For example if SSL is disabled SSL client authentication will automatically be disabled as well. These checks are performed in the getters of theattributes instead of the setters for two reasons. The JBoss JMX console can call the setters in 'random' order when saving the configuration, which could lead to unwanted behaviour if the user changes multiple features at once. By moving the checks to the getters, the setters for the attributes don't need knowledge about the prerequisites of other attributes.

With these attributes added we can now change the startService() method to use these attributes.

 @Override protected void startService() throws Exception {   getLog().info("Starting JConsoleService...");   super.startService();   MBeanServer mbeanServer =      ManagementFactory.getPlatformMBeanServer();   JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://localhost:"     + getJMXConnectorServerPort() + "/jndi/rmi://localhost:"     + getRMIRegistryPort() + "/jmxrmi");   Map<String,Object> env = createJMXConnectorServerEnvironment();   startRMIRegistry(     (RMIClientSocketFactory)env.get(       RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),      (RMIServerSocketFactory)env.get(       RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE));           this.connectorServer =     JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbeanServer);   this.connectorServer.start(); }

The most interesting changes are the use of environment settings for the creation of the JMX Connector Server and the use of socket factories for the RMI registry. The environment settings contain the features that we want for our custom JMX Connector Server as wecan see in the method used to create them.

 private static final String NO_FILE = "-";  private Map<String,Object> createJMXConnectorServerEnvironment() {   Map<String,Object> env = new HashMap<String,Object>();   // set password and access file   if (!JConsoleService.NO_FILE.equals(getPasswordFile())) {     env.put("jmx.remote.x.password.file", getPasswordFile());   }   if (!JConsoleService.NO_FILE.equals(getAccessFile())) {     env.put("jmx.remote.x.access.file", getAccessFile());   }   RMIClientSocketFactory csf = null;   RMIServerSocketFactory ssf = null;   if (isSSLEnabled()) {      csf = new SslRMIClientSocketFactory();     ssf = new SslRMIServerSocketFactory(       null, null, isSSLClientAuthenticationEnabled());     env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);     env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, ssf);     if (isSSLRegistryAuthenticationEnabled()) {       env.put("com.sun.jndi.rmi.factory.socket", csf);     }   }   return env; }

The changes to the startRMIRegistry() method involve using the socket factories if SSL client authentication is enabled for theregistry and replacing the hardcoded port number with getRMIRegistryPort().

 private void startRMIRegistry(   RMIClientSocketFactory clientSocketFactory,   RMIServerSocketFactory serverSocketFactory) throws RemoteException {   this.rmiRegistry = null;   try {     // Ensure cryptographically strong random number generator used     // to choose the object number - see java.rmi.server.ObjID     System.setProperty("java.rmi.server.randomIDs", "true");     if (isSSLRegistryAuthenticationEnabled()) {       this.rmiRegistry = LocateRegistry.createRegistry(getRMIRegistryPort(),         clientSocketFactory, serverSocketFactory);      } else {        this.rmiRegistry = LocateRegistry.createRegistry(getRMIRegistryPort());      }   } catch (ExportException ex) {       // registry already exists        // => no problem, just try to use it       // => don't set rmiRegistry instance variable so we don't stop it       //    during stopService() since it may be used for other remote        //    objects       // => log a message to indicate we're using an existing registry       getLog().warn("RMIRegistry already exists: " + ex.toString());     }   }

The final thing to do is set the default configuration for the service by adding a constructor for the MBean and configuring theparameters for it in the jboss-service.xml file.

 public JConsoleService(     int rmiRegistryPort,      int jmxConnectorServerPort,     String passwordFile,      String accessFile,      boolean sslEnabled,     boolean sslClientAuthenticationEnabled,     boolean sslRegistryAuthenticationEnabled) {   super();   setRMIRegistryPort(rmiRegistryPort);   setJMXConnectorServerPort(jmxConnectorServerPort);   setPasswordFile(passwordFile);   setAccessFile(accessFile);   setSSLEnabled(sslEnabled);   setSSLClientAuthenticationEnabled(sslClientAuthenticationEnabled);   setSSLRegistryAuthenticationEnabled(sslRegistryAuthenticationEnabled); }
 <?xml version="1.0" encoding="UTF-8"?> <server>   <mbean code="com.componative.jboss.services.jconsole.JConsoleService"      name="com.componative.jboss:service=JConsoleService">     <constructor>       <!-- RMIRegistry port -->       <arg type="int" value="11199"/>       <!-- JMXConnectorServer port -->       <arg type="int" value="12199"/>       <!-- password file -->       <arg type="java.lang.String" value="-"/>       <!-- access file -->       <arg type="java.lang.String" value="-"/>       <!-- enable SSL -->       <arg type="boolean" value="false"/>       <!-- enable SSL client authentication -->       <arg type="boolean" value="false"/>       <!-- enable SSL client authentication for registry -->       <arg type="boolean" value="false"/>     </constructor>               </mbean> </server>

If we now deploy the .sar file and go to the JConsoleService in the JBoss JMX console we can see all the attributes of the MBean that wecan configure.  




  After applying a change we can scroll down and use the stop and start operations to activate the new configuration. When changingthe configuration there are a few things to keep in mind.

If we want to enable the SSL features we need to add the keystore and the truststore (for client authentication) settings in the JBoss startup script just like we did in Part 2. As a quick reminder, the system properties to use are:

  • javax.net.ssl.keyStore
  • javax.net.ssl.keyStorePassword
  • javax.net.ssl.trustStore

Changes made to the configuration will be lost after a restart of JBoss. To permanently change the configuration modify the settings in the jboss-service.xml and redeploy the .sar file. Finally it's important to note that the JMX console ignores exceptions being thrown byoperations of ServiceMBeans, so we need to check the log file to see if the service was started successfully.

Adding tunneling functionality

Using the JConsoleService we can now use the same features offered by the out-of-the-box version of the JMX Connector Service using fixedports so we can open these ports in the firewall. But what if opening ports in the firewall is not an option? The only thing left to do in thissituation is tunneling the RMI communication over a port that's already open.

The standard way to do this for RMI is to use HTTP tunneling, but this introduces a significant security loophole because the cgi script used forthis can redirect any incoming request to any port (see Resources for more information). It also requires installationof a cgi script on the webserver for which we may need the cooperation of our webserver administrator. To avoid these problems we're going touse an SSH tunnel instead.

To create the SSH tunnel we need to use local port forwarding to forward traffic from specified local ports to the remote ports used for the RMI registry and the JMX Connector Service. We can do this using the following command:

ssh -L 11199:test-srv:11199 -L 12199:test-srv:12199 user@test-srv

This will forward all traffic sent to port 11199 and 12199 on the local machine to the same ports on the remote machine named 'test-srv'. If youwant to use putty instead, use the Tunnels configuration to add the local ports that need to be forwarded, as shown below.  




After setting up the SSH tunnel we can now start JConsole and connect to localhost:11199 using Remote Process. This will createa connection to the RMI registry running on port 11199 on the remote machine via the SSH tunnel. However after some time JConsole will report that ithas failed to create a connection. The reason for this is that after accessing the RMI registry via the tunnel JConsole will try to connect to theJMX Connector Service directly through the firewall, as we can see in the logging below (192.168.1.4 is the remote machine).  



  So although we created tunnels for both ports we can only instruct JConsole to use the local port for the RMI registry. After that we have no way totell JConsole that is should also use the local port (and the tunnel) for the JMX Connector Service, leaving the second tunnel unused. Luckilywe have total control over our custom JMX Connector Service so we can add some functionality to force JConsole to use the local port when connecting to the JMX Connector Service.

The way this works is as follows. When we create the JMX Connector Service we can control the RMIClientSocketFactory and RMIServerSocketFactory that will be used, as we have done in the createJMXConnectorServerEnvironment() method when SSL is enabled.By simply creating a RMIClientSocketFactory that will always connect to local ports we can force JConsole to use the tunnel instead oftrying to go through the firewall.

 public class RMISSHClientSocketFactory         implements RMIClientSocketFactory, Serializable {   private static final long serialVersionUID = -724208021737243414L;   private RMIClientSocketFactory socketFactory = null;   public RMISSHClientSocketFactory(RMIClientSocketFactory socketFactory) {     this.socketFactory = socketFactory;   }   public Socket createSocket(String host, int port) throws IOException {     host = "localhost";     if (this.socketFactory != null) {       return this.socketFactory.createSocket(host, port);     } else {       return new Socket(host, port);     }   }   @Override   public boolean equals(Object obj) {     if (this == obj) return true;     if (obj == null) return false;     if (getClass() != obj.getClass()) return false;     final RMISSHClientSocketFactory other =        (RMISSHClientSocketFactory) obj;            if (this.socketFactory == null) {       return other.socketFactory == null;     } else {       return this.socketFactory.equals(other.socketFactory);     }   }   @Override   public int hashCode() {     final int PRIME = 31;     int result = 1;     result = PRIME * result              + ((socketFactory == null) ? 0 : socketFactory.hashCode());     return result;   } }

The RMIClientSocketFactory above can be used as a wrapper by passing another RMIClientSocketFactory during constructionwhich comes in handy when SSL is enabled. Now al we need to do is add a new attribute to the ServiceMBean so we can enable tunneling anduse the attribute in the createJMXConnectorServerEnvironment() method.

 public class JConsoleService         extends ServiceMBeanSupport implements JConsoleServiceMBean {   ...   private boolean sshTunnelEnabled = false;   ...   public JConsoleService(       int rmiRegistryPort,        int jmxConnectorServerPort,       String passwordFile,        String accessFile,        boolean sslEnabled,       boolean sslClientAuthenticationEnabled,       boolean sslRegistryAuthenticationEnabled,       boolean sshTunnelEnabled) {     super();     setRMIRegistryPort(rmiRegistryPort);     setJMXConnectorServerPort(jmxConnectorServerPort);     setPasswordFile(passwordFile);     setAccessFile(accessFile);     setSSLEnabled(sslEnabled);     setSSLClientAuthenticationEnabled(sslClientAuthenticationEnabled);     setSSLRegistryAuthenticationEnabled(sslRegistryAuthenticationEnabled);     setSSHTunnelEnabled(sshTunnelEnabled);   }   ...   public void setSSHTunnelEnabled(boolean enabled) {     this.sshTunnelEnabled = enabled;   }   public boolean isSSHTunnelEnabled() {     return this.sshTunnelEnabled;   }   ...   private Map<String,Object> createJMXConnectorServerEnvironment() {     Map<String,Object> env = new HashMap<String,Object>();     // set password and access file     if (!JConsoleService.NO_FILE.equals(getPasswordFile())) {       env.put("jmx.remote.x.password.file", getPasswordFile());     }     if (!JConsoleService.NO_FILE.equals(getAccessFile())) {       env.put("jmx.remote.x.access.file", getAccessFile());     }     RMIClientSocketFactory csf = null;     RMIServerSocketFactory ssf = null;     if (isSSLEnabled()) {        csf = new SslRMIClientSocketFactory();       ssf = new SslRMIServerSocketFactory(         null, null, isSSLClientAuthenticationEnabled());       env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);       env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, ssf);       if (isSSLRegistryAuthenticationEnabled()) {         env.put("com.sun.jndi.rmi.factory.socket", csf);       }     }     if (isSSHTunnelEnabled()) {       RMIClientSocketFactory csfWrapper = new RMISSHClientSocketFactory(csf);       env.put(         RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csfWrapper);     }     return env;   }   ...    }

Finally we need to add the new attribute to the jboss-service.xml so it will be passed to the constructor.

 <?xml version="1.0" encoding="UTF-8"?> <server>   <mbean code="com.componative.jboss.services.jconsole.JConsoleService"      name="com.componative.jboss:service=JConsoleService">     <constructor>       <!-- RMIRegistry port -->       <arg type="int" value="11199"/>       <!-- JMXConnectorServer port -->       <arg type="int" value="12199"/>       <!-- password file -->       <arg type="java.lang.String" value="-"/>       <!-- access file -->       <arg type="java.lang.String" value="-"/>       <!-- enable SSL -->       <arg type="boolean" value="false"/>       <!-- enable SSL client authentication -->       <arg type="boolean" value="false"/>       <!-- enable SSL client authentication for registry -->       <arg type="boolean" value="false"/>       <!-- enable SSH tunnel -->       <arg type="boolean" value="true"/>     </constructor>          </mbean> </server>

Because we created our own RMIClientSocketFactory we also need to add this class to the classpath of JConsole. To do this we createa separate jar containing just this one class (see Resources for downloads) and set the classpath when we startJConsole. This gives us the following command for starting JConsole including logging and keystore settings:

 jconsole -J-Djava.util.logging.config.file=logging.properties    -J-Djavax.net.ssl.trustStore=.jconsoleKeyStore    -J-Djavax.net.ssl.keyStore=.jconsoleKeyStore    -J-Djavax.net.ssl.keyStorePassword=secret    -J-Djava.class.path=../lib/jconsole.jar;../lib/tools.jar;rmi-tunnel-factory.jar

This time JConsole will use the tunnel that we've created for port 12199 when connecting to the JMX Connector Service. If we take one finallook at the logging we'll see that the logging still shows the IP number of the remote machine since the redirection to localhost isdone on a lower level. However the logging will show us that the RMISSHClientSocketFactory is being used.  



  If JConsole still fails to create a connection via the created SSH tunnels, try adding the -v option when setting up the tunnels. Thiswill log extra debug information when the tunnels are created that may shed some light on the problem. Running netstat -a to displaythe connections and listening ports may also be useful.

Having connected JConsole to JBoss via an SSH tunnel, you may try enabling other options like password authentication or SSL client authentication. Justremember that changes made via the JBoss JMX console will be lost after a restart. If you found the configuration you need, modify the jboss-service.xml in the .sar file to make the changes permanent.

This brings us to the end of our exploration of JConsole. We've seen how we can use JConsole to monitor JBoss on local and remote machines and how wecan deal with a firewall. If you want to try out the JConsoleService that we've created in this last part, you can find the downloads in the Resources section.  


Resources

  • Read theMimicking Out-of-the-Box Management Using the JMX Remote APIsection of the Java SE Monitoring and Management Guidefor more information about using the JMX Remote API.
  • For more information about implementing a JBoss Service visit theServiceMBeanSupportpage of the JBoss Wiki.
  • To learn more about developing MBeans read the Standard MBeanssection of the Java Management Extensions tutorial.
  • ReadRMI Firewall Issues formore information about HTTP tunneling for RMI.
  • To download the JConsoleService binaries and sources go to the Download section.   

原创粉丝点击