Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ TIP: Actuator is supported natively with Spring MVC, Spring WebFlux, and Jersey.
If both Jersey and Spring MVC are available, Spring MVC is used.

NOTE: Jackson is a required dependency in order to get the correct JSON responses as documented in the xref:api:rest/actuator/index.adoc[API documentation].
Jackson 3 should be used for Spring MVC and Spring WebFlux.
Jersey does not yet have a Jackson 3 module, so you will need to use Jackson 2.
Jackson 3 should be used for Spring MVC, Spring WebFlux, and Jersey.



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,4 @@ dependencies {
testImplementation("net.minidev:json-smart")
testImplementation("org.springframework.security:spring-security-web")
testRuntimeOnly("ch.qos.logback:logback-classic")
testRuntimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") // Required for Jersey
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@
import java.util.Collection;
import java.util.HashSet;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.ext.ContextResolver;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.jackson3.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.servlet.ServletContainer;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper;

import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
Expand Down Expand Up @@ -121,7 +121,7 @@ ResourceConfig resourceConfig(Environment environment, WebEndpointDiscoverer end
new EndpointLinksResolver(endpointDiscoverer.getEndpoints()), StringUtils.hasText(endpointPath));
resourceConfig.registerResources(new HashSet<>(resources));
resourceConfig.register(JacksonFeature.class);
resourceConfig.register(new ObjectMapperContextResolver(new ObjectMapper()), ContextResolver.class);
resourceConfig.register(new JsonMapperContextResolver(new JsonMapper()), ContextResolver.class);
return resourceConfig;
}

Expand Down Expand Up @@ -154,17 +154,17 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

}

private static final class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private static final class JsonMapperContextResolver implements ContextResolver<JsonMapper> {

private final ObjectMapper objectMapper;
private final JsonMapper jsonMapper;

private ObjectMapperContextResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
private JsonMapperContextResolver(JsonMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}

@Override
public ObjectMapper getContext(Class<?> type) {
return this.objectMapper;
public JsonMapper getContext(Class<?> type) {
return this.jsonMapper;
}

}
Expand Down
8 changes: 6 additions & 2 deletions module/spring-boot-jersey/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@ dependencies {
api("org.glassfish.jersey.containers:jersey-container-servlet")
api("org.glassfish.jersey.core:jersey-server")
api("org.glassfish.jersey.ext:jersey-spring6")
api("org.glassfish.jersey.media:jersey-media-json-jackson")
api("org.glassfish.jersey.media:jersey-media-json-jackson3")

compileOnly("jakarta.servlet:jakarta.servlet-api")
compileOnly("com.google.code.findbugs:jsr305")
compileOnly("org.glassfish.jersey.media:jersey-media-json-jackson")

implementation("org.springframework:spring-web")

optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-actuator-autoconfigure"))
optional(project(":module:spring-boot-health"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-jackson2"))
optional(project(":module:spring-boot-micrometer-metrics"))
optional(project(":module:spring-boot-micrometer-observation"))
Expand All @@ -48,7 +50,7 @@ dependencies {

testFixturesApi(testFixtures(project(":module:spring-boot-actuator")))
testFixturesImplementation(project(":module:spring-boot-tomcat"))
testFixturesRuntimeOnly(project(":module:spring-boot-jackson2"))
testFixturesRuntimeOnly(project(":module:spring-boot-jackson"))

testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":module:spring-boot-restclient"))
Expand All @@ -58,6 +60,8 @@ dependencies {
testImplementation("jakarta.servlet:jakarta.servlet-api")
testImplementation("org.springframework:spring-webflux")

testCompileOnly("org.glassfish.jersey.media:jersey-media-json-jackson")

testRuntimeOnly("ch.qos.logback:logback-classic")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.glassfish.jersey.server.spring.SpringComponentProvider;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.servlet.ServletProperties;
import tools.jackson.databind.json.JsonMapper;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -66,9 +67,11 @@
* @author Andy Wilkinson
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Rene Schakmann
* @since 4.0.0
*/
@AutoConfiguration(afterName = { "org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" })
@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration",
"org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" })
@ConditionalOnClass({ SpringComponentProvider.class, ServletRegistration.class })
@ConditionalOnBean(type = "org.glassfish.jersey.server.ResourceConfig")
@ConditionalOnWebApplication(type = Type.SERVLET)
Expand Down Expand Up @@ -175,6 +178,36 @@ public void onStartup(ServletContext servletContext) throws ServletException {

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ org.glassfish.jersey.jackson3.JacksonFeature.class, JsonMapper.class })
@ConditionalOnSingleCandidate(JsonMapper.class)
static class JacksonResourceConfigCustomizerConfiguration {

@Bean
ResourceConfigCustomizer jacksonResourceConfigCustomizer(JsonMapper jsonMapper) {
return (ResourceConfig config) -> {
config.register(org.glassfish.jersey.jackson3.JacksonFeature.class);
config.register(new JsonMapperContextResolver(jsonMapper), ContextResolver.class);
};
}

private static final class JsonMapperContextResolver implements ContextResolver<JsonMapper> {

private final JsonMapper jsonMapper;

private JsonMapperContextResolver(JsonMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}

@Override
public JsonMapper getContext(Class<?> type) {
return this.jsonMapper;
}

}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JacksonFeature.class)
@ConditionalOnSingleCandidate(com.fasterxml.jackson.databind.ObjectMapper.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.jspecify.annotations.Nullable;
import tools.jackson.databind.json.JsonMapper;

import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
Expand All @@ -41,6 +42,7 @@
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.jackson.EndpointJsonMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
Expand Down Expand Up @@ -71,6 +73,7 @@
* @author Michael Simons
* @author Madhura Bhave
* @author HaiTao Zhang
* @author Rene Schakmann
*/
@ManagementContextConfiguration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
Expand All @@ -93,6 +96,13 @@ JerseyWebEndpointsResourcesRegistrar jerseyWebEndpointsResourcesRegistrar(Enviro
endpointMediaTypes, basePath, shouldRegisterLinks);
}

@Bean
@ConditionalOnBean(EndpointJsonMapper.class)
ResourceConfigCustomizer endpointJsonMapperResourceConfigCustomizer(EndpointJsonMapper endpointJsonMapper) {
return (config) -> config.register(new EndpointJsonMapperContextResolver(endpointJsonMapper),
ContextResolver.class);
}

@Bean
@ConditionalOnBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
@SuppressWarnings("removal")
Expand Down Expand Up @@ -223,6 +233,26 @@ private void register(Collection<Resource> resources, ResourceConfig config) {

}

/**
* {@link ContextResolver} used to obtain the {@link JsonMapper} that should be used
* for {@link OperationResponseBody} instances.
*/
@Priority(Priorities.USER - 100)
private static final class EndpointJsonMapperContextResolver implements ContextResolver<JsonMapper> {

private final EndpointJsonMapper mapper;

private EndpointJsonMapperContextResolver(EndpointJsonMapper mapper) {
this.mapper = mapper;
}

@Override
public @Nullable JsonMapper getContext(Class<?> type) {
return OperationResponseBody.class.isAssignableFrom(type) ? this.mapper.get() : null;
}

}

/**
* {@link ContextResolver} used to obtain the {@link ObjectMapper} that should be used
* for {@link OperationResponseBody} instances.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.http.server.LocalTestWebServer;
Expand All @@ -47,14 +48,14 @@
import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link JerseyAutoConfiguration} when using custom ObjectMapper.
* Tests for {@link JerseyAutoConfiguration} when using custom JsonMapper.
*
* @author Eddú Meléndez
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = "spring.jackson2.default-property-inclusion=non_null")
properties = "spring.jackson.default-property-inclusion=non_null")
@DirtiesContext
class JerseyAutoConfigurationCustomObjectMapperProviderTests {
class JerseyAutoConfigurationCustomJsonMapperProviderTests {

@Autowired
private ApplicationContext applicationContext;
Expand Down Expand Up @@ -116,13 +117,11 @@ public void setBody(@Nullable String body) {

}

@SuppressWarnings("removal")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ TomcatServletWebServerAutoConfiguration.class,
org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class,
@Import({ TomcatServletWebServerAutoConfiguration.class, JacksonAutoConfiguration.class,
JerseyAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2012-present 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
*
* https://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.jersey.autoconfigure;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
import jakarta.ws.rs.ext.ContextResolver;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.testsupport.classpath.ClassPathOverrides;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link JerseyAutoConfiguration} when using Jersey's Jackson 2 support.
*
* @author Andy Wilkinson
* @author Rene Schakmann
*/
@ClassPathExclusions("jersey-media-json-jackson3-*.jar")
@ClassPathOverrides("org.glassfish.jersey.media:jersey-media-json-jackson:4.0.3")
@SuppressWarnings("removal")
class JerseyAutoConfigurationJackson2Tests {

private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class,
org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class))
.withUserConfiguration(ResourceConfigConfiguration.class);

