Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

Commit ef1ba33

Browse files
committed
Add reflection configuration for Actuator Endpoints
This commit registers the necessary reflection configuration to be able to invoke operation methods via reflection. Both endpoints and endpoint extensions are supported. Closes gh-999
1 parent 9f7ca4e commit ef1ba33

File tree

4 files changed

+178
-0
lines changed

4 files changed

+178
-0
lines changed

spring-aot/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
<artifactId>spring-web</artifactId>
4444
<optional>true</optional>
4545
</dependency>
46+
<dependency>
47+
<groupId>org.springframework.boot</groupId>
48+
<artifactId>spring-boot-actuator</artifactId>
49+
<optional>true</optional>
50+
</dependency>
4651
<dependency>
4752
<groupId>org.springframework.boot</groupId>
4853
<artifactId>spring-boot-autoconfigure</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.springframework.boot.actuate.endpoint.annotation;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.lang.reflect.Method;
5+
import java.util.LinkedHashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
10+
import org.springframework.context.bootstrap.generator.infrastructure.nativex.BeanFactoryNativeConfigurationProcessor;
11+
import org.springframework.context.bootstrap.generator.infrastructure.nativex.NativeConfigurationRegistry;
12+
import org.springframework.context.bootstrap.generator.infrastructure.nativex.NativeReflectionEntry.Builder;
13+
import org.springframework.core.annotation.MergedAnnotations;
14+
import org.springframework.util.ClassUtils;
15+
import org.springframework.util.ReflectionUtils;
16+
17+
/**
18+
* A {@link BeanFactoryNativeConfigurationProcessor} that register reflection access for
19+
* actuator endpoints, specifically the methods flagged with one of the supported
20+
* operation annotations.
21+
*
22+
* @author Stephane Nicoll
23+
*/
24+
class EndpointNativeConfigurationProcessor implements BeanFactoryNativeConfigurationProcessor {
25+
26+
private static final String ENDPOINT_CLASS_NAME = "org.springframework.boot.actuate.endpoint.annotation.Endpoint";
27+
28+
@Override
29+
public void process(ConfigurableListableBeanFactory beanFactory, NativeConfigurationRegistry registry) {
30+
if (ClassUtils.isPresent(ENDPOINT_CLASS_NAME, beanFactory.getBeanClassLoader())) {
31+
new Processor().process(beanFactory, registry);
32+
}
33+
}
34+
35+
private static class Processor {
36+
37+
private static final List<Class<? extends Annotation>> ENDPOINT_ANNOTATIONS = List.of(
38+
Endpoint.class, EndpointExtension.class);
39+
40+
private static final List<Class<? extends Annotation>> OPERATION_ANNOTATIONS = List.of(
41+
ReadOperation.class, WriteOperation.class, DeleteOperation.class);
42+
43+
void process(ConfigurableListableBeanFactory beanFactory, NativeConfigurationRegistry registry) {
44+
findCandidates(beanFactory).forEach((beanName, endpoint) -> registerEndpoint(registry, endpoint));
45+
}
46+
47+
private Map<String, Class<?>> findCandidates(ConfigurableListableBeanFactory beanFactory) {
48+
Map<String, Class<?>> candidates = new LinkedHashMap<>();
49+
ENDPOINT_ANNOTATIONS.forEach((annotation) -> {
50+
String[] beanNames = beanFactory.getBeanNamesForAnnotation(annotation);
51+
for (String beanName : beanNames) {
52+
candidates.put(beanName, beanFactory.getType(beanName));
53+
}
54+
});
55+
return candidates;
56+
}
57+
58+
private void registerEndpoint(NativeConfigurationRegistry registry, Class<?> type) {
59+
Builder builder = registry.reflection().forType(type);
60+
ReflectionUtils.doWithMethods(type, builder::withMethods, this::isOperationMethod);
61+
}
62+
63+
private boolean isOperationMethod(Method method) {
64+
MergedAnnotations from = MergedAnnotations.from(method);
65+
return OPERATION_ANNOTATIONS.stream().anyMatch(from::isPresent);
66+
}
67+
}
68+
69+
}

