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}