001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.autoconfigure.jackson; 018 019import java.lang.reflect.Field; 020import java.text.DateFormat; 021import java.text.SimpleDateFormat; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028import java.util.TimeZone; 029 030import com.fasterxml.jackson.annotation.JsonAutoDetect; 031import com.fasterxml.jackson.annotation.JsonCreator; 032import com.fasterxml.jackson.annotation.PropertyAccessor; 033import com.fasterxml.jackson.databind.Module; 034import com.fasterxml.jackson.databind.ObjectMapper; 035import com.fasterxml.jackson.databind.PropertyNamingStrategy; 036import com.fasterxml.jackson.databind.SerializationFeature; 037import com.fasterxml.jackson.databind.module.SimpleModule; 038import com.fasterxml.jackson.datatype.joda.cfg.JacksonJodaDateFormat; 039import com.fasterxml.jackson.datatype.joda.ser.DateTimeSerializer; 040import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; 041import org.apache.commons.logging.Log; 042import org.apache.commons.logging.LogFactory; 043import org.joda.time.DateTime; 044import org.joda.time.format.DateTimeFormat; 045 046import org.springframework.beans.BeanUtils; 047import org.springframework.beans.factory.BeanFactoryUtils; 048import org.springframework.beans.factory.ListableBeanFactory; 049import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 050import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 051import org.springframework.boot.context.properties.EnableConfigurationProperties; 052import org.springframework.boot.jackson.JsonComponentModule; 053import org.springframework.context.ApplicationContext; 054import org.springframework.context.annotation.Bean; 055import org.springframework.context.annotation.Configuration; 056import org.springframework.context.annotation.Primary; 057import org.springframework.core.Ordered; 058import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 059import org.springframework.util.Assert; 060import org.springframework.util.ClassUtils; 061import org.springframework.util.ReflectionUtils; 062 063/** 064 * Auto configuration for Jackson. The following auto-configuration will get applied: 065 * <ul> 066 * <li>an {@link ObjectMapper} in case none is already configured.</li> 067 * <li>a {@link Jackson2ObjectMapperBuilder} in case none is already configured.</li> 068 * <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans 069 * (including the defaulted ones).</li> 070 * </ul> 071 * 072 * @author Oliver Gierke 073 * @author Andy Wilkinson 074 * @author Marcel Overdijk 075 * @author Sebastien Deleuze 076 * @author Johannes Edmeier 077 * @author Phillip Webb 078 * @author EddĂș MelĂ©ndez 079 * @since 1.1.0 080 */ 081@Configuration 082@ConditionalOnClass(ObjectMapper.class) 083public class JacksonAutoConfiguration { 084 085 private static final Map<?, Boolean> FEATURE_DEFAULTS; 086 087 static { 088 Map<Object, Boolean> featureDefaults = new HashMap<>(); 089 featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 090 FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults); 091 } 092 093 @Bean 094 public JsonComponentModule jsonComponentModule() { 095 return new JsonComponentModule(); 096 } 097 098 @Configuration 099 @ConditionalOnClass(Jackson2ObjectMapperBuilder.class) 100 static class JacksonObjectMapperConfiguration { 101 102 @Bean 103 @Primary 104 @ConditionalOnMissingBean 105 public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { 106 return builder.createXmlMapper(false).build(); 107 } 108 109 } 110 111 @Configuration 112 @ConditionalOnClass({ Jackson2ObjectMapperBuilder.class, DateTime.class, 113 DateTimeSerializer.class, JacksonJodaDateFormat.class }) 114 static class JodaDateTimeJacksonConfiguration { 115 116 private static final Log logger = LogFactory 117 .getLog(JodaDateTimeJacksonConfiguration.class); 118 119 private final JacksonProperties jacksonProperties; 120 121 JodaDateTimeJacksonConfiguration(JacksonProperties jacksonProperties) { 122 this.jacksonProperties = jacksonProperties; 123 } 124 125 @Bean 126 public SimpleModule jodaDateTimeSerializationModule() { 127 SimpleModule module = new SimpleModule(); 128 JacksonJodaDateFormat jacksonJodaFormat = getJacksonJodaDateFormat(); 129 if (jacksonJodaFormat != null) { 130 module.addSerializer(DateTime.class, 131 new DateTimeSerializer(jacksonJodaFormat, 0)); 132 } 133 return module; 134 } 135 136 private JacksonJodaDateFormat getJacksonJodaDateFormat() { 137 if (this.jacksonProperties.getJodaDateTimeFormat() != null) { 138 return new JacksonJodaDateFormat(DateTimeFormat 139 .forPattern(this.jacksonProperties.getJodaDateTimeFormat()) 140 .withZoneUTC()); 141 } 142 if (this.jacksonProperties.getDateFormat() != null) { 143 try { 144 return new JacksonJodaDateFormat(DateTimeFormat 145 .forPattern(this.jacksonProperties.getDateFormat()) 146 .withZoneUTC()); 147 } 148 catch (IllegalArgumentException ex) { 149 if (logger.isWarnEnabled()) { 150 logger.warn("spring.jackson.date-format could not be used to " 151 + "configure formatting of Joda's DateTime. You may want " 152 + "to configure spring.jackson.joda-date-time-format as " 153 + "well."); 154 } 155 } 156 } 157 return null; 158 } 159 160 } 161 162 @Configuration 163 @ConditionalOnClass(ParameterNamesModule.class) 164 static class ParameterNamesModuleConfiguration { 165 166 @Bean 167 @ConditionalOnMissingBean 168 public ParameterNamesModule parameterNamesModule() { 169 return new ParameterNamesModule(JsonCreator.Mode.DEFAULT); 170 } 171 172 } 173 174 @Configuration 175 @ConditionalOnClass(Jackson2ObjectMapperBuilder.class) 176 static class JacksonObjectMapperBuilderConfiguration { 177 178 private final ApplicationContext applicationContext; 179 180 JacksonObjectMapperBuilderConfiguration(ApplicationContext applicationContext) { 181 this.applicationContext = applicationContext; 182 } 183 184 @Bean 185 @ConditionalOnMissingBean 186 public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder( 187 List<Jackson2ObjectMapperBuilderCustomizer> customizers) { 188 Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); 189 builder.applicationContext(this.applicationContext); 190 customize(builder, customizers); 191 return builder; 192 } 193 194 private void customize(Jackson2ObjectMapperBuilder builder, 195 List<Jackson2ObjectMapperBuilderCustomizer> customizers) { 196 for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) { 197 customizer.customize(builder); 198 } 199 } 200 201 } 202 203 @Configuration 204 @ConditionalOnClass(Jackson2ObjectMapperBuilder.class) 205 @EnableConfigurationProperties(JacksonProperties.class) 206 static class Jackson2ObjectMapperBuilderCustomizerConfiguration { 207 208 @Bean 209 public StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer( 210 ApplicationContext applicationContext, 211 JacksonProperties jacksonProperties) { 212 return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, 213 jacksonProperties); 214 } 215 216 static final class StandardJackson2ObjectMapperBuilderCustomizer 217 implements Jackson2ObjectMapperBuilderCustomizer, Ordered { 218 219 private final ApplicationContext applicationContext; 220 221 private final JacksonProperties jacksonProperties; 222 223 StandardJackson2ObjectMapperBuilderCustomizer( 224 ApplicationContext applicationContext, 225 JacksonProperties jacksonProperties) { 226 this.applicationContext = applicationContext; 227 this.jacksonProperties = jacksonProperties; 228 } 229 230 @Override 231 public int getOrder() { 232 return 0; 233 } 234 235 @Override 236 public void customize(Jackson2ObjectMapperBuilder builder) { 237 238 if (this.jacksonProperties.getDefaultPropertyInclusion() != null) { 239 builder.serializationInclusion( 240 this.jacksonProperties.getDefaultPropertyInclusion()); 241 } 242 if (this.jacksonProperties.getTimeZone() != null) { 243 builder.timeZone(this.jacksonProperties.getTimeZone()); 244 } 245 configureFeatures(builder, FEATURE_DEFAULTS); 246 configureVisibility(builder, this.jacksonProperties.getVisibility()); 247 configureFeatures(builder, this.jacksonProperties.getDeserialization()); 248 configureFeatures(builder, this.jacksonProperties.getSerialization()); 249 configureFeatures(builder, this.jacksonProperties.getMapper()); 250 configureFeatures(builder, this.jacksonProperties.getParser()); 251 configureFeatures(builder, this.jacksonProperties.getGenerator()); 252 configureDateFormat(builder); 253 configurePropertyNamingStrategy(builder); 254 configureModules(builder); 255 configureLocale(builder); 256 } 257 258 private void configureFeatures(Jackson2ObjectMapperBuilder builder, 259 Map<?, Boolean> features) { 260 features.forEach((feature, value) -> { 261 if (value != null) { 262 if (value) { 263 builder.featuresToEnable(feature); 264 } 265 else { 266 builder.featuresToDisable(feature); 267 } 268 } 269 }); 270 } 271 272 private void configureVisibility(Jackson2ObjectMapperBuilder builder, 273 Map<PropertyAccessor, JsonAutoDetect.Visibility> visibilities) { 274 visibilities.forEach(builder::visibility); 275 } 276 277 private void configureDateFormat(Jackson2ObjectMapperBuilder builder) { 278 // We support a fully qualified class name extending DateFormat or a date 279 // pattern string value 280 String dateFormat = this.jacksonProperties.getDateFormat(); 281 if (dateFormat != null) { 282 try { 283 Class<?> dateFormatClass = ClassUtils.forName(dateFormat, null); 284 builder.dateFormat( 285 (DateFormat) BeanUtils.instantiateClass(dateFormatClass)); 286 } 287 catch (ClassNotFoundException ex) { 288 SimpleDateFormat simpleDateFormat = new SimpleDateFormat( 289 dateFormat); 290 // Since Jackson 2.6.3 we always need to set a TimeZone (see 291 // gh-4170). If none in our properties fallback to the Jackson's 292 // default 293 TimeZone timeZone = this.jacksonProperties.getTimeZone(); 294 if (timeZone == null) { 295 timeZone = new ObjectMapper().getSerializationConfig() 296 .getTimeZone(); 297 } 298 simpleDateFormat.setTimeZone(timeZone); 299 builder.dateFormat(simpleDateFormat); 300 } 301 } 302 } 303 304 private void configurePropertyNamingStrategy( 305 Jackson2ObjectMapperBuilder builder) { 306 // We support a fully qualified class name extending Jackson's 307 // PropertyNamingStrategy or a string value corresponding to the constant 308 // names in PropertyNamingStrategy which hold default provided 309 // implementations 310 String strategy = this.jacksonProperties.getPropertyNamingStrategy(); 311 if (strategy != null) { 312 try { 313 configurePropertyNamingStrategyClass(builder, 314 ClassUtils.forName(strategy, null)); 315 } 316 catch (ClassNotFoundException ex) { 317 configurePropertyNamingStrategyField(builder, strategy); 318 } 319 } 320 } 321 322 private void configurePropertyNamingStrategyClass( 323 Jackson2ObjectMapperBuilder builder, 324 Class<?> propertyNamingStrategyClass) { 325 builder.propertyNamingStrategy((PropertyNamingStrategy) BeanUtils 326 .instantiateClass(propertyNamingStrategyClass)); 327 } 328 329 private void configurePropertyNamingStrategyField( 330 Jackson2ObjectMapperBuilder builder, String fieldName) { 331 // Find the field (this way we automatically support new constants 332 // that may be added by Jackson in the future) 333 Field field = ReflectionUtils.findField(PropertyNamingStrategy.class, 334 fieldName, PropertyNamingStrategy.class); 335 Assert.notNull(field, () -> "Constant named '" + fieldName 336 + "' not found on " + PropertyNamingStrategy.class.getName()); 337 try { 338 builder.propertyNamingStrategy( 339 (PropertyNamingStrategy) field.get(null)); 340 } 341 catch (Exception ex) { 342 throw new IllegalStateException(ex); 343 } 344 } 345 346 private void configureModules(Jackson2ObjectMapperBuilder builder) { 347 Collection<Module> moduleBeans = getBeans(this.applicationContext, 348 Module.class); 349 builder.modulesToInstall(moduleBeans.toArray(new Module[0])); 350 } 351 352 private void configureLocale(Jackson2ObjectMapperBuilder builder) { 353 Locale locale = this.jacksonProperties.getLocale(); 354 if (locale != null) { 355 builder.locale(locale); 356 } 357 } 358 359 private static <T> Collection<T> getBeans(ListableBeanFactory beanFactory, 360 Class<T> type) { 361 return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type) 362 .values(); 363 } 364 365 } 366 367 } 368 369}