spring.jackson.default-property-inclusion 不生效問題分析

2023-02-05 15:00:11

背景

專案裡每個返回體裡都有@JsonInclude(JsonInclude.Include.NON_NULL) 這個註解,也就是不返回null欄位

想有沒有辦法全域性設定一下,這樣就不用每個類都加這個註解了

spring:
  jackson:
    default-property-inclusion: non_null

看網上說加上這個設定項就可以了,於是加上之後,發現不生效,後來又查到不生效的原因是因為專案裡手動注入了WebMvcConfigurationSupport這個類

看我們的專案裡確實是這樣的,設定如下:

@Configuration
@Import(WebMvcAutoConfiguration.EnableWebMvcConfiguration.class)
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
}

匯入了EnableWebMvcConfiguration這個類,這個類繼承了DelegatingWebMvcConfiguration, DelegatingWebMvcConfiguration這個類實現了WebMvcConfigurationSupport

那就一起來看下不生效的原因以及解決辦法

例子

web設定
@Configuration
@Import(WebMvcAutoConfiguration.EnableWebMvcConfiguration.class)
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
}

controller
@RestController
@RequestMapping("/api/jackson")
public class JacksonTestController {

    @GetMapping("/data")
    public Object getData() {
        return new Result();
    }

    @Data
    public static class Result {
        private String string1 = "hello";
        private String string2;
    }
}

application.yml
spring:
  jackson:
    default-property-inclusion: non_null

啟動類
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
存取 http://127.0.0.1:8080/api/jackson/data
返回
{
  "string1": "hello",
  "string2": null
}

可以看出確實是不生效的

先看一下比較明顯的方案:

方法一
加JsonInclude註解
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Result {
    private String string1 = "hello";
    private String string2;
}
存取返回:
{
  "string1": "hello"
}

方法二
註釋掉Import那一行
@Configuration
//@Import(WebMvcAutoConfiguration.EnableWebMvcConfiguration.class)
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
}
 @Data
//@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Result {
    private String string1 = "hello";
    private String string2;
}
存取返回:
{
  "string1": "hello"
}

原因

  1. body是怎麼返回的?
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
		implements HandlerMethodReturnValueHandler {
	protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
			ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
			throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
	}
}

body是返回結果,MappingJackson2HttpMessageConverter會去轉換結果,它是從this.messageConverters獲取

分別看一下不去掉以及去掉Import(WebMvcAutoConfiguration.EnableWebMvcConfiguration.class)註解後 messageConverters 的結果

不去掉的結果

去掉的結果

可以看到,去掉之後,多了一個MappingJackson2HttpMessageConverter範例,並且這個是根據設定生成的,而且因為它的順序在前面,所以會用這個來處理

  1. 來看一下 this.messageConverters 是怎麼被賦值的

RequestResponseBodyMethodProcessor這個類是通過RequestMappingHandlerAdapter生成的

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {

    private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
        handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice)); // this
        return handlers;
    }

    public List<HttpMessageConverter<?>> getMessageConverters() {
	return this.messageConverters; // this
    }
}

RequestMappingHandlerAdapter是由WebMvcConfigurationSupport生成的

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
		@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
		@Qualifier("mvcConversionService") FormattingConversionService conversionService,
		@Qualifier("mvcValidator") Validator validator) {

    	RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
	adapter.setMessageConverters(getMessageConverters());
    }

    protected final List<HttpMessageConverter<?>> getMessageConverters() {
		if (this.messageConverters == null) {
			this.messageConverters = new ArrayList<>();
			configureMessageConverters(this.messageConverters);  // this
			if (this.messageConverters.isEmpty()) {
				addDefaultHttpMessageConverters(this.messageConverters); // this
			}
			extendMessageConverters(this.messageConverters);
		}
		return this.messageConverters;
	}

	protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
		StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
		stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

		messageConverters.add(new ByteArrayHttpMessageConverter());
		messageConverters.add(stringHttpMessageConverter);
		messageConverters.add(new ResourceHttpMessageConverter());
		messageConverters.add(new ResourceRegionHttpMessageConverter());

		if (jackson2Present) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));  // this
		}
		else if (gsonPresent) {
			messageConverters.add(new GsonHttpMessageConverter());
		}
		else if (jsonbPresent) {
			messageConverters.add(new JsonbHttpMessageConverter());
		}
	}
}

