diff --git a/pom.xml b/pom.xml index a6dc167a03..d7ee1acbfc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.0-GH-3292-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. @@ -70,6 +70,11 @@ jackson-databind true + + tools.jackson.core + jackson-databind + true + org.springframework spring-web diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc index 1ced7779f5..7be6cf1c04 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc @@ -290,9 +290,9 @@ By default, the assembler points to the controller method it was invoked in, but == Spring Data Jackson Modules The core module, and some of the store specific ones, ship with a set of Jackson Modules for types, like `org.springframework.data.geo.Distance` and `org.springframework.data.geo.Point`, used by the Spring Data domain. + -Those Modules are imported once xref:repositories/core-extensions.adoc#core.web[web support] is enabled and `com.fasterxml.jackson.databind.ObjectMapper` is available. +Those modules are imported once xref:repositories/core-extensions.adoc#core.web[web support] is enabled and `tools.jackson.databind.ObjectMapper` is available. -During initialization `SpringDataJacksonModules`, like the `SpringDataJacksonConfiguration`, get picked up by the infrastructure, so that the declared ``com.fasterxml.jackson.databind.Module``s are made available to the Jackson `ObjectMapper`. +During initialization `SpringDataJackson3Modules`, like the `SpringDataJackson3Configuration`, get picked up by the infrastructure, so that the declared ``tools.jackson.databind.JacksonModule``s are made available to the Jackson `ObjectMapper`. Data binding mixins for the following domain types are registered by the common infrastructure. @@ -306,10 +306,15 @@ org.springframework.data.geo.Polygon [NOTE] ==== -The individual module may provide additional `SpringDataJacksonModules`. + +The individual module may provide additional `SpringDataJackson3Modules`. + Please refer to the store specific section for more details. ==== +[NOTE] +==== +Jackson 2 support is deprecated and will be removed in a future release. +==== + [[core.web.binding]] == Web Databinding Support @@ -341,7 +346,7 @@ Nested projections are supported as described in xref:repositories/projections.a If the method returns a complex, non-interface type, a Jackson `ObjectMapper` is used to map the final value. For Spring MVC, the necessary converters are registered automatically as soon as `@EnableSpringDataWebSupport` is active and the required dependencies are available on the classpath. -For usage with `RestTemplate`, register a `ProjectingJackson2HttpMessageConverter` (JSON) or `XmlBeamHttpMessageConverter` manually. +For usage with `RestTemplate`, register a `ProjectingJacksonHttpMessageConverter` (JSON) or `XmlBeamHttpMessageConverter` manually. For more information, see the https://github.com/spring-projects/spring-data-examples/tree/main/web/projection[web projection example] in the canonical https://github.com/spring-projects/spring-data-examples[Spring Data Examples repository]. diff --git a/src/main/java/org/springframework/data/geo/GeoJacksonModule.java b/src/main/java/org/springframework/data/geo/GeoJacksonModule.java new file mode 100644 index 0000000000..f13703109f --- /dev/null +++ b/src/main/java/org/springframework/data/geo/GeoJacksonModule.java @@ -0,0 +1,80 @@ +/* + * Copyright 2014-2025 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.data.geo; + +import tools.jackson.core.Version; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.module.SimpleModule; + +import java.io.Serial; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Custom module to deserialize the geo-spatial value objects using Jackson 3. + * + * @author Oliver Gierke + * @author Mark Paluch + * @since 4.0 + */ +@SuppressWarnings("unused") +public class GeoJacksonModule extends SimpleModule { + + private static final @Serial long serialVersionUID = 1L; + + /** + * Creates a new {@link GeoJacksonModule} registering mixins for common geo-spatial types. + */ + public GeoJacksonModule() { + + super("Spring Data Geo Mixins", new Version(1, 0, 0, null, "org.springframework.data", "spring-data-commons-geo")); + + setMixInAnnotation(Distance.class, DistanceMixin.class); + setMixInAnnotation(Point.class, PointMixin.class); + setMixInAnnotation(Box.class, BoxMixin.class); + setMixInAnnotation(Circle.class, CircleMixin.class); + setMixInAnnotation(Polygon.class, PolygonMixin.class); + } + + @JsonIgnoreProperties("unit") + static abstract class DistanceMixin { + + DistanceMixin(@JsonProperty("value") double value, + @JsonProperty("metric") @JsonDeserialize(as = Metrics.class) Metric metic) {} + + @JsonIgnore + abstract double getNormalizedValue(); + } + + static abstract class PointMixin { + PointMixin(@JsonProperty("x") double x, @JsonProperty("y") double y) {} + } + + static abstract class CircleMixin { + CircleMixin(@JsonProperty("center") Point center, @JsonProperty("radius") Distance radius) {} + } + + static abstract class BoxMixin { + BoxMixin(@JsonProperty("first") Point first, @JsonProperty("second") Point point) {} + } + + static abstract class PolygonMixin { + PolygonMixin(@JsonProperty("points") List points) {} + } +} diff --git a/src/main/java/org/springframework/data/geo/GeoModule.java b/src/main/java/org/springframework/data/geo/GeoModule.java index e60639b2ec..17f9b065a6 100644 --- a/src/main/java/org/springframework/data/geo/GeoModule.java +++ b/src/main/java/org/springframework/data/geo/GeoModule.java @@ -30,8 +30,10 @@ * * @author Oliver Gierke * @since 1.8 + * @deprecated since 4.0, use {@link GeoJacksonModule} instead. */ @SuppressWarnings("unused") +@Deprecated(since = "4.0", forRemoval = true) public class GeoModule extends SimpleModule { private static final @Serial long serialVersionUID = 1L; diff --git a/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java b/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java index 75b08c3145..7ce69ecab3 100644 --- a/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java +++ b/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java @@ -27,7 +27,9 @@ * @author Oliver Gierke * @author Christoph Strobl * @since 1.6 + * @deprecated since 4.0, in favor of {@link JacksonRepositoryPopulatorFactoryBean}. */ +@Deprecated(since = "4.0", forRemoval = true) public class Jackson2RepositoryPopulatorFactoryBean extends AbstractRepositoryPopulatorFactoryBean { private @Nullable ObjectMapper mapper; diff --git a/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java b/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java index 783fa9ec7d..4514fe869d 100644 --- a/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java +++ b/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java @@ -40,7 +40,9 @@ * @author Mark Paluch * @author Johannes Englmeier * @since 1.6 + * @deprecated since 4.0, in favor of {@link JacksonResourceReader}. */ +@Deprecated(since = "4.0", forRemoval = true) public class Jackson2ResourceReader implements ResourceReader { private static final String DEFAULT_TYPE_KEY = "_class"; diff --git a/src/main/java/org/springframework/data/repository/init/JacksonRepositoryPopulatorFactoryBean.java b/src/main/java/org/springframework/data/repository/init/JacksonRepositoryPopulatorFactoryBean.java new file mode 100644 index 0000000000..f6ea4a9128 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/init/JacksonRepositoryPopulatorFactoryBean.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025 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.data.repository.init; + +import tools.jackson.databind.ObjectMapper; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.FactoryBean; + +/** + * {@link FactoryBean} to set up a {@link ResourceReaderRepositoryPopulator} with a {@link JacksonResourceReader}. + * + * @author Mark Paluch + * @author Oliver Gierke + * @author Christoph Strobl + * @since 4.0 + */ +public class JacksonRepositoryPopulatorFactoryBean extends AbstractRepositoryPopulatorFactoryBean { + + private @Nullable ObjectMapper mapper; + + /** + * Configures the {@link ObjectMapper} to be used. + * + * @param mapper can be {@literal null}. + */ + public void setMapper(@Nullable ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + protected ResourceReader getResourceReader() { + return mapper == null ? new JacksonResourceReader() : new JacksonResourceReader(mapper); + } +} diff --git a/src/main/java/org/springframework/data/repository/init/JacksonResourceReader.java b/src/main/java/org/springframework/data/repository/init/JacksonResourceReader.java new file mode 100644 index 0000000000..483db7f6b8 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/init/JacksonResourceReader.java @@ -0,0 +1,123 @@ +/* + * Copyright 2013-2025 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.data.repository.init; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A {@link ResourceReader} using Jackson to read JSON into objects. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @author Mark Paluch + * @author Johannes Englmeier + * @since 4.0 + */ +public class JacksonResourceReader implements ResourceReader { + + private static final String DEFAULT_TYPE_KEY = "_class"; + private static final ObjectMapper DEFAULT_MAPPER = JsonMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build(); + + private final ObjectMapper mapper; + private String typeKey = DEFAULT_TYPE_KEY; + + /** + * Creates a new {@link JacksonResourceReader}. + */ + public JacksonResourceReader() { + this(DEFAULT_MAPPER); + } + + /** + * Creates a new {@link JacksonResourceReader} using the given {@link ObjectMapper}. + * + * @param mapper + */ + public JacksonResourceReader(ObjectMapper mapper) { + this.mapper = mapper; + } + + /** + * Configures the JSON document's key to look up the type to instantiate the object. Defaults to + * {@link JacksonResourceReader#DEFAULT_TYPE_KEY}. + * + * @param typeKey + */ + public void setTypeKey(@Nullable String typeKey) { + this.typeKey = typeKey == null ? DEFAULT_TYPE_KEY : typeKey; + } + + @Override + public Object readFrom(Resource resource, @Nullable ClassLoader classLoader) throws Exception { + + Assert.notNull(resource, "Resource must not be null"); + + InputStream stream = resource.getInputStream(); + JsonNode node = mapper.readerFor(JsonNode.class).readTree(stream); + + if (node.isArray()) { + + Iterator elements = node.iterator(); + List result = new ArrayList<>(); + + while (elements.hasNext()) { + JsonNode element = elements.next(); + result.add(readSingle(element, classLoader)); + } + + return result; + } + + return readSingle(node, classLoader); + } + + /** + * Reads the given {@link JsonNode} into an instance of the type encoded in it using the configured type key. + * + * @param node must not be {@literal null}. + * @param classLoader can be {@literal null}. + * @return + */ + private Object readSingle(JsonNode node, @Nullable ClassLoader classLoader) throws IOException { + + JsonNode typeNode = node.findValue(typeKey); + + if (typeNode == null) { + throw new IllegalArgumentException(String.format("Could not find type for type key '%s'", typeKey)); + } + + String typeName = typeNode.asString(); + Class type = ClassUtils.resolveClassName(typeName, classLoader); + + return mapper.readerFor(type).readValue(node); + } +} diff --git a/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java b/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java index 20c7b86d78..b0d6526062 100644 --- a/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java +++ b/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java @@ -131,18 +131,27 @@ private static boolean hasJsonPathAnnotation(Class type) { return false; } - private static class InputMessageProjecting implements MethodInterceptor { - - private final DocumentContext context; - - public InputMessageProjecting(DocumentContext context) { - this.context = context; - } + private record InputMessageProjecting(DocumentContext context) implements MethodInterceptor { @Override public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); + + switch (method.getName()) { + case "equals" -> { + // Only consider equal when proxies are identical. + return (invocation.getThis() == invocation.getArguments()[0]); + } + case "hashCode" -> { + // Use hashCode of EntityManager proxy. + return context.hashCode(); + } + case "toString" -> { + return context.jsonString(); + } + } + TypeInformation returnType = TypeInformation.fromReturnTypeOf(method); ResolvableType type = ResolvableType.forMethodReturnType(method); boolean isCollectionResult = type.getRawClass() != null && Collection.class.isAssignableFrom(type.getRawClass()); diff --git a/src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java b/src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java index 183b151637..3517ad0914 100644 --- a/src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java +++ b/src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java @@ -48,7 +48,9 @@ * @author Christoph Strobl * @soundtrack Richard Spaven - Ice Is Nice (Spaven's 5ive) * @since 1.13 + * @deprecated since 4.0, in favor of {@link ProjectingJacksonHttpMessageConverter}. */ +@Deprecated(since = "4.0", forRemoval = true) public class ProjectingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter implements BeanClassLoaderAware, BeanFactoryAware { diff --git a/src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java b/src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java new file mode 100644 index 0000000000..9862bf26a0 --- /dev/null +++ b/src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java @@ -0,0 +1,275 @@ +/* + * Copyright 2025 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.data.web; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.util.Assert; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.InvalidJsonException; +import com.jayway.jsonpath.TypeRef; +import com.jayway.jsonpath.spi.json.AbstractJsonProvider; +import com.jayway.jsonpath.spi.mapper.MappingException; +import com.jayway.jsonpath.spi.mapper.MappingProvider; + +/** + * {@link HttpMessageConverter} implementation to enable projected JSON binding to interfaces annotated with + * {@link ProjectedPayload}. + * + * @author Mark Paluch + * @author Oliver Gierke + * @author Christoph Strobl + * @soundtrack Richard Spaven - Ice Is Nice (Spaven's 5ive) + * @since 4.0 + */ +public class ProjectingJacksonHttpMessageConverter extends JacksonJsonHttpMessageConverter + implements BeanClassLoaderAware, BeanFactoryAware { + + private final SpelAwareProxyProjectionFactory projectionFactory; + private final Map, Boolean> supportedTypesCache = new ConcurrentHashMap<>(); + + /** + * Creates a new {@link ProjectingJacksonHttpMessageConverter} using a default {@link ObjectMapper}. + */ + public ProjectingJacksonHttpMessageConverter() { + this.projectionFactory = initProjectionFactory(getObjectMapper()); + } + + /** + * Creates a new {@link ProjectingJacksonHttpMessageConverter} for the given {@link ObjectMapper}. + * + * @param mapper must not be {@literal null}. + */ + public ProjectingJacksonHttpMessageConverter(ObjectMapper mapper) { + + super(mapper); + + this.projectionFactory = initProjectionFactory(mapper); + } + + /** + * Creates a new {@link SpelAwareProxyProjectionFactory} with the {@link JsonProjectingMethodInterceptorFactory} + * registered for the given {@link ObjectMapper}. + * + * @param mapper must not be {@literal null}. + * @return + */ + private static SpelAwareProxyProjectionFactory initProjectionFactory(ObjectMapper mapper) { + + Assert.notNull(mapper, "ObjectMapper must not be null"); + + SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + projectionFactory.registerMethodInvokerFactory(new JsonProjectingMethodInterceptorFactory( + new JacksonJsonProvider(mapper), new JacksonMappingProvider(mapper))); + + return projectionFactory; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + projectionFactory.setBeanClassLoader(classLoader); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + projectionFactory.setBeanFactory(beanFactory); + } + + @Override + protected boolean supports(Class clazz) { + + if (clazz.isInterface()) { + + Boolean result = supportedTypesCache.get(clazz); + + if (result != null) { + return result; + } + + result = AnnotationUtils.findAnnotation(clazz, ProjectedPayload.class) != null; + supportedTypesCache.put(clazz, result); + + return result; + } + + return false; + } + + @Override + public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) { + + if (!super.canRead(type, mediaType)) { + return false; + } + + Class clazz = type.resolve(); + if (clazz == null) { + return false; + } + + return supports(clazz); + } + + @Override + public boolean canWrite(Class clazz, @Nullable MediaType mediaType) { + return false; + } + + @Override + public Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map hints) + throws IOException, HttpMessageNotReadableException { + return projectionFactory.createProjection(type.resolve(Object.class), inputMessage.getBody()); + } + + record JacksonMappingProvider(ObjectMapper objectMapper) implements MappingProvider { + + @Override + public @Nullable T map(@Nullable Object source, Class targetType, Configuration configuration) { + if (source == null) { + return null; + } + try { + return objectMapper.convertValue(source, targetType); + } catch (Exception e) { + throw new MappingException(e); + } + + } + + @Override + public @Nullable T map(@Nullable Object source, final TypeRef targetType, Configuration configuration) { + if (source == null) { + return null; + } + + tools.jackson.databind.JavaType type = objectMapper.getTypeFactory().constructType(targetType.getType()); + + try { + return objectMapper.convertValue(source, type); + } catch (Exception e) { + throw new MappingException(e); + } + + } + } + + static class JacksonJsonProvider extends AbstractJsonProvider { + + private static final ObjectMapper defaultObjectMapper = new ObjectMapper(); + private static final ObjectReader defaultObjectReader = defaultObjectMapper.reader().forType(Object.class); + + protected ObjectMapper objectMapper; + protected ObjectReader objectReader; + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + /** + * Initialize the JacksonProvider with the default ObjectMapper and ObjectReader + */ + public JacksonJsonProvider() { + this(defaultObjectMapper, defaultObjectReader); + } + + /** + * Initialize the JacksonProvider with a custom ObjectMapper. + * + * @param objectMapper the ObjectMapper to use + */ + public JacksonJsonProvider(ObjectMapper objectMapper) { + this(objectMapper, objectMapper.readerFor(Object.class)); + } + + /** + * Initialize the JacksonProvider with a custom ObjectMapper and ObjectReader. + * + * @param objectMapper the ObjectMapper to use + * @param objectReader the ObjectReader to use + */ + public JacksonJsonProvider(ObjectMapper objectMapper, tools.jackson.databind.ObjectReader objectReader) { + this.objectMapper = objectMapper; + this.objectReader = objectReader; + } + + @Override + public Object parse(String json) throws InvalidJsonException { + return objectReader.readValue(json); + } + + @Override + public Object parse(InputStream jsonStream, String charset) throws InvalidJsonException { + try { + return objectReader.readValue(new InputStreamReader(jsonStream, charset)); + } catch (IOException e) { + throw new InvalidJsonException(e); + } + } + + @Override + public String toJson(Object obj) { + StringWriter writer = new StringWriter(); + try { + JsonGenerator generator = objectMapper.writer().createGenerator(writer); + objectMapper.writeValue(generator, obj); + writer.flush(); + writer.close(); + generator.close(); + return writer.getBuffer().toString(); + } catch (IOException e) { + throw new InvalidJsonException(e); + } + } + + @Override + public List createArray() { + return new LinkedList<>(); + } + + @Override + public Object createMap() { + return new LinkedHashMap(); + } + } + +} diff --git a/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java b/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java index 1956940ad5..e2e4ca5007 100644 --- a/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java +++ b/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java @@ -23,6 +23,7 @@ import org.springframework.aot.hint.TypeReference; import org.springframework.data.web.PagedModel; import org.springframework.data.web.config.EnableSpringDataWebSupport; +import org.springframework.data.web.config.SpringDataJackson3Configuration; import org.springframework.data.web.config.SpringDataJacksonConfiguration.PageModule; import org.springframework.util.ClassUtils; @@ -30,17 +31,24 @@ * {@link RuntimeHintsRegistrar} providing hints for web usage. * * @author Christoph Strobl + * @author Mark Paluch * @since 3.2.3 */ class WebRuntimeHints implements RuntimeHintsRegistrar { + private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", + WebRuntimeHints.class.getClassLoader()); + + private static final boolean JACKSON3_PRESENT = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", + WebRuntimeHints.class.getClassLoader()); + @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.reflection().registerType(org.springframework.data.web.config.SpringDataWebSettings.class, hint -> hint .withMembers(MemberCategory.INVOKE_DECLARED_METHODS).onReachableType(EnableSpringDataWebSupport.class)); - if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) { + if (JACKSON2_PRESENT || JACKSON3_PRESENT) { // Page Model for Jackson Rendering hints.reflection().registerType(org.springframework.data.web.PagedModel.class, @@ -49,24 +57,53 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) hints.reflection().registerType(PagedModel.PageMetadata.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); - // Type that might not be seen otherwise - hints.reflection().registerType(TypeReference.of("org.springframework.data.domain.Unpaged"), - hint -> hint.onReachableType(PageModule.class)); - - // Jackson Converters used via @JsonSerialize in SpringDataJacksonConfiguration - hints.reflection().registerType( - TypeReference - .of("org.springframework.data.web.config.SpringDataJacksonConfiguration$PageModule$PageModelConverter"), - hint -> { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); - hint.onReachableType(PageModule.class); - }); - hints.reflection().registerType(TypeReference.of( - "org.springframework.data.web.config.SpringDataJacksonConfiguration$PageModule$PlainPageSerializationWarning"), - hint -> { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); - hint.onReachableType(PageModule.class); - }); + hints.reflection().registerType(TypeReference.of("org.springframework.data.domain.Unpaged")); + + if (JACKSON2_PRESENT) { + contributeJackson2Hints(hints); + } + + if (JACKSON3_PRESENT) { + contributeJackson3Hints(hints); + } } } + + @SuppressWarnings("removal") + private static void contributeJackson2Hints(RuntimeHints hints) { + + // Jackson Converters used via @JsonSerialize in SpringDataJacksonConfiguration + hints.reflection().registerType( + TypeReference + .of("org.springframework.data.web.config.SpringDataJacksonConfiguration$PageModule$PageModelConverter"), + hint -> { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); + hint.onReachableType(PageModule.class); + }); + hints.reflection().registerType(TypeReference.of( + "org.springframework.data.web.config.SpringDataJacksonConfiguration$PageModule$PlainPageSerializationWarning"), + hint -> { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); + hint.onReachableType(PageModule.class); + }); + } + + private static void contributeJackson3Hints(RuntimeHints hints) { + + // Jackson Converters used via @JsonSerialize in SpringDataJacksonConfiguration + hints.reflection().registerType( + TypeReference + .of("org.springframework.data.web.config.SpringDataJackson3Configuration$PageModule$PageModelConverter"), + hint -> { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); + hint.onReachableType(SpringDataJackson3Configuration.PageModule.class); + }); + hints.reflection().registerType(TypeReference.of( + "org.springframework.data.web.config.SpringDataJackson3Configuration$PageModule$PlainPageSerializationWarning"), + hint -> { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); + hint.onReachableType(SpringDataJackson3Configuration.PageModule.class); + }); + } + } diff --git a/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java index aae4f2015d..f05cf2a1d9 100644 --- a/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java +++ b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java @@ -143,7 +143,12 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { resourceLoader// .filter(it -> ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", it))// .map(it -> SpringFactoriesLoader.loadFactoryNames(SpringDataJacksonModules.class, it))// - .ifPresent(it -> imports.addAll(it)); + .ifPresent(imports::addAll); + + resourceLoader// + .filter(it -> ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", it))// + .map(it -> SpringFactoriesLoader.loadFactoryNames(SpringDataJackson3Modules.class, it))// + .ifPresent(imports::addAll); return imports.toArray(new String[imports.size()]); } diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJackson3Configuration.java b/src/main/java/org/springframework/data/web/config/SpringDataJackson3Configuration.java new file mode 100644 index 0000000000..800ded7abd --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/SpringDataJackson3Configuration.java @@ -0,0 +1,161 @@ +/* + * Copyright 2014-2025 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.data.web.config; + +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.ser.BeanPropertyWriter; +import tools.jackson.databind.ser.ValueSerializerModifier; +import tools.jackson.databind.ser.std.ToStringSerializerBase; +import tools.jackson.databind.util.StdConverter; + +import java.io.Serial; +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.geo.GeoModule; +import org.springframework.data.web.PagedModel; +import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode; +import org.springframework.util.ClassUtils; + +/** + * JavaConfig class to export Jackson 3-specific configuration. + * + * @author Oliver Gierke + * @author Mark Paluch + * @since 4.0 + */ +public class SpringDataJackson3Configuration implements SpringDataJackson3Modules { + + @Nullable + @Autowired(required = false) SpringDataWebSettings settings; + + @Bean + public GeoModule jackson3GeoModule() { + return new GeoModule(); + } + + @Bean + public PageModule jackson3pageModule() { + return new PageModule(settings); + } + + /** + * A Jackson module customizing the serialization of {@link PageImpl} instances depending on the + * {@link SpringDataWebSettings} handed into the instance. In case of {@link PageSerializationMode#DIRECT} being + * configured, a no-op {@link StdConverter} is registered to issue a one-time warning about the mode being used (as + * it's not recommended). {@link PageSerializationMode#VIA_DTO} would register a converter wrapping {@link PageImpl} + * instances into {@link PagedModel}. + * + * @author Oliver Drotbohm + */ + public static class PageModule extends SimpleModule { + + private static final @Serial long serialVersionUID = 275254460581626332L; + + private static final String UNPAGED_TYPE_NAME = "org.springframework.data.domain.Unpaged"; + private static final Class UNPAGED_TYPE; + + static { + UNPAGED_TYPE = ClassUtils.resolveClassName(UNPAGED_TYPE_NAME, PageModule.class.getClassLoader()); + } + + /** + * Creates a new {@link PageModule} for the given {@link SpringDataWebSettings}. + * + * @param settings can be {@literal null}. + */ + public PageModule(@Nullable SpringDataWebSettings settings) { + + addSerializer(UNPAGED_TYPE, new UnpagedAsInstanceSerializer()); + + if (settings == null || settings.pageSerializationMode() == PageSerializationMode.DIRECT) { + setSerializerModifier(new WarningLoggingModifier()); + + } else { + setMixInAnnotation(PageImpl.class, WrappingMixing.class); + } + } + + /** + * A Jackson serializer rendering instances of {@code org.springframework.data.domain.Unpaged} as {@code INSTANCE} + * as it was previous rendered. + * + * @author Oliver Drotbohm + */ + static class UnpagedAsInstanceSerializer extends ToStringSerializerBase { + + public UnpagedAsInstanceSerializer() { + super(Object.class); + } + + @Override + public String valueToString(@Nullable Object value) { + return "INSTANCE"; + } + } + + @JsonSerialize(converter = PageModelConverter.class) + abstract static class WrappingMixing {} + + static class PageModelConverter extends StdConverter, PagedModel> { + + @Override + public @Nullable PagedModel convert(@Nullable Page value) { + return value == null ? null : new PagedModel<>(value); + } + } + + /** + * A {@link ValueSerializerModifier} that logs a warning message if an instance of {@link Page} will be rendered. + * + * @author Oliver Drotbohm + */ + static class WarningLoggingModifier extends ValueSerializerModifier { + + private static final Logger LOGGER = LoggerFactory.getLogger(WarningLoggingModifier.class); + private static final String MESSAGE = """ + Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure! + For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)) + or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables. + """; + + private static final @Serial long serialVersionUID = 954857444010009875L; + + private boolean warningRendered = false; + + @Override + public List changeProperties(tools.jackson.databind.SerializationConfig config, + tools.jackson.databind.BeanDescription.Supplier beanDesc, List beanProperties) { + + if (Page.class.isAssignableFrom(beanDesc.getBeanClass()) && !warningRendered) { + + this.warningRendered = true; + LOGGER.warn(MESSAGE); + } + + return super.changeProperties(config, beanDesc, beanProperties); + } + } + } +} diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJackson3Modules.java b/src/main/java/org/springframework/data/web/config/SpringDataJackson3Modules.java new file mode 100644 index 0000000000..7db0817e74 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/SpringDataJackson3Modules.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2025 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.data.web.config; + +/** + * Marker interface to describe configuration classes that ship Jackson modules that are supposed to be added to the + * Jackson 3 {@link tools.jackson.databind.ObjectMapper} configured for {@link EnableSpringDataWebSupport}. + * + * @author Oliver Gierke + * @author Mark Paluch + * @since 4.0 + */ +public interface SpringDataJackson3Modules {} diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java index 4e4014a5cd..67f2320747 100644 --- a/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.data.web.config; + import java.io.Serial; import java.util.List; @@ -44,10 +45,15 @@ * JavaConfig class to export Jackson specific configuration. * * @author Oliver Gierke + * @author Mark Paluch + * @deprecated since 4.0, in favor of {@link SpringDataJackson3Configuration} which uses Jackson 3. */ +@SuppressWarnings("removal") +@Deprecated(since = "4.0", forRemoval = true) public class SpringDataJacksonConfiguration implements SpringDataJacksonModules { - @Nullable @Autowired(required = false) SpringDataWebSettings settings; + @Nullable + @Autowired(required = false) SpringDataWebSettings settings; @Bean public GeoModule jacksonGeoModule() { @@ -61,12 +67,10 @@ public PageModule pageModule() { /** * A Jackson module customizing the serialization of {@link PageImpl} instances depending on the - * {@link SpringDataWebSettings} handed into the instance. In case of - * {@link org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode#DIRECT} being + * {@link SpringDataWebSettings} handed into the instance. In case of {@link PageSerializationMode#DIRECT} being * configured, a no-op {@link StdConverter} is registered to issue a one-time warning about the mode being used (as - * it's not recommended). - * {@link org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode#VIA_DTO} would register - * a converter wrapping {@link PageImpl} instances into {@link PagedModel}. + * it's not recommended). {@link PageSerializationMode#VIA_DTO} would register a converter wrapping {@link PageImpl} + * instances into {@link PagedModel}. * * @author Oliver Drotbohm */ @@ -99,7 +103,7 @@ public PageModule(@Nullable SpringDataWebSettings settings) { } /** - * A Jackson serializer rendering instances of {@link org.springframework.data.domain.Unpaged} as {@code INSTANCE} + * A Jackson serializer rendering instances of {@code org.springframework.data.domain.Unpaged} as {@code INSTANCE} * as it was previous rendered. * * @author Oliver Drotbohm @@ -119,7 +123,7 @@ public String valueToString(@Nullable Object value) { } @JsonSerialize(converter = PageModelConverter.class) - abstract class WrappingMixing {} + abstract static class WrappingMixing {} static class PageModelConverter extends StdConverter, PagedModel> { @@ -161,4 +165,5 @@ public List changeProperties(SerializationConfig config, Bea } } } + } diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java b/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java index 4269bf34d1..927c92aa2a 100644 --- a/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java +++ b/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java @@ -23,5 +23,7 @@ * * @author Oliver Gierke * @since 1.13 + * @deprecated since 4.0, in favor of {@link SpringDataJackson3Configuration} which uses Jackson 3. */ +@Deprecated(since = "4.0", forRemoval = true) public interface SpringDataJacksonModules {} diff --git a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java index 505bb8c875..d402ecf459 100644 --- a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.data.web.config; +import java.util.ArrayList; import java.util.List; import org.jspecify.annotations.Nullable; @@ -33,18 +34,19 @@ import org.springframework.data.web.OffsetScrollPositionHandlerMethodArgumentResolver; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.data.web.ProjectingJackson2HttpMessageConverter; +import org.springframework.data.web.ProjectingJacksonHttpMessageConverter; import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver; import org.springframework.data.web.SortHandlerMethodArgumentResolver; import org.springframework.data.web.XmlBeamHttpMessageConverter; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverters; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import com.fasterxml.jackson.databind.ObjectMapper; /** * Configuration class to register {@link PageableHandlerMethodArgumentResolver}, @@ -151,18 +153,43 @@ public void addArgumentResolvers(List argumentRes } @Override - public void extendMessageConverters(List> converters) { + public void configureMessageConverters(HttpMessageConverters.Builder builder) { - if (ClassUtils.isPresent("com.jayway.jsonpath.DocumentContext", context.getClassLoader()) - && ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", context.getClassLoader())) { + List> converters = new ArrayList<>(); + configureMessageConverters(converters); - ObjectMapper mapper = context.getBeanProvider(ObjectMapper.class).getIfUnique(ObjectMapper::new); + for (HttpMessageConverter converter : converters) { + builder.additionalMessageConverter(converter); + } + } + + @Override + public void configureMessageConverters(List> converters) { + + if (ClassUtils.isPresent("com.jayway.jsonpath.DocumentContext", context.getClassLoader())) { + + if (ClassUtils.isPresent("tools.jackson.databind.ObjectReader", context.getClassLoader())) { + + tools.jackson.databind.ObjectMapper mapper = context.getBeanProvider(tools.jackson.databind.ObjectMapper.class) + .getIfUnique(tools.jackson.databind.ObjectMapper::new); + + ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter(mapper); + converter.setBeanFactory(context); + forwardBeanClassLoader(converter); + + converters.add(0, converter); + } else if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", context.getClassLoader())) { + + com.fasterxml.jackson.databind.ObjectMapper mapper = context + .getBeanProvider(com.fasterxml.jackson.databind.ObjectMapper.class) + .getIfUnique(com.fasterxml.jackson.databind.ObjectMapper::new); - ProjectingJackson2HttpMessageConverter converter = new ProjectingJackson2HttpMessageConverter(mapper); - converter.setBeanFactory(context); - forwardBeanClassLoader(converter); + ProjectingJackson2HttpMessageConverter converter = new ProjectingJackson2HttpMessageConverter(mapper); + converter.setBeanFactory(context); + forwardBeanClassLoader(converter); - converters.add(0, converter); + converters.add(0, converter); + } } if (ClassUtils.isPresent("org.xmlbeam.XBProjector", context.getClassLoader())) { diff --git a/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt b/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt index 3224afff8d..b8a94b80ad 100644 --- a/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt +++ b/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt @@ -49,7 +49,7 @@ internal class KIterablePropertyPath( * @author Tjeu Kayim * @author Mikhail Polivakha */ -internal fun asString(property: KProperty<*>): String { +fun asString(property: KProperty<*>): String { return when (property) { is KPropertyPath<*, *> -> "${asString(property.parent)}.${property.child.name}" diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories index 506aaa02df..1ad50e1622 100644 --- a/src/main/resources/META-INF/spring.factories +++ b/src/main/resources/META-INF/spring.factories @@ -1,4 +1,5 @@ org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SpringDataJacksonConfiguration +org.springframework.data.web.config.SpringDataJackson3Modules=org.springframework.data.web.config.SpringDataJackson3Configuration org.springframework.data.util.CustomCollectionRegistrar=org.springframework.data.util.CustomCollections.VavrCollections, \ org.springframework.data.util.CustomCollections.EclipseCollections org.springframework.beans.BeanInfoFactory=org.springframework.data.util.KotlinBeanInfoFactory diff --git a/src/test/java/org/springframework/data/geo/GeoJacksonModuleIntegrationTests.java b/src/test/java/org/springframework/data/geo/GeoJacksonModuleIntegrationTests.java new file mode 100755 index 0000000000..87a6d4cab4 --- /dev/null +++ b/src/test/java/org/springframework/data/geo/GeoJacksonModuleIntegrationTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2025 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.data.geo; + +import static org.assertj.core.api.Assertions.*; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for {@link GeoModule}. + * + * @author Oliver Gierke + * @author Mark Paluch + */ +class GeoJacksonModuleIntegrationTests { + + ObjectMapper mapper; + + @BeforeEach + void setUp() { + + this.mapper = JsonMapper.builder().addModule(new GeoJacksonModule()).build(); + } + + @Test // DATACMNS-475 + void deserializesDistance() throws Exception { + + var json = "{\"value\":10.0,\"metric\":\"KILOMETERS\"}"; + var reference = new Distance(10.0, Metrics.KILOMETERS); + + assertThat(mapper.readValue(json, Distance.class)).isEqualTo(reference); + assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); + } + + @Test // DATACMNS-475 + void deserializesPoint() throws Exception { + + var json = "{\"x\":10.0,\"y\":20.0}"; + var reference = new Point(10.0, 20.0); + + assertThat(mapper.readValue(json, Point.class)).isEqualTo(reference); + assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); + } + + @Test // DATACMNS-475 + void deserializesCircle() throws Exception { + + var json = "{\"center\":{\"x\":10.0,\"y\":20.0},\"radius\":{\"value\":10.0,\"metric\":\"KILOMETERS\"}}"; + var reference = new Circle(new Point(10.0, 20.0), new Distance(10, Metrics.KILOMETERS)); + + assertThat(mapper.readValue(json, Circle.class)).isEqualTo(reference); + assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); + } + + @Test // DATACMNS-475 + void deserializesBox() throws Exception { + + var json = "{\"first\":{\"x\":1.0,\"y\":2.0},\"second\":{\"x\":2.0,\"y\":3.0}}"; + var reference = new Box(new Point(1, 2), new Point(2, 3)); + + assertThat(mapper.readValue(json, Box.class)).isEqualTo(reference); + assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); + } + + @Test // DATACMNS-475 + void deserializesPolygon() throws Exception { + + var json = "{\"points\":[{\"x\":1.0,\"y\":2.0},{\"x\":2.0,\"y\":3.0},{\"x\":3.0,\"y\":4.0}]}"; + var reference = new Polygon(new Point(1, 2), new Point(2, 3), new Point(3, 4)); + + assertThat(mapper.readValue(json, Polygon.class)).isEqualTo(reference); + assertThat(mapper.writeValueAsString(reference)).isEqualTo(json); + } +} diff --git a/src/test/java/org/springframework/data/repository/init/JacksonResourceReaderIntegrationTests.java b/src/test/java/org/springframework/data/repository/init/JacksonResourceReaderIntegrationTests.java new file mode 100755 index 0000000000..72616925ad --- /dev/null +++ b/src/test/java/org/springframework/data/repository/init/JacksonResourceReaderIntegrationTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2025 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.data.repository.init; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collection; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +/** + * Integration tests for {@link JacksonResourceReader}. + * + * @author Mark Paluch + */ +class JacksonResourceReaderIntegrationTests { + + @Test + void readsFileWithMultipleObjects() throws Exception { + + ResourceReader reader = new JacksonResourceReader(); + var result = reader.readFrom(new ClassPathResource("data.json", getClass()), null); + + assertThat(result).isInstanceOf(Collection.class); + assertThat((Collection) result).hasSize(1); + } +} diff --git a/src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java b/src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java index 252c1a832a..586fa6ecaa 100755 --- a/src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.*; +import tools.jackson.databind.ObjectMapper; + import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.List; @@ -25,14 +27,12 @@ import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.util.ObjectUtils; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jayway.jsonpath.spi.json.JacksonJsonProvider; import com.jayway.jsonpath.spi.json.JsonProvider; -import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; import com.jayway.jsonpath.spi.mapper.MappingProvider; /** @@ -59,8 +59,8 @@ void setUp() { var projectionFactory = new SpelAwareProxyProjectionFactory(); var objectMapper = new ObjectMapper(); - MappingProvider mappingProvider = new JacksonMappingProvider(objectMapper); - JsonProvider jsonProvider = new JacksonJsonProvider(objectMapper); + MappingProvider mappingProvider = new ProjectingJacksonHttpMessageConverter.JacksonMappingProvider(objectMapper); + JsonProvider jsonProvider = new ProjectingJacksonHttpMessageConverter.JacksonJsonProvider(objectMapper); projectionFactory .registerMethodInvokerFactory(new JsonProjectingMethodInterceptorFactory(jsonProvider, mappingProvider)); diff --git a/src/test/java/org/springframework/data/web/PageImplJsonJackson2SerializationUnitTests.java b/src/test/java/org/springframework/data/web/PageImplJsonJackson2SerializationUnitTests.java new file mode 100644 index 0000000000..4aa616c1bf --- /dev/null +++ b/src/test/java/org/springframework/data/web/PageImplJsonJackson2SerializationUnitTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024-2025 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.data.web; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.PageImpl; +import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode; +import org.springframework.data.web.config.SpringDataJacksonConfiguration; +import org.springframework.data.web.config.SpringDataWebSettings; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; + +/** + * Unit tests for PageImpl serialization. + * + * @author Oliver Drotbohm + * @author Mark Paluch + */ +class PageImplJsonJackson2SerializationUnitTests { + + @Test // GH-3024 + void serializesPageImplAsJson() { + assertJsonRendering(PageSerializationMode.DIRECT, "$.pageable", "$.last", "$.first"); + } + + @Test // GH-3024 + void serializesPageImplAsPagedModel() { + assertJsonRendering(PageSerializationMode.VIA_DTO, "$.content", "$.page"); + } + + @Test // GH-3137 + void serializesCustomPageAsPageImpl() { + assertJsonRendering(PageSerializationMode.DIRECT, new Extension<>("header"), "$.pageable", "$.last", "$.first"); + } + + private static void assertJsonRendering(PageSerializationMode mode, String... jsonPaths) { + assertJsonRendering(mode, new PageImpl<>(Collections.emptyList()), jsonPaths); + } + + private static void assertJsonRendering(PageSerializationMode mode, PageImpl page, String... jsonPaths) { + + SpringDataWebSettings settings = new SpringDataWebSettings(mode); + + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new SpringDataJacksonConfiguration.PageModule(settings)); + + assertThatNoException().isThrownBy(() -> { + + String result = mapper.writeValueAsString(page); + + for (String jsonPath : jsonPaths) { + assertThat(JsonPath. read(result, jsonPath)).isNotNull(); + } + }); + } + + static class Extension extends PageImpl { + + private Object header; + + public Extension(Object header) { + super(Collections.emptyList()); + } + + public Object getHeader() { + return header; + } + } +} diff --git a/src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java b/src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java index 77f88028bb..7ebab17cac 100644 --- a/src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java +++ b/src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java @@ -17,21 +17,25 @@ import static org.assertj.core.api.Assertions.*; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + import java.util.Collections; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.PageImpl; import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode; -import org.springframework.data.web.config.SpringDataJacksonConfiguration; +import org.springframework.data.web.config.SpringDataJackson3Configuration; import org.springframework.data.web.config.SpringDataWebSettings; -import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; /** * Unit tests for PageImpl serialization. * * @author Oliver Drotbohm + * @author Mark Paluch */ class PageImplJsonSerializationUnitTests { @@ -58,8 +62,8 @@ private static void assertJsonRendering(PageSerializationMode mode, PageImpl SpringDataWebSettings settings = new SpringDataWebSettings(mode); - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new SpringDataJacksonConfiguration.PageModule(settings)); + ObjectMapper mapper = JsonMapper.builder().addModule(new SpringDataJackson3Configuration.PageModule(settings)) + .build(); assertThatNoException().isThrownBy(() -> { diff --git a/src/test/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverterUnitTests.java b/src/test/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverterUnitTests.java new file mode 100755 index 0000000000..b8a1b96794 --- /dev/null +++ b/src/test/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverterUnitTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2025 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.data.web; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; + +/** + * Unit tests for {@link ProjectingJacksonHttpMessageConverter}. + * + * @author Oliver Gierke + * @author Mark Paluch + */ +class ProjectingJacksonHttpMessageConverterUnitTests { + + ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter(); + MediaType ANYTHING_JSON = MediaType.parseMediaType("application/*+json"); + + @Test // DATCMNS-885 + void canReadJsonIntoAnnotatedInterface() { + assertThat(converter.canRead(SampleInterface.class, ANYTHING_JSON)).isTrue(); + } + + @Test // DATCMNS-885 + void cannotReadUnannotatedInterface() { + assertThat(converter.canRead(UnannotatedInterface.class, ANYTHING_JSON)).isFalse(); + } + + @Test // DATCMNS-885 + void cannotReadClass() { + assertThat(converter.canRead(SampleClass.class, ANYTHING_JSON)).isFalse(); + } + + @Test // DATACMNS-972 + void doesNotConsiderTypeVariableBoundTo() throws Throwable { + + var method = BaseController.class.getDeclaredMethod("createEntity", AbstractDto.class); + + assertThat(converter.canRead(ResolvableType.forMethodParameter(method, 0), ANYTHING_JSON)).isFalse(); + } + + @Test // DATACMNS-972 + void genericTypeOnConcreteOne() throws Throwable { + + var method = ConcreteController.class.getMethod("createEntity", AbstractDto.class); + + assertThat(converter.canRead(ResolvableType.forMethodParameter(method, 0), ANYTHING_JSON)).isFalse(); + } + + @ProjectedPayload + interface SampleInterface {} + + interface UnannotatedInterface {} + + class SampleClass {} + + class AbstractDto {} + + abstract class BaseController { + public void createEntity(D dto) {} + } + + class ConcreteController extends BaseController {} +} diff --git a/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java b/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java index 5413974db6..9a35b4ba5e 100644 --- a/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java +++ b/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java @@ -15,11 +15,11 @@ */ package org.springframework.data.web.aot; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import org.junit.jupiter.api.Test; + import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; @@ -48,7 +48,21 @@ void shouldRegisterRuntimeHintWhenJacksonPresent() { @Test // GH-3033, GH-3171 @ClassPathExclusions(packages = { "com.fasterxml.jackson.databind" }) - void shouldRegisterRuntimeHintWithTypeNameWhenJacksonNotPresent() { + void shouldRegisterRuntimeHintWithTypeNameWhenJackson2NotPresent() { + + ReflectionHints reflectionHints = new ReflectionHints(); + RuntimeHints runtimeHints = mock(RuntimeHints.class); + when(runtimeHints.reflection()).thenReturn(reflectionHints); + + new WebRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + assertThat(runtimeHints).matches(RuntimeHintsPredicates.reflection() + .onType(TypeReference.of("org.springframework.data.web.config.SpringDataWebSettings"))); + } + + @Test // GH-3292 + @ClassPathExclusions(packages = { "tools.jackson" }) + void shouldRegisterRuntimeHintWithTypeNameWhenJackson3NotPresent() { ReflectionHints reflectionHints = new ReflectionHints(); RuntimeHints runtimeHints = mock(RuntimeHints.class); diff --git a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java index d680dc3c29..2d565fd915 100755 --- a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java +++ b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java @@ -19,10 +19,14 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.json.JsonMapper; + import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -41,10 +45,9 @@ import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver; import org.springframework.data.web.SortHandlerMethodArgumentResolver; import org.springframework.data.web.WebTestUtils; -import org.springframework.data.web.config.SpringDataJacksonConfiguration.PageModule; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.data.web.config.SpringDataJackson3Configuration.PageModule; +import org.springframework.http.converter.HttpMessageConverters; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -52,7 +55,6 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.util.UriComponentsBuilder; -import com.fasterxml.jackson.databind.Module; /** * Integration tests for {@link EnableSpringDataWebSupport}. @@ -124,7 +126,7 @@ SimpleEntityPathResolver entityPathResolver() { @Configuration static class PageSampleConfig extends WebMvcConfigurationSupport { - @Autowired private List modules; + @Autowired private List modules; @Bean PageSampleController controller() { @@ -132,10 +134,11 @@ PageSampleController controller() { } @Override - protected void configureMessageConverters(List> converters) { - Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json().modules(modules); - converters.add(0, new MappingJackson2HttpMessageConverter(builder.build())); + protected void configureMessageConverters(HttpMessageConverters.Builder builder) { + builder.jsonMessageConverter(new JacksonJsonHttpMessageConverter( + JsonMapper.builder().addModules(modules.toArray(new JacksonModule[0])).build())); } + } @EnableSpringDataWebSupport @@ -188,7 +191,7 @@ void registersJacksonSpecificBeanDefinitions() { } @Test // DATACMNS-475 - @ClassPathExclusions(packages = { "com.fasterxml.jackson.databind" }) + @ClassPathExclusions(packages = { "com.fasterxml.jackson.databind", "tools.jackson.databind" }) void doesNotRegisterJacksonSpecificComponentsIfJacksonNotPresent() { ApplicationContext context = WebTestUtils.createApplicationContext(SampleConfig.class); diff --git a/src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java b/src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java index 6586701e13..748df322a8 100644 --- a/src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java +++ b/src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java @@ -16,26 +16,31 @@ package org.springframework.data.web.config; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import tools.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; -import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.classloadersupport.HidingClassLoader; -import org.springframework.data.web.ProjectingJackson2HttpMessageConverter; +import org.springframework.data.web.ProjectingJacksonHttpMessageConverter; import org.springframework.data.web.XmlBeamHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverters; import org.springframework.instrument.classloading.ShadowingClassLoader; import org.springframework.util.ReflectionUtils; + import org.xmlbeam.XBProjector; -import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.DocumentContext; /** @@ -50,45 +55,48 @@ class SpringDataWebConfigurationIntegrationTests { @Test // DATACMNS-987 void shouldNotLoadJacksonConverterWhenJacksonNotPresent() { - List> converters = new ArrayList>(); + HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class); - createConfigWithClassLoader(HidingClassLoader.hide(ObjectMapper.class), - it -> it.extendMessageConverters(converters)); + createConfigWithClassLoader( + HidingClassLoader.hide(ObjectMapper.class, com.fasterxml.jackson.databind.ObjectMapper.class), + it -> it.configureMessageConverters(builder)); - assertThat(converters).areNot(instanceWithClassName(ProjectingJackson2HttpMessageConverter.class)); + verify(builder).additionalMessageConverter(any(XmlBeamHttpMessageConverter.class)); + verifyNoMoreInteractions(builder); } @Test // DATACMNS-987 void shouldNotLoadJacksonConverterWhenJaywayNotPresent() { - List> converters = new ArrayList>(); + HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class); createConfigWithClassLoader(HidingClassLoader.hide(DocumentContext.class), - it -> it.extendMessageConverters(converters)); + it -> it.configureMessageConverters(builder)); - assertThat(converters).areNot(instanceWithClassName(ProjectingJackson2HttpMessageConverter.class)); + verify(builder).additionalMessageConverter(any(XmlBeamHttpMessageConverter.class)); + verifyNoMoreInteractions(builder); } @Test // DATACMNS-987 void shouldNotLoadXBeamConverterWhenXBeamNotPresent() throws Exception { - List> converters = new ArrayList>(); + HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class); ClassLoader classLoader = HidingClassLoader.hide(XBProjector.class); - createConfigWithClassLoader(classLoader, it -> it.extendMessageConverters(converters)); + createConfigWithClassLoader(classLoader, it -> it.configureMessageConverters(builder)); - assertThat(converters).areNot(instanceWithClassName(XmlBeamHttpMessageConverter.class)); + verify(builder, never()).additionalMessageConverter(any(XmlBeamHttpMessageConverter.class)); } @Test // DATACMNS-987 void shouldLoadAllConvertersWhenDependenciesArePresent() throws Exception { - List> converters = new ArrayList>(); + HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class); - createConfigWithClassLoader(getClass().getClassLoader(), it -> it.extendMessageConverters(converters)); + createConfigWithClassLoader(getClass().getClassLoader(), it -> it.configureMessageConverters(builder)); - assertThat(converters).haveAtLeastOne(instanceWithClassName(XmlBeamHttpMessageConverter.class)); - assertThat(converters).haveAtLeastOne(instanceWithClassName(ProjectingJackson2HttpMessageConverter.class)); + verify(builder).additionalMessageConverter(any(XmlBeamHttpMessageConverter.class)); + verify(builder).additionalMessageConverter(any(ProjectingJacksonHttpMessageConverter.class)); } @Test // DATACMNS-1152 @@ -96,16 +104,19 @@ void usesCustomObjectMapper() { createConfigWithClassLoader(getClass().getClassLoader(), it -> { - List> converters = new ArrayList<>(); - it.extendMessageConverters(converters); + HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpMessageConverter.class); + + it.configureMessageConverters(builder); + verify(builder, atLeast(1)).additionalMessageConverter(captor.capture()); // Converters contains ProjectingJackson2HttpMessageConverter with custom ObjectMapper - assertThat(converters).anySatisfy(converter -> { - assertThat(converter).isInstanceOfSatisfying(ProjectingJackson2HttpMessageConverter.class, __ -> { + + assertThat(captor.getAllValues()).anySatisfy(converter -> { + assertThat(converter).isInstanceOfSatisfying(ProjectingJacksonHttpMessageConverter.class, __ -> { assertThat(__.getObjectMapper()).isSameAs(SomeConfiguration.MAPPER); }); }); - }, SomeConfiguration.class); } @@ -138,20 +149,6 @@ private static Class loadWithout(Class configurationClass, Class... typ return loader.loadClass(configurationClass.getName()); } - /** - * Creates a {@link Condition} that checks if an object is an instance of a class with the same name as the provided - * class. This is necessary since we are dealing with multiple classloaders which would make a simple instanceof fail - * all the time - * - * @param expectedClass the class that is expected (possibly loaded by a different classloader). - * @return a {@link Condition} - */ - private static Condition instanceWithClassName(Class expectedClass) { - - return new Condition<>(it -> it.getClass().getName().equals(expectedClass.getName()), // - "with class name %s", expectedClass.getName()); - } - @Configuration static class SomeConfiguration {