Skip to content

Commit

Permalink
Avoid need for reflection hints for MBeanExporter in native image
Browse files Browse the repository at this point in the history
Prior to this commit, MBeanExporter used
org.springframework.core.Constants which used reflection to find
constant fields in the MBeanExporter class. Consequently, one had to
register reflection hints in order to use MBeanExporter in a GraalVM
native image.

This commit addresses this by replacing the use of the `Constants`
class with a simple java.util.Map which maps constant names to constant
values for the autodetect constants defined in MBeanExporter.

See gh-30851
Closes gh-30846
  • Loading branch information
sbrannen committed Jul 10, 2023
1 parent 676daa9 commit 679b668
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 30 deletions.
Expand Up @@ -53,7 +53,6 @@
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.Constants;
import org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler;
import org.springframework.jmx.export.assembler.MBeanInfoAssembler;
import org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler;
Expand Down Expand Up @@ -135,12 +134,18 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo
/** Constant for the JMX {@code mr_type} "ObjectReference". */
private static final String MR_TYPE_OBJECT_REFERENCE = "ObjectReference";

/** Prefix for the autodetect constants defined in this class. */
private static final String CONSTANT_PREFIX_AUTODETECT = "AUTODETECT_";

/**
* Map of constant names to constant values for the autodetect constants defined
* in this class.
* @since 6.0.11
*/
private static final Map<String, Integer> constants = Map.of(
"AUTODETECT_NONE", AUTODETECT_NONE,
"AUTODETECT_MBEAN", AUTODETECT_MBEAN,
"AUTODETECT_ASSEMBLER", AUTODETECT_ASSEMBLER,
"AUTODETECT_ALL", AUTODETECT_ALL
);

/** Constants instance for this class. */
private static final Constants constants = new Constants(MBeanExporter.class);

