View Javadoc
1   /*
2    * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package javax.management;
27  
28  import com.sun.jmx.mbeanserver.MXBeanProxy;
29  
30  import java.lang.ref.WeakReference;
31  import java.lang.reflect.InvocationHandler;
32  import java.lang.reflect.Method;
33  import java.lang.reflect.Proxy;
34  import java.util.Arrays;
35  import java.util.WeakHashMap;
36  
37  /**
38   * <p>{@link InvocationHandler} that forwards methods in an MBean's
39   * management interface through the MBean server to the MBean.</p>
40   *
41   * <p>Given an {@link MBeanServerConnection}, the {@link ObjectName}
42   * of an MBean within that MBean server, and a Java interface
43   * <code>Intf</code> that describes the management interface of the
44   * MBean using the patterns for a Standard MBean or an MXBean, this
45   * class can be used to construct a proxy for the MBean.  The proxy
46   * implements the interface <code>Intf</code> such that all of its
47   * methods are forwarded through the MBean server to the MBean.</p>
48   *
49   * <p>If the {@code InvocationHandler} is for an MXBean, then the parameters of
50   * a method are converted from the type declared in the MXBean
51   * interface into the corresponding mapped type, and the return value
52   * is converted from the mapped type into the declared type.  For
53   * example, with the method<br>
54  
55   * {@code public List<String> reverse(List<String> list);}<br>
56  
57   * and given that the mapped type for {@code List<String>} is {@code
58   * String[]}, a call to {@code proxy.reverse(someList)} will convert
59   * {@code someList} from a {@code List<String>} to a {@code String[]},
60   * call the MBean operation {@code reverse}, then convert the returned
61   * {@code String[]} into a {@code List<String>}.</p>
62   *
63   * <p>The method Object.toString(), Object.hashCode(), or
64   * Object.equals(Object), when invoked on a proxy using this
65   * invocation handler, is forwarded to the MBean server as a method on
66   * the proxied MBean only if it appears in one of the proxy's
67   * interfaces.  For a proxy created with {@link
68   * JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class)
69   * JMX.newMBeanProxy} or {@link
70   * JMX#newMXBeanProxy(MBeanServerConnection, ObjectName, Class)
71   * JMX.newMXBeanProxy}, this means that the method must appear in the
72   * Standard MBean or MXBean interface.  Otherwise these methods have
73   * the following behavior:
74   * <ul>
75   * <li>toString() returns a string representation of the proxy
76   * <li>hashCode() returns a hash code for the proxy such
77   * that two equal proxies have the same hash code
78   * <li>equals(Object)
79   * returns true if and only if the Object argument is of the same
80   * proxy class as this proxy, with an MBeanServerInvocationHandler
81   * that has the same MBeanServerConnection and ObjectName; if one
82   * of the {@code MBeanServerInvocationHandler}s was constructed with
83   * a {@code Class} argument then the other must have been constructed
84   * with the same {@code Class} for {@code equals} to return true.
85   * </ul>
86   *
87   * @since 1.5
88   */
89  public class MBeanServerInvocationHandler implements InvocationHandler {
90      /**
91       * <p>Invocation handler that forwards methods through an MBean
92       * server to a Standard MBean.  This constructor may be called
93       * instead of relying on {@link
94       * JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class)
95       * JMX.newMBeanProxy}, for instance if you need to supply a
96       * different {@link ClassLoader} to {@link Proxy#newProxyInstance
97       * Proxy.newProxyInstance}.</p>
98       *
99       * <p>This constructor is not appropriate for an MXBean.  Use
100      * {@link #MBeanServerInvocationHandler(MBeanServerConnection,
101      * ObjectName, boolean)} for that.  This constructor is equivalent
102      * to {@code new MBeanServerInvocationHandler(connection,
103      * objectName, false)}.</p>
104      *
105      * @param connection the MBean server connection through which all
106      * methods of a proxy using this handler will be forwarded.
107      *
108      * @param objectName the name of the MBean within the MBean server
109      * to which methods will be forwarded.
110      */
111     public MBeanServerInvocationHandler(MBeanServerConnection connection,
112                                         ObjectName objectName) {
113 
114         this(connection, objectName, false);
115     }
116 
117     /**
118      * <p>Invocation handler that can forward methods through an MBean
119      * server to a Standard MBean or MXBean.  This constructor may be called
120      * instead of relying on {@link
121      * JMX#newMXBeanProxy(MBeanServerConnection, ObjectName, Class)
122      * JMX.newMXBeanProxy}, for instance if you need to supply a
123      * different {@link ClassLoader} to {@link Proxy#newProxyInstance
124      * Proxy.newProxyInstance}.</p>
125      *
126      * @param connection the MBean server connection through which all
127      * methods of a proxy using this handler will be forwarded.
128      *
129      * @param objectName the name of the MBean within the MBean server
130      * to which methods will be forwarded.
131      *
132      * @param isMXBean if true, the proxy is for an {@link MXBean}, and
133      * appropriate mappings will be applied to method parameters and return
134      * values.
135      *
136      * @since 1.6
137      */
138     public MBeanServerInvocationHandler(MBeanServerConnection connection,
139                                         ObjectName objectName,
140                                         boolean isMXBean) {
141         if (connection == null) {
142             throw new IllegalArgumentException("Null connection");
143         }
144         if (objectName == null) {
145             throw new IllegalArgumentException("Null object name");
146         }
147         this.connection = connection;
148         this.objectName = objectName;
149         this.isMXBean = isMXBean;
150     }
151 
152     /**
153      * <p>The MBean server connection through which the methods of
154      * a proxy using this handler are forwarded.</p>
155      *
156      * @return the MBean server connection.
157      *
158      * @since 1.6
159      */
160     public MBeanServerConnection getMBeanServerConnection() {
161         return connection;
162     }
163 
164     /**
165      * <p>The name of the MBean within the MBean server to which methods
166      * are forwarded.
167      *
168      * @return the object name.
169      *
170      * @since 1.6
171      */
172     public ObjectName getObjectName() {
173         return objectName;
174     }
175 
176     /**
177      * <p>If true, the proxy is for an MXBean, and appropriate mappings
178      * are applied to method parameters and return values.
179      *
180      * @return whether the proxy is for an MXBean.
181      *
182      * @since 1.6
183      */
184     public boolean isMXBean() {
185         return isMXBean;
186     }
187 
188     /**
189      * <p>Return a proxy that implements the given interface by
190      * forwarding its methods through the given MBean server to the
191      * named MBean.  As of 1.6, the methods {@link
192      * JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class)} and
193      * {@link JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class,
194      * boolean)} are preferred to this method.</p>
195      *
196      * <p>This method is equivalent to {@link Proxy#newProxyInstance
197      * Proxy.newProxyInstance}<code>(interfaceClass.getClassLoader(),
198      * interfaces, handler)</code>.  Here <code>handler</code> is the
199      * result of {@link #MBeanServerInvocationHandler new
200      * MBeanServerInvocationHandler(connection, objectName)}, and
201      * <code>interfaces</code> is an array that has one element if
202      * <code>notificationBroadcaster</code> is false and two if it is
203      * true.  The first element of <code>interfaces</code> is
204      * <code>interfaceClass</code> and the second, if present, is
205      * <code>NotificationEmitter.class</code>.
206      *
207      * @param connection the MBean server to forward to.
208      * @param objectName the name of the MBean within
209      * <code>connection</code> to forward to.
210      * @param interfaceClass the management interface that the MBean
211      * exports, which will also be implemented by the returned proxy.
212      * @param notificationBroadcaster make the returned proxy
213      * implement {@link NotificationEmitter} by forwarding its methods
214      * via <code>connection</code>. A call to {@link
215      * NotificationBroadcaster#addNotificationListener} on the proxy will
216      * result in a call to {@link
217      * MBeanServerConnection#addNotificationListener(ObjectName,
218      * NotificationListener, NotificationFilter, Object)}, and likewise
219      * for the other methods of {@link NotificationBroadcaster} and {@link
220      * NotificationEmitter}.
221      *
222      * @param <T> allows the compiler to know that if the {@code
223      * interfaceClass} parameter is {@code MyMBean.class}, for example,
224      * then the return type is {@code MyMBean}.
225      *
226      * @return the new proxy instance.
227      *
228      * @see JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class, boolean)
229      */
230     public static <T> T newProxyInstance(MBeanServerConnection connection,
231                                          ObjectName objectName,
232                                          Class<T> interfaceClass,
233                                          boolean notificationBroadcaster) {
234         return JMX.newMBeanProxy(connection, objectName, interfaceClass, notificationBroadcaster);
235     }
236 
237     public Object invoke(Object proxy, Method method, Object[] args)
238             throws Throwable {
239         final Class<?> methodClass = method.getDeclaringClass();
240 
241         if (methodClass.equals(NotificationBroadcaster.class)
242             || methodClass.equals(NotificationEmitter.class))
243             return invokeBroadcasterMethod(proxy, method, args);
244 
245         // local or not: equals, toString, hashCode
246         if (shouldDoLocally(proxy, method))
247             return doLocally(proxy, method, args);
248 
249         try {
250             if (isMXBean()) {
251                 MXBeanProxy p = findMXBeanProxy(methodClass);
252                 return p.invoke(connection, objectName, method, args);
253             } else {
254                 final String methodName = method.getName();
255                 final Class<?>[] paramTypes = method.getParameterTypes();
256                 final Class<?> returnType = method.getReturnType();
257 
258                 /* Inexplicably, InvocationHandler specifies that args is null
259                    when the method takes no arguments rather than a
260                    zero-length array.  */
261                 final int nargs = (args == null) ? 0 : args.length;
262 
263                 if (methodName.startsWith("get")
264                     && methodName.length() > 3
265                     && nargs == 0
266                     && !returnType.equals(Void.TYPE)) {
267                     return connection.getAttribute(objectName,
268                         methodName.substring(3));
269                 }
270 
271                 if (methodName.startsWith("is")
272                     && methodName.length() > 2
273                     && nargs == 0
274                     && (returnType.equals(Boolean.TYPE)
275                     || returnType.equals(Boolean.class))) {
276                     return connection.getAttribute(objectName,
277                         methodName.substring(2));
278                 }
279 
280                 if (methodName.startsWith("set")
281                     && methodName.length() > 3
282                     && nargs == 1
283                     && returnType.equals(Void.TYPE)) {
284                     Attribute attr = new Attribute(methodName.substring(3), args[0]);
285                     connection.setAttribute(objectName, attr);
286                     return null;
287                 }
288 
289                 final String[] signature = new String[paramTypes.length];
290                 for (int i = 0; i < paramTypes.length; i++)
291                     signature[i] = paramTypes[i].getName();
292                 return connection.invoke(objectName, methodName,
293                                          args, signature);
294             }
295         } catch (MBeanException e) {
296             throw e.getTargetException();
297         } catch (RuntimeMBeanException re) {
298             throw re.getTargetException();
299         } catch (RuntimeErrorException rre) {
300             throw rre.getTargetError();
301         }
302         /* The invoke may fail because it can't get to the MBean, with
303            one of the these exceptions declared by
304            MBeanServerConnection.invoke:
305            - RemoteException: can't talk to MBeanServer;
306            - InstanceNotFoundException: objectName is not registered;
307            - ReflectionException: objectName is registered but does not
308              have the method being invoked.
309            In all of these cases, the exception will be wrapped by the
310            proxy mechanism in an UndeclaredThrowableException unless
311            it happens to be declared in the "throws" clause of the
312            method being invoked on the proxy.
313          */
314     }
315 
316     private static MXBeanProxy findMXBeanProxy(Class<?> mxbeanInterface) {
317         synchronized (mxbeanProxies) {
318             WeakReference<MXBeanProxy> proxyRef =
319                     mxbeanProxies.get(mxbeanInterface);
320             MXBeanProxy p = (proxyRef == null) ? null : proxyRef.get();
321             if (p == null) {
322                 try {
323                     p = new MXBeanProxy(mxbeanInterface);
324                 } catch (IllegalArgumentException e) {
325                     String msg = "Cannot make MXBean proxy for " +
326                             mxbeanInterface.getName() + ": " + e.getMessage();
327                     IllegalArgumentException iae =
328                             new IllegalArgumentException(msg, e.getCause());
329                     iae.setStackTrace(e.getStackTrace());
330                     throw iae;
331                 }
332                 mxbeanProxies.put(mxbeanInterface,
333                                   new WeakReference<MXBeanProxy>(p));
334             }
335             return p;
336         }
337     }
338     private static final WeakHashMap<Class<?>, WeakReference<MXBeanProxy>>
339             mxbeanProxies = new WeakHashMap<Class<?>, WeakReference<MXBeanProxy>>();
340 
341     private Object invokeBroadcasterMethod(Object proxy, Method method,
342                                            Object[] args) throws Exception {
343         final String methodName = method.getName();
344         final int nargs = (args == null) ? 0 : args.length;
345 
346         if (methodName.equals("addNotificationListener")) {
347             /* The various throws of IllegalArgumentException here
348                should not happen, since we know what the methods in
349                NotificationBroadcaster and NotificationEmitter
350                are.  */
351             if (nargs != 3) {
352                 final String msg =
353                     "Bad arg count to addNotificationListener: " + nargs;
354                 throw new IllegalArgumentException(msg);
355             }
356             /* Other inconsistencies will produce ClassCastException
357                below.  */
358 
359             NotificationListener listener = (NotificationListener) args[0];
360             NotificationFilter filter = (NotificationFilter) args[1];
361             Object handback = args[2];
362             connection.addNotificationListener(objectName,
363                                                listener,
364                                                filter,
365                                                handback);
366             return null;
367 
368         } else if (methodName.equals("removeNotificationListener")) {
369 
370             /* NullPointerException if method with no args, but that
371                shouldn't happen because removeNL does have args.  */
372             NotificationListener listener = (NotificationListener) args[0];
373 
374             switch (nargs) {
375             case 1:
376                 connection.removeNotificationListener(objectName, listener);
377                 return null;
378 
379             case 3:
380                 NotificationFilter filter = (NotificationFilter) args[1];
381                 Object handback = args[2];
382                 connection.removeNotificationListener(objectName,
383                                                       listener,
384                                                       filter,
385                                                       handback);
386                 return null;
387 
388             default:
389                 final String msg =
390                     "Bad arg count to removeNotificationListener: " + nargs;
391                 throw new IllegalArgumentException(msg);
392             }
393 
394         } else if (methodName.equals("getNotificationInfo")) {
395 
396             if (args != null) {
397                 throw new IllegalArgumentException("getNotificationInfo has " +
398                                                    "args");
399             }
400 
401             MBeanInfo info = connection.getMBeanInfo(objectName);
402             return info.getNotifications();
403 
404         } else {
405             throw new IllegalArgumentException("Bad method name: " +
406                                                methodName);
407         }
408     }
409 
410     private boolean shouldDoLocally(Object proxy, Method method) {
411         final String methodName = method.getName();
412         if ((methodName.equals("hashCode") || methodName.equals("toString"))
413             && method.getParameterTypes().length == 0
414             && isLocal(proxy, method))
415             return true;
416         if (methodName.equals("equals")
417             && Arrays.equals(method.getParameterTypes(),
418                              new Class<?>[] {Object.class})
419             && isLocal(proxy, method))
420             return true;
421         return false;
422     }
423 
424     private Object doLocally(Object proxy, Method method, Object[] args) {
425         final String methodName = method.getName();
426 
427         if (methodName.equals("equals")) {
428 
429             if (this == args[0]) {
430                 return true;
431             }
432 
433             if (!(args[0] instanceof Proxy)) {
434                 return false;
435             }
436 
437             final InvocationHandler ihandler =
438                 Proxy.getInvocationHandler(args[0]);
439 
440             if (ihandler == null ||
441                 !(ihandler instanceof MBeanServerInvocationHandler)) {
442                 return false;
443             }
444 
445             final MBeanServerInvocationHandler handler =
446                 (MBeanServerInvocationHandler)ihandler;
447 
448             return connection.equals(handler.connection) &&
449                 objectName.equals(handler.objectName) &&
450                 proxy.getClass().equals(args[0].getClass());
451         } else if (methodName.equals("toString")) {
452             return (isMXBean() ? "MX" : "M") + "BeanProxy(" +
453                 connection + "[" + objectName + "])";
454         } else if (methodName.equals("hashCode")) {
455             return objectName.hashCode()+connection.hashCode();
456         }
457 
458         throw new RuntimeException("Unexpected method name: " + methodName);
459     }
460 
461     private static boolean isLocal(Object proxy, Method method) {
462         final Class<?>[] interfaces = proxy.getClass().getInterfaces();
463         if(interfaces == null) {
464             return true;
465         }
466 
467         final String methodName = method.getName();
468         final Class<?>[] params = method.getParameterTypes();
469         for (Class<?> intf : interfaces) {
470             try {
471                 intf.getMethod(methodName, params);
472                 return false; // found method in one of our interfaces
473             } catch (NoSuchMethodException nsme) {
474                 // OK.
475             }
476         }
477 
478         return true;  // did not find in any interface
479     }
480 
481     private final MBeanServerConnection connection;
482     private final ObjectName objectName;
483     private final boolean isMXBean;
484 }