1 /*
2 * Copyright 2002-2008 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.springframework.jmx.access;
18
19 import java.beans.PropertyDescriptor;
20 import java.io.IOException;
21 import java.lang.reflect.Method;
22 import java.net.MalformedURLException;
23 import java.util.Arrays;
24 import java.util.HashMap;
25 import java.util.Map;
26
27 import javax.management.Attribute;
28 import javax.management.InstanceNotFoundException;
29 import javax.management.IntrospectionException;
30 import javax.management.JMException;
31 import javax.management.JMX;
32 import javax.management.MBeanAttributeInfo;
33 import javax.management.MBeanException;
34 import javax.management.MBeanInfo;
35 import javax.management.MBeanOperationInfo;
36 import javax.management.MBeanServerConnection;
37 import javax.management.MBeanServerInvocationHandler;
38 import javax.management.MalformedObjectNameException;
39 import javax.management.ObjectName;
40 import javax.management.OperationsException;
41 import javax.management.ReflectionException;
42 import javax.management.RuntimeErrorException;
43 import javax.management.RuntimeMBeanException;
44 import javax.management.RuntimeOperationsException;
45 import javax.management.openmbean.CompositeData;
46 import javax.management.openmbean.TabularData;
47 import javax.management.remote.JMXServiceURL;
48
49 import org.aopalliance.intercept.MethodInterceptor;
50 import org.aopalliance.intercept.MethodInvocation;
51 import org.apache.commons.logging.Log;
52 import org.apache.commons.logging.LogFactory;
53
54 import org.springframework.beans.BeanUtils;
55 import org.springframework.beans.factory.BeanClassLoaderAware;
56 import org.springframework.beans.factory.DisposableBean;
57 import org.springframework.beans.factory.InitializingBean;
58 import org.springframework.core.JdkVersion;
59 import org.springframework.jmx.support.JmxUtils;
60 import org.springframework.jmx.support.ObjectNameManager;
61 import org.springframework.util.ClassUtils;
62 import org.springframework.util.ReflectionUtils;
63
64 /**
65 * {@link org.aopalliance.intercept.MethodInterceptor} that routes calls to an
66 * MBean running on the supplied <code>MBeanServerConnection</code>.
67 * Works for both local and remote <code>MBeanServerConnection</code>s.
68 *
69 * <p>By default, the <code>MBeanClientInterceptor</code> will connect to the
70 * <code>MBeanServer</code> and cache MBean metadata at startup. This can
71 * be undesirable when running against a remote <code>MBeanServer</code>
72 * that may not be running when the application starts. Through setting the
73 * {@link #setConnectOnStartup(boolean) connectOnStartup} property to "false",
74 * you can defer this process until the first invocation against the proxy.
75 *
76 * <p>Requires JMX 1.2's <code>MBeanServerConnection</code> feature.
77 * As a consequence, this class will not work on JMX 1.0.
78 *
79 * <p>This functionality is usually used through {@link MBeanProxyFactoryBean}.
80 * See the javadoc of that class for more information.
81 *
82 * @author Rob Harrop
83 * @author Juergen Hoeller
84 * @since 1.2
85 * @see MBeanProxyFactoryBean
86 * @see #setConnectOnStartup
87 */
88 public class MBeanClientInterceptor
89 implements MethodInterceptor, BeanClassLoaderAware, InitializingBean, DisposableBean {
90
91 /** Logger available to subclasses */
92 protected final Log logger = LogFactory.getLog(getClass());
93
94 private MBeanServerConnection server;
95
96 private JMXServiceURL serviceUrl;
97
98 private Map environment;
99
100 private String agentId;
101
102 private boolean connectOnStartup = true;
103
104 private boolean refreshOnConnectFailure = false;
105
106 private ObjectName objectName;
107
108 private boolean useStrictCasing = true;
109
110 private Class managementInterface;
111
112 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
113
114 private final ConnectorDelegate connector = new ConnectorDelegate();
115
116 private MBeanServerConnection serverToUse;
117
118 private MBeanServerInvocationHandler invocationHandler;
119
120 private Map allowedAttributes;
121
122 private Map allowedOperations;
123
124 private final Map signatureCache = new HashMap();
125
126 private final Object preparationMonitor = new Object();
127
128
129 /**
130 * Set the <code>MBeanServerConnection</code> used to connect to the
131 * MBean which all invocations are routed to.
132 */
133 public void setServer(MBeanServerConnection server) {
134 this.server = server;
135 }
136
137 /**
138 * Set the service URL of the remote <code>MBeanServer</code>.
139 */
140 public void setServiceUrl(String url) throws MalformedURLException {
141 this.serviceUrl = new JMXServiceURL(url);
142 }
143
144 /**
145 * Specify the environment for the JMX connector.
146 * @see javax.management.remote.JMXConnectorFactory#connect(javax.management.remote.JMXServiceURL, java.util.Map)
147 */
148 public void setEnvironment(Map environment) {
149 this.environment = environment;
150 }
151
152 /**
153 * Allow Map access to the environment to be set for the connector,
154 * with the option to add or override specific entries.
155 * <p>Useful for specifying entries directly, for example via
156 * "environment[myKey]". This is particularly useful for
157 * adding or overriding entries in child bean definitions.
158 */
159 public Map getEnvironment() {
160 return this.environment;
161 }
162
163 /**
164 * Set the agent id of the <code>MBeanServer</code> to locate.
165 * <p>Default is none. If specified, this will result in an
166 * attempt being made to locate the attendant MBeanServer, unless
167 * the {@link #setServiceUrl "serviceUrl"} property has been set.
168 * @see javax.management.MBeanServerFactory#findMBeanServer(String)
169 */
170 public void setAgentId(String agentId) {
171 this.agentId = agentId;
172 }
173
174 /**
175 * Set whether or not the proxy should connect to the <code>MBeanServer</code>
176 * at creation time ("true") or the first time it is invoked ("false").
177 * Default is "true".
178 */
179 public void setConnectOnStartup(boolean connectOnStartup) {
180 this.connectOnStartup = connectOnStartup;
181 }
182
183 /**
184 * Set whether to refresh the MBeanServer connection on connect failure.
185 * Default is "false".
186 * <p>Can be turned on to allow for hot restart of the JMX server,
187 * automatically reconnecting and retrying in case of an IOException.
188 */
189 public void setRefreshOnConnectFailure(boolean refreshOnConnectFailure) {
190 this.refreshOnConnectFailure = refreshOnConnectFailure;
191 }
192
193 /**
194 * Set the <code>ObjectName</code> of the MBean which calls are routed to,
195 * as <code>ObjectName</code> instance or as <code>String</code>.
196 */
197 public void setObjectName(Object objectName) throws MalformedObjectNameException {
198 this.objectName = ObjectNameManager.getInstance(objectName);
199 }
200
201 /**
202 * Set whether to use strict casing for attributes. Enabled by default.
203 * <p>When using strict casing, a JavaBean property with a getter such as
204 * <code>getFoo()</code> translates to an attribute called <code>Foo</code>.
205 * With strict casing disabled, <code>getFoo()</code> would translate to just
206 * <code>foo</code>.
207 */
208 public void setUseStrictCasing(boolean useStrictCasing) {
209 this.useStrictCasing = useStrictCasing;
210 }
211
212 /**
213 * Set the management interface of the target MBean, exposing bean property
214 * setters and getters for MBean attributes and conventional Java methods
215 * for MBean operations.
216 */
217 public void setManagementInterface(Class managementInterface) {
218 this.managementInterface = managementInterface;
219 }
220
221 /**
222 * Return the management interface of the target MBean,
223 * or <code>null</code> if none specified.
224 */
225 protected final Class getManagementInterface() {
226 return this.managementInterface;
227 }
228
229 public void setBeanClassLoader(ClassLoader beanClassLoader) {
230 this.beanClassLoader = beanClassLoader;
231 }
232
233
234 /**
235 * Prepares the <code>MBeanServerConnection</code> if the "connectOnStartup"
236 * is turned on (which it is by default).
237 */
238 public void afterPropertiesSet() {
239 if (this.server != null && this.refreshOnConnectFailure) {
240 throw new IllegalArgumentException("'refreshOnConnectFailure' does not work when setting " +
241 "a 'server' reference. Prefer 'serviceUrl' etc instead.");
242 }
243 if (this.connectOnStartup) {
244 prepare();
245 }
246 }
247
248 /**
249 * Ensures that an <code>MBeanServerConnection</code> is configured and attempts
250 * to detect a local connection if one is not supplied.
251 */
252 public void prepare() {
253 synchronized (this.preparationMonitor) {
254 if (this.server != null) {
255 this.serverToUse = this.server;
256 }
257 else {
258 this.serverToUse = null;
259 this.serverToUse = this.connector.connect(this.serviceUrl, this.environment, this.agentId);
260 }
261 this.invocationHandler = null;
262 if (this.useStrictCasing) {
263 // Use the JDK's own MBeanServerInvocationHandler,
264 // in particular for native MXBean support on Java 6.
265 if (JdkVersion.isAtLeastJava16()) {
266 this.invocationHandler =
267 new MBeanServerInvocationHandler(this.serverToUse, this.objectName,
268 (this.managementInterface != null && JMX.isMXBeanInterface(this.managementInterface)));
269 }
270 else {
271 this.invocationHandler = new MBeanServerInvocationHandler(this.serverToUse, this.objectName);
272 }
273 }
274 else {
275 // Non-strict casing can only be achieved through custom
276 // invocation handling. Only partial MXBean support available!
277 retrieveMBeanInfo();
278 }
279 }
280 }
281 /**
282 * Loads the management interface info for the configured MBean into the caches.
283 * This information is used by the proxy when determining whether an invocation matches
284 * a valid operation or attribute on the management interface of the managed resource.
285 */
286 private void retrieveMBeanInfo() throws MBeanInfoRetrievalException {
287 try {
288 MBeanInfo info = this.serverToUse.getMBeanInfo(this.objectName);
289
290 MBeanAttributeInfo[] attributeInfo = info.getAttributes();
291 this.allowedAttributes = new HashMap(attributeInfo.length);
292 for (int x = 0; x < attributeInfo.length; x++) {
293 this.allowedAttributes.put(attributeInfo[x].getName(), attributeInfo[x]);
294 }
295
296 MBeanOperationInfo[] operationInfo = info.getOperations();
297 this.allowedOperations = new HashMap(operationInfo.length);
298 for (int x = 0; x < operationInfo.length; x++) {
299 MBeanOperationInfo opInfo = operationInfo[x];
300 Class[] paramTypes = JmxUtils.parameterInfoToTypes(opInfo.getSignature(), this.beanClassLoader);
301 this.allowedOperations.put(new MethodCacheKey(opInfo.getName(), paramTypes), opInfo);
302 }
303 }
304 catch (ClassNotFoundException ex) {
305 throw new MBeanInfoRetrievalException("Unable to locate class specified in method signature", ex);
306 }
307 catch (IntrospectionException ex) {
308 throw new MBeanInfoRetrievalException("Unable to obtain MBean info for bean [" + this.objectName + "]", ex);
309 }
310 catch (InstanceNotFoundException ex) {
311 // if we are this far this shouldn't happen, but...
312 throw new MBeanInfoRetrievalException("Unable to obtain MBean info for bean [" + this.objectName +
313 "]: it is likely that this bean was unregistered during the proxy creation process",
314 ex);
315 }
316 catch (ReflectionException ex) {
317 throw new MBeanInfoRetrievalException("Unable to read MBean info for bean [ " + this.objectName + "]", ex);
318 }
319 catch (IOException ex) {
320 throw new MBeanInfoRetrievalException("An IOException occurred when communicating with the " +
321 "MBeanServer. It is likely that you are communicating with a remote MBeanServer. " +
322 "Check the inner exception for exact details.", ex);
323 }
324 }
325
326 /**
327 * Return whether this client interceptor has already been prepared,
328 * i.e. has already looked up the server and cached all metadata.
329 */
330 protected boolean isPrepared() {
331 synchronized (this.preparationMonitor) {
332 return (this.serverToUse != null);
333 }
334 }
335
336
337 /**
338 * Route the invocation to the configured managed resource..
339 * @param invocation the <code>MethodInvocation</code> to re-route
340 * @return the value returned as a result of the re-routed invocation
341 * @throws Throwable an invocation error propagated to the user
342 * @see #doInvoke
343 * @see #handleConnectFailure
344 */
345 public Object invoke(MethodInvocation invocation) throws Throwable {
346 // Lazily connect to MBeanServer if necessary.
347 synchronized (this.preparationMonitor) {
348 if (!isPrepared()) {
349 prepare();
350 }
351 }
352 try {
353 return doInvoke(invocation);
354 }
355 catch (MBeanConnectFailureException ex) {
356 return handleConnectFailure(invocation, ex);
357 }
358 catch (IOException ex) {
359 return handleConnectFailure(invocation, ex);
360 }
361 }
362
363 /**
364 * Refresh the connection and retry the MBean invocation if possible.
365 * <p>If not configured to refresh on connect failure, this method
366 * simply rethrows the original exception.
367 * @param invocation the invocation that failed
368 * @param ex the exception raised on remote invocation
369 * @return the result value of the new invocation, if succeeded
370 * @throws Throwable an exception raised by the new invocation,
371 * if it failed as well
372 * @see #setRefreshOnConnectFailure
373 * @see #doInvoke
374 */
375 protected Object handleConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable {
376 if (this.refreshOnConnectFailure) {
377 String msg = "Could not connect to JMX server - retrying";
378 if (logger.isDebugEnabled()) {
379 logger.warn(msg, ex);
380 }
381 else if (logger.isWarnEnabled()) {
382 logger.warn(msg);
383 }
384 prepare();
385 return doInvoke(invocation);
386 }
387 else {
388 throw ex;
389 }
390 }
391
392 /**
393 * Route the invocation to the configured managed resource. Correctly routes JavaBean property
394 * access to <code>MBeanServerConnection.get/setAttribute</code> and method invocation to
395 * <code>MBeanServerConnection.invoke</code>.
396 * @param invocation the <code>MethodInvocation</code> to re-route
397 * @return the value returned as a result of the re-routed invocation
398 * @throws Throwable an invocation error propagated to the user
399 */
400 protected Object doInvoke(MethodInvocation invocation) throws Throwable {
401 Method method = invocation.getMethod();
402 try {
403 Object result = null;
404 if (this.invocationHandler != null) {
405 result = this.invocationHandler.invoke(invocation.getThis(), method, invocation.getArguments());
406 }
407 else {
408 PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method);
409 if (pd != null) {
410 result = invokeAttribute(pd, invocation);
411 }
412 else {
413 result = invokeOperation(method, invocation.getArguments());
414 }
415 }
416 return convertResultValueIfNecessary(result, method.getReturnType());
417 }
418 catch (MBeanException ex) {
419 throw ex.getTargetException();
420 }
421 catch (RuntimeMBeanException ex) {
422 throw ex.getTargetException();
423 }
424 catch (RuntimeErrorException ex) {
425 throw ex.getTargetError();
426 }
427 catch (RuntimeOperationsException ex) {
428 // This one is only thrown by the JMX 1.2 RI, not by the JDK 1.5 JMX code.
429 RuntimeException rex = ex.getTargetException();
430 if (rex instanceof RuntimeMBeanException) {
431 throw ((RuntimeMBeanException) rex).getTargetException();
432 }
433 else if (rex instanceof RuntimeErrorException) {
434 throw ((RuntimeErrorException) rex).getTargetError();
435 }
436 else {
437 throw rex;
438 }
439 }
440 catch (OperationsException ex) {
441 if (ReflectionUtils.declaresException(method, ex.getClass())) {
442 throw ex;
443 }
444 else {
445 throw new InvalidInvocationException(ex.getMessage());
446 }
447 }
448 catch (JMException ex) {
449 if (ReflectionUtils.declaresException(method, ex.getClass())) {
450 throw ex;
451 }
452 else {
453 throw new InvocationFailureException("JMX access failed", ex);
454 }
455 }
456 catch (IOException ex) {
457 if (ReflectionUtils.declaresException(method, ex.getClass())) {
458 throw ex;
459 }
460 else {
461 throw new MBeanConnectFailureException("I/O failure during JMX access", ex);
462 }
463 }
464 }
465
466 private Object invokeAttribute(PropertyDescriptor pd, MethodInvocation invocation)
467 throws JMException, IOException {
468
469 String attributeName = JmxUtils.getAttributeName(pd, this.useStrictCasing);
470 MBeanAttributeInfo inf = (MBeanAttributeInfo) this.allowedAttributes.get(attributeName);
471 // If no attribute is returned, we know that it is not defined in the
472 // management interface.
473 if (inf == null) {
474 throw new InvalidInvocationException(
475 "Attribute '" + pd.getName() + "' is not exposed on the management interface");
476 }
477 if (invocation.getMethod().equals(pd.getReadMethod())) {
478 if (inf.isReadable()) {
479 return this.serverToUse.getAttribute(this.objectName, attributeName);
480 }
481 else {
482 throw new InvalidInvocationException("Attribute '" + attributeName + "' is not readable");
483 }
484 }
485 else if (invocation.getMethod().equals(pd.getWriteMethod())) {
486 if (inf.isWritable()) {
487 this.serverToUse.setAttribute(this.objectName, new Attribute(attributeName, invocation.getArguments()[0]));
488 return null;
489 }
490 else {
491 throw new InvalidInvocationException("Attribute '" + attributeName + "' is not writable");
492 }
493 }
494 else {
495 throw new IllegalStateException(
496 "Method [" + invocation.getMethod() + "] is neither a bean property getter nor a setter");
497 }
498 }
499
500 /**
501 * Routes a method invocation (not a property get/set) to the corresponding
502 * operation on the managed resource.
503 * @param method the method corresponding to operation on the managed resource.
504 * @param args the invocation arguments
505 * @return the value returned by the method invocation.
506 */
507 private Object invokeOperation(Method method, Object[] args) throws JMException, IOException {
508 MethodCacheKey key = new MethodCacheKey(method.getName(), method.getParameterTypes());
509 MBeanOperationInfo info = (MBeanOperationInfo) this.allowedOperations.get(key);
510 if (info == null) {
511 throw new InvalidInvocationException("Operation '" + method.getName() +
512 "' is not exposed on the management interface");
513 }
514 String[] signature = null;
515 synchronized (this.signatureCache) {
516 signature = (String[]) this.signatureCache.get(method);
517 if (signature == null) {
518 signature = JmxUtils.getMethodSignature(method);
519 this.signatureCache.put(method, signature);
520 }
521 }
522 return this.serverToUse.invoke(this.objectName, method.getName(), args, signature);
523 }
524
525 /**
526 * Convert the given result object (from attribute access or operation invocation)
527 * to the specified target class for returning from the proxy method.
528 * @param result the result object as returned by the <code>MBeanServer</code>
529 * @param targetClass the result type of the proxy method that's been invoked
530 * @return the converted result object, or the passed-in object if no conversion
531 * is necessary
532 */
533 protected Object convertResultValueIfNecessary(Object result, Class targetClass) {
534 try {
535 if (result == null) {
536 return null;
537 }
538 if (ClassUtils.isAssignableValue(targetClass, result)) {
539 return result;
540 }
541 if (result instanceof CompositeData) {
542 Method fromMethod = targetClass.getMethod("from", new Class[] {CompositeData.class});
543 return ReflectionUtils.invokeMethod(fromMethod, null, new Object[] {result});
544 }
545 else if (result instanceof TabularData) {
546 Method fromMethod = targetClass.getMethod("from", new Class[] {TabularData.class});
547 return ReflectionUtils.invokeMethod(fromMethod, null, new Object[] {result});
548 }
549 else {
550 throw new InvocationFailureException(
551 "Incompatible result value [" + result + "] for target type [" + targetClass.getName() + "]");
552 }
553 }
554 catch (NoSuchMethodException ex) {
555 throw new InvocationFailureException(
556 "Could not obtain 'find(CompositeData)' / 'find(TabularData)' method on target type [" +
557 targetClass.getName() + "] for conversion of MXBean data structure [" + result + "]");
558 }
559 }
560
561 public void destroy() {
562 this.connector.close();
563 }
564
565
566 /**
567 * Simple wrapper class around a method name and its signature.
568 * Used as the key when caching methods.
569 */
570 private static class MethodCacheKey {
571
572 private final String name;
573
574 private final Class[] parameterTypes;
575
576 /**
577 * Create a new instance of <code>MethodCacheKey</code> with the supplied
578 * method name and parameter list.
579 * @param name the name of the method
580 * @param parameterTypes the arguments in the method signature
581 */
582 public MethodCacheKey(String name, Class[] parameterTypes) {
583 this.name = name;
584 this.parameterTypes = (parameterTypes != null ? parameterTypes : new Class[0]);
585 }
586
587 public boolean equals(Object other) {
588 if (other == this) {
589 return true;
590 }
591 MethodCacheKey otherKey = (MethodCacheKey) other;
592 return (this.name.equals(otherKey.name) && Arrays.equals(this.parameterTypes, otherKey.parameterTypes));
593 }
594
595 public int hashCode() {
596 return this.name.hashCode();
597 }
598 }
599
600 }