001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.jmx.access;
018:
019: import java.beans.PropertyDescriptor;
020: import java.io.IOException;
021: import java.lang.reflect.Method;
022: import java.net.MalformedURLException;
023: import java.util.Arrays;
024: import java.util.HashMap;
025: import java.util.Map;
026:
027: import javax.management.Attribute;
028: import javax.management.InstanceNotFoundException;
029: import javax.management.IntrospectionException;
030: import javax.management.JMException;
031: import javax.management.JMX;
032: import javax.management.MBeanAttributeInfo;
033: import javax.management.MBeanException;
034: import javax.management.MBeanInfo;
035: import javax.management.MBeanOperationInfo;
036: import javax.management.MBeanServer;
037: import javax.management.MBeanServerConnection;
038: import javax.management.MBeanServerInvocationHandler;
039: import javax.management.MalformedObjectNameException;
040: import javax.management.ObjectName;
041: import javax.management.OperationsException;
042: import javax.management.ReflectionException;
043: import javax.management.RuntimeErrorException;
044: import javax.management.RuntimeMBeanException;
045: import javax.management.RuntimeOperationsException;
046: import javax.management.openmbean.CompositeData;
047: import javax.management.openmbean.TabularData;
048: import javax.management.remote.JMXConnector;
049: import javax.management.remote.JMXConnectorFactory;
050: import javax.management.remote.JMXServiceURL;
051:
052: import org.aopalliance.intercept.MethodInterceptor;
053: import org.aopalliance.intercept.MethodInvocation;
054: import org.apache.commons.logging.Log;
055: import org.apache.commons.logging.LogFactory;
056:
057: import org.springframework.beans.BeanUtils;
058: import org.springframework.beans.factory.BeanClassLoaderAware;
059: import org.springframework.beans.factory.DisposableBean;
060: import org.springframework.beans.factory.InitializingBean;
061: import org.springframework.core.JdkVersion;
062: import org.springframework.jmx.MBeanServerNotFoundException;
063: import org.springframework.jmx.support.JmxUtils;
064: import org.springframework.jmx.support.ObjectNameManager;
065: import org.springframework.util.ClassUtils;
066: import org.springframework.util.ReflectionUtils;
067:
068: /**
069: * {@link org.aopalliance.intercept.MethodInterceptor} that routes calls to an
070: * MBean running on the supplied <code>MBeanServerConnection</code>.
071: * Works for both local and remote <code>MBeanServerConnection</code>s.
072: *
073: * <p>By default, the <code>MBeanClientInterceptor</code> will connect to the
074: * <code>MBeanServer</code> and cache MBean metadata at startup. This can
075: * be undesirable when running against a remote <code>MBeanServer</code>
076: * that may not be running when the application starts. Through setting the
077: * {@link #setConnectOnStartup(boolean) connectOnStartup} property to "false",
078: * you can defer this process until the first invocation against the proxy.
079: *
080: * <p>Requires JMX 1.2's <code>MBeanServerConnection</code> feature.
081: * As a consequence, this class will not work on JMX 1.0.
082: *
083: * <p>This functionality is usually used through {@link MBeanProxyFactoryBean}.
084: * See the javadoc of that class for more information.
085: *
086: * @author Rob Harrop
087: * @author Juergen Hoeller
088: * @since 1.2
089: * @see MBeanProxyFactoryBean
090: * @see #setConnectOnStartup
091: */
092: public class MBeanClientInterceptor implements MethodInterceptor,
093: BeanClassLoaderAware, InitializingBean, DisposableBean {
094:
095: /** Logger available to subclasses */
096: protected final Log logger = LogFactory.getLog(getClass());
097:
098: private MBeanServerConnection server;
099:
100: private JMXServiceURL serviceUrl;
101:
102: private String agentId;
103:
104: private boolean connectOnStartup = true;
105:
106: private ObjectName objectName;
107:
108: private boolean useStrictCasing = true;
109:
110: private Class managementInterface;
111:
112: private ClassLoader beanClassLoader = ClassUtils
113: .getDefaultClassLoader();
114:
115: private JMXConnector connector;
116:
117: private MBeanServerInvocationHandler invocationHandler;
118:
119: private Map allowedAttributes;
120:
121: private Map allowedOperations;
122:
123: private final Map signatureCache = new HashMap();
124:
125: private final Object preparationMonitor = new Object();
126:
127: /**
128: * Set the <code>MBeanServerConnection</code> used to connect to the
129: * MBean which all invocations are routed to.
130: */
131: public void setServer(MBeanServerConnection server) {
132: this .server = server;
133: }
134:
135: /**
136: * Set the service URL of the remote <code>MBeanServer</code>.
137: */
138: public void setServiceUrl(String url) throws MalformedURLException {
139: this .serviceUrl = new JMXServiceURL(url);
140: }
141:
142: /**
143: * Set the agent id of the <code>MBeanServer</code> to locate.
144: * <p>Default is none. If specified, this will result in an
145: * attempt being made to locate the attendant MBeanServer, unless
146: * the {@link #setServiceUrl "serviceUrl"} property has been set.
147: * @see javax.management.MBeanServerFactory#findMBeanServer(String)
148: */
149: public void setAgentId(String agentId) {
150: this .agentId = agentId;
151: }
152:
153: /**
154: * Set whether or not the proxy should connect to the <code>MBeanServer</code>
155: * at creation time ("true") or the first time it is invoked ("false").
156: * Default is "true".
157: */
158: public void setConnectOnStartup(boolean connectOnStartup) {
159: this .connectOnStartup = connectOnStartup;
160: }
161:
162: /**
163: * Set the <code>ObjectName</code> of the MBean which calls are routed to,
164: * as <code>ObjectName</code> instance or as <code>String</code>.
165: */
166: public void setObjectName(Object objectName)
167: throws MalformedObjectNameException {
168: this .objectName = ObjectNameManager.getInstance(objectName);
169: }
170:
171: /**
172: * Set whether to use strict casing for attributes. Enabled by default.
173: * <p>When using strict casing, a JavaBean property with a getter such as
174: * <code>getFoo()</code> translates to an attribute called <code>Foo</code>.
175: * With strict casing disabled, <code>getFoo()</code> would translate to just
176: * <code>foo</code>.
177: */
178: public void setUseStrictCasing(boolean useStrictCasing) {
179: this .useStrictCasing = useStrictCasing;
180: }
181:
182: /**
183: * Set the management interface of the target MBean, exposing bean property
184: * setters and getters for MBean attributes and conventional Java methods
185: * for MBean operations.
186: */
187: public void setManagementInterface(Class managementInterface) {
188: this .managementInterface = managementInterface;
189: }
190:
191: /**
192: * Return the management interface of the target MBean,
193: * or <code>null</code> if none specified.
194: */
195: protected final Class getManagementInterface() {
196: return this .managementInterface;
197: }
198:
199: public void setBeanClassLoader(ClassLoader beanClassLoader) {
200: this .beanClassLoader = beanClassLoader;
201: }
202:
203: /**
204: * Prepares the <code>MBeanServerConnection</code> if the "connectOnStartup"
205: * is turned on (which it is by default).
206: */
207: public void afterPropertiesSet() {
208: if (this .connectOnStartup) {
209: prepare();
210: }
211: }
212:
213: /**
214: * Ensures that an <code>MBeanServerConnection</code> is configured and attempts
215: * to detect a local connection if one is not supplied.
216: */
217: public void prepare() {
218: synchronized (this .preparationMonitor) {
219: if (this .server == null) {
220: connect();
221: }
222: this .invocationHandler = null;
223: if (this .useStrictCasing) {
224: // Use the JDK's own MBeanServerInvocationHandler,
225: // in particular for native MXBean support on Java 6.
226: if (JdkVersion.isAtLeastJava16()) {
227: this .invocationHandler = new MBeanServerInvocationHandler(
228: this .server,
229: this .objectName,
230: (this .managementInterface != null && JMX
231: .isMXBeanInterface(this .managementInterface)));
232: } else {
233: this .invocationHandler = new MBeanServerInvocationHandler(
234: this .server, this .objectName);
235: }
236: } else {
237: // Non-strict asing can only be achieved through custom
238: // invocation handling. Only partial MXBean support available!
239: retrieveMBeanInfo();
240: }
241: }
242: }
243:
244: /**
245: * Connects to the remote <code>MBeanServer</code> using the configured <code>JMXServiceURL</code>.
246: * @see #setServiceUrl(String)
247: * @see #setConnectOnStartup(boolean)
248: */
249: private void connect() throws MBeanServerNotFoundException {
250: if (this .serviceUrl != null) {
251: if (logger.isDebugEnabled()) {
252: logger
253: .debug("Connecting to remote MBeanServer at URL ["
254: + this .serviceUrl + "]");
255: }
256: try {
257: this .connector = JMXConnectorFactory
258: .connect(this .serviceUrl);
259: this .server = this .connector.getMBeanServerConnection();
260: } catch (IOException ex) {
261: throw new MBeanServerNotFoundException(
262: "Could not connect to remote MBeanServer at URL ["
263: + this .serviceUrl + "]", ex);
264: }
265: } else {
266: logger.debug("Attempting to locate local MBeanServer");
267: this .server = locateMBeanServer(this .agentId);
268: }
269: }
270:
271: /**
272: * Attempt to locate an existing <code>MBeanServer</code>.
273: * Called if no {@link #setServiceUrl "serviceUrl"} was specified.
274: * <p>The default implementation attempts to find an <code>MBeanServer</code> using
275: * a standard lookup. Subclasses may override to add additional location logic.
276: * @param agentId the agent identifier of the MBeanServer to retrieve.
277: * If this parameter is <code>null</code>, all registered MBeanServers are
278: * considered.
279: * @return the <code>MBeanServer</code> if found
280: * @throws org.springframework.jmx.MBeanServerNotFoundException
281: * if no <code>MBeanServer</code> could be found
282: * @see JmxUtils#locateMBeanServer(String)
283: * @see javax.management.MBeanServerFactory#findMBeanServer(String)
284: */
285: protected MBeanServer locateMBeanServer(String agentId)
286: throws MBeanServerNotFoundException {
287: return JmxUtils.locateMBeanServer(agentId);
288: }
289:
290: /**
291: * Loads the management interface info for the configured MBean into the caches.
292: * This information is used by the proxy when determining whether an invocation matches
293: * a valid operation or attribute on the management interface of the managed resource.
294: */
295: private void retrieveMBeanInfo() throws MBeanInfoRetrievalException {
296: try {
297: MBeanInfo info = this .server.getMBeanInfo(this .objectName);
298:
299: // get attributes
300: MBeanAttributeInfo[] attributeInfo = info.getAttributes();
301: this .allowedAttributes = new HashMap(attributeInfo.length);
302:
303: for (int x = 0; x < attributeInfo.length; x++) {
304: this .allowedAttributes.put(attributeInfo[x].getName(),
305: attributeInfo[x]);
306: }
307:
308: // get operations
309: MBeanOperationInfo[] operationInfo = info.getOperations();
310: this .allowedOperations = new HashMap(operationInfo.length);
311:
312: for (int x = 0; x < operationInfo.length; x++) {
313: MBeanOperationInfo opInfo = operationInfo[x];
314: Class[] paramTypes = JmxUtils.parameterInfoToTypes(
315: opInfo.getSignature(), this .beanClassLoader);
316: this .allowedOperations.put(new MethodCacheKey(opInfo
317: .getName(), paramTypes), opInfo);
318: }
319: } catch (ClassNotFoundException ex) {
320: throw new MBeanInfoRetrievalException(
321: "Unable to locate class specified in method signature",
322: ex);
323: } catch (IntrospectionException ex) {
324: throw new MBeanInfoRetrievalException(
325: "Unable to obtain MBean info for bean ["
326: + this .objectName + "]", ex);
327: } catch (InstanceNotFoundException ex) {
328: // if we are this far this shouldn't happen, but...
329: throw new MBeanInfoRetrievalException(
330: "Unable to obtain MBean info for bean ["
331: + this .objectName
332: + "]: it is likely that this bean was unregistered during the proxy creation process",
333: ex);
334: } catch (ReflectionException ex) {
335: throw new MBeanInfoRetrievalException(
336: "Unable to read MBean info for bean [ "
337: + this .objectName + "]", ex);
338: } catch (IOException ex) {
339: throw new MBeanInfoRetrievalException(
340: "An IOException occurred when communicating with the MBeanServer. "
341: + "It is likely that you are communicating with a remote MBeanServer. "
342: + "Check the inner exception for exact details.",
343: ex);
344: }
345: }
346:
347: /**
348: * Return whether this client interceptor has already been prepared,
349: * i.e. has already looked up the server and cached all metadata.
350: */
351: protected boolean isPrepared() {
352: synchronized (this .preparationMonitor) {
353: return (this .invocationHandler != null || this .allowedAttributes != null);
354: }
355: }
356:
357: /**
358: * Route the invocation to the configured managed resource. Correctly routes JavaBean property
359: * access to <code>MBeanServerConnection.get/setAttribute</code> and method invocation to
360: * <code>MBeanServerConnection.invoke</code>. Any attempt to invoke a method that does not
361: * correspond to an attribute or operation defined in the management interface of the managed
362: * resource results in an <code>InvalidInvocationException</code>.
363: * @param invocation the <code>MethodInvocation</code> to re-route.
364: * @return the value returned as a result of the re-routed invocation.
365: * @throws InvocationFailureException if the invocation does not match an attribute or
366: * operation on the management interface of the resource.
367: * @throws Throwable typically as the result of an error during invocation
368: */
369: public Object invoke(MethodInvocation invocation) throws Throwable {
370: // Lazily connect to MBeanServer if necessary.
371: synchronized (this .preparationMonitor) {
372: if (!isPrepared()) {
373: prepare();
374: }
375: }
376: Method method = invocation.getMethod();
377: try {
378: Object result = null;
379: if (this .invocationHandler != null) {
380: result = this .invocationHandler.invoke(invocation
381: .getThis(), method, invocation.getArguments());
382: } else {
383: PropertyDescriptor pd = BeanUtils
384: .findPropertyForMethod(method);
385: if (pd != null) {
386: result = invokeAttribute(pd, invocation);
387: } else {
388: result = invokeOperation(method, invocation
389: .getArguments());
390: }
391: }
392: return convertResultValueIfNecessary(result, method
393: .getReturnType());
394: } catch (MBeanException ex) {
395: throw ex.getTargetException();
396: } catch (RuntimeMBeanException ex) {
397: throw ex.getTargetException();
398: } catch (RuntimeErrorException ex) {
399: throw ex.getTargetError();
400: } catch (RuntimeOperationsException ex) {
401: // This one is only thrown by the JMX 1.2 RI, not by the JDK 1.5 JMX code.
402: RuntimeException rex = ex.getTargetException();
403: if (rex instanceof RuntimeMBeanException) {
404: throw ((RuntimeMBeanException) rex)
405: .getTargetException();
406: } else if (rex instanceof RuntimeErrorException) {
407: throw ((RuntimeErrorException) rex).getTargetError();
408: } else {
409: throw rex;
410: }
411: } catch (OperationsException ex) {
412: if (ReflectionUtils
413: .declaresException(method, ex.getClass())) {
414: throw ex;
415: } else {
416: throw new InvalidInvocationException(ex.getMessage());
417: }
418: } catch (JMException ex) {
419: if (ReflectionUtils
420: .declaresException(method, ex.getClass())) {
421: throw ex;
422: } else {
423: throw new InvocationFailureException(
424: "JMX access failed", ex);
425: }
426: } catch (IOException ex) {
427: if (ReflectionUtils
428: .declaresException(method, ex.getClass())) {
429: throw ex;
430: } else {
431: throw new InvocationFailureException(
432: "I/O failure during JMX access", ex);
433: }
434: }
435: }
436:
437: private Object invokeAttribute(PropertyDescriptor pd,
438: MethodInvocation invocation) throws JMException,
439: IOException {
440:
441: String attributeName = JmxUtils.getAttributeName(pd,
442: this .useStrictCasing);
443: MBeanAttributeInfo inf = (MBeanAttributeInfo) this .allowedAttributes
444: .get(attributeName);
445: // If no attribute is returned, we know that it is not defined in the
446: // management interface.
447: if (inf == null) {
448: throw new InvalidInvocationException("Attribute '"
449: + pd.getName()
450: + "' is not exposed on the management interface");
451: }
452: if (invocation.getMethod().equals(pd.getReadMethod())) {
453: if (inf.isReadable()) {
454: return this .server.getAttribute(this .objectName,
455: attributeName);
456: } else {
457: throw new InvalidInvocationException("Attribute '"
458: + attributeName + "' is not readable");
459: }
460: } else if (invocation.getMethod().equals(pd.getWriteMethod())) {
461: if (inf.isWritable()) {
462: this .server.setAttribute(this .objectName,
463: new Attribute(attributeName, invocation
464: .getArguments()[0]));
465: return null;
466: } else {
467: throw new InvalidInvocationException("Attribute '"
468: + attributeName + "' is not writable");
469: }
470: } else {
471: throw new IllegalStateException(
472: "Method ["
473: + invocation.getMethod()
474: + "] is neither a bean property getter nor a setter");
475: }
476: }
477:
478: /**
479: * Routes a method invocation (not a property get/set) to the corresponding
480: * operation on the managed resource.
481: * @param method the method corresponding to operation on the managed resource.
482: * @param args the invocation arguments
483: * @return the value returned by the method invocation.
484: */
485: private Object invokeOperation(Method method, Object[] args)
486: throws JMException, IOException {
487: MethodCacheKey key = new MethodCacheKey(method.getName(),
488: method.getParameterTypes());
489: MBeanOperationInfo info = (MBeanOperationInfo) this .allowedOperations
490: .get(key);
491: if (info == null) {
492: throw new InvalidInvocationException("Operation '"
493: + method.getName()
494: + "' is not exposed on the management interface");
495: }
496: String[] signature = null;
497: synchronized (this .signatureCache) {
498: signature = (String[]) this .signatureCache.get(method);
499: if (signature == null) {
500: signature = JmxUtils.getMethodSignature(method);
501: this .signatureCache.put(method, signature);
502: }
503: }
504: return this .server.invoke(this .objectName, method.getName(),
505: args, signature);
506: }
507:
508: /**
509: * Convert the given result object (from attribute access or operation invocation)
510: * to the specified target class for returning from the proxy method.
511: * @param result the result object as returned by the <code>MBeanServer</code>
512: * @param targetClass the result type of the proxy method that's been invoked
513: * @return the converted result object, or the passed-in object if no conversion
514: * is necessary
515: */
516: protected Object convertResultValueIfNecessary(Object result,
517: Class targetClass) {
518: try {
519: if (result == null) {
520: return null;
521: }
522: if (ClassUtils.isAssignableValue(targetClass, result)) {
523: return result;
524: }
525: if (result instanceof CompositeData) {
526: Method fromMethod = targetClass.getMethod("from",
527: new Class[] { CompositeData.class });
528: return ReflectionUtils.invokeMethod(fromMethod, null,
529: new Object[] { result });
530: } else if (result instanceof TabularData) {
531: Method fromMethod = targetClass.getMethod("from",
532: new Class[] { TabularData.class });
533: return ReflectionUtils.invokeMethod(fromMethod, null,
534: new Object[] { result });
535: } else {
536: throw new InvocationFailureException(
537: "Incompatible result value [" + result
538: + "] for target type ["
539: + targetClass.getName() + "]");
540: }
541: } catch (NoSuchMethodException ex) {
542: throw new InvocationFailureException(
543: "Could not obtain 'find(CompositeData)' / 'find(TabularData)' method on target type ["
544: + targetClass.getName()
545: + "] for conversion of MXBean data structure ["
546: + result + "]");
547: }
548: }
549:
550: /**
551: * Closes any <code>JMXConnector</code> that may be managed by this interceptor.
552: */
553: public void destroy() throws Exception {
554: if (this .connector != null) {
555: this .connector.close();
556: }
557: }
558:
559: /**
560: * Simple wrapper class around a method name and its signature.
561: * Used as the key when caching methods.
562: */
563: private static class MethodCacheKey {
564:
565: private final String name;
566:
567: private final Class[] parameterTypes;
568:
569: /**
570: * Create a new instance of <code>MethodCacheKey</code> with the supplied
571: * method name and parameter list.
572: * @param name the name of the method
573: * @param parameterTypes the arguments in the method signature
574: */
575: public MethodCacheKey(String name, Class[] parameterTypes) {
576: this .name = name;
577: this .parameterTypes = (parameterTypes != null ? parameterTypes
578: : new Class[0]);
579: }
580:
581: public boolean equals(Object other) {
582: if (other == this ) {
583: return true;
584: }
585: MethodCacheKey otherKey = (MethodCacheKey) other;
586: return (this .name.equals(otherKey.name) && Arrays.equals(
587: this .parameterTypes, otherKey.parameterTypes));
588: }
589:
590: public int hashCode() {
591: return this.name.hashCode();
592: }
593: }
594:
595: }
|