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