Bug report
- Creating
@ConfigurationProperties
property class that is used by a@Service
that's annotated with@RefreshScope
using JAVA 14 fails withcom.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]
Describe the bug
- Create a
@ConfigurationProperties
class
@Configuration// only loaded the bean when this was added
@ConfigurationProperties("gateway")
public class GatewayProperies {
private String type;
private String region;
...
}
- Create a Service with
@RefreshScope
@Service
@RefreshScope
public class GatewayService {
@Autowired
private GatewayProperies properties;
...
...
- The serialization fails with the following:
com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]->jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]->
- Fails when calling the controller endpoing
/payments/props
@Controller
@RequestMapping("/v1")
public class GatewayController {
@Autowired
private GatewayService gatewayService;
@RequestMapping(value = "/payments/props", method = RequestMethod.GET)
public ResponseEntity<GatewayProperies> handle() {
return new ResponseEntity<>(gatewayService.getProperties(), headers, HttpStatus.OK);
}
}
- Trying to serialize the value fails with Jackson directly using an instance of
ObjectMapper
also fails
ObjectMapper mapper = new ObjectMapper();
String jsonInString = "{}";
try {
jsonInString = mapper.writeValueAsString(gatewayService.getProperties());
System.out.println(jsonInString);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
Version information
- SpringBoot version
id 'org.springframework.boot' version '2.3.4.RELEASE'
- Java version
$ java -version
java version "14.0.2" 2020-07-14
Java(TM) SE Runtime Environment (build 14.0.2+12-46)
Java HotSpot(TM) 64-Bit Server VM (build 14.0.2+12-46, mixed mode, sharing)
- Jackson version
$ ./gradlew dependencies | grep -m 7 "jackson"
| +--- com.fasterxml.jackson.core:jackson-databind:2.11.2
| | +--- com.fasterxml.jackson.core:jackson-annotations:2.11.2
| | \--- com.fasterxml.jackson.core:jackson-core:2.11.2
| +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.2
| | +--- com.fasterxml.jackson.core:jackson-core:2.11.2
| | \--- com.fasterxml.jackson.core:jackson-databind:2.11.2 (*)
| +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.2
Expected behavior
- We should see the properties of the class
Additional context
- The full stacktrace of the calls....
com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]->jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]->jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]-
...
...
>jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]->jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]->jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"])
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:755)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:755)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
-->
Properties needed to be serialized
- The properties are rendered properly
- However, the instance of the bean
....$$EnhancerBySpringCGLIB$$8581eaa6#$$beanFactory
includes property'$$beanFactory'
is being added to the serialization...
property '$$beanFactory' (field "com....platform.payment.gateway.GatewayProperies$$EnhancerBySpringCGLIB$$8581eaa6#$$beanFactory, no static serializer)
Could this above ^^^^ be the problem?
Workaround
- The only way to get it properly working is to ignore the property
"$$beanFactory
by adding@JsonIgnoreProperties
@Configuration// only loaded the bean when this was added
@ConfigurationProperties("gateway")
@JsonIgnoreProperties({"$$beanFactory"})
public class GatewayProperies {}
Comment From: wilkinsona
I believe the $$beanFactory
is present due to the proxy that's created as a result of @Configuration
. @Configuration
is intended for classes that define beans using @Bean
and doesn't really belong on a @ConfigurationProperties
class. The recommended approach is to use @EnableConfigurationProperties
or @ConfigurationPropertiesScan
. Alternatively you could use @Component
.
If the above doesn't help and you would like us to take another look, please provide a complete and minimal sample that we can use to reproduce the problem. You can share it with us by zipping it up and attaching it to this issue or by pushing it to a separate repository on GitHub. We can then re-open the issue and take another look.
Comment From: marcellodesales
@wilkinsona You were right... Just replacing @Configuration
with @Component
worked!
Comment From: DheereshJoshi
This can be solved by specifying type in ObjectMapper.
mapper.writerFor(GatewayProperies.class).writeValueAsString(gatewayService.getProperties());
Comment From: gongshw
Inspired by @DheereshJoshi , I think you can use @JsonSerialize(as = XXXX.class)
to let Jackson serialize the generated proxy as the raw class.
@Component
@RefreshScope
// This class is a @RefreshScope @Component,
// so it will be proxied by a cglib generated class at runtime,
// and can't be serialized simply by Jackson.
// To fix it, we have to let Jackson directly serialize the raw class
@JsonSerialize(as = SomeComponent.class)
class SomeComponent {
}