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.MBeanAttributeInfo;
032: import javax.management.MBeanInfo;
033: import javax.management.MBeanOperationInfo;
034: import javax.management.MBeanServer;
035: import javax.management.MBeanServerConnection;
036: import javax.management.MalformedObjectNameException;
037: import javax.management.ObjectName;
038: import javax.management.ReflectionException;
039: import javax.management.remote.JMXConnector;
040: import javax.management.remote.JMXConnectorFactory;
041: import javax.management.remote.JMXServiceURL;
042:
043: import org.aopalliance.intercept.MethodInterceptor;
044: import org.aopalliance.intercept.MethodInvocation;
045: import org.apache.commons.logging.Log;
046: import org.apache.commons.logging.LogFactory;
047:
048: import org.springframework.beans.BeanUtils;
049: import org.springframework.beans.factory.DisposableBean;
050: import org.springframework.beans.factory.InitializingBean;
051: import org.springframework.jmx.MBeanServerNotFoundException;
052: import org.springframework.jmx.support.JmxUtils;
053: import org.springframework.jmx.support.ObjectNameManager;
054:
055: /**
056: * <code>MethodInterceptor</code> implementation that routes calls to an MBean
057: * running on the supplied <code>MBeanServerConnection</code>. Works for both
058: * local and remote <code>MBeanServerConnection</code>s.
059: *
060: * <p>By default, the <code>MBeanClientInterceptor</code> will connect to the
061: * <code>MBeanServer</code> and cache MBean metadata at startup. This can
062: * be undesirable when running against a remote <code>MBeanServer</code>
063: * that may not be running when the application starts. Through setting the
064: * {@link #setConnectOnStartup(boolean) connectOnStartup} property to "false",
065: * you can defer this process until the first invocation against the proxy.
066: *
067: * <p>Requires JMX 1.2's <code>MBeanServerConnection</code> feature.
068: * As a consequence, this class will not work on JMX 1.0.
069: *
070: * <p>This functionality is usually used through <code>MBeanProxyFactoryBean</code>.
071: * See the javadoc of that class for more information.
072: *
073: * @author Rob Harrop
074: * @author Juergen Hoeller
075: * @since 1.2
076: * @see MBeanProxyFactoryBean
077: * @see #setConnectOnStartup
078: */
079: public class MBeanClientInterceptor implements MethodInterceptor,
080: InitializingBean, DisposableBean {
081:
082: /** Logger available to subclasses */
083: protected final Log logger = LogFactory.getLog(getClass());
084:
085: private MBeanServerConnection server;
086:
087: private JMXServiceURL serviceUrl;
088:
089: private String agentId;
090:
091: private boolean connectOnStartup = true;
092:
093: private ObjectName objectName;
094:
095: private boolean useStrictCasing = true;
096:
097: private JMXConnector connector;
098:
099: private Map allowedAttributes;
100:
101: private Map allowedOperations;
102:
103: private final Map signatureCache = new HashMap();
104:
105: /**
106: * Set the <code>MBeanServerConnection</code> used to connect to the
107: * MBean which all invocations are routed to.
108: */
109: public void setServer(MBeanServerConnection server) {
110: this .server = server;
111: }
112:
113: /**
114: * Set the service URL of the remote <code>MBeanServer</code>.
115: */
116: public void setServiceUrl(String url) throws MalformedURLException {
117: this .serviceUrl = new JMXServiceURL(url);
118: }
119:
120: /**
121: * Set the agent id of the <code>MBeanServer</code> to locate.
122: * <p>Default is none. If specified, this will result in an
123: * attempt being made to locate the attendant MBeanServer, unless
124: * the {@link #setServiceUrl "serviceUrl"} property has been set.
125: * @see javax.management.MBeanServerFactory#findMBeanServer(String)
126: */
127: public void setAgentId(String agentId) {
128: this .agentId = agentId;
129: }
130:
131: /**
132: * Set whether or not the proxy should connect to the <code>MBeanServer</code>
133: * at creation time ("true") or the first time it is invoked ("false").
134: * Default is "true".
135: */
136: public void setConnectOnStartup(boolean connectOnStartup) {
137: this .connectOnStartup = connectOnStartup;
138: }
139:
140: /**
141: * Set the <code>ObjectName</code> of the MBean which calls are routed to,
142: * as <code>ObjectName</code> instance or as <code>String</code>.
143: */
144: public void setObjectName(Object objectName)
145: throws MalformedObjectNameException {
146: this .objectName = ObjectNameManager.getInstance(objectName);
147: }
148:
149: /**
150: * Set whether to use strict casing for attributes. Enabled by default.
151: * <p>When using strict casing, a JavaBean property with a getter such as
152: * <code>getFoo()</code> translates to an attribute called <code>Foo</code>.
153: * With strict casing disabled, <code>getFoo()</code> would translate to just
154: * <code>foo</code>.
155: */
156: public void setUseStrictCasing(boolean useStrictCasing) {
157: this .useStrictCasing = useStrictCasing;
158: }
159:
160: /**
161: * Ensures that an <code>MBeanServerConnection</code> is configured and attempts to
162: * detect a local connection if one is not supplied.
163: */
164: public void afterPropertiesSet()
165: throws MBeanServerNotFoundException,
166: MBeanInfoRetrievalException {
167: if (this .connectOnStartup) {
168: if (this .server == null) {
169: connect();
170: }
171: retrieveMBeanInfo();
172: }
173: }
174:
175: /**
176: * Connects to the remote <code>MBeanServer</code> using the configured <code>JMXServiceURL</code>.
177: * @see #setServiceUrl(String)
178: * @see #setConnectOnStartup(boolean)
179: */
180: private void connect() throws MBeanServerNotFoundException {
181: if (this .serviceUrl != null) {
182: if (logger.isDebugEnabled()) {
183: logger
184: .debug("Connecting to remote MBeanServer at URL ["
185: + this .serviceUrl + "]");
186: }
187: try {
188: this .connector = JMXConnectorFactory
189: .connect(this .serviceUrl);
190: this .server = this .connector.getMBeanServerConnection();
191: } catch (IOException ex) {
192: throw new MBeanServerNotFoundException(
193: "Could not connect to remote MBeanServer at URL ["
194: + this .serviceUrl + "]", ex);
195: }
196: } else {
197: logger.debug("Attempting to locate local MBeanServer");
198: this .server = locateMBeanServer(this .agentId);
199: }
200: }
201:
202: /**
203: * Attempt to locate an existing <code>MBeanServer</code>.
204: * Called if no {@link #setServiceUrl "serviceUrl"} was specified.
205: * <p>The default implementation attempts to find an <code>MBeanServer</code> using
206: * a standard lookup. Subclasses may override to add additional location logic.
207: * @param agentId the agent identifier of the MBeanServer to retrieve.
208: * If this parameter is <code>null</code>, all registered MBeanServers are
209: * considered.
210: * @return the <code>MBeanServer</code> if found
211: * @throws org.springframework.jmx.MBeanServerNotFoundException
212: * if no <code>MBeanServer</code> could be found
213: * @see JmxUtils#locateMBeanServer(String)
214: * @see javax.management.MBeanServerFactory#findMBeanServer(String)
215: */
216: protected MBeanServer locateMBeanServer(String agentId)
217: throws MBeanServerNotFoundException {
218: return JmxUtils.locateMBeanServer(agentId);
219: }
220:
221: /**
222: * Loads the management interface info for the configured MBean into the caches.
223: * This information is used by the proxy when determining whether an invocation matches
224: * a valid operation or attribute on the management interface of the managed resource.
225: */
226: private void retrieveMBeanInfo()
227: throws MBeanServerNotFoundException,
228: MBeanInfoRetrievalException {
229: try {
230: MBeanInfo info = this .server.getMBeanInfo(this .objectName);
231:
232: // get attributes
233: MBeanAttributeInfo[] attributeInfo = info.getAttributes();
234: this .allowedAttributes = new HashMap(attributeInfo.length);
235:
236: for (int x = 0; x < attributeInfo.length; x++) {
237: this .allowedAttributes.put(attributeInfo[x].getName(),
238: attributeInfo[x]);
239: }
240:
241: // get operations
242: MBeanOperationInfo[] operationInfo = info.getOperations();
243: this .allowedOperations = new HashMap(operationInfo.length);
244:
245: for (int x = 0; x < operationInfo.length; x++) {
246: MBeanOperationInfo opInfo = operationInfo[x];
247: this .allowedOperations.put(new MethodCacheKey(opInfo
248: .getName(), JmxUtils
249: .parameterInfoToTypes(opInfo.getSignature())),
250: opInfo);
251: }
252: } catch (ClassNotFoundException ex) {
253: throw new MBeanInfoRetrievalException(
254: "Unable to locate class specified in method signature",
255: ex);
256: } catch (IntrospectionException ex) {
257: throw new MBeanInfoRetrievalException(
258: "Unable to obtain MBean info for bean ["
259: + this .objectName + "]", ex);
260: } catch (InstanceNotFoundException ex) {
261: // if we are this far this shouldn't happen, but...
262: throw new MBeanInfoRetrievalException(
263: "Unable to obtain MBean info for bean ["
264: + this .objectName
265: + "]: it is likely that this bean was unregistered during the proxy creation process",
266: ex);
267: } catch (ReflectionException ex) {
268: throw new MBeanInfoRetrievalException(
269: "Unable to read MBean info for bean [ "
270: + this .objectName + "]", ex);
271: } catch (IOException ex) {
272: throw new MBeanInfoRetrievalException(
273: "An IOException occurred when communicating with the MBeanServer. "
274: + "It is likely that you are communicating with a remote MBeanServer. "
275: + "Check the inner exception for exact details.",
276: ex);
277: }
278: }
279:
280: /**
281: * Route the invocation to the configured managed resource. Correctly routes JavaBean property
282: * access to <code>MBeanServerConnection.get/setAttribute</code> and method invocation to
283: * <code>MBeanServerConnection.invoke</code>. Any attempt to invoke a method that does not
284: * correspond to an attribute or operation defined in the management interface of the managed
285: * resource results in an <code>InvalidInvocationException</code>.
286: * @param invocation the <code>MethodInvocation</code> to re-route.
287: * @return the value returned as a result of the re-routed invocation.
288: * @throws InvocationFailureException if the invocation does not match an attribute or
289: * operation on the management interface of the resource.
290: * @throws Throwable typically as the result of an error during invocation
291: */
292: public Object invoke(MethodInvocation invocation) throws Throwable {
293: // Lazily connect to MBeanServer?
294: if (!this .connectOnStartup) {
295: synchronized (this ) {
296: if (this .server == null) {
297: logger
298: .debug("Lazily establishing MBeanServer connection");
299: connect();
300: }
301:
302: if (this .allowedAttributes == null) {
303: logger.debug("Lazily initializing MBeanInfo cache");
304: retrieveMBeanInfo();
305: }
306: }
307: }
308:
309: try {
310: PropertyDescriptor pd = BeanUtils
311: .findPropertyForMethod(invocation.getMethod());
312: if (pd != null) {
313: return invokeAttribute(pd, invocation);
314: } else {
315: return invokeOperation(invocation.getMethod(),
316: invocation.getArguments());
317: }
318: } catch (JMException ex) {
319: throw new InvocationFailureException("JMX access failed",
320: ex);
321: } catch (IOException ex) {
322: throw new InvocationFailureException("JMX access failed",
323: ex);
324: }
325: }
326:
327: private Object invokeAttribute(PropertyDescriptor pd,
328: MethodInvocation invocation) throws JMException,
329: IOException {
330:
331: String attributeName = JmxUtils.getAttributeName(pd,
332: this .useStrictCasing);
333: MBeanAttributeInfo inf = (MBeanAttributeInfo) this .allowedAttributes
334: .get(attributeName);
335:
336: // If no attribute is returned, we know that it is not defined in the
337: // management interface.
338: if (inf == null) {
339: throw new InvalidInvocationException("Attribute '"
340: + pd.getName()
341: + "' is not exposed on the management interface");
342: }
343: if (invocation.getMethod().equals(pd.getReadMethod())) {
344: if (inf.isReadable()) {
345: return this .server.getAttribute(this .objectName,
346: attributeName);
347: } else {
348: throw new InvalidInvocationException("Attribute '"
349: + attributeName + "' is not readable");
350: }
351: } else if (invocation.getMethod().equals(pd.getWriteMethod())) {
352: if (inf.isWritable()) {
353: server.setAttribute(this .objectName, new Attribute(
354: attributeName, invocation.getArguments()[0]));
355: return null;
356: } else {
357: throw new InvalidInvocationException("Attribute '"
358: + attributeName + "' is not writable");
359: }
360: } else {
361: throw new IllegalStateException(
362: "Method ["
363: + invocation.getMethod()
364: + "] is neither a bean property getter nor a setter");
365: }
366: }
367:
368: /**
369: * Routes a method invocation (not a property get/set) to the corresponding
370: * operation on the managed resource.
371: * @param method the method corresponding to operation on the managed resource.
372: * @param args the invocation arguments
373: * @return the value returned by the method invocation.
374: */
375: private Object invokeOperation(Method method, Object[] args)
376: throws JMException, IOException {
377: MethodCacheKey key = new MethodCacheKey(method.getName(),
378: method.getParameterTypes());
379: MBeanOperationInfo info = (MBeanOperationInfo) this .allowedOperations
380: .get(key);
381: if (info == null) {
382: throw new InvalidInvocationException("Operation '"
383: + method.getName()
384: + "' is not exposed on the management interface");
385: }
386:
387: String[] signature = null;
388: synchronized (this .signatureCache) {
389: signature = (String[]) this .signatureCache.get(method);
390: if (signature == null) {
391: signature = JmxUtils.getMethodSignature(method);
392: this .signatureCache.put(method, signature);
393: }
394: }
395: return this .server.invoke(this .objectName, method.getName(),
396: args, signature);
397: }
398:
399: /**
400: * Closes any <code>JMXConnector</code> that may be managed by this interceptor.
401: */
402: public void destroy() throws Exception {
403: if (this .connector != null) {
404: this .connector.close();
405: }
406: }
407:
408: /**
409: * Simple wrapper class around a method name and its signature.
410: * Used as the key when caching methods.
411: */
412: private static class MethodCacheKey {
413:
414: /**
415: * the name of the method
416: */
417: private final String name;
418:
419: /**
420: * the arguments in the method signature.
421: */
422: private final Class[] parameters;
423:
424: /**
425: * Create a new instance of <code>MethodCacheKey</code> with the supplied
426: * method name and parameter list.
427: *
428: * @param name the name of the method.
429: * @param parameters the arguments in the method signature.
430: */
431: public MethodCacheKey(String name, Class[] parameters) {
432: this .name = name;
433: if (parameters == null) {
434: this .parameters = new Class[] {};
435: } else {
436: this .parameters = parameters;
437: }
438: }
439:
440: public boolean equals(Object other) {
441: if (other == null) {
442: return false;
443: }
444: if (other == this ) {
445: return true;
446: }
447: MethodCacheKey otherKey = null;
448: if (other instanceof MethodCacheKey) {
449: otherKey = (MethodCacheKey) other;
450: return this .name.equals(otherKey.name)
451: && Arrays.equals(this .parameters,
452: otherKey.parameters);
453: } else {
454: return false;
455: }
456: }
457:
458: public int hashCode() {
459: return this.name.hashCode();
460: }
461: }
462:
463: }
|