spring-aot/src/main/resources/META-INF/spring.factories

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrat
99
org.springframework.context.bootstrap.generator.bean.DefaultBeanRegistrationWriterSupplier
1010

1111
org.springframework.context.bootstrap.generator.infrastructure.nativex.BeanFactoryNativeConfigurationProcessor=\
12+
org.springframework.boot.actuate.endpoint.annotation.EndpointNativeConfigurationProcessor,\
1213
org.springframework.boot.context.properties.ConfigurationPropertiesNativeConfigurationProcessor
1314

1415
org.springframework.context.bootstrap.generator.infrastructure.nativex.BeanNativeConfigurationProcessor=\
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.springframework.boot.actuate.endpoint.annotation;
2+
3+
import java.lang.reflect.Method;
4+
import java.util.List;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
9+
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
10+
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
11+
import org.springframework.context.bootstrap.generator.infrastructure.nativex.NativeConfigurationRegistry;
12+
import org.springframework.context.bootstrap.generator.infrastructure.nativex.NativeReflectionEntry;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
/**
17+
* Tests for {@link EndpointNativeConfigurationProcessor}.
18+
*
19+
* @author Stephane Nicoll
20+
*/
21+
class EndpointNativeConfigurationProcessorTests {
22+
23+
@Test
24+
void registerSimpleEndpoint() {
25+
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
26+
beanFactory.registerBeanDefinition("noise", BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition());
27+
beanFactory.registerBeanDefinition("endpoint", BeanDefinitionBuilder.rootBeanDefinition(TestEndpoint.class).getBeanDefinition());
28+
NativeConfigurationRegistry registry = process(beanFactory);
29+
List<NativeReflectionEntry> entries = registry.reflection().getEntries();
30+
assertThat(entries).anySatisfy((entry) -> {
31+
assertThat(entry.getType()).isEqualTo(TestEndpoint.class);
32+
assertThat(entry.getMethods().stream().map(Method::getName))
33+
.containsOnly("getAll", "get", "set", "delete");
34+
});
35+
assertThat(entries).hasSize(1);
36+
}
37+
38+
@Test
39+
void registerSimpleEndpointExtension() {
40+
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
41+
beanFactory.registerBeanDefinition("noise", BeanDefinitionBuilder.rootBeanDefinition(String.class).getBeanDefinition());
42+
beanFactory.registerBeanDefinition("endpoint", BeanDefinitionBuilder.rootBeanDefinition(TestEndpointWebExtension.class).getBeanDefinition());
43+
NativeConfigurationRegistry registry = process(beanFactory);
44+
List<NativeReflectionEntry> entries = registry.reflection().getEntries();
45+
assertThat(entries).anySatisfy((entry) -> {
46+
assertThat(entry.getType()).isEqualTo(TestEndpointWebExtension.class);
47+
assertThat(entry.getMethods().stream().map(Method::getName)).containsOnly("get");
48+
});
49+
assertThat(entries).hasSize(1);
50+
}
51+
52+
private NativeConfigurationRegistry process(DefaultListableBeanFactory beanFactory) {
53+
NativeConfigurationRegistry registry = new NativeConfigurationRegistry();
54+
new EndpointNativeConfigurationProcessor().process(beanFactory, registry);
55+
return registry;
56+
}
57+
58+
@Endpoint(id = "test")
59+
@SuppressWarnings("unused")
60+
static class TestEndpoint {
61+
62+
@ReadOperation
63+
public List<String> getAll() {
64+
return List.of(get());
65+
}
66+
67+
@ReadOperation
68+
public String get() {
69+
return "test";
70+
}
71+
72+
@WriteOperation
73+
public void set(String name) {
74+
75+
}
76+
77+
@DeleteOperation
78+
public void delete(String name) {
79+
80+
}
81+
82+
public void ignoredMethod(String name) {
83+
84+
}
85+
86+
}
87+
88+
@EndpointWebExtension(endpoint = TestEndpoint.class)
89+
@SuppressWarnings("unused")
90+
static class TestEndpointWebExtension {
91+
92+
@ReadOperation
93+
public String get() {
94+
return "web";
95+
}
96+
97+
public void noise(String name) {
98+
99+
}
100+
101+
}
102+
103+
}

0 commit comments

Comments
 (0)