本文基于 SpringBoot 2.6.3 版本,spring-webmvc 5.3.15版本
官方文档:https://docs.spring.io/spring-boot/docs/2.6.3/reference/htmlsingle/#web.servlet.spring-mvc.content-negotiation
默认情况下,SpringBoot中SpringMVC接口返回的数据是json格式,但有些时候同样的数据我们可能需要根据请求来返回不同的格式。即同一个接口可以返回json格式,又可以返回xml格式。
需要额外添加如下依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-xmlartifactId>
dependency>
默认情况下SpringMVC根据Accept
请求头来确定返回什么格式给客户端。所以在添加了jackson-dataformat-xml
直接在浏览器访问接口返回的是xml格式的数据。如果想要返回json格式,可以手动设置Accept
为application/json
。
但如果http客户端无法设置Accept
,可以使用请求参数format=json
来指定返回的数据格式,优先级高于Accept
,但是默认没有开启,需要添加如下参数开启:
spring.mvc.contentnegotiation.favor-parameter=true
在spring-webmvc 5.2.4之前的版本中支持根据url后缀来返回数据格式,但是该版本开始,已经被标记过时,不推荐该方式。
WebMvcConfigurationSupport
中会自己推断支持哪些类型,但是对于默认不支持的类型则需要自己通过如下属性设置支持哪些类型. 和配置解析器. 如果key相同则会覆盖推断出来的.
spring.mvc.contentnegotiation.media-types.customType=customType
如下是SpringMVC默认添加的所支持的media-type:
下面我们通过自定义HttpMessageConverter
并配置其所支持的MediaType
来返回jsonp
格式的数据。
首先是创建一个JsonpHttpMessageConverter
实现HttpMessageConverter
/**
* Created by bruce on 2022/2/14 19:13
*/
public class JsonpHttpMessageConverter implements HttpMessageConverter<Object> {
public static final MediaType JSONP = new MediaType("application", "jsonp");
private ObjectMapper objectMapper;
public JsonpHttpMessageConverter() {
objectMapper = Jackson2ObjectMapperBuilder.json().build();
}
@Override
public boolean canRead(Class clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class clazz, MediaType mediaType) {
return objectMapper.canSerialize(clazz);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return List.of(JSONP);
}
@Override
public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
HttpHeaders headers = outputMessage.getHeaders();
if (headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
}
String data = "callback(" + objectMapper.writeValueAsString(o) + ")";
StreamUtils.copy(data, StandardCharsets.UTF_8, outputMessage.getBody());
}
}
接下来就是通过org.springframework.web.servlet.config.annotation.WebMvcConfigurer
来配置JsonpHttpMessageConverter
。
@Order(1)
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// configurer.mediaType("json", MediaType.APPLICATION_JSON);
// configurer.mediaType("xml", MediaType.APPLICATION_XML);
configurer.mediaType("jsonp", JsonpHttpMessageConverter.JSONP);
}
/**
* @link {https://github.com/spring-projects/spring-boot/issues/21374}
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters(java.util.List)
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//移除重复的HttpMessageConverter
HttpMessageConverter<?> preConverter = null;
HttpMessageConverter<?> currentConverter = null;
Iterator<HttpMessageConverter<?>> iterator = converters.iterator();
while (iterator.hasNext()) {
if (preConverter == null) {
preConverter = iterator.next();
continue;
}
currentConverter = iterator.next();
List<MediaType> preSupports = preConverter.getSupportedMediaTypes();
List<MediaType> currentSupports = currentConverter.getSupportedMediaTypes();
if (preConverter.getClass() == currentConverter.getClass()) {
boolean allIn = currentSupports.stream().allMatch(item -> item.isPresentIn(preSupports));
if (allIn) {
iterator.remove();
}
}
preConverter = currentConverter;
}
//添加自己的
JsonpHttpMessageConverter jsonpHttpMessageConverter = new JsonpHttpMessageConverter();
converters.add(jsonpHttpMessageConverter);
}
}
默认情况下SpringBoot中还会配置org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter
,@Order(1)
用于指定多个WebMvcConfigurer
之间的排序顺序。
默认情况下,SpringBoot对SpringMVC的自动装配会产生如下几个重复的:
SpringBoot官方解释这是正确的,详情看我提的这个issue:Duplicate httpmessageconverter
但是个人认为这是没有必要的。
因此在WebConfig
中有一段是移除重复的HttpMessageConverter
的逻辑
后面两行则是添加自己的JsonpHttpMessageConverter
最后在浏览器访问查看效果:
如何在JsonpHttpMessageConverter
中获取客户端的请求参数呢?
方案1:通过 RequestContextHolder.currentRequestAttributes()
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
String jsonpCallback = requestAttributes.getRequest().getParameter("jsonpCallback");
System.out.println("获取http请求参数jsonpCallback=" + jsonpCallback);
方案2:通过自定义org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
具体不详讲,有需要请点赞后留言。
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcContentNegotiationManager
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#mvcContentNegotiationManager
MediaType
,并设置到ContentNegotiationConfigurer
中WebMvcConfigurer#configureContentNegotiation(ContentNegotiationConfigurer)
接口方法,方便用户自定义ContentNegotiationConfigurer
。WebMvcConfigurer
之间可以排序。ContentNegotiationConfigurer
中会创建成员变量ContentNegotiationManagerFactoryBean factory
。ContentNegotiationConfigurer#buildContentNegotiationManager
实际上就是调用ContentNegotiationManagerFactoryBean#build
来创建ContentNegotiationManager
对象ContentNegotiationConfigurer
设置ContentNegotiationManagerFactoryBean
中的一些参数,例如设置自定义支持的MediaType
、设置自定义策略(ContentNegotiationStrategy
)如何解析请求中MediaType
等。SpringMVC中提供的策略有ParameterContentNegotiationStrategy
、HeaderContentNegotiationStrategy
(默认)等。一般这两种策略够用了。接下来会创建RequestMappingHandlerAdapter
:
springboot提供了接口org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations
,只要将实现类配置成Bean就行,方便用户提供自己的如下实例:
RequestMappingHandlerMapping
、RequestMappingHandlerAdapter
、ExceptionHandlerExceptionResolver
1.#getMessageConverters
方法会回调WebMvcConfigurer#configureMessageConverters(messageConverters)
6. 开始messageConverters
集合中为空,调用完WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureMessageConverters
之后,会往集合设置默认支持的HttpMessageConverters
7. 我们可以通过配置实现WebMvcConfigurer
接口的bean来添加自己的HttpMessageConverter,或者调整messageConverters
中的HttpMessageConverter
当初次访问http地址时会执行DispatcherServlet#initHandlerAdapters
方法从Context中获取HandlerAdapter
涉及源码比较多,根据这些慢慢看吧!!!