From 26da45aa9aa4071c8c0959e3630ab16e770ee79c Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Fri, 8 Feb 2019 18:24:55 -0800 Subject: [PATCH] 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 --- ...ndpointManagementContextConfiguration.java | 48 +++++++++-- ...ntManagementContextConfigurationTests.java | 85 +++++++++++++++++-- .../JerseyEndpointIntegrationTests.java | 30 +++++-- .../jersey/JerseyAutoConfiguration.java | 23 +---- .../servlet/DefaultJerseyApplicationPath.java | 64 ++++++++++++++ 5 files changed, 212 insertions(+), 38 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index f303a53150e..0b587228551 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -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 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 jerseyServletRegistration( + ObjectProvider resourceConfigCustomizers, + JerseyApplicationPath jerseyApplicationPath) { + return new ServletRegistrationBean<>( + new ServletContainer(resourceConfig(resourceConfigCustomizers)), + jerseyApplicationPath.getUrlMapping()); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java index 17e2fbecb89..56f8b661fa9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java @@ -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 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); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java index 43a22a047f3..c6147eb632e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java @@ -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(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java index fc08534a918..0e3fb473390 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java @@ -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(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java new file mode 100644 index 00000000000..3470ab9c232 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java @@ -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(); + } + +}