Browse Source

Initial draft of Jackson 3 support.

See GH-3292.
issue/3353
Mark Paluch 5 months ago committed by Oliver Drotbohm
parent
commit
84bfc04529
No known key found for this signature in database
GPG Key ID: 9EB72C54FB72F2A7
  1. 5
      pom.xml
  2. 13
      src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc
  3. 80
      src/main/java/org/springframework/data/geo/GeoJacksonModule.java
  4. 2
      src/main/java/org/springframework/data/geo/GeoModule.java
  5. 2
      src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java
  6. 2
      src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java
  7. 49
      src/main/java/org/springframework/data/repository/init/JacksonRepositoryPopulatorFactoryBean.java
  8. 123
      src/main/java/org/springframework/data/repository/init/JacksonResourceReader.java
  9. 23
      src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java
  10. 2
      src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java
  11. 275
      src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java
  12. 75
      src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java
  13. 7
      src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java
  14. 161
      src/main/java/org/springframework/data/web/config/SpringDataJackson3Configuration.java
  15. 26
      src/main/java/org/springframework/data/web/config/SpringDataJackson3Modules.java
  16. 21
      src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java
  17. 2
      src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java
  18. 45
      src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java
  19. 1
      src/main/resources/META-INF/spring.factories
  20. 91
      src/test/java/org/springframework/data/geo/GeoJacksonModuleIntegrationTests.java
  21. 41
      src/test/java/org/springframework/data/repository/init/JacksonResourceReaderIntegrationTests.java
  22. 10
      src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java
  23. 88
      src/test/java/org/springframework/data/web/PageImplJsonJackson2SerializationUnitTests.java
  24. 12
      src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java
  25. 81
      src/test/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverterUnitTests.java
  26. 22
      src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java
  27. 23
      src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java
  28. 69
      src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java

5
pom.xml

@ -70,6 +70,11 @@ @@ -70,6 +70,11 @@
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>

13
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 @@ -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 @@ -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 @@ -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].

80
src/main/java/org/springframework/data/geo/GeoJacksonModule.java

@ -0,0 +1,80 @@ @@ -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<Point> points) {}
}
}

2
src/main/java/org/springframework/data/geo/GeoModule.java

@ -30,8 +30,10 @@ import com.fasterxml.jackson.databind.module.SimpleModule; @@ -30,8 +30,10 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
*
* @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;

2
src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java

@ -27,7 +27,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; @@ -27,7 +27,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* @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;

2
src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java

@ -40,7 +40,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; @@ -40,7 +40,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* @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";

49
src/main/java/org/springframework/data/repository/init/JacksonRepositoryPopulatorFactoryBean.java

@ -0,0 +1,49 @@ @@ -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);
}
}

123
src/main/java/org/springframework/data/repository/init/JacksonResourceReader.java

@ -0,0 +1,123 @@ @@ -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<JsonNode> elements = node.iterator();
List<Object> 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);
}
}

23
src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java

@ -131,18 +131,27 @@ public class JsonProjectingMethodInterceptorFactory implements MethodInterceptor @@ -131,18 +131,27 @@ public class JsonProjectingMethodInterceptorFactory implements MethodInterceptor
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());

2
src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java

@ -48,7 +48,9 @@ import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; @@ -48,7 +48,9 @@ import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
* @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 {

275
src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java

@ -0,0 +1,275 @@ @@ -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<Class<?>, 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<String, Object> hints)
throws IOException, HttpMessageNotReadableException {
return projectionFactory.createProjection(type.resolve(Object.class), inputMessage.getBody());
}
record JacksonMappingProvider(ObjectMapper objectMapper) implements MappingProvider {
@Override
public <T> @Nullable T map(@Nullable Object source, Class<T> targetType, Configuration configuration) {
if (source == null) {
return null;
}
try {
return objectMapper.convertValue(source, targetType);
} catch (Exception e) {
throw new MappingException(e);
}
}
@Override
public <T> @Nullable T map(@Nullable Object source, final TypeRef<T> 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<Object> createArray() {
return new LinkedList<>();
}
@Override
public Object createMap() {
return new LinkedHashMap<String, Object>();
}
}
}

