Skip to content

Commit 49a21de

Browse files
committed
Create endpoint beans as late as possible
Update `EndpointDiscoverer` so that `@Endpoint` and `@EndpointExtension` beans are created as late as possible. Prior to this commit, endpoint beans and extension beans would be created during the discovery phase which could cause early bean initialization. The problem was especially nasty when using an embedded servlet container since `ServletEndpointRegistrar` is loaded as the container is initialized. This would trigger discovery and load all endpoint beans, including the health endpoint, and all health indicator beans. Fixes gh-20714
1 parent 27ada02 commit 49a21de

File tree

4 files changed

+71
-33
lines changed

4 files changed

+71
-33
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,21 @@ public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext,
5858
}
5959

6060
@Override
61-
protected boolean isExtensionExposed(Object extensionBean) {
62-
if (isHealthEndpointExtension(extensionBean) && !isCloudFoundryHealthEndpointExtension(extensionBean)) {
61+
protected boolean isExtensionTypeExposed(Class<?> extensionBeanType) {
62+
if (isHealthEndpointExtension(extensionBeanType) && !isCloudFoundryHealthEndpointExtension(extensionBeanType)) {
6363
// Filter regular health endpoint extensions so a CF version can replace them
6464
return false;
6565
}
6666
return true;
6767
}
6868

69-
private boolean isHealthEndpointExtension(Object extensionBean) {
70-
return MergedAnnotations.from(extensionBean.getClass()).get(EndpointWebExtension.class)
69+
private boolean isHealthEndpointExtension(Class<?> extensionBeanType) {
70+
return MergedAnnotations.from(extensionBeanType).get(EndpointWebExtension.class)
7171
.getValue("endpoint", Class.class).map(HealthEndpoint.class::isAssignableFrom).orElse(false);
7272
}
7373

74-
private boolean isCloudFoundryHealthEndpointExtension(Object extensionBean) {
75-
return MergedAnnotations.from(extensionBean.getClass()).isPresent(EndpointCloudFoundryExtension.class);
74+
private boolean isCloudFoundryHealthEndpointExtension(Class<?> extensionBeanType) {
75+
return MergedAnnotations.from(extensionBeanType).isPresent(EndpointCloudFoundryExtension.class);
7676
}
7777

7878
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
5050
import org.springframework.core.env.Environment;
5151
import org.springframework.util.Assert;
52+
import org.springframework.util.ClassUtils;
5253
import org.springframework.util.CollectionUtils;
5354
import org.springframework.util.LinkedMultiValueMap;
5455
import org.springframework.util.MultiValueMap;
@@ -140,8 +141,9 @@ private Collection<EndpointBean> createEndpointBeans() {
140141
}
141142

142143
private EndpointBean createEndpointBean(String beanName) {
143-
Object bean = this.applicationContext.getBean(beanName);
144-
return new EndpointBean(this.applicationContext.getEnvironment(), beanName, bean);
144+
Class<?> beanType = ClassUtils.getUserClass(this.applicationContext.getType(beanName, false));
145+
Supplier<Object> beanSupplier = () -> this.applicationContext.getBean(beanName);
146+
return new EndpointBean(this.applicationContext.getEnvironment(), beanName, beanType, beanSupplier);
145147
}
146148

147149
private void addExtensionBeans(Collection<EndpointBean> endpointBeans) {
@@ -159,8 +161,9 @@ private void addExtensionBeans(Collection<EndpointBean> endpointBeans) {
159161
}
160162

161163
private ExtensionBean createExtensionBean(String beanName) {
162-
Object bean = this.applicationContext.getBean(beanName);
163-
return new ExtensionBean(this.applicationContext.getEnvironment(), beanName, bean);
164+
Class<?> beanType = ClassUtils.getUserClass(this.applicationContext.getType(beanName));
165+
Supplier<Object> beanSupplier = () -> this.applicationContext.getBean(beanName);
166+
return new ExtensionBean(this.applicationContext.getEnvironment(), beanName, beanType, beanSupplier);
164167
}
165168

166169
private void addExtensionBean(EndpointBean endpointBean, ExtensionBean extensionBean) {
@@ -233,7 +236,8 @@ private void assertNoDuplicateOperations(EndpointBean endpointBean, MultiValueMa
233236
}
234237

235238
private boolean isExtensionExposed(EndpointBean endpointBean, ExtensionBean extensionBean) {
236-
return isFilterMatch(extensionBean.getFilter(), endpointBean) && isExtensionExposed(extensionBean.getBean());
239+
return isFilterMatch(extensionBean.getFilter(), endpointBean)
240+
&& isExtensionTypeExposed(extensionBean.getBeanType());
237241
}
238242

239243
/**
@@ -242,10 +246,21 @@ private boolean isExtensionExposed(EndpointBean endpointBean, ExtensionBean exte
242246
* @param extensionBean the extension bean
243247
* @return {@code true} if the extension is exposed
244248
*/
249+
@Deprecated
245250
protected boolean isExtensionExposed(Object extensionBean) {
246251
return true;
247252
}
248253

254+
/**
255+
* Determine if an extension bean should be exposed. Subclasses can override this
256+
* method to provide additional logic.
257+
* @param extensionBeanType the extension bean type
258+
* @return {@code true} if the extension is exposed
259+
*/
260+
protected boolean isExtensionTypeExposed(Class<?> extensionBeanType) {
261+
return true;
262+
}
263+
249264
private boolean isEndpointExposed(EndpointBean endpointBean) {
250265
return isFilterMatch(endpointBean.getFilter(), endpointBean) && !isEndpointFiltered(endpointBean)
251266
&& isEndpointExposed(endpointBean.getBean());
@@ -257,10 +272,21 @@ private boolean isEndpointExposed(EndpointBean endpointBean) {
257272
* @param endpointBean the endpoint bean
258273
* @return {@code true} if the endpoint is exposed
259274
*/
275+
@Deprecated
260276
protected boolean isEndpointExposed(Object endpointBean) {
261277
return true;
262278
}
263279

280+
/**
281+
* Determine if an endpoint bean should be exposed. Subclasses can override this
282+
* method to provide additional logic.
283+
* @param beanType the endpoint bean type
284+
* @return {@code true} if the endpoint is exposed
285+
*/
286+
protected boolean isEndpointTypeExposed(Class<?> beanType) {
287+
return true;
288+
}
289+
264290
private boolean isEndpointFiltered(EndpointBean endpointBean) {
265291
for (EndpointFilter<E> filter : this.filters) {
266292
if (!isFilterMatch(filter, endpointBean)) {
@@ -272,7 +298,7 @@ private boolean isEndpointFiltered(EndpointBean endpointBean) {
272298

273299
@SuppressWarnings("unchecked")
274300
private boolean isFilterMatch(Class<?> filter, EndpointBean endpointBean) {
275-
if (!isEndpointExposed(endpointBean.getBean())) {
301+
if (!isEndpointTypeExposed(endpointBean.getBeanType())) {
276302
return false;
277303
}
278304
if (filter == null) {
@@ -392,7 +418,9 @@ private static class EndpointBean {
392418

393419
private final String beanName;
394420

395-
private final Object bean;
421+
private final Class<?> beanType;
422+
423+
private final Supplier<Object> beanSupplier;
396424

397425
private final EndpointId id;
398426

@@ -402,17 +430,18 @@ private static class EndpointBean {
402430

403431
private Set<ExtensionBean> extensions = new LinkedHashSet<>();
404432

405-
EndpointBean(Environment environment, String beanName, Object bean) {
406-
MergedAnnotation<Endpoint> annotation = MergedAnnotations
407-
.from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class);
433+
EndpointBean(Environment environment, String beanName, Class<?> beanType, Supplier<Object> beanSupplier) {
434+
MergedAnnotation<Endpoint> annotation = MergedAnnotations.from(beanType, SearchStrategy.TYPE_HIERARCHY)
435+
.get(Endpoint.class);
408436
String id = annotation.getString("id");
409437
Assert.state(StringUtils.hasText(id),
410-
() -> "No @Endpoint id attribute specified for " + bean.getClass().getName());
438+
() -> "No @Endpoint id attribute specified for " + beanType.getName());
411439
this.beanName = beanName;
412-
this.bean = bean;
440+
this.beanType = beanType;
441+
this.beanSupplier = beanSupplier;
413442
this.id = EndpointId.of(environment, id);
414443
this.enabledByDefault = annotation.getBoolean("enableByDefault");
415-
this.filter = getFilter(this.bean.getClass());
444+
this.filter = getFilter(beanType);
416445
}
417446

418447
void addExtension(ExtensionBean extensionBean) {
@@ -432,8 +461,12 @@ String getBeanName() {
432461
return this.beanName;
433462
}
434463

464+
Class<?> getBeanType() {
465+
return this.beanType;
466+
}
467+
435468
Object getBean() {
436-
return this.bean;
469+
return this.beanSupplier.get();
437470
}
438471

439472
EndpointId getId() {
@@ -457,17 +490,20 @@ private static class ExtensionBean {
457490

458491
private final String beanName;
459492

460-
private final Object bean;
493+
private final Class<?> beanType;
494+
495+
private final Supplier<Object> beanSupplier;
461496

462497
private final EndpointId endpointId;
463498

464499
private final Class<?> filter;
465500

466-
ExtensionBean(Environment environment, String beanName, Object bean) {
467-
this.bean = bean;
501+
ExtensionBean(Environment environment, String beanName, Class<?> beanType, Supplier<Object> beanSupplier) {
468502
this.beanName = beanName;
503+
this.beanType = beanType;
504+
this.beanSupplier = beanSupplier;
469505
MergedAnnotation<EndpointExtension> extensionAnnotation = MergedAnnotations
470-
.from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY).get(EndpointExtension.class);
506+
.from(beanType, SearchStrategy.TYPE_HIERARCHY).get(EndpointExtension.class);
471507
Class<?> endpointType = extensionAnnotation.getClass("endpoint");
472508
MergedAnnotation<Endpoint> endpointAnnotation = MergedAnnotations
473509
.from(endpointType, SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class);
@@ -481,8 +517,12 @@ String getBeanName() {
481517
return this.beanName;
482518
}
483519

520+
Class<?> getBeanType() {
521+
return this.beanType;
522+
}
523+
484524
Object getBean() {
485-
return this.bean;
525+
return this.beanSupplier.get();
486526
}
487527

488528
EndpointId getEndpointId() {

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscoverer.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import org.springframework.boot.actuate.endpoint.web.PathMapper;
3131
import org.springframework.context.ApplicationContext;
3232
import org.springframework.core.annotation.MergedAnnotations;
33-
import org.springframework.util.ClassUtils;
33+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
3434

3535
/**
3636
* {@link EndpointDiscoverer} for {@link ExposableControllerEndpoint controller
@@ -57,9 +57,8 @@ public ControllerEndpointDiscoverer(ApplicationContext applicationContext, List<
5757
}
5858

5959
@Override
60-
protected boolean isEndpointExposed(Object endpointBean) {
61-
Class<?> type = ClassUtils.getUserClass(endpointBean.getClass());
62-
MergedAnnotations annotations = MergedAnnotations.from(type);
60+
protected boolean isEndpointTypeExposed(Class<?> beanType) {
61+
MergedAnnotations annotations = MergedAnnotations.from(beanType, SearchStrategy.SUPERCLASS);
6362
return annotations.isPresent(ControllerEndpoint.class) || annotations.isPresent(RestControllerEndpoint.class);
6463
}
6564

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import org.springframework.boot.actuate.endpoint.web.PathMapper;
3232
import org.springframework.context.ApplicationContext;
3333
import org.springframework.core.annotation.MergedAnnotations;
34-
import org.springframework.util.ClassUtils;
34+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
3535

3636
/**
3737
* {@link EndpointDiscoverer} for {@link ExposableServletEndpoint servlet endpoints}.
@@ -57,9 +57,8 @@ public ServletEndpointDiscoverer(ApplicationContext applicationContext, List<Pat
5757
}
5858

5959
@Override
60-
protected boolean isEndpointExposed(Object endpointBean) {
61-
Class<?> type = ClassUtils.getUserClass(endpointBean.getClass());
62-
return MergedAnnotations.from(type).isPresent(ServletEndpoint.class);
60+
protected boolean isEndpointTypeExposed(Class<?> beanType) {
61+
return MergedAnnotations.from(beanType, SearchStrategy.SUPERCLASS).isPresent(ServletEndpoint.class);
6362
}
6463

6564
@Override

0 commit comments

Comments
 (0)