/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.openejb.monitoring; import org.apache.openejb.api.internal.Internal; import org.apache.openejb.api.jmx.Description; import org.apache.openejb.api.jmx.MBean; import org.apache.openejb.api.jmx.ManagedAttribute; import org.apache.openejb.api.jmx.ManagedOperation; import org.apache.openejb.api.jmx.NotificationInfo; import org.apache.openejb.api.jmx.NotificationInfos; import org.apache.openejb.util.LogCategory; import org.apache.openejb.util.Logger; import org.apache.webbeans.config.WebBeansContext; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import javax.management.Attribute; import javax.management.AttributeList; import javax.management.AttributeNotFoundException; import javax.management.Descriptor; import javax.management.DynamicMBean; import javax.management.ImmutableDescriptor; import javax.management.IntrospectionException; import javax.management.InvalidAttributeValueException; import javax.management.MBeanAttributeInfo; import javax.management.MBeanException; import javax.management.MBeanInfo; import javax.management.MBeanNotificationInfo; import javax.management.MBeanOperationInfo; import javax.management.MBeanParameterInfo; import javax.management.MBeanRegistration; import javax.management.MBeanServer; import javax.management.ObjectName; import javax.management.ReflectionException; public class DynamicMBeanWrapper implements DynamicMBean, MBeanRegistration { public static final Logger logger = Logger.getInstance(LogCategory.OPENEJB_DEPLOY, DynamicMBeanWrapper.class); private static final Map<Class<?>, CacheInfo> CACHE = new HashMap<Class<?>, CacheInfo>(); private static final Map<Class<?>, Class<? extends Annotation>> OPENEJB_API_TO_JAVAX = new HashMap<>(); static { final ClassLoader loader = DynamicMBeanWrapper.class.getClassLoader(); try { // all these dont work on java 9 cause of java.management module OPENEJB_API_TO_JAVAX.put(MBean.class, (Class<? extends Annotation>) loader.loadClass("javax.management.MBean")); OPENEJB_API_TO_JAVAX.put(Description.class, (Class<? extends Annotation>) loader.loadClass("javax.management.Description")); OPENEJB_API_TO_JAVAX.put(ManagedOperation.class, (Class<? extends Annotation>) loader.loadClass("javax.management.ManagedOperation")); OPENEJB_API_TO_JAVAX.put(ManagedAttribute.class, (Class<? extends Annotation>) loader.loadClass("javax.management.ManagedAttribute")); OPENEJB_API_TO_JAVAX.put(NotificationInfo.class, (Class<? extends Annotation>) loader.loadClass("javax.management.NotificationInfo")); OPENEJB_API_TO_JAVAX.put(NotificationInfos.class, (Class<? extends Annotation>) loader.loadClass("javax.management.NotificationInfos")); } catch (final ClassNotFoundException | NoClassDefFoundError cnfe) { // ignored } } private final MBeanInfo info; private final Map<String, Method> getters = new HashMap<String, Method>(); private final Map<String, Method> setters = new HashMap<String, Method>(); private final Map<String, Method> operations = new HashMap<String, Method>(); private final Object instance; private final ClassLoader classloader; public DynamicMBeanWrapper(final Object givenInstance) { this(null, givenInstance); } public DynamicMBeanWrapper(final WebBeansContext wc, final Object givenInstance) { Class<?> annotatedMBean = givenInstance.getClass(); // javaassist looses annotation so simply unwrap it if (wc != null) { if (givenInstance.getClass().getName().contains("$Owb")) { // isProxy annotatedMBean = annotatedMBean.getSuperclass(); } } classloader = annotatedMBean.getClassLoader(); instance = givenInstance; final CacheInfo cache = CACHE.get(annotatedMBean); if (cache == null) { final String description; final List<MBeanAttributeInfo> attributeInfos = new ArrayList<MBeanAttributeInfo>(); final List<MBeanOperationInfo> operationInfos = new ArrayList<MBeanOperationInfo>(); final List<MBeanNotificationInfo> notificationInfos = new ArrayList<MBeanNotificationInfo>(); // class final Description classDescription = findAnnotation(annotatedMBean, Description.class); description = getDescription(classDescription, "a MBean built by OpenEJB"); final NotificationInfo notification = findAnnotation(annotatedMBean, NotificationInfo.class); if (notification != null) { final MBeanNotificationInfo notificationInfo = getNotificationInfo(notification); notificationInfos.add(notificationInfo); } final NotificationInfos notifications = findAnnotation(annotatedMBean, NotificationInfos.class); if (notifications != null && notifications.value() != null) { for (final NotificationInfo n : notifications.value()) { final MBeanNotificationInfo notificationInfo = getNotificationInfo(n); notificationInfos.add(notificationInfo); } } // methods for (final Method m : annotatedMBean.getMethods()) { final int modifiers = m.getModifiers(); if (m.getDeclaringClass().equals(Object.class) || !Modifier.isPublic(modifiers) || Modifier.isAbstract(modifiers)) { continue; } if (findAnnotation(m, ManagedAttribute.class) != null) { final String methodName = m.getName(); String attrName = methodName; if ((attrName.startsWith("get") && m.getParameterTypes().length == 0 || attrName.startsWith("set") && m.getParameterTypes().length == 1) && attrName.length() > 3) { attrName = attrName.substring(3); if (attrName.length() > 1) { attrName = Character.toLowerCase(attrName.charAt(0)) + attrName.substring(1); } else { attrName = attrName.toLowerCase(); } } else { logger.warning("ignoring attribute " + m.getName() + " for " + annotatedMBean.getName()); } if (methodName.startsWith("get")) { getters.put(attrName, m); } else if (methodName.startsWith("set")) { setters.put(attrName, m); } } else if (findAnnotation(m, ManagedOperation.class) != null) { operations.put(m.getName(), m); String operationDescr = ""; final Description descr = findAnnotation(m, Description.class); if (descr != null) { operationDescr = getDescription(descr, "-"); } operationInfos.add(newMethodDescriptor(operationDescr, m)); } } for (final Map.Entry<String, Method> e : getters.entrySet()) { final String key = e.getKey(); final Method mtd = e.getValue(); String attrDescr = ""; final Description descr = findAnnotation(mtd, Description.class); if (descr != null) { attrDescr = getDescription(descr, "-"); } try { attributeInfos.add(new MBeanAttributeInfo(key, attrDescr, mtd, setters.get(key))); } catch (final IntrospectionException ex) { logger.warning("can't manage " + key + " for " + mtd.getName(), ex); } } // for updatable but not readable attributes for (final Map.Entry<String, Method> e : setters.entrySet()) { final String key = e.getKey(); if (getters.get(key) != null) { continue; //already done } final Method mtd = e.getValue(); String attrDescr = ""; final Description descr = findAnnotation(mtd, Description.class); if (descr != null) { attrDescr = getDescription(descr, "-"); } try { attributeInfos.add(new MBeanAttributeInfo(key, attrDescr, null, setters.get(key))); } catch (final IntrospectionException ex) { logger.warning("can't manage " + key + " for " + mtd.getName(), ex); } } info = new MBeanInfo(annotatedMBean.getName(), description, attributeInfos.toArray(new MBeanAttributeInfo[attributeInfos.size()]), null, // default constructor is mandatory operationInfos.toArray(new MBeanOperationInfo[operationInfos.size()]), notificationInfos.toArray(new MBeanNotificationInfo[notificationInfos.size()])); if (annotatedMBean.getAnnotation(Internal.class) != null) { CACHE.put(annotatedMBean, new CacheInfo(info, getters, setters, operations)); } } else { info = cache.mBeanInfo; getters.putAll(cache.getters); setters.putAll(cache.setters); operations.putAll(cache.operations); } } private MBeanOperationInfo newMethodDescriptor(final String operationDescr, final Method m) { final MBeanOperationInfo jvmInfo = new MBeanOperationInfo(operationDescr, m); return new MBeanOperationInfo( m.getName(), operationDescr, methodSignature(jvmInfo, m), m.getReturnType().getName(), MBeanOperationInfo.UNKNOWN, jvmInfo.getDescriptor()); // avoid to copy the logic } private static MBeanParameterInfo[] methodSignature(final MBeanOperationInfo jvmInfo, final Method method) { final Class<?>[] classes = method.getParameterTypes(); final Annotation[][] annots = method.getParameterAnnotations(); return parameters(jvmInfo, classes, annots); } static MBeanParameterInfo[] parameters(final MBeanOperationInfo jvmInfo, final Class<?>[] classes, final Annotation[][] annots) { final MBeanParameterInfo[] params = new MBeanParameterInfo[classes.length]; assert classes.length == annots.length; String desc = ""; for (int i = 0; i < classes.length; i++) { final Descriptor d = jvmInfo.getSignature()[i].getDescriptor(); final String pn = "arg" + i; for (final Annotation a : annots[i]) { final Class<? extends Annotation> type = a.annotationType(); if (type.equals(Description.class) || type.equals(OPENEJB_API_TO_JAVAX.get(Description.class))) { desc = getDescription(annotationProxy(a, Description.class), desc); break; } } params[i] = new MBeanParameterInfo(pn, classes[i].getName(), desc, d); } return params; } private <T extends Annotation> T findAnnotation(final Method method, final Class<T> searchedAnnotation) { final T annotation = method.getAnnotation(searchedAnnotation); if (annotation != null) { return annotation; } if (OPENEJB_API_TO_JAVAX.containsKey(searchedAnnotation)) { final Class<? extends Annotation> clazz = OPENEJB_API_TO_JAVAX.get(searchedAnnotation); final Object javaxAnnotation = method.getAnnotation(clazz); if (javaxAnnotation != null) { return annotationProxy(javaxAnnotation, searchedAnnotation); } } return null; } private <T extends Annotation> T findAnnotation(final Class<?> annotatedMBean, final Class<T> searchedAnnotation) { final T annotation = annotatedMBean.getAnnotation(searchedAnnotation); if (annotation != null) { return annotation; } if (OPENEJB_API_TO_JAVAX.containsKey(searchedAnnotation)) { final Class<? extends Annotation> clazz = OPENEJB_API_TO_JAVAX.get(searchedAnnotation); final Object javaxAnnotation = annotatedMBean.getAnnotation(clazz); if (javaxAnnotation != null) { return annotationProxy(javaxAnnotation, searchedAnnotation); } } return null; } private static <T extends Annotation> T annotationProxy(final Object javaxAnnotation, final Class<T> clazz) { return (T) Proxy.newProxyInstance(DynamicMBeanWrapper.class.getClassLoader(), new Class<?>[]{clazz}, new AnnotationHandler(javaxAnnotation)); } private static MBeanNotificationInfo getNotificationInfo(final NotificationInfo n) { final String description = getDescription(n.description(), "-"); return new MBeanNotificationInfo(n.types(), n.notificationClass().getName(), description, new ImmutableDescriptor(n.descriptorFields())); } private static String getDescription(final Description d, final String defaultValue) { if (d != null) { if (d.bundleBaseName() != null && d.key() != null) { try { return ResourceBundle .getBundle(d.bundleBaseName()) .getString(d.key()); } catch (final RuntimeException re) { return d.value(); } } else { return d.value(); } } return defaultValue; } @Override public MBeanInfo getMBeanInfo() { return info; } @Override public Object getAttribute(final String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException { if (getters.containsKey(attribute)) { final ClassLoader oldCl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classloader); try { return getters.get(attribute).invoke(instance); } catch (final IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { logger.error("can't get " + attribute + " value", e); } finally { Thread.currentThread().setContextClassLoader(oldCl); } } throw new AttributeNotFoundException(); } @Override public void setAttribute(final Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { if (setters.containsKey(attribute.getName())) { final ClassLoader oldCl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classloader); try { setters.get(attribute.getName()).invoke(instance, attribute.getValue()); } catch (final IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { logger.error("can't set " + attribute + " value", e); } finally { Thread.currentThread().setContextClassLoader(oldCl); } } else { throw new AttributeNotFoundException(); } } @Override public AttributeList getAttributes(final String[] attributes) { final AttributeList list = new AttributeList(); for (final String n : attributes) { try { list.add(new Attribute(n, getAttribute(n))); } catch (final Exception ignore) { // no-op } } return list; } @Override public AttributeList setAttributes(final AttributeList attributes) { final AttributeList list = new AttributeList(); for (final Object o : attributes) { final Attribute attr = (Attribute) o; try { setAttribute(attr); list.add(attr); } catch (final Exception ignore) { // no-op } } return list; } @Override public Object invoke(final String actionName, final Object[] params, final String[] signature) throws MBeanException, ReflectionException { if (operations.containsKey(actionName)) { final ClassLoader oldCl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classloader); try { return operations.get(actionName).invoke(instance, params); } catch (final IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { logger.error(actionName + "can't be invoked", e); } finally { Thread.currentThread().setContextClassLoader(oldCl); } } throw new MBeanException(new IllegalArgumentException(), actionName + " doesn't exist"); } @Override public ObjectName preRegister(final MBeanServer server, final ObjectName name) throws Exception { final Thread thread = Thread.currentThread(); final ClassLoader oldCl = thread.getContextClassLoader(); thread.setContextClassLoader(classloader); try { if (MBeanRegistration.class.isInstance(instance)) { return MBeanRegistration.class.cast(instance).preRegister(server, name); } return name; } finally { thread.setContextClassLoader(oldCl); } } @Override public void postRegister(final Boolean registrationDone) { final Thread thread = Thread.currentThread(); final ClassLoader oldCl = thread.getContextClassLoader(); thread.setContextClassLoader(classloader); try { if (MBeanRegistration.class.isInstance(instance)) { MBeanRegistration.class.cast(instance).postRegister(registrationDone); } } finally { thread.setContextClassLoader(oldCl); } } @Override public void preDeregister() throws Exception { final Thread thread = Thread.currentThread(); final ClassLoader oldCl = thread.getContextClassLoader(); thread.setContextClassLoader(classloader); try { if (MBeanRegistration.class.isInstance(instance)) { MBeanRegistration.class.cast(instance).preDeregister(); } } finally { thread.setContextClassLoader(oldCl); } } @Override public void postDeregister() { final Thread thread = Thread.currentThread(); final ClassLoader oldCl = thread.getContextClassLoader(); thread.setContextClassLoader(classloader); try { if (MBeanRegistration.class.isInstance(instance)) { MBeanRegistration.class.cast(instance).postDeregister(); } } finally { thread.setContextClassLoader(oldCl); } } private static class AnnotationHandler implements InvocationHandler { private final Object delegate; public AnnotationHandler(final Object javaxAnnotation) { delegate = javaxAnnotation; } @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { Object result = null; for (final Method mtd : delegate.getClass().getMethods()) { // simple heurisitc which should be enough if (mtd.getName().equals(method.getName())) { result = mtd.invoke(delegate, args); break; } } if (result == null) { return null; } if (result.getClass().isArray()) { final Object[] array = (Object[]) result; if (array.length == 0 || !OPENEJB_API_TO_JAVAX.containsValue(array[0].getClass())) { return array; } final Object[] translated = new Object[array.length]; for (int i = 0; i < translated.length; i++) { translated[i] = annotationProxy(array[i], OPENEJB_API_TO_JAVAX.get(array[i].getClass())); } } return result; } } private static final class CacheInfo { public final MBeanInfo mBeanInfo; public final Map<String, Method> getters; public final Map<String, Method> setters; public final Map<String, Method> operations; private CacheInfo(final MBeanInfo mBeanInfo, final Map<String, Method> getters, final Map<String, Method> setters, final Map<String, Method> operations) { this.mBeanInfo = mBeanInfo; this.getters = getters; this.setters = setters; this.operations = operations; } } }