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}