Configure a JerseyApplicationPath bean for the actuators

This commit also ensures that Jersey-based actuator endpoints are
available before the user has configured a `ResourceConfig` bean

Fixes gh-15625
Fixes gh-15877
This commit is contained in:
Madhura Bhave 2019-02-08 18:24:55 -08:00
parent c24f02696f
commit 26da45aa9a
5 changed files with 212 additions and 38 deletions

View File

@ -23,7 +23,9 @@ import java.util.HashSet;
import java.util.List;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
@ -40,8 +42,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jersey.JerseyProperties;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link ManagementContextConfiguration} for Jersey {@link Endpoint} concerns.
@ -49,6 +57,7 @@ import org.springframework.context.annotation.Bean;
* @author Andy Wilkinson
* @author Phillip Webb
* @author Michael Simons
* @author Madhura Bhave
*/
@ManagementContextConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ -57,12 +66,6 @@ import org.springframework.context.annotation.Bean;
@ConditionalOnMissingBean(type = "org.springframework.web.servlet.DispatcherServlet")
class JerseyWebEndpointManagementContextConfiguration {
@ConditionalOnMissingBean(ResourceConfig.class)
@Bean
public ResourceConfig resourceConfig() {
return new ResourceConfig();
}
@Bean
public ResourceConfigCustomizer webEndpointRegistrar(
WebEndpointsSupplier webEndpointsSupplier,
@ -85,4 +88,37 @@ class JerseyWebEndpointManagementContextConfiguration {
};
}
@Configuration
@ConditionalOnMissingBean(ResourceConfig.class)
@EnableConfigurationProperties(JerseyProperties.class)
static class ResourceConfigConfiguration {
@Bean
public ResourceConfig resourceConfig(
ObjectProvider<ResourceConfigCustomizer> resourceConfigCustomizers) {
ResourceConfig resourceConfig = new ResourceConfig();
resourceConfigCustomizers.orderedStream()
.forEach((customizer) -> customizer.customize(resourceConfig));
return resourceConfig;
}
@Bean
@ConditionalOnMissingBean
public JerseyApplicationPath jerseyApplicationPath(JerseyProperties properties,
ResourceConfig config) {
return new DefaultJerseyApplicationPath(properties.getApplicationPath(),
config);
}
@Bean
public ServletRegistrationBean<ServletContainer> jerseyServletRegistration(
ObjectProvider<ResourceConfigCustomizer> resourceConfigCustomizers,
JerseyApplicationPath jerseyApplicationPath) {
return new ServletRegistrationBean<>(
new ServletContainer(resourceConfig(resourceConfigCustomizers)),
jerseyApplicationPath.getUrlMapping());
}
}
}

View File