/** The beans to be exposed as JMX managed resources, with JMX names as keys. */
@Nullable
Expand Down Expand Up @@ -235,10 +240,10 @@ public void setAutodetect(boolean autodetect) {
* @see #AUTODETECT_NONE
*/
public void setAutodetectModeName(String constantName) {
if (!constantName.startsWith(CONSTANT_PREFIX_AUTODETECT)) {
throw new IllegalArgumentException("Only autodetect constants allowed");
}
this.autodetectMode = (Integer) constants.asNumber(constantName);
Assert.hasText(constantName, "'constantName' must not be null or blank");
Integer mode = constants.get(constantName);
Assert.notNull(mode, "Only autodetect constants allowed");
this.autodetectMode = mode;
}

/**
Expand All @@ -253,9 +258,8 @@ public void setAutodetectModeName(String constantName) {
* @see #AUTODETECT_NONE
*/
public void setAutodetectMode(int autodetectMode) {
if (!constants.getValues(CONSTANT_PREFIX_AUTODETECT).contains(autodetectMode)) {
throw new IllegalArgumentException("Only values of autodetect constants allowed");
}
Assert.isTrue(constants.containsValue(autodetectMode),
"Only values of autodetect constants allowed");
this.autodetectMode = autodetectMode;
}

Expand Down
Expand Up @@ -16,13 +16,15 @@

package org.springframework.jmx.export;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import javax.management.Attribute;
import javax.management.InstanceNotFoundException;
Expand Down Expand Up @@ -55,10 +57,12 @@
import org.springframework.jmx.export.naming.SelfNaming;
import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.jmx.support.RegistrationPolicy;
import org.springframework.util.ReflectionUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;

/**
Expand Down Expand Up @@ -353,7 +357,6 @@ void bonaFideMBeanIsNotExportedWhenAutodetectIsTotallyTurnedOff() {
exporter.setAutodetectMode(MBeanExporter.AUTODETECT_NONE);
// MBean has a bad ObjectName, so if said MBean is autodetected, an exception will be thrown...
start(exporter);

}

@Test
Expand Down Expand Up @@ -438,60 +441,88 @@ void bonaFideMBeanExplicitlyExportedAndAutodetectionIsOn() throws Exception {
ObjectNameManager.getInstance(OBJECT_NAME));
}

@Test
void setAutodetectModeToSupportedValue() {
exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ASSEMBLER);
assertThat(exporter.getAutodetectMode()).isEqualTo(MBeanExporter.AUTODETECT_ASSEMBLER);
}

@Test
void setAutodetectModeToOutOfRangeNegativeValue() {
assertThatIllegalArgumentException()
.isThrownBy(() -> exporter.setAutodetectMode(-1));
.isThrownBy(() -> exporter.setAutodetectMode(-1))
.withMessage("Only values of autodetect constants allowed");
assertThat(exporter.getAutodetectMode()).isNull();
}

@Test
void setAutodetectModeToOutOfRangePositiveValue() {
assertThatIllegalArgumentException()
.isThrownBy(() -> exporter.setAutodetectMode(5));
.isThrownBy(() -> exporter.setAutodetectMode(5))
.withMessage("Only values of autodetect constants allowed");
assertThat(exporter.getAutodetectMode()).isNull();
}

/**
* This test effectively verifies that the internal 'constants' map is properly
* configured for all autodetect constants defined in {@link MBeanExporter}.
*/
@Test
void setAutodetectModeNameToSupportedValue() {
exporter.setAutodetectModeName("AUTODETECT_ASSEMBLER");
void setAutodetectModeToAllSupportedValues() {
streamConstants(MBeanExporter.class)
.map(MBeanExporterTests::getFieldValue)
.forEach(mode -> assertThatNoException().isThrownBy(() -> exporter.setAutodetectMode(mode)));
}

@Test
void setAutodetectModeToSupportedValue() {
exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ASSEMBLER);
assertThat(exporter.getAutodetectMode()).isEqualTo(MBeanExporter.AUTODETECT_ASSEMBLER);
}

@Test
void setAutodetectModeNameToNull() {
assertThatIllegalArgumentException()
.isThrownBy(() -> exporter.setAutodetectModeName(null));
.isThrownBy(() -> exporter.setAutodetectModeName(null))
.withMessage("'constantName' must not be null or blank");
assertThat(exporter.getAutodetectMode()).isNull();
}

@Test
void setAutodetectModeNameToAnEmptyString() {
assertThatIllegalArgumentException()
.isThrownBy(() -> exporter.setAutodetectModeName(""));
.isThrownBy(() -> exporter.setAutodetectModeName(""))
.withMessage("'constantName' must not be null or blank");
assertThat(exporter.getAutodetectMode()).isNull();
}

@Test
void setAutodetectModeNameToAWhitespacedString() {
void setAutodetectModeNameToWhitespace() {
assertThatIllegalArgumentException()
.isThrownBy(() -> exporter.setAutodetectModeName(" \t"));
.isThrownBy(() -> exporter.setAutodetectModeName(" \t"))
.withMessage("'constantName' must not be null or blank");
assertThat(exporter.getAutodetectMode()).isNull();
}

@Test
void setAutodetectModeNameToARubbishValue() {
void setAutodetectModeNameToBogusValue() {
assertThatIllegalArgumentException()
.isThrownBy(() -> exporter.setAutodetectModeName("That Hansel is... *sssooo* hot right now!"));
.isThrownBy(() -> exporter.setAutodetectModeName("Bogus"))
.withMessage("Only autodetect constants allowed");
assertThat(exporter.getAutodetectMode()).isNull();
}

/**
* This test effectively verifies that the internal 'constants' map is properly
* configured for all autodetect constants defined in {@link MBeanExporter}.
*/
@Test
void setAutodetectModeNameToAllSupportedValues() {
streamConstants(MBeanExporter.class)
.map(Field::getName)
.forEach(name -> assertThatNoException().isThrownBy(() -> exporter.setAutodetectModeName(name)));
}

@Test
void setAutodetectModeNameToSupportedValue() {
exporter.setAutodetectModeName("AUTODETECT_ASSEMBLER");
assertThat(exporter.getAutodetectMode()).isEqualTo(MBeanExporter.AUTODETECT_ASSEMBLER);
}

@Test
void notRunningInBeanFactoryAndPassedBeanNameToExport() throws Exception {
Map<String, Object> beans = Map.of(OBJECT_NAME, "beanName");
Expand Down Expand Up @@ -672,6 +703,19 @@ public ModelMBeanInfo getMBeanInfo(Object managedResource, String beanKey) throw
}
}

private static Stream<Field> streamConstants(Class<?> clazz) {
return Arrays.stream(clazz.getFields()).filter(ReflectionUtils::isPublicStaticFinal);
}

private static Integer getFieldValue(Field field) {
try {
return (Integer) field.get(null);
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}


private static class MockMBeanExporterListener implements MBeanExporterListener {

Expand Down

0 comments on commit 679b668

Please sign in to comment.