Bug report

  • Creating @ConfigurationProperties property class that is used by a @Service that's annotated with @RefreshScope using JAVA 14 fails with com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: jdk.internal.loader.ClassLoaders$PlatformClassLoader["unnamedModule"]->java.lang.Module["classLoader"]

Describe the bug

  1. Create a @ConfigurationProperties class
@Configuration// only loaded the bean when this was added
@ConfigurationProperties("gateway")
public class GatewayProperies {

  private String type;
  private String region;
...
}
  1. Create a Service with @RefreshScope
@Service
@RefreshScope
public class GatewayService {

  @Autowired
  private GatewayProperies properties;
...
...
  1. 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"]->

  1. 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);
  }
}
  1. 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

Screen Shot 2020-10-14 at 4 59 32 PM

  • 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?

Screen Shot 2020-10-14 at 5 01 42 PM

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 {
}