可以看到先呼叫configureMessageConverters方法,如果為空就呼叫addDefaultHttpMessageConverters方法新增預設的。

看一下configureMessageConverters方法

class WebMvcConfigurerComposite implements WebMvcConfigurer {
	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
		for (WebMvcConfigurer delegate : this.delegates) {
			delegate.configureMessageConverters(converters);
		}
	}
}

不去掉的結果

去掉的結果

可以看到,不去掉的話,這裡只有一個我們自己定義的WebMvcConfigurer,而且是不會做任何操作的,所以就會呼叫addDefaultHttpMessageConverters,生成預設的。
如果去掉的話,會呼叫WebMvcAutoConfigurationAdapter的configureMessageConverters方法,這個方法的返回結果不會空,就不會呼叫addDefaultHttpMessageConverters,然後直接返回。

下面看一下這個方法。

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
	@Configuration(proxyBeanMethods = false)
	@Import(EnableWebMvcConfiguration.class)
	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {

		private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;

		public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
				ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
				ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
				ObjectProvider<DispatcherServletPath> dispatcherServletPath,
				ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {

			this.messageConvertersProvider = messageConvertersProvider; // this

		}

		@Override
		public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
			this.messageConvertersProvider
					.ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters())); // this
		}
}

會注入HttpMessageConverters

@Configuration
@ConditionalOnClass(HttpMessageConverter.class)
@AutoConfigureAfter({ GsonAutoConfiguration.class, JacksonAutoConfiguration.class,
		JsonbAutoConfiguration.class })
@Import({ JacksonHttpMessageConvertersConfiguration.class,
		GsonHttpMessageConvertersConfiguration.class,
		JsonbHttpMessageConvertersConfiguration.class })
public class HttpMessageConvertersAutoConfiguration {

	public HttpMessageConvertersAutoConfiguration(
			ObjectProvider<HttpMessageConverter<?>> convertersProvider) {
		this.converters = convertersProvider.orderedStream().collect(Collectors.toList());
	}

	@Bean
	@ConditionalOnMissingBean
	public HttpMessageConverters messageConverters() {
		return new HttpMessageConverters(this.converters);
	}

	public HttpMessageConverters(Collection<HttpMessageConverter<?>> additionalConverters) {
		this(true, additionalConverters);
	}

	public HttpMessageConverters(boolean addDefaultConverters,
			Collection<HttpMessageConverter<?>> converters) {
		List<HttpMessageConverter<?>> combined = getCombinedConverters(converters,
				addDefaultConverters ? getDefaultConverters() : Collections.emptyList());
		combined = postProcessConverters(combined);
		this.converters = Collections.unmodifiableList(combined);
	}

	private List<HttpMessageConverter<?>> getDefaultConverters() {
		List<HttpMessageConverter<?>> converters = new ArrayList<>();
		if (ClassUtils.isPresent("org.springframework.web.servlet.config.annotation."
				+ "WebMvcConfigurationSupport", null)) {
			converters.addAll(new WebMvcConfigurationSupport() {

				public List<HttpMessageConverter<?>> defaultMessageConverters() {
					return super.getMessageConverters(); // this
				}

			}.defaultMessageConverters());
		}
		else {
			converters.addAll(new RestTemplate().getMessageConverters());
		}
		reorderXmlConvertersToEnd(converters);
		return converters;
	}

	private List<HttpMessageConverter<?>> getCombinedConverters(
			Collection<HttpMessageConverter<?>> converters,
			List<HttpMessageConverter<?>> defaultConverters) {
		List<HttpMessageConverter<?>> combined = new ArrayList<>();
		List<HttpMessageConverter<?>> processing = new ArrayList<>(converters);
		for (HttpMessageConverter<?> defaultConverter : defaultConverters) {
			Iterator<HttpMessageConverter<?>> iterator = processing.iterator();
			while (iterator.hasNext()) {
				HttpMessageConverter<?> candidate = iterator.next();
				if (isReplacement(defaultConverter, candidate)) {
					combined.add(candidate);
					iterator.remove();
				}
			}
			combined.add(defaultConverter);
			if (defaultConverter instanceof AllEncompassingFormHttpMessageConverter) {
				configurePartConverters(
						(AllEncompassingFormHttpMessageConverter) defaultConverter,
						converters);
			}
		}
		combined.addAll(0, processing);
		return combined;
	}
}