@Test
void jacksonFeatureAndObjectMapperContextResolverAreRegistered() {
this.contextRunner.run((context) -> {
ResourceConfig config = context.getBean(ResourceConfig.class);
assertThat(config.isRegistered(JacksonFeature.class)).isTrue();
assertThat(config.getInstances()).anyMatch(ContextResolver.class::isInstance);
});
}

@Test
void whenJaxbIsAvailableTheObjectMapperIsCustomizedWithAnAnnotationIntrospector() {
this.contextRunner.run((context) -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
assertThat(objectMapper.getSerializationConfig()
.getAnnotationIntrospector()
.allIntrospectors()
.stream()
.filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).hasSize(1);
});
}

@Test
void whenJaxbIsNotAvailableTheObjectMapperCustomizationBacksOff() {
this.contextRunner.withClassLoader(new FilteredClassLoader("jakarta.xml.bind.annotation")).run((context) -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
assertThat(objectMapper.getSerializationConfig()
.getAnnotationIntrospector()
.allIntrospectors()
.stream()
.filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).isEmpty();
});
}

@Test
void whenJacksonJaxbModuleIsNotAvailableTheObjectMapperCustomizationBacksOff() {
this.contextRunner.withClassLoader(new FilteredClassLoader(JakartaXmlBindAnnotationIntrospector.class))
.run((context) -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
assertThat(objectMapper.getSerializationConfig()
.getAnnotationIntrospector()
.allIntrospectors()
.stream()
.filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).isEmpty();
});
}

@Configuration(proxyBeanMethods = false)
static class ResourceConfigConfiguration {

@Bean
ResourceConfig resourceConfig() {
return new ResourceConfig();
}

}

}
Loading