@ -19,43 +19,108 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey;
import java.util.Collections;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link JerseyWebEndpointManagementContextConfiguration}.
*
* @author Michael Simons
* @author Madhura Bhave
*/
public class JerseyWebEndpointManagementContextConfigurationTests {
private final WebApplicationContextRunner runner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebEndpointAutoConfiguration.class,
JerseyWebEndpointManagementContextConfiguration.class));
JerseyWebEndpointManagementContextConfiguration.class))
.withUserConfiguration(WebEndpointsSupplierConfig.class);
@Test
public void resourceConfigIsAutoConfiguredWhenNeeded() {
this.runner.withUserConfiguration(WebEndpointsSupplierConfig.class).run(
this.runner.run(
(context) -> assertThat(context).hasSingleBean(ResourceConfig.class));
}
@Test
public void existingResourceConfigIsUsedWhenAvailable() {
this.runner.withUserConfiguration(WebEndpointsSupplierConfig.class,
ConfigWithResourceConfig.class).run((context) -> {
public void jerseyApplicationPathIsAutoConfiguredWhenNeeded() {
this.runner.run((context) -> assertThat(context)
.hasSingleBean(DefaultJerseyApplicationPath.class));
}
@Test
public void jerseyApplicationPathIsConditionalOnMissinBean() {
this.runner.withUserConfiguration(ConfigWithJerseyApplicationPath.class)
.run((context) -> {
assertThat(context).hasSingleBean(JerseyApplicationPath.class);
assertThat(context).hasBean("testJerseyApplicationPath");
});
}
@Test
@SuppressWarnings("unchecked")
public void servletRegistrationBeanIsAutoConfiguredWhenNeeded() {
this.runner.withPropertyValues("spring.jersey.application-path=/jersey")
.run((context) -> {
ServletRegistrationBean<ServletContainer> bean = context
.getBean(ServletRegistrationBean.class);
assertThat(bean.getUrlMappings()).containsExactly("/jersey/*");
});
}
@Test
public void existingResourceConfigBeanShouldNotAutoConfigureRelatedBeans() {
this.runner.withUserConfiguration(ConfigWithResourceConfig.class)
.run((context) -> {
assertThat(context).hasSingleBean(ResourceConfig.class);
assertThat(context).doesNotHaveBean(JerseyApplicationPath.class);
assertThat(context).doesNotHaveBean(ServletRegistrationBean.class);
assertThat(context).hasBean("customResourceConfig");
});
}
@Test
public void resourceConfigIsCustomizedWithResourceConfigCustomizerBean() {
this.runner.withUserConfiguration(CustomizerConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(ResourceConfig.class);
ResourceConfig config = context.getBean(ResourceConfig.class);
ResourceConfigCustomizer customizer = (ResourceConfigCustomizer) context
.getBean("testResourceConfigCustomizer");
verify(customizer).customize(config);
});
}
@Test
public void resourceConfigCustomizerBeanIsNotRequired() {
this.runner.run(
(context) -> assertThat(context).hasSingleBean(ResourceConfig.class));
}
@Configuration
static class CustomizerConfiguration {
@Bean
ResourceConfigCustomizer testResourceConfigCustomizer() {
return mock(ResourceConfigCustomizer.class);
}
}
@Configuration
static class WebEndpointsSupplierConfig {
@ -76,4 +141,14 @@ public class JerseyWebEndpointManagementContextConfigurationTests {
}
@Configuration
static class ConfigWithJerseyApplicationPath {
@Bean
public JerseyApplicationPath testJerseyApplicationPath() {
return mock(JerseyApplicationPath.class);
}
}
}

View File

@ -41,11 +41,22 @@ import org.springframework.web.servlet.DispatcherServlet;
* Integration tests for the Jersey actuator endpoints.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
public class JerseyEndpointIntegrationTests {
@Test
public void linksAreProvidedToAllEndpointTypes() throws Exception {
public void linksAreProvidedToAllEndpointTypes() {
testJerseyEndpoints(new Class[] { EndpointsConfiguration.class,
ResourceConfigConfiguration.class });
}
@Test
public void actuatorEndpointsWhenUserProvidedResourceConfigBeanNotAvailable() {
testJerseyEndpoints(new Class[] { EndpointsConfiguration.class });
}
protected void testJerseyEndpoints(Class[] userConfigurations) {
FilteredClassLoader classLoader = new FilteredClassLoader(
DispatcherServlet.class);
new WebApplicationContextRunner(
@ -59,7 +70,7 @@ public class JerseyEndpointIntegrationTests {
WebEndpointAutoConfiguration.class,
ManagementContextAutoConfiguration.class,
BeansEndpointAutoConfiguration.class))
.withUserConfiguration(EndpointsConfiguration.class)
.withUserConfiguration(userConfigurations)
.withPropertyValues("management.endpoints.web.exposure.include:*",
"server.port:0")
.run((context) -> {
@ -88,11 +99,6 @@ public class JerseyEndpointIntegrationTests {
@Configuration
static class EndpointsConfiguration {
@Bean
ResourceConfig testResourceConfig() {
return new ResourceConfig();
}
@Bean
TestControllerEndpoint testControllerEndpoint() {
return new TestControllerEndpoint();
@ -105,4 +111,14 @@ public class JerseyEndpointIntegrationTests {
}
@Configuration
static class ResourceConfigConfiguration {
@Bean
ResourceConfig testResourceConfig() {
return new ResourceConfig();
}
}
}

View File

@ -24,7 +24,6 @@ import javax.servlet.DispatcherType;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.ext.ContextResolver;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
@ -51,6 +50,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandi
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -60,10 +60,8 @@ import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ServletContextAware;
@ -114,15 +112,8 @@ public class JerseyAutoConfiguration implements ServletContextAware {
@Bean
@ConditionalOnMissingBean
public JerseyApplicationPath jerseyApplicationPath() {
return this::resolveApplicationPath;
}
private String resolveApplicationPath() {
if (StringUtils.hasLength(this.jersey.getApplicationPath())) {
return this.jersey.getApplicationPath();
}
return findApplicationPath(AnnotationUtils.findAnnotation(
this.config.getApplication().getClass(), ApplicationPath.class));
return new DefaultJerseyApplicationPath(this.jersey.getApplicationPath(),
this.config);
}
@Bean
@ -171,14 +162,6 @@ public class JerseyAutoConfiguration implements ServletContextAware {
this.jersey.getInit().forEach(registration::addInitParameter);
}
private static String findApplicationPath(ApplicationPath annotation) {
// Jersey doesn't like to be the default servlet, so map to /* as a fallback
if (annotation == null) {
return "/*";
}
return annotation.value();
}
@Override
public void setServletContext(ServletContext servletContext) {
String servletRegistrationName = getServletRegistrationName();

View File

@ -0,0 +1,64 @@
/*
* Copyright 2012-2018 the original author or authors.
*
* 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 org.springframework.boot.autoconfigure.web.servlet;
import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.autoconfigure.jersey.JerseyProperties;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link JerseyApplicationPath} that derives the path from
* {@link JerseyProperties} or the {@code @ApplicationPath} annotation.
*
* @author Madhura Bhave
*/
public class DefaultJerseyApplicationPath implements JerseyApplicationPath {
private final String applicationPath;
private final ResourceConfig config;
public DefaultJerseyApplicationPath(String applicationPath, ResourceConfig config) {
this.applicationPath = applicationPath;
this.config = config;
}
@Override
public String getPath() {
return resolveApplicationPath();
}
private String resolveApplicationPath() {
if (StringUtils.hasLength(this.applicationPath)) {
return this.applicationPath;
}
return findApplicationPath(AnnotationUtils.findAnnotation(
this.config.getApplication().getClass(), ApplicationPath.class));
}
private static String findApplicationPath(ApplicationPath annotation) {
// Jersey doesn't like to be the default servlet, so map to /* as a fallback
if (annotation == null) {
return "/*";
}
return annotation.value();
}
}