/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the License at the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apereo.portal.jmx;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.HashMap;
import java.util.Map;
import javax.management.MBeanServer;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;
import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
/**
* A bean to make starting and stopping a JMX server easier. <br>
* <br>
* The only required property is {@link #setPortOne(int)}, if portTwo is not set is will be
* calculated as portOne + 1. faileOnException is set to false buy default. <br>
* <br>
* The bean heeds the following system properties:
*
* <table>
* <tr>
* <th>Property</th>
* <th>Function</th>
* </tr>
* <tr>
* <td>com.sun.management.jmxremote</td>
* <td>Having this property set (value doesn't matter) enables starting of the JMX Server</td>
* </tr>
* <tr>
* <td>com.sun.management.jmxremote.ssl</td>
* <td>Enables SSL based connections</td>
* </tr>
* <tr>
* <td>com.sun.management.jmxremote.password.file</td>
* <td>The password file that contains usernames and passwords for connecting to the server</td>
* </tr>
* <tr>
* <td>com.sun.management.jmxremote.access.file</td>
* <td>The password file that contains ACLs for users connecting to the server</td>
* </tr>
* </table>
*
* <br>
*
* <p><b>IMPORTNANT NOTE</b> Using this bean starts an RMI server. If this bean is used in a JVM
* that is long running and has any sort of generational garbage collector configured the system
* properties sun.rmi.dgc.client.gcInterval and sun.rmi.dgc.server.gcInterval need to be set to some
* high value. They configure how often the RMI server requests a full GC. To disable the RMI
* induced GCs set the properties to '0x7ffffffffffffffe' Only do this if you can rely on the
* generational GC to reguarly clean up RMI objects.
*/
public class JavaManagementServerBean {
//System properties
private static final String JMX_ENABLED_PROPERTY = "com.sun.management.jmxremote";
private static final String JMX_SSL_PROPERTY = "com.sun.management.jmxremote.ssl";
private static final String JMX_PASSWORD_FILE_PROPERTY =
"com.sun.management.jmxremote.password.file";
private static final String JMX_ACCESS_FILE_PROPERTY =
"com.sun.management.jmxremote.access.file";
//JMX properties
private static final String JMX_REMOTE_X_ACCESS_FILE = "jmx.remote.x.access.file";
private static final String JMX_REMOTE_X_PASSWORD_FILE = "jmx.remote.x.password.file";
protected final Log logger = LogFactory.getLog(this.getClass());
private String host = null;
private int portOne = -1;
private int portTwo = -1;
private boolean failOnException = false;
private JMXConnectorServer jmxConnectorServer;
public boolean isFailOnException() {
return this.failOnException;
}
/** If an exception should be thrown if setting up the JMX server fails */
public void setFailOnException(boolean failOnException) {
this.failOnException = failOnException;
}
public int getPortTwo() {
return this.portTwo;
}
/** Second port in the JMX connection string */
public void setPortTwo(int portTwo) {
this.portTwo = portTwo;
}
public int getPortOne() {
return portOne;
}
/** First port in the JMX connection string */
@Required
public void setPortOne(int portOne) {
Validate.isTrue(portOne > 0, "portOne must be greater than 0");
this.portOne = portOne;
}
public String getHost() {
return host;
}
/** The host to listen on */
public void setHost(String host) {
this.host = host;
}
/** Starts the RMI server and JMX connector server */
public void startServer() {
if (!System.getProperties().containsKey(JMX_ENABLED_PROPERTY)) {
this.logger.info(
"System Property '"
+ JMX_ENABLED_PROPERTY
+ "' is not set, skipping initialization.");
return;
}
try {
//Get the base rmi port
final int portOne = this.getPortOne();
//Get the second rmi port or calculate it
final int portTwo = this.calculatePortTwo(portOne);
//Create the RMI registry on the base port
try {
LocateRegistry.createRegistry(portOne);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Started RMI Registry on port " + portOne);
}
} catch (RemoteException re) {
throw new IllegalStateException(
"Could not create RMI Registry on port " + portOne, re);
}
//Generate the JMX Service URL
final JMXServiceURL jmxServiceUrl = this.getServiceUrl(portOne, portTwo);
//Map for the JMX environment configuration
final Map<String, Object> jmxEnv = this.getJmxServerEnvironment();
//Create the MBean Server
final MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
//Create the JMX Connector
try {
this.jmxConnectorServer =
JMXConnectorServerFactory.newJMXConnectorServer(
jmxServiceUrl, jmxEnv, mbeanServer);
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Created JMXConnectorServer for JMXServiceURL='"
+ jmxServiceUrl
+ "', jmxEnv='"
+ jmxEnv
+ "' MBeanServer='"
+ mbeanServer
+ "'");
}
} catch (IOException ioe) {
throw new IllegalStateException(
"Failed to create a new JMXConnectorServer for JMXServiceURL='"
+ jmxServiceUrl
+ "', jmxEnv='"
+ jmxEnv
+ "' MBeanServer='"
+ mbeanServer
+ "'",
ioe);
}
//Start the JMX Connector
try {
this.jmxConnectorServer.start();
this.logger.info(
"Started JMXConnectorServer. Listening on '" + jmxServiceUrl + "'");
} catch (IOException ioe) {
throw new IllegalStateException("Failed to start the JMXConnectorServer", ioe);
}
} catch (RuntimeException re) {
if (this.failOnException) {
throw re;
}
this.logger.error("Failed to initialize the JMX Server", re);
}
}
/** Stops the JMX connector server and RMI server */
public void stopServer() {
if (this.jmxConnectorServer == null) {
this.logger.info("No JMXConnectorServer to stop");
return;
}
try {
try {
this.jmxConnectorServer.stop();
this.logger.info("Stopped JMXConnectorServer");
} catch (IOException ioe) {
throw new IllegalStateException("Failed to stop the JMXConnectorServer", ioe);
}
this.jmxConnectorServer = null;
} catch (RuntimeException re) {
if (this.failOnException) {
throw re;
}
this.logger.error("Failed to shutdown the JMX Server", re);
}
}
/**
* Get the second rmi port from the init parameters or calculate it
*
* @param portOne Base port to calculate the second port from if needed.
* @return The second port
*/
protected int calculatePortTwo(final int portOne) {
int portTwo = this.portTwo;
if (portTwo <= 0) {
portTwo = portOne + 1;
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Using " + portTwo + " for portTwo.");
}
return portTwo;
}
/**
* Generates the JMXServiceURL for the two specified ports.
*
* @return A JMXServiceURL for this host using the two specified ports.
* @throws IllegalStateException If localhost cannot be resolved or if the JMXServiceURL is
* malformed.
*/
protected JMXServiceURL getServiceUrl(final int portOne, int portTwo) {
final String jmxHost;
if (this.host == null) {
final InetAddress inetHost;
try {
inetHost = InetAddress.getLocalHost();
} catch (UnknownHostException uhe) {
throw new IllegalStateException("Cannot resolve localhost InetAddress.", uhe);
}
jmxHost = inetHost.getHostName();
} else {
jmxHost = this.host;
}
final String jmxUrl =
"service:jmx:rmi://"
+ jmxHost
+ ":"
+ portTwo
+ "/jndi/rmi://"
+ jmxHost
+ ":"
+ portOne
+ "/server";
final JMXServiceURL jmxServiceUrl;
try {
jmxServiceUrl = new JMXServiceURL(jmxUrl);
} catch (MalformedURLException mue) {
throw new IllegalStateException(
"Failed to create JMXServiceURL for url String '" + jmxUrl + "'", mue);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Generated JMXServiceURL='" + jmxServiceUrl + "' from String " + jmxUrl + "'.");
}
return jmxServiceUrl;
}
/**
* Generates the environment Map for the JMX server based on system properties
*
* @return A non-null Map of environment settings for the JMX server.
*/
protected Map<String, Object> getJmxServerEnvironment() {
final Map<String, Object> jmxEnv = new HashMap<String, Object>();
//SSL Options
final String enableSSL = System.getProperty(JMX_SSL_PROPERTY);
if (Boolean.getBoolean(enableSSL)) {
SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
jmxEnv.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory();
jmxEnv.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, ssf);
}
//Password file options
final String passwordFile = System.getProperty(JMX_PASSWORD_FILE_PROPERTY);
if (passwordFile != null) {
jmxEnv.put(JMX_REMOTE_X_PASSWORD_FILE, passwordFile);
}
//Access file options
final String accessFile = System.getProperty(JMX_ACCESS_FILE_PROPERTY);
if (accessFile != null) {
jmxEnv.put(JMX_REMOTE_X_ACCESS_FILE, accessFile);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Configured JMX Server Environment = '" + jmxEnv + "'");
}
return jmxEnv;
}
}