/*
* Copyright 2002-2008 the original author or authors.
*
* Licensed 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 fr.xebia.management;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.servlet.ServletContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jmx.support.JmxUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.ServletContextAware;
/**
* <p>
* Instantiate {@link MBeanServer} which add a "path" property with value
* {@link ServletContext#getContextPath()} to each {@link ObjectName} passed as
* method parameter. The goal is to prevent collisions between MBeans declared
* in different web applications.
* </p>
* <p>
* On Tomcat, an extra property named <code>"host"</code> with the name of the
* Tomcat host in which the webapp is declared is added (value
* <code>"org.apache.catalina.core.ApplicationContextFacade#context#context#hostName"</code>
* ).
* </p>
* <p>
* Sample : EHCache's {@link net.sf.ehcache.management.ManagementService} will
* register Hibernate's {@linkplain org.hibernate.cache.StandardQueryCache} as
* <code>"net.sf.ehcache:CacheManager=my-cachemanager,name=org.hibernate.cache.StandardQueryCache,type=CacheStatistics"</code>
* that could collide with other applications and this MBeanServer will add the
* <code>"path"</code> attribute to prevent problems :
* <code>"net.sf.ehcache:CacheManager=my-cachemanager,name=org.hibernate.cache.StandardQueryCache,
* type=CacheStatistics,path=/my-application</code> " .
* </p>
* <p>
* Implementation decisions:
* </p>
* <ul>
* <li>The added property was named <code>"path"</code> to follow Tomcat JMX
* beans naming convention,</li>
* <li>This {@link FactoryBean} doesn't extend
* {@link org.springframework.beans.factory.config.AbstractFactoryBean} due to
* <a href="http://jira.springframework.org/browse/SPR-4968">SPR-4968 : Error
* "Singleton instance not initialized yet" triggered by toString call in case
* of circular references</a></li>
* </ul>
* <hr/>
* <p>
* <strong>Configuration sample:</strong>
* </p>
*
* <pre>
* <code>
* <beans ... >
* <context:mbean-server id="rawMbeanServer" />
* <bean id="mbeanServer" class="fr.xebia.management.ServletContextAwareMBeanServerFactory">
* <property name="mbeanServer" ref="rawMbeanServer" />
* </bean>
* <context:mbean-export server="mbeanServer" />
* ...
* </beans>
* </code>
* </pre>
*
* <p>
* An object name
* <code>"net.sf.ehcache:CacheManager=my-cache-manager,name=my-cache,type=Cache</code>
* " will be registered as
* <code>"net.sf.ehcache:CacheManager=my-cache-manager,name=my-cache,type=Cache,host=localhost,path=/my-application"</code>
* for an application "my-application" declared in the "localhost" host of a
* Tomcat server: attributes <code>"path=/my-application"</code> and
* <code>"host=localhost"</code> are added to the object name.
* </p>
* <p>
* <strong>Advanced configuration sample:</strong>
* </p>
* <p>
* The <code>"objectNameExtraAttributes"</code> property allows to manually add
* extra attributes in addition to the <code>"path"</code> (and
* <code>"host"</code>) attribute that is automatically added.
* </p>
*
* <pre>
* <code>
* <beans ... >
* <context:mbean-server id="rawMbeanServer" />
* <bean id="mbeanServer" class="fr.xebia.management.ServletContextAwareMBeanServerFactory">
* <property name="mbeanServer" ref="rawMbeanServer" />
* <property name="objectNameExtraAttributes" >
* <map>
* <entry key="ze-app-id-asked-by-ze-monitoring-team" value="my-application-id" />
* </map>
* </property>
* </bean>
* <context:mbean-export server="mbeanServer" />
* ...
* </beans>
* </code>
* </pre>
*
* <p>
* An object name
* <code>"net.sf.ehcache:CacheManager=my-cache-manager,name=my-cache,type=Cache"</code>
* will be registered as
* <code>"net.sf.ehcache:CacheManager=my-cache-manager,ze-app-id-asked-by-ze-monitoring-team=my-application-id,
* name=my-cache,type=Cache,host=localhost,path=/my-application"</code>
* for an application "my-application" declared in the "localhost" host of a
* Tomcat server: attributes <code>"path=/my-application"</code>,
* <code>"host=localhost"</code>and
* <code>"ze-app-id-asked-by-ze-monitoring-team=my-application-id"</code> are
* added to the object name.
* </p>
*
* @author <a href="mailto:cyrille@cyrilleleclerc.com">Cyrille Le Clerc</a>
*/
public class ServletContextAwareMBeanServerFactory implements FactoryBean<MBeanServer>, ServletContextAware, InitializingBean {
protected MBeanServer instance;
protected final Logger logger = LoggerFactory.getLogger(ServletContextAwareMBeanServerFactory.class);
protected Map<String, String> objectNameExtraAttributes = new HashMap<String, String>();
protected MBeanServer server;
protected ServletContext servletContext;
public void afterPropertiesSet() throws Exception {
if (this.server == null) {
this.server = JmxUtils.locateMBeanServer();
}
Assert.notNull(this.servletContext, "servletContext can NOT be null");
objectNameExtraAttributes.put("path", servletContext.getContextPath());
if ("org.apache.catalina.core.ApplicationContextFacade".equals(servletContext.getClass().getName())) {
Field applicationContextFacadeContextField = Class.forName("org.apache.catalina.core.ApplicationContextFacade")
.getDeclaredField("context");
applicationContextFacadeContextField.setAccessible(true);
Field applicationContextContextField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("context");
applicationContextContextField.setAccessible(true);
Field standardContextHostNameField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("hostName");
standardContextHostNameField.setAccessible(true);
Object applicationContext = applicationContextFacadeContextField.get(servletContext);
Object standardContext = applicationContextContextField.get(applicationContext);
String hostName = (String) standardContextHostNameField.get(standardContext);
objectNameExtraAttributes.put("host", hostName);
}
logger.trace("Extra objectname attributes : {}" + this.objectNameExtraAttributes);
}
public MBeanServer getObject() throws Exception {
if (instance == null) {
InvocationHandler invocationHandler = new InvocationHandler() {
/**
* <p>
* Copy the given <code>objectName</code> adding the extra
* attributes.
* </p>
*/
protected ObjectName addExtraAttributesToObjectName(ObjectName objectName) throws MalformedObjectNameException {
Hashtable<String, String> table = new Hashtable<String, String>(objectName.getKeyPropertyList());
table.putAll(objectNameExtraAttributes);
ObjectName result = ObjectName.getInstance(objectName.getDomain(), table);
logger.trace("addExtraAttributesToObjectName({}): {}", objectName, result);
return result;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object[] modifiedArgs = args.clone();
for (int i = 0; i < modifiedArgs.length; i++) {
Object arg = modifiedArgs[i];
if (arg instanceof ObjectName) {
ObjectName objectName = (ObjectName) arg;
modifiedArgs[i] = addExtraAttributesToObjectName(objectName);
}
}
if (logger.isDebugEnabled()) {
logger.debug(method + " : " + Arrays.asList(modifiedArgs));
}
try {
return method.invoke(server, modifiedArgs);
} catch (InvocationTargetException ite) {
throw ite.getCause();
}
}
};
instance = (MBeanServer) Proxy.newProxyInstance(ClassUtils.getDefaultClassLoader(), new Class[] { MBeanServer.class },
invocationHandler);
}
return instance;
}
public Class<? extends MBeanServer> getObjectType() {
return MBeanServer.class;
}
public boolean isSingleton() {
return true;
}
/**
* Deprecated to match
* {@link org.springframework.jmx.export.MBeanExporter#setServer(MBeanServer)}
* .
*
* @deprecated Use {@link #setServer(MBeanServer)}.
*/
@Deprecated
public void setMbeanServer(MBeanServer mbeanServer) {
this.server = mbeanServer;
}
public void setObjectNameExtraAttributes(Map<String, String> objectNameExtraAttributes) {
this.objectNameExtraAttributes = objectNameExtraAttributes;
}
public void setServer(MBeanServer server) {
this.server = server;
}
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
}