可以看到converters由兩部分組成,一部分是注入的HttpMessageConverter,另一部分是WebMvcConfigurationSupport.defaultMessageConverters方法,然後進行合併,當然如果有相同的類,前面的順序在前。

會注入MappingJackson2HttpMessageConverter

@Configuration
class JacksonHttpMessageConvertersConfiguration {
	@Configuration
	@ConditionalOnClass(ObjectMapper.class)
	@ConditionalOnBean(ObjectMapper.class)
	@ConditionalOnProperty(
			name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
			havingValue = "jackson", matchIfMissing = true)
	protected static class MappingJackson2HttpMessageConverterConfiguration {

		@Bean
		@ConditionalOnMissingBean(value = MappingJackson2HttpMessageConverter.class,
				ignoredType = {
						"org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter",
						"org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter" })
		public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(
				ObjectMapper objectMapper) {
			return new MappingJackson2HttpMessageConverter(objectMapper);
		}

	}
}

會注入ObjectMapper相關的範例

@Configuration
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {

        // 注入jacksonObjectMapper
	@Configuration
	@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
	static class JacksonObjectMapperConfiguration {
		@Bean
		@Primary
		@ConditionalOnMissingBean
		public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
			return builder.createXmlMapper(false).build();
		}
	}
        
        // 注入jacksonObjectMapperBuilder
	@Configuration
	@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
	static class JacksonObjectMapperBuilderConfiguration {
		private final ApplicationContext applicationContext;

		JacksonObjectMapperBuilderConfiguration(ApplicationContext applicationContext) {
			this.applicationContext = applicationContext;
		}

		@Bean
		@ConditionalOnMissingBean
		public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(
				List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
			Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
			builder.applicationContext(this.applicationContext);
			customize(builder, customizers);
			return builder;
		}

		private void customize(Jackson2ObjectMapperBuilder builder,
				List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
			for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
				customizer.customize(builder);
			}
		}

	}

        // 注入standardJacksonObjectMapperBuilderCustomizer
	@Configuration
	@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
	@EnableConfigurationProperties(JacksonProperties.class)
	static class Jackson2ObjectMapperBuilderCustomizerConfiguration {

		@Bean
		StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
				ApplicationContext applicationContext, JacksonProperties jacksonProperties) {
			return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
		}

		static final class StandardJackson2ObjectMapperBuilderCustomizer
				implements Jackson2ObjectMapperBuilderCustomizer, Ordered {
			private final JacksonProperties jacksonProperties;

			@Override
			public void customize(Jackson2ObjectMapperBuilder builder) {
				if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
					builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
				}
			}
		}

        }
}

// 會注入JacksonProperties
@ConfigurationProperties(prefix = "spring.jackson")
public class JacksonProperties {
	private JsonInclude.Include defaultPropertyInclusion;
}

這個過程就生成了一個新的根據組態檔設定的MappingJackson2HttpMessageConverter,並把它新增到messageConverters中

總結

最後還有一種不去掉import註解就可以解決的方法,就是採用和WebMvcAutoConfigurationAdapter一樣的方法,如下:

@Configuration
@Import(WebMvcAutoConfiguration.EnableWebMvcConfiguration.class)
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {

    private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;

    public WebConfig(ObjectProvider<HttpMessageConverters> messageConvertersProvider) {
        this.messageConvertersProvider = messageConvertersProvider;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        this.messageConvertersProvider.ifAvailable((customConverters) -> converters
                .addAll(customConverters.getConverters()));
    }
}

參考

Spring Boot 中自定義 SpringMVC 設定,到底繼承誰?
自定義SpringBoot預設MVC設定?好幾個坑,這篇文章必須珍藏