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
This commit is contained in:
Phillip Webb
2020-04-23 20:44:06 -07:00
parent 27ada029b5
commit 49a21ded7a
4 changed files with 71 additions and 33 deletions

View File

@@ -58,21 +58,21 @@ public class CloudFoundryWebEndpointDiscoverer extends WebEndpointDiscoverer {
}
@Override
protected boolean isExtensionExposed(Object extensionBean) {
if (isHealthEndpointExtension(extensionBean) && !isCloudFoundryHealthEndpointExtension(extensionBean)) {
protected boolean isExtensionTypeExposed(Class<?> extensionBeanType) {
if (isHealthEndpointExtension(extensionBeanType) && !isCloudFoundryHealthEndpointExtension(extensionBeanType)) {
// Filter regular health endpoint extensions so a CF version can replace them
return false;
}
return true;
}
private boolean isHealthEndpointExtension(Object extensionBean) {
return MergedAnnotations.from(extensionBean.getClass()).get(EndpointWebExtension.class)
private boolean isHealthEndpointExtension(Class<?> extensionBeanType) {
return MergedAnnotations.from(extensionBeanType).get(EndpointWebExtension.class)
.getValue("endpoint", Class.class).map(HealthEndpoint.class::isAssignableFrom).orElse(false);
}
private boolean isCloudFoundryHealthEndpointExtension(Object extensionBean) {
return MergedAnnotations.from(extensionBean.getClass()).isPresent(EndpointCloudFoundryExtension.class);
private boolean isCloudFoundryHealthEndpointExtension(Class<?> extensionBeanType) {
return MergedAnnotations.from(extensionBeanType).isPresent(EndpointCloudFoundryExtension.class);
}
}

View File

@@ -49,6 +49,7 @@ import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -140,8 +141,9 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
}
private EndpointBean createEndpointBean(String beanName) {
Object bean = this.applicationContext.getBean(beanName);
return new EndpointBean(this.applicationContext.getEnvironment(), beanName, bean);
Class<?> beanType = ClassUtils.getUserClass(this.applicationContext.getType(beanName, false));
Supplier<Object> beanSupplier = () -> this.applicationContext.getBean(beanName);
return new EndpointBean(this.applicationContext.getEnvironment(), beanName, beanType, beanSupplier);
}
private void addExtensionBeans(Collection<EndpointBean> endpointBeans) {
@@ -159,8 +161,9 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
}
private ExtensionBean createExtensionBean(String beanName) {
Object bean = this.applicationContext.getBean(beanName);
return new ExtensionBean(this.applicationContext.getEnvironment(), beanName, bean);
Class<?> beanType = ClassUtils.getUserClass(this.applicationContext.getType(beanName));
Supplier<Object> beanSupplier = () -> this.applicationContext.getBean(beanName);
return new ExtensionBean(this.applicationContext.getEnvironment(), beanName, beanType, beanSupplier);
}
private void addExtensionBean(EndpointBean endpointBean, ExtensionBean extensionBean) {
@@ -233,7 +236,8 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
}
private boolean isExtensionExposed(EndpointBean endpointBean, ExtensionBean extensionBean) {
return isFilterMatch(extensionBean.getFilter(), endpointBean) && isExtensionExposed(extensionBean.getBean());
return isFilterMatch(extensionBean.getFilter(), endpointBean)
&& isExtensionTypeExposed(extensionBean.getBeanType());
}
/**
@@ -242,10 +246,21 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
* @param extensionBean the extension bean
* @return {@code true} if the extension is exposed
*/
@Deprecated
protected boolean isExtensionExposed(Object extensionBean) {
return true;
}
/**
* Determine if an extension bean should be exposed. Subclasses can override this
* method to provide additional logic.
* @param extensionBeanType the extension bean type
* @return {@code true} if the extension is exposed
*/
protected boolean isExtensionTypeExposed(Class<?> extensionBeanType) {
return true;
}
private boolean isEndpointExposed(EndpointBean endpointBean) {
return isFilterMatch(endpointBean.getFilter(), endpointBean) && !isEndpointFiltered(endpointBean)
&& isEndpointExposed(endpointBean.getBean());
@@ -257,10 +272,21 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
* @param endpointBean the endpoint bean
* @return {@code true} if the endpoint is exposed
*/
@Deprecated
protected boolean isEndpointExposed(Object endpointBean) {
return true;
}
/**
* Determine if an endpoint bean should be exposed. Subclasses can override this
* method to provide additional logic.
* @param beanType the endpoint bean type
* @return {@code true} if the endpoint is exposed
*/
protected boolean isEndpointTypeExposed(Class<?> beanType) {
return true;
}
private boolean isEndpointFiltered(EndpointBean endpointBean) {
for (EndpointFilter<E> filter : this.filters) {
if (!isFilterMatch(filter, endpointBean)) {
@@ -272,7 +298,7 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
@SuppressWarnings("unchecked")
private boolean isFilterMatch(Class<?> filter, EndpointBean endpointBean) {
if (!isEndpointExposed(endpointBean.getBean())) {
if (!isEndpointTypeExposed(endpointBean.getBeanType())) {
return false;
}
if (filter == null) {
@@ -392,7 +418,9 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
private final String beanName;
private final Object bean;
private final Class<?> beanType;
private final Supplier<Object> beanSupplier;
private final EndpointId id;
@@ -402,17 +430,18 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
private Set<ExtensionBean> extensions = new LinkedHashSet<>();
EndpointBean(Environment environment, String beanName, Object bean) {
MergedAnnotation<Endpoint> annotation = MergedAnnotations
.from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class);
EndpointBean(Environment environment, String beanName, Class<?> beanType, Supplier<Object> beanSupplier) {
MergedAnnotation<Endpoint> annotation = MergedAnnotations.from(beanType, SearchStrategy.TYPE_HIERARCHY)
.get(Endpoint.class);
String id = annotation.getString("id");
Assert.state(StringUtils.hasText(id),
() -> "No @Endpoint id attribute specified for " + bean.getClass().getName());
() -> "No @Endpoint id attribute specified for " + beanType.getName());
this.beanName = beanName;
this.bean = bean;
this.beanType = beanType;
this.beanSupplier = beanSupplier;
this.id = EndpointId.of(environment, id);
this.enabledByDefault = annotation.getBoolean("enableByDefault");
this.filter = getFilter(this.bean.getClass());
this.filter = getFilter(beanType);
}
void addExtension(ExtensionBean extensionBean) {
@@ -432,8 +461,12 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
return this.beanName;
}
Class<?> getBeanType() {
return this.beanType;
}
Object getBean() {
return this.bean;
return this.beanSupplier.get();
}
EndpointId getId() {
@@ -457,17 +490,20 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
private final String beanName;
private final Object bean;
private final Class<?> beanType;
private final Supplier<Object> beanSupplier;
private final EndpointId endpointId;
private final Class<?> filter;
ExtensionBean(Environment environment, String beanName, Object bean) {
this.bean = bean;
ExtensionBean(Environment environment, String beanName, Class<?> beanType, Supplier<Object> beanSupplier) {
this.beanName = beanName;
this.beanType = beanType;
this.beanSupplier = beanSupplier;
MergedAnnotation<EndpointExtension> extensionAnnotation = MergedAnnotations
.from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY).get(EndpointExtension.class);
.from(beanType, SearchStrategy.TYPE_HIERARCHY).get(EndpointExtension.class);
Class<?> endpointType = extensionAnnotation.getClass("endpoint");
MergedAnnotation<Endpoint> endpointAnnotation = MergedAnnotations
.from(endpointType, SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class);
@@ -481,8 +517,12 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
return this.beanName;
}
Class<?> getBeanType() {
return this.beanType;
}
Object getBean() {
return this.bean;
return this.beanSupplier.get();
}
EndpointId getEndpointId() {

View File

@@ -30,7 +30,7 @@ import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.PathMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.ClassUtils;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
/**
* {@link EndpointDiscoverer} for {@link ExposableControllerEndpoint controller
@@ -57,9 +57,8 @@ public class ControllerEndpointDiscoverer extends EndpointDiscoverer<ExposableCo
}
@Override
protected boolean isEndpointExposed(Object endpointBean) {
Class<?> type = ClassUtils.getUserClass(endpointBean.getClass());
MergedAnnotations annotations = MergedAnnotations.from(type);
protected boolean isEndpointTypeExposed(Class<?> beanType) {
MergedAnnotations annotations = MergedAnnotations.from(beanType, SearchStrategy.SUPERCLASS);
return annotations.isPresent(ControllerEndpoint.class) || annotations.isPresent(RestControllerEndpoint.class);
}

View File

@@ -31,7 +31,7 @@ import org.springframework.boot.actuate.endpoint.web.ExposableServletEndpoint;
import org.springframework.boot.actuate.endpoint.web.PathMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.ClassUtils;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
/**
* {@link EndpointDiscoverer} for {@link ExposableServletEndpoint servlet endpoints}.
@@ -57,9 +57,8 @@ public class ServletEndpointDiscoverer extends EndpointDiscoverer<ExposableServl
}
@Override
protected boolean isEndpointExposed(Object endpointBean) {
Class<?> type = ClassUtils.getUserClass(endpointBean.getClass());
return MergedAnnotations.from(type).isPresent(ServletEndpoint.class);
protected boolean isEndpointTypeExposed(Class<?> beanType) {
return MergedAnnotations.from(beanType, SearchStrategy.SUPERCLASS).isPresent(ServletEndpoint.class);
}
@Override