75
src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java

@ -23,6 +23,7 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -23,6 +23,7 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
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 @@ import org.springframework.util.ClassUtils; @@ -30,17 +31,24 @@ import org.springframework.util.ClassUtils;
* {@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 @@ class WebRuntimeHints implements RuntimeHintsRegistrar { @@ -49,24 +57,53 @@ class WebRuntimeHints implements RuntimeHintsRegistrar {
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);
});
}
}

7
src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java

@ -143,7 +143,12 @@ public @interface EnableSpringDataWebSupport { @@ -143,7 +143,12 @@ public @interface EnableSpringDataWebSupport {
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()]);
}

161
src/main/java/org/springframework/data/web/config/SpringDataJackson3Configuration.java

@ -0,0 +1,161 @@ @@ -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<Page<?>, 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<BeanPropertyWriter> changeProperties(tools.jackson.databind.SerializationConfig config,
tools.jackson.databind.BeanDescription.Supplier beanDesc, List<BeanPropertyWriter> beanProperties) {
if (Page.class.isAssignableFrom(beanDesc.getBeanClass()) && !warningRendered) {
this.warningRendered = true;
LOGGER.warn(MESSAGE);
}
return super.changeProperties(config, beanDesc, beanProperties);
}
}
}
}

26
src/main/java/org/springframework/data/web/config/SpringDataJackson3Modules.java

@ -0,0 +1,26 @@ @@ -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 {}

