Apply HATEOAS module to primary ObjectMapper

Update HypermediaAutoConfiguration to apply the Jackson2HalModule to
the primary ObjectMapper. This restores the behavior of Spring Boot
1.1 where HATEOAS types could be serialized for both `application/json`
and `application/json+hal` content types.

A `spring.hateoas.apply-to-primary-object-mapper` property has also been
provided to opt-out if necessary.

Fixes gh-2147
This commit is contained in:
Phillip Webb 2014-12-22 15:12:51 -08:00
parent ea84479e9a
commit fd97c7553c
4 changed files with 132 additions and 27 deletions

View File

@ -0,0 +1,43 @@
/*
* Copyright 2012-2014 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
*
* http://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.boot.autoconfigure.hateoas;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties properties} for Spring HATEOAS.
*
* @author Phillip webb
* @since 1.2.1
*/
@ConfigurationProperties(prefix = "spring.hateoas")
public class HateoasProperties {
/**
* If HATEOAS support should be applied to the primary ObjectMapper.
*/
private boolean applyToPrimaryObjectMapper = true;
public boolean isApplyToPrimaryObjectMapper() {
return this.applyToPrimaryObjectMapper;
}
public void setApplyToPrimaryObjectMapper(boolean applyToPrimaryObjectMapper) {
this.applyToPrimaryObjectMapper = applyToPrimaryObjectMapper;
}
}

View File

@ -16,8 +16,14 @@
package org.springframework.boot.autoconfigure.hateoas;
import javax.annotation.PostConstruct;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -27,14 +33,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.LinkDiscoverers;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.config.EnableEntityLinks;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.plugin.core.Plugin;
import org.springframework.web.bind.annotation.RequestMapping;
@ -55,6 +65,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
@ConditionalOnWebApplication
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class })
@EnableConfigurationProperties(HateoasProperties.class)
public class HypermediaAutoConfiguration {
@Configuration
@ -65,40 +76,90 @@ public class HypermediaAutoConfiguration {
@ConditionalOnClass({ Jackson2ObjectMapperBuilder.class, ObjectMapper.class })
protected static class HalObjectMapperConfiguration {
@Autowired
private HateoasProperties hateoasProperties;
@Autowired(required = false)
private Jackson2ObjectMapperBuilder objectMapperBuilder;
private CurieProvider curieProvider;
@Autowired
@Qualifier("_relProvider")
private RelProvider relProvider;
@Autowired(required = false)
private ObjectMapper primaryObjectMapper;
@PostConstruct
public void configurePrimaryObjectMapper() {
if (this.primaryObjectMapper != null
&& this.hateoasProperties.isApplyToPrimaryObjectMapper()) {
registerHalModule(this.primaryObjectMapper);
}
}
private void registerHalModule(ObjectMapper objectMapper) {
objectMapper.registerModule(new Jackson2HalModule());
Jackson2HalModule.HalHandlerInstantiator instantiator = new Jackson2HalModule.HalHandlerInstantiator(
HalObjectMapperConfiguration.this.relProvider,
HalObjectMapperConfiguration.this.curieProvider);
objectMapper.setHandlerInstantiator(instantiator);
}
@Bean
public BeanPostProcessor halObjectMapperConfigurer() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean,
String beanName) throws BeansException {
if (HalObjectMapperConfiguration.this.objectMapperBuilder != null
&& bean instanceof ObjectMapper
&& "_halObjectMapper".equals(beanName)) {
HalObjectMapperConfiguration.this.objectMapperBuilder
.configure((ObjectMapper) bean);
}
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean,
String beanName) throws BeansException {
return bean;
}
};
public static HalObjectMapperConfigurer halObjectMapperConfigurer() {
return new HalObjectMapperConfigurer();
}
}
}
@Configuration
@ConditionalOnMissingBean(EntityLinks.class)
@EnableEntityLinks
protected static class EntityLinksConfiguration {
}
/**
* {@link BeanPostProcessor} to apply any {@link Jackson2ObjectMapperBuilder}
* configuration to the HAL {@link ObjectMapper}.
*/
private static class HalObjectMapperConfigurer implements BeanPostProcessor,
BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof ObjectMapper && "_halObjectMapper".equals(beanName)) {
postProcessHalObjectMapper((ObjectMapper) bean);
}
return bean;
}
private void postProcessHalObjectMapper(ObjectMapper objectMapper) {
try {
Jackson2ObjectMapperBuilder builder = this.beanFactory
.getBean(Jackson2ObjectMapperBuilder.class);
builder.configure(objectMapper);
}
catch (NoSuchBeanDefinitionException ex) {
// No Jackson configuration required
}
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
}

View File

@ -75,19 +75,17 @@ public class HypermediaAutoConfigurationTests {
@Test
public void doesBackOffIfEnableHypermediaSupportIsDeclaredManually() {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SampleConfig.class, HypermediaAutoConfiguration.class);
this.context.refresh();
this.context.getBean(LinkDiscoverers.class);
}
@Test
public void jacksonConfigurationIsAppliedToTheHalObjectMapper() {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(HypermediaAutoConfiguration.class,
JacksonAutoConfiguration.class);
this.context.register(JacksonAutoConfiguration.class,
HypermediaAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.jackson.serialization.INDENT_OUTPUT:true");
this.context.refresh();

View File

@ -96,6 +96,9 @@ content into your application; rather pick only the properties that you need.
spring.resources.cache-period= # cache timeouts in headers sent to browser
spring.resources.add-mappings=true # if default mappings should be added
# SPRING HATEOS ({sc-spring-boot-autoconfigure}/hateoas/HateoasProperties.{sc-ext}[HateoasProperties])
spring.hateoas.apply-to-primary-object-mapper=true # if the primary mapper should also be configured
# HTTP encoding ({sc-spring-boot-autoconfigure}/web/HttpEncodingProperties.{sc-ext}[HttpEncodingProperties])
spring.http.encoding.charset=UTF-8 # the encoding of HTTP requests/responses
spring.http.encoding.enabled=true # enable http encoding support