spring-projects / spring-boot

Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss.
https://spring.io/projects/spring-boot
Apache License 2.0
75.43k stars 40.76k forks source link

if overriding SpringBootRepositoryRestMvcConfiguration jackson extra modules are not used #2914

Closed leon closed 9 years ago

leon commented 9 years ago

If I overwrite SpringBootRepositoryRestMvcConfiguration to enable some extra conf (simplified in the example below) the jsr-310 / jdk8 jackson modules are not being used

@Configuration
public class RestConfig extends SpringBootRepositoryRestMvcConfiguration {

    @Override
    protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.setBasePath("/restapi");
        config.setReturnBodyOnCreate(true);
    }

}

As soon as I remove the RestConfig class the modules are in use and the dates come out right.

I've created a demo repo. https://github.com/leon/temp-spring-boot-data-rest-jackson-problem

Another strange thing is that the JSR-310 module gets autoregistered, but the JDK8Module doesn't so I need to add it explicitly?!

Solution

Can we somehow wait for the jackson auto configuration to complete before creating the rest object mapper, or even better why not use the same object mapper?

wilkinsona commented 9 years ago

The intention is for Spring Data REST to use the same Jackson configuration as the rest of your app. That's what configureJacksonObjectMapper in SpringBootRepositoryRestMvcConfiguration does. It's imported by an auto-configuration class that is configured to be processed after auto-configutration of Jackson has occurred. Unfortunately, when you sub-class SpringBootRepositoryRestMvcConfiguration it is then processed much earlier and, crucially, before JacksonAutoConfiguration. This prevents the creation of the auto-configured jacksonObjectMapper bean as it's @ConditionalOnMissingBean(ObjectMapper.class) and, by this point, there's already an ObjectMapper bean in the application context.

A workaround is to use application.properties rather than a SpringBootRepositoryRestMvcConfiguration subclass:

spring.data.rest.base-path = /restapi
spring.data.rest.return-body-on-create = true

The non-registration of the JDK 8 module is covered in #2789. The change actually needs to be made in Spring Framework's Jackson2ObjectMapperBuilder. You may want to raise in issue in the Spring Framework JIRA if @olivergierke has not already done so. /cc @sdeleuze.

leon commented 9 years ago

@wilkinsona How can i configure the expose for id with a application.yml file?

config.exposeIdsFor(Customer.class, Account.class);
leon commented 9 years ago

The preferred way would be to be able to specify a list of classes

spring.data.rest:
  basePath: /restapi
  returnBodyOnCreate: true
  exposeIdsFor:
    - se.radley.domain.Account
    - se.radley.domain.Customer

Since the RepositoryRestConfiguration doesn't expose a

setExposeIdsFor(List<Class<?>> classes)

The @ConfigurationProperties(prefix = "spring.data.rest") doesn't work

If we add the setter wouldn't that fix that problem?

wilkinsona commented 9 years ago

I think that'd more be an extension of the workaround, rather than a fix for the underlying problem.

leon commented 9 years ago

I first tried getting it working with only the application.yml but i got stuck when I got to the exposeIdsFor

I then got the id's working but then the dates stopped working :)

https://jira.spring.io/browse/SPR-12983

I think both scenarios should be supported. There must be some way to tell the overridden class to wait for the jackson autoconfig? @Order?

wilkinsona commented 9 years ago

@Order won't help here. Auto-configuration classes are processed using a DeferredImportSelector which is "a variation of ImportSelector that runs after all @Configuration beans have been processed". No matter what @Order you assign to your RestConfig class, it will be processed before the auto-configuration and will trigger the problem.

wilkinsona commented 9 years ago

I've dug into this a little bit more and the problem is actually caused by the auto-configuration, or not, of a MappingJackson2HttpMessageConverter.

In the successful case, Spring Boot's HttpMessageConvertersAutoConfiguration runs before a MappingJackson2HttpMessageConverter bean exists in the application context. This means that JacksonHttpMessageConvertersConfiguration creates such a bean that uses an ObjectMapper configured via the environment. HttpMessageConverters gives the converter priority over Spring MVC's default JSON converter and it handles the request and serialises the dates correctly.

When a SpringBootRepositoryRestMvcConfiguration subclass exists it creates two Mapping Jackson2HttpMessageConverter beans, both of which are actually TypeConstrainedMappingJackson2HttpMessageConverter, which use an ObjectMapper configured via the environment. So far so good, however the type-constrained nature of the message converters means that, when HttpMessageConverters is ordering the converters, the Spring MVC default MappingJackson2HttpMessageConverter is preferred and it handles the request. It uses an ObjectMapper in its default configuration rather than the one that's configured via the environment so the dates are serialised as timestamps.

There are (at least) two ways forward:

The latter option can be implemented as a workaround by declaring this HttpMessageConverters bean in your application:

@Bean
public HttpMessageConverters httpMessageConverters(
        final Jackson2ObjectMapperBuilder builder,
        List<HttpMessageConverter<?>> converters) {
    return new HttpMessageConverters(converters) {

        @Override
        protected List<HttpMessageConverter<?>> postProcessConverters(
                List<HttpMessageConverter<?>> converters) {
            for (HttpMessageConverter<?> converter : converters) {
                if (converter instanceof MappingJackson2HttpMessageConverter) {
                    builder.configure(((MappingJackson2HttpMessageConverter) converter)
                            .getObjectMapper());
                }
            }
            return converters;
        }
    };
}
wilkinsona commented 9 years ago

A related problem is that, with the default converters disabled:

@Bean
public HttpMessageConverters httpMessageConverters(List<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(false, converters);
}

a request for json will result in a 406 response as the type-constrained converter has prevented auto-configuration of the general converter and the type-constrained converter will return false from canWrite. This makes me think that the auto-configuration should be updated, if possible, so that a type-constrained converter doesn't prevent the auto-configuration of the general purpose converter.

wilkinsona commented 9 years ago

I've pushed a fix for this problem here. I haven't merged it as it won't fix the problem until the Spring Data REST change that is referenced above has been made.

philwebb commented 9 years ago

The Spring Data fix will be in Spring Data Evans-SR3 so we should be able to pick this up in 1.2.5.