21
src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
*/
package org.springframework.data.web.config;
import java.io.Serial;
import java.util.List;
@ -44,10 +45,15 @@ import com.fasterxml.jackson.databind.util.StdConverter; @@ -44,10 +45,15 @@ import com.fasterxml.jackson.databind.util.StdConverter;
* 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 class SpringDataJacksonConfiguration implements SpringDataJacksonModules @@ -61,12 +67,10 @@ public class SpringDataJacksonConfiguration implements SpringDataJacksonModules
/**
* 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 class SpringDataJacksonConfiguration implements SpringDataJacksonModules @@ -99,7 +103,7 @@ public class SpringDataJacksonConfiguration implements SpringDataJacksonModules
}
/**
* 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 class SpringDataJacksonConfiguration implements SpringDataJacksonModules @@ -119,7 +123,7 @@ public class SpringDataJacksonConfiguration implements SpringDataJacksonModules
}
@JsonSerialize(converter = PageModelConverter.class)
abstract class WrappingMixing {}
abstract static class WrappingMixing {}
static class PageModelConverter extends StdConverter<Page<?>, PagedModel<?>> {
@ -161,4 +165,5 @@ public class SpringDataJacksonConfiguration implements SpringDataJacksonModules @@ -161,4 +165,5 @@ public class SpringDataJacksonConfiguration implements SpringDataJacksonModules
}
}
}
}

2
src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java

@ -23,5 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; @@ -23,5 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*
* @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 {}

45
src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java

@ -15,6 +15,7 @@ @@ -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.util.Lazy; @@ -33,18 +34,19 @@ import org.springframework.data.util.Lazy;
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 class SpringDataWebConfiguration implements WebMvcConfigurer, BeanClassLo @@ -151,18 +153,43 @@ public class SpringDataWebConfiguration implements WebMvcConfigurer, BeanClassLo
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> 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())) {

1
src/main/resources/META-INF/spring.factories

@ -1,4 +1,5 @@ @@ -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

91
src/test/java/org/springframework/data/geo/GeoJacksonModuleIntegrationTests.java

@ -0,0 +1,91 @@ @@ -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);
}
}

41
src/test/java/org/springframework/data/repository/init/JacksonResourceReaderIntegrationTests.java

@ -0,0 +1,41 @@ @@ -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);
}
}

10
src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java

@ -17,6 +17,8 @@ package org.springframework.data.web; @@ -17,6 +17,8 @@ package org.springframework.data.web;
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 java.util.Set; @@ -25,14 +27,12 @@ import java.util.Set;
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 @@ class JsonProjectingMethodInterceptorFactoryUnitTests { @@ -59,8 +59,8 @@ class JsonProjectingMethodInterceptorFactoryUnitTests {
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));

88
src/test/java/org/springframework/data/web/PageImplJsonJackson2SerializationUnitTests.java

@ -0,0 +1,88 @@ @@ -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.<Object> read(result, jsonPath)).isNotNull();
}
});
}
static class Extension<T> extends PageImpl<T> {
private Object header;
public Extension(Object header) {
super(Collections.emptyList());
}
public Object getHeader() {
return header;
}
}
}

12
src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java

@ -17,21 +17,25 @@ package org.springframework.data.web; @@ -17,21 +17,25 @@ package org.springframework.data.web;
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 @@ class PageImplJsonSerializationUnitTests { @@ -58,8 +62,8 @@ class PageImplJsonSerializationUnitTests {
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(() -> {

81
src/test/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverterUnitTests.java

@ -0,0 +1,81 @@ @@ -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<D extends AbstractDto> {
public void createEntity(D dto) {}
}
class ConcreteController extends BaseController<AbstractDto> {}
}

22
src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java

@ -15,11 +15,11 @@ @@ -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 @@ class WebRuntimeHintsUnitTests { @@ -48,7 +48,21 @@ class WebRuntimeHintsUnitTests {
@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);

23
src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java

@ -19,10 +19,14 @@ import static org.assertj.core.api.Assertions.*; @@ -19,10 +19,14 @@ import static org.assertj.core.api.Assertions.*;
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.PagedResourcesAssemblerArgumentResolver; @@ -41,10 +45,9 @@ import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver;
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.config.annotation.WebMvcConfigurationSupp @@ -52,7 +55,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupp
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 @@ class EnableSpringDataWebSupportIntegrationTests { @@ -124,7 +126,7 @@ class EnableSpringDataWebSupportIntegrationTests {
@Configuration
static class PageSampleConfig extends WebMvcConfigurationSupport {
@Autowired private List<Module> modules;
@Autowired private List<JacksonModule> modules;
@Bean
PageSampleController controller() {
@ -132,10 +134,11 @@ class EnableSpringDataWebSupportIntegrationTests { @@ -132,10 +134,11 @@ class EnableSpringDataWebSupportIntegrationTests {
}
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> 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 @@ class EnableSpringDataWebSupportIntegrationTests { @@ -188,7 +191,7 @@ class EnableSpringDataWebSupportIntegrationTests {
}
@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);

69
src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java

@ -16,26 +16,31 @@ @@ -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 { @@ -50,45 +55,48 @@ class SpringDataWebConfigurationIntegrationTests {
@Test // DATACMNS-987
void shouldNotLoadJacksonConverterWhenJacksonNotPresent() {
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
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<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
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<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
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<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
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 @@ class SpringDataWebConfigurationIntegrationTests { @@ -96,16 +104,19 @@ class SpringDataWebConfigurationIntegrationTests {
createConfigWithClassLoader(getClass().getClassLoader(), it -> {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
it.extendMessageConverters(converters);
HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class);
ArgumentCaptor<HttpMessageConverter> 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 @@ class SpringDataWebConfigurationIntegrationTests { @@ -138,20 +149,6 @@ class SpringDataWebConfigurationIntegrationTests {
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<Object> instanceWithClassName(Class<?> expectedClass) {
return new Condition<>(it -> it.getClass().getName().equals(expectedClass.getName()), //
"with class name %s", expectedClass.getName());
}
@Configuration
static class SomeConfiguration {

Loading…
Cancel
Save