001/* 002 * Copyright 2002-2017 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 * https://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 017package org.springframework.jmx.access; 018 019import java.beans.PropertyDescriptor; 020import java.io.IOException; 021import java.lang.reflect.Array; 022import java.lang.reflect.Method; 023import java.net.MalformedURLException; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.HashMap; 027import java.util.Map; 028import javax.management.Attribute; 029import javax.management.InstanceNotFoundException; 030import javax.management.IntrospectionException; 031import javax.management.JMException; 032import javax.management.JMX; 033import javax.management.MBeanAttributeInfo; 034import javax.management.MBeanException; 035import javax.management.MBeanInfo; 036import javax.management.MBeanOperationInfo; 037import javax.management.MBeanServerConnection; 038import javax.management.MBeanServerInvocationHandler; 039import javax.management.MalformedObjectNameException; 040import javax.management.ObjectName; 041import javax.management.OperationsException; 042import javax.management.ReflectionException; 043import javax.management.RuntimeErrorException; 044import javax.management.RuntimeMBeanException; 045import javax.management.RuntimeOperationsException; 046import javax.management.openmbean.CompositeData; 047import javax.management.openmbean.TabularData; 048import javax.management.remote.JMXServiceURL; 049 050import org.aopalliance.intercept.MethodInterceptor; 051import org.aopalliance.intercept.MethodInvocation; 052import org.apache.commons.logging.Log; 053import org.apache.commons.logging.LogFactory; 054 055import org.springframework.beans.BeanUtils; 056import org.springframework.beans.factory.BeanClassLoaderAware; 057import org.springframework.beans.factory.DisposableBean; 058import org.springframework.beans.factory.InitializingBean; 059import org.springframework.core.CollectionFactory; 060import org.springframework.core.MethodParameter; 061import org.springframework.core.ResolvableType; 062import org.springframework.jmx.support.JmxUtils; 063import org.springframework.jmx.support.ObjectNameManager; 064import org.springframework.util.ClassUtils; 065import org.springframework.util.ReflectionUtils; 066import org.springframework.util.StringUtils; 067 068/** 069 * {@link org.aopalliance.intercept.MethodInterceptor} that routes calls to an 070 * MBean running on the supplied {@code MBeanServerConnection}. 071 * Works for both local and remote {@code MBeanServerConnection}s. 072 * 073 * <p>By default, the {@code MBeanClientInterceptor} will connect to the 074 * {@code MBeanServer} and cache MBean metadata at startup. This can 075 * be undesirable when running against a remote {@code MBeanServer} 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>This functionality is usually used through {@link MBeanProxyFactoryBean}. 081 * See the javadoc of that class for more information. 082 * 083 * @author Rob Harrop 084 * @author Juergen Hoeller 085 * @since 1.2 086 * @see MBeanProxyFactoryBean 087 * @see #setConnectOnStartup 088 */ 089public class MBeanClientInterceptor 090 implements MethodInterceptor, BeanClassLoaderAware, InitializingBean, DisposableBean { 091 092 /** Logger available to subclasses */ 093 protected final Log logger = LogFactory.getLog(getClass()); 094 095 private MBeanServerConnection server; 096 097 private JMXServiceURL serviceUrl; 098 099 private Map<String, ?> environment; 100 101 private String agentId; 102 103 private boolean connectOnStartup = true; 104 105 private boolean refreshOnConnectFailure = false; 106 107 private ObjectName objectName; 108 109 private boolean useStrictCasing = true; 110 111 private Class<?> managementInterface; 112 113 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); 114 115 private final ConnectorDelegate connector = new ConnectorDelegate(); 116 117 private MBeanServerConnection serverToUse; 118 119 private MBeanServerInvocationHandler invocationHandler; 120 121 private Map<String, MBeanAttributeInfo> allowedAttributes; 122 123 private Map<MethodCacheKey, MBeanOperationInfo> allowedOperations; 124 125 private final Map<Method, String[]> signatureCache = new HashMap<Method, String[]>(); 126 127 private final Object preparationMonitor = new Object(); 128 129 130 /** 131 * Set the {@code MBeanServerConnection} used to connect to the 132 * MBean which all invocations are routed to. 133 */ 134 public void setServer(MBeanServerConnection server) { 135 this.server = server; 136 } 137 138 /** 139 * Set the service URL of the remote {@code MBeanServer}. 140 */ 141 public void setServiceUrl(String url) throws MalformedURLException { 142 this.serviceUrl = new JMXServiceURL(url); 143 } 144 145 /** 146 * Specify the environment for the JMX connector. 147 * @see javax.management.remote.JMXConnectorFactory#connect(javax.management.remote.JMXServiceURL, java.util.Map) 148 */ 149 public void setEnvironment(Map<String, ?> environment) { 150 this.environment = environment; 151 } 152 153 /** 154 * Allow Map access to the environment to be set for the connector, 155 * with the option to add or override specific entries. 156 * <p>Useful for specifying entries directly, for example via 157 * "environment[myKey]". This is particularly useful for 158 * adding or overriding entries in child bean definitions. 159 */ 160 public Map<String, ?> getEnvironment() { 161 return this.environment; 162 } 163 164 /** 165 * Set the agent id of the {@code MBeanServer} to locate. 166 * <p>Default is none. If specified, this will result in an 167 * attempt being made to locate the attendant MBeanServer, unless 168 * the {@link #setServiceUrl "serviceUrl"} property has been set. 169 * @see javax.management.MBeanServerFactory#findMBeanServer(String) 170 * <p>Specifying the empty String indicates the platform MBeanServer. 171 */ 172 public void setAgentId(String agentId) { 173 this.agentId = agentId; 174 } 175 176 /** 177 * Set whether or not the proxy should connect to the {@code MBeanServer} 178 * at creation time ("true") or the first time it is invoked ("false"). 179 * Default is "true". 180 */ 181 public void setConnectOnStartup(boolean connectOnStartup) { 182 this.connectOnStartup = connectOnStartup; 183 } 184 185 /** 186 * Set whether to refresh the MBeanServer connection on connect failure. 187 * Default is "false". 188 * <p>Can be turned on to allow for hot restart of the JMX server, 189 * automatically reconnecting and retrying in case of an IOException. 190 */ 191 public void setRefreshOnConnectFailure(boolean refreshOnConnectFailure) { 192 this.refreshOnConnectFailure = refreshOnConnectFailure; 193 } 194 195 /** 196 * Set the {@code ObjectName} of the MBean which calls are routed to, 197 * as {@code ObjectName} instance or as {@code String}. 198 */ 199 public void setObjectName(Object objectName) throws MalformedObjectNameException { 200 this.objectName = ObjectNameManager.getInstance(objectName); 201 } 202 203 /** 204 * Set whether to use strict casing for attributes. Enabled by default. 205 * <p>When using strict casing, a JavaBean property with a getter such as 206 * {@code getFoo()} translates to an attribute called {@code Foo}. 207 * With strict casing disabled, {@code getFoo()} would translate to just 208 * {@code foo}. 209 */ 210 public void setUseStrictCasing(boolean useStrictCasing) { 211 this.useStrictCasing = useStrictCasing; 212 } 213 214 /** 215 * Set the management interface of the target MBean, exposing bean property 216 * setters and getters for MBean attributes and conventional Java methods 217 * for MBean operations. 218 */ 219 public void setManagementInterface(Class<?> managementInterface) { 220 this.managementInterface = managementInterface; 221 } 222 223 /** 224 * Return the management interface of the target MBean, 225 * or {@code null} if none specified. 226 */ 227 protected final Class<?> getManagementInterface() { 228 return this.managementInterface; 229 } 230 231 @Override 232 public void setBeanClassLoader(ClassLoader beanClassLoader) { 233 this.beanClassLoader = beanClassLoader; 234 } 235 236 237 /** 238 * Prepares the {@code MBeanServerConnection} if the "connectOnStartup" 239 * is turned on (which it is by default). 240 */ 241 @Override 242 public void afterPropertiesSet() { 243 if (this.server != null && this.refreshOnConnectFailure) { 244 throw new IllegalArgumentException("'refreshOnConnectFailure' does not work when setting " + 245 "a 'server' reference. Prefer 'serviceUrl' etc instead."); 246 } 247 if (this.connectOnStartup) { 248 prepare(); 249 } 250 } 251 252 /** 253 * Ensures that an {@code MBeanServerConnection} is configured and attempts 254 * to detect a local connection if one is not supplied. 255 */ 256 public void prepare() { 257 synchronized (this.preparationMonitor) { 258 if (this.server != null) { 259 this.serverToUse = this.server; 260 } 261 else { 262 this.serverToUse = null; 263 this.serverToUse = this.connector.connect(this.serviceUrl, this.environment, this.agentId); 264 } 265 this.invocationHandler = null; 266 if (this.useStrictCasing) { 267 // Use the JDK's own MBeanServerInvocationHandler, in particular for native MXBean support. 268 this.invocationHandler = new MBeanServerInvocationHandler(this.serverToUse, this.objectName, 269 (this.managementInterface != null && JMX.isMXBeanInterface(this.managementInterface))); 270 } 271 else { 272 // Non-strict casing can only be achieved through custom invocation handling. 273 // Only partial MXBean support available! 274 retrieveMBeanInfo(); 275 } 276 } 277 } 278 /** 279 * Loads the management interface info for the configured MBean into the caches. 280 * This information is used by the proxy when determining whether an invocation matches 281 * a valid operation or attribute on the management interface of the managed resource. 282 */ 283 private void retrieveMBeanInfo() throws MBeanInfoRetrievalException { 284 try { 285 MBeanInfo info = this.serverToUse.getMBeanInfo(this.objectName); 286 287 MBeanAttributeInfo[] attributeInfo = info.getAttributes(); 288 this.allowedAttributes = new HashMap<String, MBeanAttributeInfo>(attributeInfo.length); 289 for (MBeanAttributeInfo infoEle : attributeInfo) { 290 this.allowedAttributes.put(infoEle.getName(), infoEle); 291 } 292 293 MBeanOperationInfo[] operationInfo = info.getOperations(); 294 this.allowedOperations = new HashMap<MethodCacheKey, MBeanOperationInfo>(operationInfo.length); 295 for (MBeanOperationInfo infoEle : operationInfo) { 296 Class<?>[] paramTypes = JmxUtils.parameterInfoToTypes(infoEle.getSignature(), this.beanClassLoader); 297 this.allowedOperations.put(new MethodCacheKey(infoEle.getName(), paramTypes), infoEle); 298 } 299 } 300 catch (ClassNotFoundException ex) { 301 throw new MBeanInfoRetrievalException("Unable to locate class specified in method signature", ex); 302 } 303 catch (IntrospectionException ex) { 304 throw new MBeanInfoRetrievalException("Unable to obtain MBean info for bean [" + this.objectName + "]", ex); 305 } 306 catch (InstanceNotFoundException ex) { 307 // if we are this far this shouldn't happen, but... 308 throw new MBeanInfoRetrievalException("Unable to obtain MBean info for bean [" + this.objectName + 309 "]: it is likely that this bean was unregistered during the proxy creation process", 310 ex); 311 } 312 catch (ReflectionException ex) { 313 throw new MBeanInfoRetrievalException("Unable to read MBean info for bean [ " + this.objectName + "]", ex); 314 } 315 catch (IOException ex) { 316 throw new MBeanInfoRetrievalException("An IOException occurred when communicating with the " + 317 "MBeanServer. It is likely that you are communicating with a remote MBeanServer. " + 318 "Check the inner exception for exact details.", ex); 319 } 320 } 321 322 /** 323 * Return whether this client interceptor has already been prepared, 324 * i.e. has already looked up the server and cached all metadata. 325 */ 326 protected boolean isPrepared() { 327 synchronized (this.preparationMonitor) { 328 return (this.serverToUse != null); 329 } 330 } 331 332 333 /** 334 * Route the invocation to the configured managed resource.. 335 * @param invocation the {@code MethodInvocation} to re-route 336 * @return the value returned as a result of the re-routed invocation 337 * @throws Throwable an invocation error propagated to the user 338 * @see #doInvoke 339 * @see #handleConnectFailure 340 */ 341 @Override 342 public Object invoke(MethodInvocation invocation) throws Throwable { 343 // Lazily connect to MBeanServer if necessary. 344 synchronized (this.preparationMonitor) { 345 if (!isPrepared()) { 346 prepare(); 347 } 348 } 349 try { 350 return doInvoke(invocation); 351 } 352 catch (MBeanConnectFailureException ex) { 353 return handleConnectFailure(invocation, ex); 354 } 355 catch (IOException ex) { 356 return handleConnectFailure(invocation, ex); 357 } 358 } 359 360 /** 361 * Refresh the connection and retry the MBean invocation if possible. 362 * <p>If not configured to refresh on connect failure, this method 363 * simply rethrows the original exception. 364 * @param invocation the invocation that failed 365 * @param ex the exception raised on remote invocation 366 * @return the result value of the new invocation, if succeeded 367 * @throws Throwable an exception raised by the new invocation, 368 * if it failed as well 369 * @see #setRefreshOnConnectFailure 370 * @see #doInvoke 371 */ 372 protected Object handleConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { 373 if (this.refreshOnConnectFailure) { 374 String msg = "Could not connect to JMX server - retrying"; 375 if (logger.isDebugEnabled()) { 376 logger.warn(msg, ex); 377 } 378 else if (logger.isWarnEnabled()) { 379 logger.warn(msg); 380 } 381 prepare(); 382 return doInvoke(invocation); 383 } 384 else { 385 throw ex; 386 } 387 } 388 389 /** 390 * Route the invocation to the configured managed resource. Correctly routes JavaBean property 391 * access to {@code MBeanServerConnection.get/setAttribute} and method invocation to 392 * {@code MBeanServerConnection.invoke}. 393 * @param invocation the {@code MethodInvocation} to re-route 394 * @return the value returned as a result of the re-routed invocation 395 * @throws Throwable an invocation error propagated to the user 396 */ 397 protected Object doInvoke(MethodInvocation invocation) throws Throwable { 398 Method method = invocation.getMethod(); 399 try { 400 Object result = null; 401 if (this.invocationHandler != null) { 402 result = this.invocationHandler.invoke(invocation.getThis(), method, invocation.getArguments()); 403 } 404 else { 405 PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); 406 if (pd != null) { 407 result = invokeAttribute(pd, invocation); 408 } 409 else { 410 result = invokeOperation(method, invocation.getArguments()); 411 } 412 } 413 return convertResultValueIfNecessary(result, new MethodParameter(method, -1)); 414 } 415 catch (MBeanException ex) { 416 throw ex.getTargetException(); 417 } 418 catch (RuntimeMBeanException ex) { 419 throw ex.getTargetException(); 420 } 421 catch (RuntimeErrorException ex) { 422 throw ex.getTargetError(); 423 } 424 catch (RuntimeOperationsException ex) { 425 // This one is only thrown by the JMX 1.2 RI, not by the JDK 1.5 JMX code. 426 RuntimeException rex = ex.getTargetException(); 427 if (rex instanceof RuntimeMBeanException) { 428 throw ((RuntimeMBeanException) rex).getTargetException(); 429 } 430 else if (rex instanceof RuntimeErrorException) { 431 throw ((RuntimeErrorException) rex).getTargetError(); 432 } 433 else { 434 throw rex; 435 } 436 } 437 catch (OperationsException ex) { 438 if (ReflectionUtils.declaresException(method, ex.getClass())) { 439 throw ex; 440 } 441 else { 442 throw new InvalidInvocationException(ex.getMessage()); 443 } 444 } 445 catch (JMException ex) { 446 if (ReflectionUtils.declaresException(method, ex.getClass())) { 447 throw ex; 448 } 449 else { 450 throw new InvocationFailureException("JMX access failed", ex); 451 } 452 } 453 catch (IOException ex) { 454 if (ReflectionUtils.declaresException(method, ex.getClass())) { 455 throw ex; 456 } 457 else { 458 throw new MBeanConnectFailureException("I/O failure during JMX access", ex); 459 } 460 } 461 } 462 463 private Object invokeAttribute(PropertyDescriptor pd, MethodInvocation invocation) 464 throws JMException, IOException { 465 466 String attributeName = JmxUtils.getAttributeName(pd, this.useStrictCasing); 467 MBeanAttributeInfo inf = this.allowedAttributes.get(attributeName); 468 // If no attribute is returned, we know that it is not defined in the 469 // management interface. 470 if (inf == null) { 471 throw new InvalidInvocationException( 472 "Attribute '" + pd.getName() + "' is not exposed on the management interface"); 473 } 474 if (invocation.getMethod().equals(pd.getReadMethod())) { 475 if (inf.isReadable()) { 476 return this.serverToUse.getAttribute(this.objectName, attributeName); 477 } 478 else { 479 throw new InvalidInvocationException("Attribute '" + attributeName + "' is not readable"); 480 } 481 } 482 else if (invocation.getMethod().equals(pd.getWriteMethod())) { 483 if (inf.isWritable()) { 484 this.serverToUse.setAttribute(this.objectName, new Attribute(attributeName, invocation.getArguments()[0])); 485 return null; 486 } 487 else { 488 throw new InvalidInvocationException("Attribute '" + attributeName + "' is not writable"); 489 } 490 } 491 else { 492 throw new IllegalStateException( 493 "Method [" + invocation.getMethod() + "] is neither a bean property getter nor a setter"); 494 } 495 } 496 497 /** 498 * Routes a method invocation (not a property get/set) to the corresponding 499 * operation on the managed resource. 500 * @param method the method corresponding to operation on the managed resource. 501 * @param args the invocation arguments 502 * @return the value returned by the method invocation. 503 */ 504 private Object invokeOperation(Method method, Object[] args) throws JMException, IOException { 505 MethodCacheKey key = new MethodCacheKey(method.getName(), method.getParameterTypes()); 506 MBeanOperationInfo info = this.allowedOperations.get(key); 507 if (info == null) { 508 throw new InvalidInvocationException("Operation '" + method.getName() + 509 "' is not exposed on the management interface"); 510 } 511 String[] signature = null; 512 synchronized (this.signatureCache) { 513 signature = this.signatureCache.get(method); 514 if (signature == null) { 515 signature = JmxUtils.getMethodSignature(method); 516 this.signatureCache.put(method, signature); 517 } 518 } 519 return this.serverToUse.invoke(this.objectName, method.getName(), args, signature); 520 } 521 522 /** 523 * Convert the given result object (from attribute access or operation invocation) 524 * to the specified target class for returning from the proxy method. 525 * @param result the result object as returned by the {@code MBeanServer} 526 * @param parameter the method parameter of the proxy method that's been invoked 527 * @return the converted result object, or the passed-in object if no conversion 528 * is necessary 529 */ 530 protected Object convertResultValueIfNecessary(Object result, MethodParameter parameter) { 531 Class<?> targetClass = parameter.getParameterType(); 532 try { 533 if (result == null) { 534 return null; 535 } 536 if (ClassUtils.isAssignableValue(targetClass, result)) { 537 return result; 538 } 539 if (result instanceof CompositeData) { 540 Method fromMethod = targetClass.getMethod("from", CompositeData.class); 541 return ReflectionUtils.invokeMethod(fromMethod, null, result); 542 } 543 else if (result instanceof CompositeData[]) { 544 CompositeData[] array = (CompositeData[]) result; 545 if (targetClass.isArray()) { 546 return convertDataArrayToTargetArray(array, targetClass); 547 } 548 else if (Collection.class.isAssignableFrom(targetClass)) { 549 Class<?> elementType = 550 ResolvableType.forMethodParameter(parameter).asCollection().resolveGeneric(); 551 if (elementType != null) { 552 return convertDataArrayToTargetCollection(array, targetClass, elementType); 553 } 554 } 555 } 556 else if (result instanceof TabularData) { 557 Method fromMethod = targetClass.getMethod("from", TabularData.class); 558 return ReflectionUtils.invokeMethod(fromMethod, null, result); 559 } 560 else if (result instanceof TabularData[]) { 561 TabularData[] array = (TabularData[]) result; 562 if (targetClass.isArray()) { 563 return convertDataArrayToTargetArray(array, targetClass); 564 } 565 else if (Collection.class.isAssignableFrom(targetClass)) { 566 Class<?> elementType = 567 ResolvableType.forMethodParameter(parameter).asCollection().resolveGeneric(); 568 if (elementType != null) { 569 return convertDataArrayToTargetCollection(array, targetClass, elementType); 570 } 571 } 572 } 573 throw new InvocationFailureException( 574 "Incompatible result value [" + result + "] for target type [" + targetClass.getName() + "]"); 575 } 576 catch (NoSuchMethodException ex) { 577 throw new InvocationFailureException( 578 "Could not obtain 'from(CompositeData)' / 'from(TabularData)' method on target type [" + 579 targetClass.getName() + "] for conversion of MXBean data structure [" + result + "]"); 580 } 581 } 582 583 private Object convertDataArrayToTargetArray(Object[] array, Class<?> targetClass) throws NoSuchMethodException { 584 Class<?> targetType = targetClass.getComponentType(); 585 Method fromMethod = targetType.getMethod("from", array.getClass().getComponentType()); 586 Object resultArray = Array.newInstance(targetType, array.length); 587 for (int i = 0; i < array.length; i++) { 588 Array.set(resultArray, i, ReflectionUtils.invokeMethod(fromMethod, null, array[i])); 589 } 590 return resultArray; 591 } 592 593 private Collection<?> convertDataArrayToTargetCollection(Object[] array, Class<?> collectionType, Class<?> elementType) 594 throws NoSuchMethodException { 595 596 Method fromMethod = elementType.getMethod("from", array.getClass().getComponentType()); 597 Collection<Object> resultColl = CollectionFactory.createCollection(collectionType, Array.getLength(array)); 598 for (int i = 0; i < array.length; i++) { 599 resultColl.add(ReflectionUtils.invokeMethod(fromMethod, null, array[i])); 600 } 601 return resultColl; 602 } 603 604 605 @Override 606 public void destroy() { 607 this.connector.close(); 608 } 609 610 611 /** 612 * Simple wrapper class around a method name and its signature. 613 * Used as the key when caching methods. 614 */ 615 private static final class MethodCacheKey implements Comparable<MethodCacheKey> { 616 617 private final String name; 618 619 private final Class<?>[] parameterTypes; 620 621 /** 622 * Create a new instance of {@code MethodCacheKey} with the supplied 623 * method name and parameter list. 624 * @param name the name of the method 625 * @param parameterTypes the arguments in the method signature 626 */ 627 public MethodCacheKey(String name, Class<?>[] parameterTypes) { 628 this.name = name; 629 this.parameterTypes = (parameterTypes != null ? parameterTypes : new Class<?>[0]); 630 } 631 632 @Override 633 public boolean equals(Object other) { 634 if (this == other) { 635 return true; 636 } 637 MethodCacheKey otherKey = (MethodCacheKey) other; 638 return (this.name.equals(otherKey.name) && Arrays.equals(this.parameterTypes, otherKey.parameterTypes)); 639 } 640 641 @Override 642 public int hashCode() { 643 return this.name.hashCode(); 644 } 645 646 @Override 647 public String toString() { 648 return this.name + "(" + StringUtils.arrayToCommaDelimitedString(this.parameterTypes) + ")"; 649 } 650 651 @Override 652 public int compareTo(MethodCacheKey other) { 653 int result = this.name.compareTo(other.name); 654 if (result != 0) { 655 return result; 656 } 657 if (this.parameterTypes.length < other.parameterTypes.length) { 658 return -1; 659 } 660 if (this.parameterTypes.length > other.parameterTypes.length) { 661 return 1; 662 } 663 for (int i = 0; i < this.parameterTypes.length; i++) { 664 result = this.parameterTypes[i].getName().compareTo(other.parameterTypes[i].getName()); 665 if (result != 0) { 666 return result; 667 } 668 } 669 return 0; 670 } 671 } 672 673}