/* * Copyright (C) 2015 SoftIndex LLC. * * Licensed 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 io.datakernel.jmx; import io.datakernel.eventloop.Eventloop; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.management.*; import javax.management.openmbean.OpenType; import javax.management.openmbean.SimpleType; import java.lang.annotation.Annotation; import java.lang.reflect.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import static io.datakernel.jmx.ReflectionUtils.*; import static io.datakernel.util.Preconditions.*; import static java.lang.Math.ceil; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; public final class JmxMBeans implements DynamicMBeanFactory { private static final Logger logger = LoggerFactory.getLogger(JmxMBeans.class); // refreshing jmx public static final double DEFAULT_REFRESH_PERIOD_IN_SECONDS = 1.0; // one second public static final int MAX_JMX_REFRESHES_PER_ONE_CYCLE_DEFAULT = 500; private int maxJmxRefreshesPerOneCycle; private long specifiedRefreshPeriod; private final Map<Eventloop, List<JmxRefreshable>> eventloopToJmxRefreshables = new ConcurrentHashMap<>(); private final Map<Eventloop, Integer> refreshableStatsCounts = new ConcurrentHashMap<>(); private final Map<Eventloop, Integer> effectiveRefreshPeriods = new ConcurrentHashMap<>(); private static final JmxReducer<?> DEFAULT_REDUCER = new JmxReducers.JmxReducerDistinct(); // JmxStats creator methods private static final String CREATE = "create"; private static final String CREATE_ACCUMULATOR = "createAccumulator"; private static final JmxMBeans INSTANCE_WITH_DEFAULT_REFRESH_PERIOD = new JmxMBeans(DEFAULT_REFRESH_PERIOD_IN_SECONDS, MAX_JMX_REFRESHES_PER_ONE_CYCLE_DEFAULT); // region constructor and factory methods private JmxMBeans(double refreshPeriod, int maxJmxRefreshesPerOneCycle) { this.specifiedRefreshPeriod = secondsToMillis(refreshPeriod); this.maxJmxRefreshesPerOneCycle = maxJmxRefreshesPerOneCycle; } public static JmxMBeans factory() { return INSTANCE_WITH_DEFAULT_REFRESH_PERIOD; } public static JmxMBeans factory(double refreshPeriod, int maxJmxRefreshesPerOneCycle) { return new JmxMBeans(refreshPeriod, maxJmxRefreshesPerOneCycle); } // endregion // region exportable stats for JmxRegistry public Map<Eventloop, Integer> getRefreshableStatsCounts() { return refreshableStatsCounts; } public Map<Eventloop, Integer> getEffectiveRefreshPeriods() { return effectiveRefreshPeriods; } public double getSpecifiedRefreshPeriod() { return (specifiedRefreshPeriod / 1000.0); } public void setRefreshPeriod(double refreshPeriod) { this.specifiedRefreshPeriod = secondsToMillis(refreshPeriod); } public int getMaxJmxRefreshesPerOneCycle() { return maxJmxRefreshesPerOneCycle; } public void setMaxJmxRefreshesPerOneCycle(int maxJmxRefreshesPerOneCycle) { this.maxJmxRefreshesPerOneCycle = maxJmxRefreshesPerOneCycle; } // endregion @Override public DynamicMBean createFor(List<?> monitorables, MBeanSettings setting, boolean enableRefresh) { checkNotNull(monitorables); checkArgument(monitorables.size() > 0); checkArgument(!listContainsNullValues(monitorables), "monitorable can not be null"); checkArgument(allObjectsAreOfSameType(monitorables)); Object firstMBean = monitorables.get(0); Class<?> mbeanClass = firstMBean.getClass(); boolean isRefreshEnabled = enableRefresh; List<MBeanWrapper> mbeanWrappers = new ArrayList<>(monitorables.size()); if (ConcurrentJmxMBean.class.isAssignableFrom(mbeanClass)) { checkArgument(monitorables.size() == 1, "ConcurrentJmxMBeans cannot be used in pool. " + "Only EventloopJmxMBeans can be used in pool"); isRefreshEnabled = false; mbeanWrappers.add(new ConcurrentJmxMBeanWrapper((ConcurrentJmxMBean) monitorables.get(0))); } else if (EventloopJmxMBean.class.isAssignableFrom(mbeanClass)) { for (Object monitorable : monitorables) { mbeanWrappers.add(new EventloopJmxMBeanWrapper((EventloopJmxMBean) monitorable)); } } else { throw new IllegalArgumentException("MBeans should implement either ConcurrentJmxMBean " + "or EventloopJmxMBean interface"); } AttributeNodeForPojo rootNode = createAttributesTree(mbeanClass); rootNode.hideNullPojos(monitorables); for (String included : setting.getIncludedOptionals()) { rootNode.setVisible(included); } // TODO(vmykhalko): check in JmxRegistry that modifiers are applied only once in case of workers and pool registartion for (String attrName : setting.getModifiers().keySet()) { AttributeModifier<?> modifier = setting.getModifiers().get(attrName); try { rootNode.applyModifier(attrName, modifier, monitorables); } catch (ClassCastException cce) { throw new IllegalArgumentException("Cannot apply modifier \"" + modifier.getClass().getName() + "\" for attribute \"" + attrName + "\": " + cce.toString()); } } MBeanInfo mBeanInfo = createMBeanInfo(rootNode, mbeanClass, isRefreshEnabled); Map<OperationKey, Method> opkeyToMethod = fetchOpkeyToMethod(mbeanClass); DynamicMBeanAggregator mbean = new DynamicMBeanAggregator( mBeanInfo, mbeanWrappers, rootNode, opkeyToMethod, isRefreshEnabled ); // TODO(vmykhalko): maybe try to get all attributes and log warn message in case of exception? (to prevent potential errors during viewing jmx stats using jconsole) // tryGetAllAttributes(mbean); if (isRefreshEnabled) { handleJmxRefreshables(mbeanWrappers, rootNode); } return mbean; } // region building tree of AttributeNodes private static List<AttributeNode> createNodesFor(Class<?> clazz, Class<?> mbeanClass, String[] includedOptionalAttrs, Method getter) { Set<String> includedOptionals = new HashSet<>(asList(includedOptionalAttrs)); List<AttributeDescriptor> attrDescriptors = fetchAttributeDescriptors(clazz); List<AttributeNode> attrNodes = new ArrayList<>(); for (AttributeDescriptor descriptor : attrDescriptors) { check(descriptor.getGetter() != null, "@JmxAttribute \"%s\" does not have getter", descriptor.getName()); String attrName; Method attrGetter = descriptor.getGetter(); JmxAttribute attrAnnotation = attrGetter.getAnnotation(JmxAttribute.class); String attrAnnotationName = attrAnnotation.name(); if (attrAnnotationName.equals(JmxAttribute.USE_GETTER_NAME)) { attrName = extractFieldNameFromGetter(attrGetter); } else { attrName = attrAnnotationName; } checkArgument(!attrName.contains("_"), "@JmxAttribute with name \"%s\" contains underscores", attrName); String attrDescription = null; if (!attrAnnotation.description().equals(JmxAttribute.NO_DESCRIPTION)) { attrDescription = attrAnnotation.description(); } boolean included = attrAnnotation.optional() ? includedOptionals.contains(attrName) : true; includedOptionals.remove(attrName); Type type = attrGetter.getGenericReturnType(); Method attrSetter = descriptor.getSetter(); AttributeNode attrNode = createAttributeNodeFor(attrName, attrDescription, type, included, attrAnnotation, attrGetter, attrSetter, mbeanClass); attrNodes.add(attrNode); } if (includedOptionals.size() > 0) { // in this case getter cannot be null throw new RuntimeException(format("Error in \"extraSubAttributes\" parameter in @JmxAnnotation" + " on %s.%s(). There is no field \"%s\" in %s.", getter.getDeclaringClass().getName(), getter.getName(), includedOptionals.iterator().next(), getter.getReturnType().getName())); } return attrNodes; } private static List<AttributeDescriptor> fetchAttributeDescriptors(Class<?> clazz) { Map<String, AttributeDescriptor> nameToAttr = new HashMap<>(); for (Method method : clazz.getMethods()) { if (method.isAnnotationPresent(JmxAttribute.class)) { if (isGetter(method)) { processGetter(nameToAttr, method); } else if (isSetter(method)) { processSetter(nameToAttr, method); } else { throw new RuntimeException(format("Method \"%s\" of class \"%s\" is annotated with @JmxAnnotation " + "but is neither getter nor setter", method.getName(), method.getClass().getName()) ); } } } return new ArrayList<>(nameToAttr.values()); } private static void processGetter(Map<String, AttributeDescriptor> nameToAttr, Method getter) { String name = extractFieldNameFromGetter(getter); Type attrType = getter.getReturnType(); if (nameToAttr.containsKey(name)) { AttributeDescriptor previousDescriptor = nameToAttr.get(name); check(previousDescriptor.getGetter() == null, "More that one getter with name" + getter.getName()); check(previousDescriptor.getType().equals(attrType), "Getter with name \"%s\" has different type than appropriate setter", getter.getName()); nameToAttr.put(name, new AttributeDescriptor( name, attrType, getter, previousDescriptor.getSetter())); } else { nameToAttr.put(name, new AttributeDescriptor(name, attrType, getter, null)); } } private static void processSetter(Map<String, AttributeDescriptor> nameToAttr, Method setter) { checkArgument(isSimpleType(setter.getParameterTypes()[0]), "Setters are allowed only on SimpleType attributes." + " But setter \"%s\" is not SimpleType setter", setter.getName()); String name = extractFieldNameFromSetter(setter); Type attrType = setter.getParameterTypes()[0]; if (nameToAttr.containsKey(name)) { AttributeDescriptor previousDescriptor = nameToAttr.get(name); check(previousDescriptor.getSetter() == null, "More that one setter with name" + setter.getName()); check(previousDescriptor.getType().equals(attrType), "Setter with name \"%s\" has different type than appropriate getter", setter.getName()); nameToAttr.put(name, new AttributeDescriptor( name, attrType, previousDescriptor.getGetter(), setter)); } else { nameToAttr.put(name, new AttributeDescriptor(name, attrType, null, setter)); } } @SuppressWarnings("unchecked") private static AttributeNode createAttributeNodeFor(String attrName, String attrDescription, Type attrType, boolean included, JmxAttribute attrAnnotation, Method getter, Method setter, Class<?> mbeanClass) { ValueFetcher defaultFetcher = getter != null ? new ValueFetcherFromGetter(getter) : new ValueFetcherDirect(); if (attrType instanceof Class) { // 3 cases: simple-type, JmxRefreshableStats, POJO Class<?> returnClass = (Class<?>) attrType; if (isSimpleType(returnClass)) { JmxReducer<?> reducer; try { reducer = fetchReducerFrom(getter); } catch (Exception e) { throw new RuntimeException(e); } return new AttributeNodeForSimpleType( attrName, attrDescription, included, defaultFetcher, setter, returnClass, reducer ); } else if (isThrowable(returnClass)) { return new AttributeNodeForThrowable(attrName, attrDescription, included, defaultFetcher); } else if (returnClass.isArray()) { Class<?> elementType = returnClass.getComponentType(); checkNotNull(getter, "Arrays can be used only directly in POJO, JmxRefreshableStats or JmxMBeans"); ValueFetcher fetcher = new ValueFetcherFromGetterArrayAdapter(getter); return createListAttributeNodeFor(attrName, attrDescription, included, fetcher, elementType, mbeanClass); } else if (isJmxStats(returnClass)) { // JmxRefreshableStats case checkJmxStatsAreValid(returnClass, mbeanClass, getter); String[] extraSubAttributes = attrAnnotation != null ? attrAnnotation.extraSubAttributes() : new String[0]; List<AttributeNode> subNodes = createNodesFor(returnClass, mbeanClass, extraSubAttributes, getter); if (subNodes.size() == 0) { throw new IllegalArgumentException(format( "JmxRefreshableStats of type \"%s\" does not have JmxAttributes", returnClass.getName())); } return new AttributeNodeForPojo(attrName, attrDescription, included, defaultFetcher, createReducerForJmxStats(returnClass), subNodes); } else { String[] extraSubAttributes = attrAnnotation != null ? attrAnnotation.extraSubAttributes() : new String[0]; List<AttributeNode> subNodes = createNodesFor(returnClass, mbeanClass, extraSubAttributes, getter); if (subNodes.size() == 0) { return new AttributeNodeForAnyOtherType(attrName, attrDescription, included, defaultFetcher); } else { // POJO case JmxReducer<?> reducer; try { reducer = fetchReducerFrom(getter); } catch (Exception e) { throw new RuntimeException(e); } if (reducer.getClass() == JmxAttribute.DEFAULT_REDUCER) { return new AttributeNodeForPojo( attrName, attrDescription, included, defaultFetcher, null, subNodes); } else { return new AttributeNodeForPojo(attrName, attrDescription, included, defaultFetcher, reducer, subNodes); } } } } else if (attrType instanceof ParameterizedType) { return createNodeForParametrizedType( attrName, attrDescription, (ParameterizedType) attrType, included, getter, mbeanClass ); } else { throw new RuntimeException(); } } @SuppressWarnings("unchecked") private static JmxReducer<?> createReducerForJmxStats(final Class<?> jmxStatsClass) { return new JmxReducer<Object>() { @Override public Object reduce(List<?> sources) { JmxStats accumulator; try { accumulator = createJmxAccumulator(jmxStatsClass); } catch (ReflectiveOperationException e) { throw new IllegalStateException("Cannot create JmxStats accumulator instance: " + jmxStatsClass.getName(), e); } for (Object pojo : sources) { JmxStats jmxStats = (JmxStats) pojo; if (jmxStats != null) { accumulator.add(jmxStats); } } return accumulator; } }; } private static JmxStats createJmxAccumulator(Class<?> jmxStatsClass) throws ReflectiveOperationException { assert JmxStats.class.isAssignableFrom(jmxStatsClass); if (ReflectionUtils.classHasPublicStaticFactoryMethod(jmxStatsClass, CREATE_ACCUMULATOR)) { return (JmxStats) jmxStatsClass.getDeclaredMethod(CREATE_ACCUMULATOR).invoke(null); } else if (ReflectionUtils.classHasPublicStaticFactoryMethod(jmxStatsClass, CREATE)) { return (JmxStats) jmxStatsClass.getDeclaredMethod(CREATE).invoke(null); } else if (ReflectionUtils.classHasPublicNoArgConstructor(jmxStatsClass)) { return (JmxStats) jmxStatsClass.newInstance(); } else { throw new RuntimeException("Cannot create instance of class: " + jmxStatsClass.getName()); } } private static JmxReducer<?> fetchReducerFrom(Method getter) throws IllegalAccessException, InstantiationException { if (getter == null) { return DEFAULT_REDUCER; } else { JmxAttribute attrAnnotation = getter.getAnnotation(JmxAttribute.class); Class<? extends JmxReducer<?>> reducerClass = attrAnnotation.reducer(); if (reducerClass == DEFAULT_REDUCER.getClass()) { return DEFAULT_REDUCER; } else { return reducerClass.newInstance(); } } } private static void checkJmxStatsAreValid(Class<?> returnClass, Class<?> mbeanClass, Method getter) { if (JmxRefreshableStats.class.isAssignableFrom(returnClass) && !EventloopJmxMBean.class.isAssignableFrom(mbeanClass)) { logger.warn("JmxRefreshableStats won't be refreshed when used in classes that do not implement" + " EventloopJmxMBean. MBean class: " + mbeanClass.getName()); } if (returnClass.isInterface()) { throw new IllegalArgumentException(createErrorMessageForInvalidJmxStatsAttribute(getter)); } if (Modifier.isAbstract(returnClass.getModifiers())) { throw new IllegalArgumentException(createErrorMessageForInvalidJmxStatsAttribute(getter)); } if (!(classHasPublicStaticFactoryMethod(returnClass, CREATE_ACCUMULATOR) || classHasPublicStaticFactoryMethod(returnClass, CREATE) || classHasPublicNoArgConstructor(returnClass))) { throw new IllegalArgumentException(createErrorMessageForInvalidJmxStatsAttribute(getter)); } } private static String createErrorMessageForInvalidJmxStatsAttribute(Method getter) { String msg = "Return type of JmxRefreshableStats attribute must be concrete class that implements" + " JmxRefreshableStats interface and contains" + " static factory \"" + CREATE_ACCUMULATOR + "()\" method or" + " static factory \"" + CREATE + "()\" method or" + " public no-arg constructor"; if (getter != null) { msg += format(". Error at %s.%s()", getter.getDeclaringClass().getName(), getter.getName()); } return msg; } private static AttributeNode createNodeForParametrizedType(String attrName, String attrDescription, ParameterizedType pType, boolean included, Method getter, Class<?> mbeanClass) { ValueFetcher fetcher = createAppropriateFetcher(getter); Class<?> rawType = (Class<?>) pType.getRawType(); if (rawType == List.class) { Type listElementType = pType.getActualTypeArguments()[0]; return createListAttributeNodeFor( attrName, attrDescription, included, fetcher, listElementType, mbeanClass); } else if (rawType == Map.class) { Type valueType = pType.getActualTypeArguments()[1]; return createMapAttributeNodeFor(attrName, attrDescription, included, fetcher, valueType, mbeanClass); } else { throw new RuntimeException("There is no support for Generic classes other than List or Map"); } } private static AttributeNodeForList createListAttributeNodeFor(String attrName, String attrDescription, boolean included, ValueFetcher fetcher, Type listElementType, Class<?> mbeanClass) { if (listElementType instanceof Class<?>) { Class<?> listElementClass = (Class<?>) listElementType; boolean isListOfJmxRefreshable = (JmxRefreshable.class.isAssignableFrom(listElementClass)); return new AttributeNodeForList( attrName, attrDescription, included, fetcher, createAttributeNodeFor("", attrDescription, listElementType, true, null, null, null, mbeanClass), isListOfJmxRefreshable ); } else if (listElementType instanceof ParameterizedType) { String typeName = ((Class<?>) ((ParameterizedType) listElementType).getRawType()).getSimpleName(); return new AttributeNodeForList( attrName, attrDescription, included, fetcher, createNodeForParametrizedType( typeName, attrDescription, (ParameterizedType) listElementType, true, null, mbeanClass ), false ); } else { throw new RuntimeException(); } } private static AttributeNodeForMap createMapAttributeNodeFor(String attrName, String attrDescription, boolean included, ValueFetcher fetcher, Type valueType, Class<?> mbeanClass) { if (valueType instanceof Class<?>) { Class<?> valueClass = (Class<?>) valueType; boolean isMapOfJmxRefreshable = (JmxRefreshable.class.isAssignableFrom(valueClass)); return new AttributeNodeForMap( attrName, attrDescription, included, fetcher, createAttributeNodeFor("", attrDescription, valueType, true, null, null, null, mbeanClass), isMapOfJmxRefreshable ); } else if (valueType instanceof ParameterizedType) { String typeName = ((Class<?>) ((ParameterizedType) valueType).getRawType()).getSimpleName(); return new AttributeNodeForMap( attrName, attrDescription, included, fetcher, createNodeForParametrizedType( typeName, attrDescription, (ParameterizedType) valueType, true, null, mbeanClass ), false ); } else { throw new RuntimeException(); } } private static boolean isSimpleType(Class<?> clazz) { return isPrimitiveType(clazz) || isPrimitiveTypeWrapper(clazz) || isString(clazz); } private static ValueFetcher createAppropriateFetcher(Method getter) { return getter != null ? new ValueFetcherFromGetter(getter) : new ValueFetcherDirect(); } // endregion // region refreshing jmx private void handleJmxRefreshables(List<MBeanWrapper> mbeanWrappers, AttributeNodeForPojo rootNode) { for (MBeanWrapper mbeanWrapper : mbeanWrappers) { Eventloop eventloop = mbeanWrapper.getEventloop(); List<JmxRefreshable> currentRefreshables = rootNode.getAllRefreshables(mbeanWrapper.getMBean()); if (!eventloopToJmxRefreshables.containsKey(eventloop)) { eventloopToJmxRefreshables.put( eventloop, currentRefreshables ); eventloop.post(createRefreshTask(eventloop, null, 0)); } else { List<JmxRefreshable> previousRefreshables = eventloopToJmxRefreshables.get(eventloop); List<JmxRefreshable> allRefreshables = new ArrayList<>(previousRefreshables); allRefreshables.addAll(currentRefreshables); eventloopToJmxRefreshables.put(eventloop, allRefreshables); } refreshableStatsCounts.put(eventloop, eventloopToJmxRefreshables.get(eventloop).size()); } } private Runnable createRefreshTask(final Eventloop eventloop, final List<JmxRefreshable> previousList, final int previousRefreshes) { return new Runnable() { @Override public void run() { long currentTime = eventloop.currentTimeMillis(); List<JmxRefreshable> jmxRefreshableList = previousList; if (jmxRefreshableList == null) { // list might be updated in case of several mbeans in one eventloop jmxRefreshableList = eventloopToJmxRefreshables.get(eventloop); effectiveRefreshPeriods.put(eventloop, (int) computeEffectiveRefreshPeriod(jmxRefreshableList.size())); } int currentRefreshes = 0; while (currentRefreshes < maxJmxRefreshesPerOneCycle) { int index = currentRefreshes + previousRefreshes; if (index == jmxRefreshableList.size()) { break; } jmxRefreshableList.get(index).refresh(currentTime); currentRefreshes++; } long nextTimestamp = currentTime + computeEffectiveRefreshPeriod(jmxRefreshableList.size()); int totalRefreshes = currentRefreshes + previousRefreshes; if (totalRefreshes == jmxRefreshableList.size()) { eventloop.scheduleBackground(nextTimestamp, createRefreshTask(eventloop, null, 0)); } else { eventloop.scheduleBackground(nextTimestamp, createRefreshTask(eventloop, jmxRefreshableList, totalRefreshes)); } } }; } private long computeEffectiveRefreshPeriod(int jmxRefreshablesCount) { if (jmxRefreshablesCount == 0) { return specifiedRefreshPeriod; } double ratio = ceil(jmxRefreshablesCount / (double) maxJmxRefreshesPerOneCycle); return (long) (specifiedRefreshPeriod / ratio); } private static AttributeNodeForPojo createAttributesTree(Class<?> clazz) { List<AttributeNode> subNodes = createNodesFor(clazz, clazz, new String[0], null); AttributeNodeForPojo root = new AttributeNodeForPojo("", null, true, new ValueFetcherDirect(), null, subNodes); return root; } // endregion // region creating jmx metadata - MBeanInfo private static MBeanInfo createMBeanInfo(AttributeNodeForPojo rootNode, Class<?> monitorableClass, boolean enableRefresh) { String monitorableName = ""; String monitorableDescription = ""; MBeanAttributeInfo[] attributes = rootNode != null ? fetchAttributesInfo(rootNode, enableRefresh) : new MBeanAttributeInfo[0]; MBeanOperationInfo[] operations = fetchOperationsInfo(monitorableClass, enableRefresh); return new MBeanInfo( monitorableName, monitorableDescription, attributes, null, // constructors operations, null); //notifications } private static MBeanAttributeInfo[] fetchAttributesInfo(AttributeNodeForPojo rootNode, boolean refreshEnabled) { Set<String> visibleAttrs = rootNode.getVisibleAttributes(); Map<String, OpenType<?>> nameToType = rootNode.getOpenTypes(); Map<String, Map<String, String>> nameToDescriptions = rootNode.getDescriptions(); List<MBeanAttributeInfo> attrsInfo = new ArrayList<>(); for (String attrName : visibleAttrs) { String description = createDescription(attrName, nameToDescriptions.get(attrName)); OpenType<?> attrType = nameToType.get(attrName); boolean writable = rootNode.isSettable(attrName); boolean isIs = attrType.equals(SimpleType.BOOLEAN); attrsInfo.add(new MBeanAttributeInfo(attrName, attrType.getClassName(), description, true, writable, isIs)); } return attrsInfo.toArray(new MBeanAttributeInfo[attrsInfo.size()]); } private static String createDescription(String name, Map<String, String> groupDescriptions) { if (groupDescriptions.isEmpty()) { return name; } if (!name.contains("_")) { assert groupDescriptions.size() == 1; return groupDescriptions.values().iterator().next(); } String descriptionTemplate = "\"%s\": %s"; String separator = " | "; StringBuilder totalDescription = new StringBuilder(""); for (String groupName : groupDescriptions.keySet()) { String groupDescription = groupDescriptions.get(groupName); totalDescription.append(String.format(descriptionTemplate, groupName, groupDescription)); totalDescription.append(separator); } totalDescription.delete(totalDescription.length() - separator.length(), totalDescription.length()); return totalDescription.toString(); } private static MBeanOperationInfo[] fetchOperationsInfo(Class<?> monitorableClass, boolean enableRefresh) { List<MBeanOperationInfo> operations = new ArrayList<>(); Method[] methods = monitorableClass.getMethods(); for (Method method : methods) { if (method.isAnnotationPresent(JmxOperation.class)) { JmxOperation annotation = method.getAnnotation(JmxOperation.class); String opName = annotation.name(); if (opName.equals("")) { opName = method.getName(); } String opDescription = annotation.description(); Class<?> returnType = method.getReturnType(); List<MBeanParameterInfo> params = new ArrayList<>(); Class<?>[] paramTypes = method.getParameterTypes(); Annotation[][] paramAnnotations = method.getParameterAnnotations(); assert paramAnnotations.length == paramTypes.length; for (int i = 0; i < paramTypes.length; i++) { String paramName = String.format("arg%d", i); Class<?> paramType = paramTypes[i]; JmxParameter nameAnnotation = findJmxNamedParameterAnnotation(paramAnnotations[i]); if (nameAnnotation != null) { paramName = nameAnnotation.value(); } MBeanParameterInfo paramInfo = new MBeanParameterInfo(paramName, paramType.getName(), ""); params.add(paramInfo); } MBeanParameterInfo[] paramsArray = params.toArray(new MBeanParameterInfo[params.size()]); MBeanOperationInfo operationInfo = new MBeanOperationInfo( opName, opDescription, paramsArray, returnType.getName(), MBeanOperationInfo.ACTION); operations.add(operationInfo); } } return operations.toArray(new MBeanOperationInfo[operations.size()]); } private static JmxParameter findJmxNamedParameterAnnotation(Annotation[] annotations) { for (Annotation annotation : annotations) { if (annotation.annotationType().equals(JmxParameter.class)) { return (JmxParameter) annotation; } } return null; } // endregion // region jmx operations fetching private static Map<OperationKey, Method> fetchOpkeyToMethod(Class<?> mbeanClass) { Map<OperationKey, Method> opkeyToMethod = new HashMap<>(); Method[] methods = mbeanClass.getMethods(); for (Method method : methods) { if (method.isAnnotationPresent(JmxOperation.class)) { JmxOperation annotation = method.getAnnotation(JmxOperation.class); String opName = annotation.name(); if (opName.equals("")) { opName = method.getName(); } Class<?>[] paramTypes = method.getParameterTypes(); Annotation[][] paramAnnotations = method.getParameterAnnotations(); assert paramAnnotations.length == paramTypes.length; String[] paramTypesNames = new String[paramTypes.length]; for (int i = 0; i < paramTypes.length; i++) { paramTypesNames[i] = paramTypes[i].getName(); } opkeyToMethod.put(new OperationKey(opName, paramTypesNames), method); } } return opkeyToMethod; } // endregion // region etc private static int secondsToMillis(double seconds) { return (int) (seconds * 1000); } private static <T> boolean listContainsNullValues(List<T> list) { for (int i = 0; i < list.size(); i++) { if (list.get(i) == null) { return true; } } return false; } private static boolean allObjectsAreOfSameType(List<?> objects) { for (int i = 0; i < objects.size() - 1; i++) { Object current = objects.get(i); Object next = objects.get(i + 1); if (!current.getClass().equals(next.getClass())) { return false; } } return true; } // endregion // region helper classes private static final class AttributeDescriptor { private final String name; private final Type type; private final Method getter; private final Method setter; public AttributeDescriptor(String name, Type type, Method getter, Method setter) { this.name = name; this.type = type; this.getter = getter; this.setter = setter; } public String getName() { return name; } public Type getType() { return type; } public Method getGetter() { return getter; } public Method getSetter() { return setter; } } private static final class OperationKey { private final String name; private final String[] argTypes; public OperationKey(String name, String[] argTypes) { checkNotNull(name); checkNotNull(argTypes); this.name = name; this.argTypes = argTypes; } public String getName() { return name; } public String[] getArgTypes() { return argTypes; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof OperationKey)) return false; OperationKey that = (OperationKey) o; if (!name.equals(that.name)) return false; return Arrays.equals(argTypes, that.argTypes); } @Override public int hashCode() { int result = name.hashCode(); result = 31 * result + Arrays.hashCode(argTypes); return result; } } private static final class DynamicMBeanAggregator implements DynamicMBean { private final MBeanInfo mBeanInfo; private final List<? extends MBeanWrapper> mbeanWrappers; private final List<?> mbeans; private final AttributeNodeForPojo rootNode; private final Map<OperationKey, Method> opkeyToMethod; public DynamicMBeanAggregator(MBeanInfo mBeanInfo, List<? extends MBeanWrapper> mbeanWrappers, AttributeNodeForPojo rootNode, Map<OperationKey, Method> opkeyToMethod, boolean refreshEnabled) { this.mBeanInfo = mBeanInfo; this.mbeanWrappers = mbeanWrappers; List<Object> extractedMBeans = new ArrayList<>(mbeanWrappers.size()); for (MBeanWrapper mbeanWrapper : mbeanWrappers) { extractedMBeans.add(mbeanWrapper.getMBean()); } this.mbeans = extractedMBeans; this.rootNode = rootNode; this.opkeyToMethod = opkeyToMethod; } @SuppressWarnings("unchecked") @Override public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException { Object value = rootNode.aggregateAttributes(singleton(attribute), mbeans).get(attribute); if (value instanceof Throwable) { propagate((Throwable) value); } return value; } @Override public void setAttribute(final Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { final String attrName = attribute.getName(); final Object attrValue = attribute.getValue(); final CountDownLatch latch = new CountDownLatch(mbeanWrappers.size()); final AtomicReference<Exception> exceptionReference = new AtomicReference<>(); for (MBeanWrapper mbeanWrapper : mbeanWrappers) { final Object mbean = mbeanWrapper.getMBean(); mbeanWrapper.execute((new Runnable() { @Override public void run() { try { rootNode.setAttribute(attrName, attrValue, singletonList(mbean)); latch.countDown(); } catch (Exception e) { exceptionReference.set(e); latch.countDown(); } } })); } try { latch.await(); } catch (InterruptedException e) { throw new MBeanException(e); } Exception exception = exceptionReference.get(); if (exception != null) { Exception actualException = exception; if (exception instanceof SetterException) { SetterException setterException = (SetterException) exception; actualException = setterException.getCausedException(); } propagate(actualException); } } @Override public AttributeList getAttributes(String[] attributes) { checkArgument(attributes != null); AttributeList attrList = new AttributeList(); Set<String> attrNames = new HashSet<>(Arrays.asList(attributes)); try { Map<String, Object> aggregatedAttrs = rootNode.aggregateAttributes(attrNames, mbeans); for (String aggregatedAttrName : aggregatedAttrs.keySet()) { Object aggregatedValue = aggregatedAttrs.get(aggregatedAttrName); if (!(aggregatedValue instanceof Throwable)) { attrList.add(new Attribute(aggregatedAttrName, aggregatedValue)); } } } catch (Exception e) { logger.error("Cannot get attributes: " + attrNames, e); } return attrList; } @Override public AttributeList setAttributes(AttributeList attributes) { AttributeList resultList = new AttributeList(); for (int i = 0; i < attributes.size(); i++) { Attribute attribute = (Attribute) attributes.get(i); try { setAttribute(attribute); resultList.add(new Attribute(attribute.getName(), attribute.getValue())); } catch (AttributeNotFoundException | InvalidAttributeValueException | MBeanException | ReflectionException e) { logger.error("Cannot set attribute: " + attribute.getName(), e); } } return resultList; } @Override public Object invoke(final String actionName, final Object[] params, final String[] signature) throws MBeanException, ReflectionException { String[] argTypes = signature != null ? signature : new String[0]; final Object[] args = params != null ? params : new Object[0]; OperationKey opkey = new OperationKey(actionName, argTypes); final Method opMethod = opkeyToMethod.get(opkey); if (opMethod == null) { String operationName = prettyOperationName(actionName, argTypes); String errorMsg = "There is no operation \"" + operationName + "\""; throw new RuntimeOperationsException(new IllegalArgumentException("Operation not found"), errorMsg); } final CountDownLatch latch = new CountDownLatch(mbeanWrappers.size()); final AtomicReference<Exception> exceptionReference = new AtomicReference<>(); final AtomicReference lastValue = new AtomicReference(); for (MBeanWrapper mbeanWrapper : mbeanWrappers) { final Object mbean = mbeanWrapper.getMBean(); mbeanWrapper.execute((new Runnable() { @Override public void run() { try { Object result = opMethod.invoke(mbean, args); lastValue.set(result); latch.countDown(); } catch (Exception e) { exceptionReference.set(e); latch.countDown(); } } })); } try { latch.await(); } catch (InterruptedException e) { throw new MBeanException(e); } Exception exception = exceptionReference.get(); if (exception != null) { propagate(exception); } // We don't know how to aggregate return values if there are several mbeans return mbeanWrappers.size() == 1 ? lastValue.get() : null; } private void propagate(Throwable throwable) throws MBeanException { if (throwable instanceof InvocationTargetException) { Throwable targetException = ((InvocationTargetException) throwable).getTargetException(); if (targetException instanceof Exception) { throw new MBeanException((Exception) targetException); } else { throw new MBeanException( new Exception(format("Throwable of type \"%s\" and message \"%s\" " + "was thrown during method invocation", targetException.getClass().getName(), targetException.getMessage()) ) ); } } else { if (throwable instanceof Exception) { throw new MBeanException((Exception) throwable); } else { throw new MBeanException( new Exception(format("Throwable of type \"%s\" and message \"%s\" " + "was thrown", throwable.getClass().getName(), throwable.getMessage()) ) ); } } } private static String prettyOperationName(String name, String[] argTypes) { String operationName = name + "("; if (argTypes.length > 0) { for (int i = 0; i < argTypes.length - 1; i++) { operationName += argTypes[i] + ", "; } operationName += argTypes[argTypes.length - 1]; } operationName += ")"; return operationName; } @Override public MBeanInfo getMBeanInfo() { return mBeanInfo; } } private interface MBeanWrapper { void execute(Runnable command); Object getMBean(); Eventloop getEventloop(); } private static final class ConcurrentJmxMBeanWrapper implements MBeanWrapper { private final ConcurrentJmxMBean mbean; public ConcurrentJmxMBeanWrapper(ConcurrentJmxMBean mbean) { this.mbean = mbean; } @Override public void execute(Runnable command) { command.run(); } @Override public Object getMBean() { return mbean; } @Override public Eventloop getEventloop() { return null; } } private static final class EventloopJmxMBeanWrapper implements MBeanWrapper { private final EventloopJmxMBean mbean; public EventloopJmxMBeanWrapper(EventloopJmxMBean mbean) { this.mbean = mbean; } @Override public void execute(Runnable command) { mbean.getEventloop().execute(command); } @Override public Object getMBean() { return mbean; } @Override public Eventloop getEventloop() { return mbean.getEventloop(); } } // endregion }