blob: 1e6e9a346a07ea3f28bb5d272fecb51f73bc6447 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.apache.camel.component.jmx;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.management.AttributeChangeNotificationFilter;
import javax.management.MBeanServerConnection;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectionNotification;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.camel.impl.DefaultConsumer;
import org.apache.camel.util.ServiceHelper;
import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Consumer will add itself as a NotificationListener on the object
* specified by the objectName param.
*/
public class JMXConsumer extends DefaultConsumer implements NotificationListener {
private static final Logger LOG = LoggerFactory.getLogger(JMXConsumer.class);
private JMXEndpoint mJmxEndpoint;
private JMXConnector mConnector;
private String mConnectionId;
/**
* Used for processing notifications (should not block notification thread)
*/
private ExecutorService executorService;
private boolean shutdownExecutorService;
/**
* Used to schedule delayed connection attempts
*/
private ScheduledExecutorService mScheduledExecutor;
/**
* Used to receive notifications about lost connections
*/
private ConnectionNotificationListener mConnectionNotificationListener;
/**
* connection to the mbean server (local or remote)
*/
private MBeanServerConnection mServerConnection;
/**
* used to format Notification objects as xml
*/
private NotificationXmlFormatter mFormatter;
public JMXConsumer(JMXEndpoint endpoint, Processor processor) {
super(endpoint, processor);
this.mJmxEndpoint = endpoint;
this.mFormatter = new NotificationXmlFormatter();
}
@Override
public JMXEndpoint getEndpoint() {
return (JMXEndpoint) super.getEndpoint();
}
/**
* Initializes the mbean server connection and starts listening for
* Notification events from the object.
*/
@Override
protected void doStart() throws Exception {
ServiceHelper.startService(mFormatter);
if (executorService == null) {
if (getEndpoint().getExecutorService() != null) {
// use shared thread-pool
executorService = getEndpoint().getExecutorService();
} else {
// lets just use a single threaded thread-pool to process these notifications
String name = "JMXConsumer[" + getEndpoint().getJMXObjectName().getCanonicalName() + "]";
executorService = getEndpoint().getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this, name);
shutdownExecutorService = true;
}
}
// connect to the mbean server
if (mJmxEndpoint.isPlatformServer()) {
setServerConnection(ManagementFactory.getPlatformMBeanServer());
} else {
try {
initNetworkConnection();
} catch (IOException e) {
if (!mJmxEndpoint.getTestConnectionOnStartup()) {
LOG.warn("Failed to connect to JMX server. >> {}", e.getMessage());
scheduleDelayedStart();
return;
} else {
throw e;
}
}
}
// subscribe
addNotificationListener();
super.doStart();
}
/**
* Initializes a network connection to the configured JMX server and registers a connection
* notification listener to to receive notifications of connection loss
*/
private void initNetworkConnection() throws IOException {
if (mConnector != null) {
try {
mConnector.close();
} catch (Exception e) {
// ignore, as this is best effort
}
}
JMXServiceURL url = new JMXServiceURL(mJmxEndpoint.getServerURL());
String[] creds = {mJmxEndpoint.getUser(), mJmxEndpoint.getPassword()};
Map<String, String[]> map = Collections.singletonMap(JMXConnector.CREDENTIALS, creds);
mConnector = JMXConnectorFactory.connect(url, map);
mConnector.addConnectionNotificationListener(getConnectionNotificationListener(), null, null);
mConnectionId = mConnector.getConnectionId();
setServerConnection(mConnector.getMBeanServerConnection());
}
/**
* Returns the connection notification listener; creates the default listener if one does not
* already exist
*/
protected ConnectionNotificationListener getConnectionNotificationListener() {
if (mConnectionNotificationListener == null) {
mConnectionNotificationListener = new ConnectionNotificationListener();
}
return mConnectionNotificationListener;
}
/**
* Schedules execution of the doStart() operation to occur again after the reconnect delay
*/
protected void scheduleDelayedStart() throws Exception {
Runnable startRunnable = new Runnable() {
@Override
public void run() {
try {
doStart();
} catch (Exception e) {
LOG.error("An unrecoverable exception has occurred while starting the JMX consumer"
+ "for endpoint {}", URISupport.sanitizeUri(mJmxEndpoint.getEndpointUri()), e);
}
}
};
LOG.info("Delaying JMX consumer startup for endpoint {}. Trying again in {} seconds.",
URISupport.sanitizeUri(mJmxEndpoint.getEndpointUri()), mJmxEndpoint.getReconnectDelay());
getExecutor().schedule(startRunnable, mJmxEndpoint.getReconnectDelay(), TimeUnit.SECONDS);
}
/**
* Helper class used for receiving connection loss notifications
*/
private class ConnectionNotificationListener implements NotificationListener {
@Override
public void handleNotification(Notification notification, Object handback) {
JMXConnectionNotification connectionNotification = (JMXConnectionNotification)notification;
// only reset the connection if the notification is for the connection from this endpoint
if (!connectionNotification.getConnectionId().equals(mConnectionId)) {
return;
}
if (connectionNotification.getType().equals(JMXConnectionNotification.NOTIFS_LOST)
|| connectionNotification.getType().equals(JMXConnectionNotification.CLOSED)
|| connectionNotification.getType().equals(JMXConnectionNotification.FAILED)) {
LOG.warn("Lost JMX connection for : {}", URISupport.sanitizeUri(mJmxEndpoint.getEndpointUri()));
if (mJmxEndpoint.getReconnectOnConnectionFailure()) {
scheduleReconnect();
} else {
LOG.warn("The JMX consumer will not be reconnected. Use 'reconnectOnConnectionFailure' to "
+ "enable reconnections.");
}
}
}
}
/**
* Schedules an attempt to re-initialize a lost connection after the reconnect delay
*/
protected void scheduleReconnect() {
Runnable startRunnable = new Runnable() {
@Override
public void run() {
try {
initNetworkConnection();
addNotificationListener();
} catch (Exception e) {
LOG.warn("Failed to reconnect to JMX server. >> {}", e.getMessage());
scheduleReconnect();
}
}
};
LOG.info("Delaying JMX consumer reconnection for endpoint {}. Trying again in {} seconds.",
URISupport.sanitizeUri(mJmxEndpoint.getEndpointUri()), mJmxEndpoint.getReconnectDelay());
getExecutor().schedule(startRunnable, mJmxEndpoint.getReconnectDelay(), TimeUnit.SECONDS);
}
/**
* Returns the thread executor used for scheduling delayed connection events. Creates the executor
* if it does not already exist
*/
private ScheduledExecutorService getExecutor() {
if (this.mScheduledExecutor == null) {
mScheduledExecutor = mJmxEndpoint.getCamelContext().getExecutorServiceManager()
.newSingleThreadScheduledExecutor(this, "JMXConnectionExecutor");
}
return mScheduledExecutor;
}
/**
* Adds a notification listener to the target bean.
*/
protected void addNotificationListener() throws Exception {
JMXEndpoint ep = getEndpoint();
NotificationFilter nf = ep.getNotificationFilter();
// if we should observe a single attribute then use filter
if (nf == null && ep.getObservedAttribute() != null) {
LOG.debug("Observing attribute: {}", ep.getObservedAttribute());
boolean match = !ep.isNotifyDiffer();
nf = new JMXConsumerNotificationFilter(ep.getObservedAttribute(), ep.getStringToCompare(), match);
}
ObjectName objectName = ep.getJMXObjectName();
getServerConnection().addNotificationListener(objectName, this, nf, ep.getHandback());
}
/**
* Removes the notification listeners and terminates the background connection polling process if it exists
*/
@Override
protected void doStop() throws Exception {
super.doStop();
if (mScheduledExecutor != null) {
getEndpoint().getCamelContext().getExecutorServiceManager().shutdownNow(mScheduledExecutor);
mScheduledExecutor = null;
}
removeNotificationListeners();
if (mConnector != null) {
mConnector.close();
}
ServiceHelper.stopService(mFormatter);
if (shutdownExecutorService && executorService != null) {
getEndpoint().getCamelContext().getExecutorServiceManager().shutdownNow(executorService);
executorService = null;
}
}
/**
* Removes the configured notification listener and the connection notification listener from the
* connection
*/
protected void removeNotificationListeners() throws Exception {
getServerConnection().removeNotificationListener(mJmxEndpoint.getJMXObjectName(), this);
if (mConnectionNotificationListener != null) {
mConnector.removeConnectionNotificationListener(mConnectionNotificationListener);
mConnectionNotificationListener = null;
}
}
protected MBeanServerConnection getServerConnection() {
return mServerConnection;
}
protected void setServerConnection(MBeanServerConnection aServerConnection) {
mServerConnection = aServerConnection;
}
/**
* Processes the Notification received. The handback will be set as
* the header "jmx.handback" while the Notification will be set as
* the body.
* <p/>
* If the format is set to "xml" then the Notification will be converted
* to XML first using {@link NotificationXmlFormatter}
*
* @see javax.management.NotificationListener#handleNotification(javax.management.Notification, java.lang.Object)
*/
public void handleNotification(Notification aNotification, Object aHandback) {
JMXEndpoint ep = getEndpoint();
Exchange exchange = getEndpoint().createExchange();
Message message = exchange.getIn();
message.setHeader("jmx.handback", aHandback);
try {
if (ep.isXML()) {
message.setBody(getFormatter().format(aNotification));
} else {
message.setBody(aNotification);
}
// process the notification from thred pool to not block this notification callback thread from the JVM
executorService.submit(() -> {
try {
getProcessor().process(exchange);
} catch (Exception e) {
getExceptionHandler().handleException("Failed to process notification", e);
}
});
} catch (NotificationFormatException e) {
getExceptionHandler().handleException("Failed to marshal notification", e);
}
}
protected NotificationXmlFormatter getFormatter() {
return mFormatter;
}
}