排查过程:
@RequestMapping(value = "sms/sendsms") @ResponseBody public ResultInfo sendSmsHandler(@RequestParam(defaultValue = "") String otherPhones, @RequestParam(defaultValue = "") String msgText, HttpServletRequest request) throws Exception{ otherPhones = checkAndConvertStr(otherPhones); msgText = checkAndConvertStr(msgText); ResultInfo result = mobileManager.sendSMSByUrlCon(otherPhones,msgText); return result; }
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.9.8</version> </dependency>
分析过程
/** * Writes the given return type to the given output message. * @param returnValue the value to write to the output message * @param returnType the type of the value * @param inputMessage the input messages. Used to inspect the {@code Accept} header. * @param outputMessage the output message to write to * @throws IOException thrown in case of I/O errors * @throws HttpMediaTypeNotAcceptableException thrown when the conditions indicated by {@code Accept} header on * the request cannot be met by the message converters */ @SuppressWarnings("unchecked") protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException { Class<?> returnValueClass = getReturnValueType(returnValue, returnType); HttpServletRequest servletRequest = inputMessage.getServletRequest(); List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest); //这里根据Request的accept来获取客户端支持的MediaType,如果没有设置accpet,则为"*/*" List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass);//根据接口的Produces或者MessageConvert来找到能够生成的MediaType,这个一个关键的方法 Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); //取requestedMediaTypes和producibleMediaTypes互相兼容的交集 for (MediaType requestedType : requestedMediaTypes) { for (MediaType producibleType : producibleMediaTypes) { if (requestedType.isCompatibleWith(producibleType)) { compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (compatibleMediaTypes.isEmpty()) { if (returnValue != null) { throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); } return; } List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes); //MediaType列表进行排序 MediaType.sortBySpecificityAndQuality(mediaTypes); MediaType selectedMediaType = null; for (MediaType mediaType : mediaTypes) { //如果mediaType的类型是具体的(不是 "*/*"),则直接返回。由于我们引入了jackson-dataformat-xml包,导致mediaTypes的第一个MediaType就是application/xml;charset=UTF-8 //所以isConcrete方法返回true,这就决定了返回的MediaType格式是xml格式 if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } //当我们知道了返回的MediaType为application/xml;charset=UTF-8 后,就需要找到对应的MessageConverter,将对象转换为xml格式输出到http客户端,如果找不到对应的MessageConverter,spring就会抛出异常 if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> messageConverter : this.messageConverters) { if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType, (Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); if (returnValue != null) { ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } } return; } } } if (returnValue != null) { throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); } }
getAcceptableMediaTypes方法:
- 使用Request的URL后缀(扩展名)来实现 (eg .xml/.json) , 默认开启
- 根据Request中的URL参数来实现(eg ?format=json) , 默认关闭
- 根据Request的header中的 Accept属性来决定,默认开启
public class ContentNegotiationManagerFactoryBean extends Object implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean
ContentNegotiationManager
and configure it with ContentNegotiationStrategy
instances.This factory offers properties that in turn result in configuring the underlying strategies. The table below shows the property names, their default settings, as well as the strategies that they help to configure:
Property Setter | Default Value | Underlying Strategy | Enabled Or Not |
---|---|---|---|
favorPathExtension |
true | PathExtensionContentNegotiationStrategy |
Enabled |
favorParameter |
false | ParameterContentNegotiationStrategy |
Off |
ignoreAcceptHeader |
false | HeaderContentNegotiationStrategy |
Enabled |
defaultContentType |
null | FixedContentNegotiationStrategy |
Off |
defaultContentTypeStrategy |
null | ContentNegotiationStrategy |
Off |
As of 5.0 you can set the exact strategies to use via setStrategies(List)
.
Note: if you must use URL-based content type resolution, the use of a query parameter is simpler and preferable to the use of a path extension since the latter can cause issues with URI variables, path parameters, and URI decoding. Consider setting setFavorPathExtension(boolean)
to false or otherwise set the strategies to use explicitly via setStrategies(List)
.
@Override public void afterPropertiesSet() { List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>(); // this.mediaTypes表示springmvc初始化后支持的MediaType类型,并且将mediaTypes塞入到各种默认的协商策略对象中,用于内容协商时计算 if (this.favorPathExtension) { PathExtensionContentNegotiationStrategy strategy; if (this.servletContext != null) { strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes); } else { strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes); } strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions); if (this.useJaf != null) { strategy.setUseJaf(this.useJaf); } strategies.add(strategy); } if (this.favorParameter) { ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes); strategy.setParameterName(this.parameterName); strategies.add(strategy); } if (!this.ignoreAcceptHeader) { strategies.add(new HeaderContentNegotiationStrategy()); } if(this.defaultNegotiationStrategy != null) { strategies.add(defaultNegotiationStrategy); } this.contentNegotiationManager = new ContentNegotiationManager(strategies); }
/** * Return a {@link ContentNegotiationManager} instance to use to determine * requested {@linkplain MediaType media types} in a given request. */ @Bean public ContentNegotiationManager mvcContentNegotiationManager() { if (this.contentNegotiationManager == null) { ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext); configurer.mediaTypes(getDefaultMediaTypes()); //这里获取默认的MediaType configureContentNegotiation(configurer); try { this.contentNegotiationManager = configurer.getContentNegotiationManager(); } catch (Exception ex) { throw new BeanInitializationException("Could not create ContentNegotiationManager", ex); } } return this.contentNegotiationManager; }
protected Map<String, MediaType> getDefaultMediaTypes() { Map<String, MediaType> map = new HashMap<String, MediaType>(); if (romePresent) { map.put("atom", MediaType.APPLICATION_ATOM_XML); map.put("rss", MediaType.valueOf("application/rss+xml")); } if (jaxb2Present || jackson2XmlPresent) { map.put("xml", MediaType.APPLICATION_XML); } if (jackson2Present || gsonPresent) { map.put("json", MediaType.APPLICATION_JSON); } return map; }
private static boolean romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", WebMvcConfigurationSupport.class.getClassLoader()); private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", WebMvcConfigurationSupport.class.getClassLoader()); private static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", WebMvcConfigurationSupport.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", WebMvcConfigurationSupport.class.getClassLoader()); private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", WebMvcConfigurationSupport.class.getClassLoader()); private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", WebMvcConfigurationSupport.class.getClassLoader());
if (jaxb2Present || jackson2XmlPresent) {//只要jaxb2Present或jackson2XmlPresent任意一个为真,则就加入xml格式 map.put("xml", MediaType.APPLICATION_XML); }
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); }
getProducibleMediaTypes方法
/** * Returns the media types that can be produced: * <ul> * <li>The producible media types specified in the request mappings, or * <li>Media types of configured converters that can write the specific return value, or * <li>{@link MediaType#ALL} * </ul> */ @SuppressWarnings("unchecked") protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass) { //判断Controller的@RequestMapping否设置了produces属性,如果设置了,那肯定就只返回设置的MediaType类型 Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<MediaType>(mediaTypes); } else if (!this.allSupportedMediaTypes.isEmpty()) { //判断spring支持的所有的MediaType,那么就需要看下这个allSupportedMediaTypes值是从哪里来的 List<MediaType> result = new ArrayList<MediaType>(); for (HttpMessageConverter<?> converter : this.messageConverters) { if (converter.canWrite(returnValueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } return result; } else { return Collections.singletonList(MediaType.ALL); } }
public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) { Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); this.messageConverters = messageConverters; this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters); } /** * springmvc会根据提供的MessageConverter来决定支持哪些MediaType,那么我们就需要知道springmvc有哪些MessageConverter了,看messageConverters这个列表是在哪里赋值的。 * Return the media types supported by all provided message converters sorted * by specificity via {@link MediaType#sortBySpecificity(List)}. */ private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) { Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<MediaType>(); for (HttpMessageConverter<?> messageConverter : messageConverters) { allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); } List<MediaType> result = new ArrayList<MediaType>(allSupportedMediaTypes); MediaType.sortBySpecificity(result); return Collections.unmodifiableList(result); }
/** * Provides access to the shared {@link HttpMessageConverter}s used by the * {@link RequestMappingHandlerAdapter} and the * {@link ExceptionHandlerExceptionResolver}. * This method cannot be overridden. * Use {@link #configureMessageConverters(List)} instead. * Also see {@link #addDefaultHttpMessageConverters(List)} that can be * used to add default message converters. */ protected final List<HttpMessageConverter<?>> getMessageConverters() { if (this.messageConverters == null) { this.messageConverters = new ArrayList<HttpMessageConverter<?>>(); configureMessageConverters(this.messageConverters); //第一次this.messageConverters是空的,所以继续往下走 if (this.messageConverters.isEmpty()) { addDefaultHttpMessageConverters(this.messageConverters); //初是化默认的HttpMessageConverter列表 } } return this.messageConverters; }
/** * Adds a set of default HttpMessageConverter instances to the given list. * Subclasses can call this method from {@link #configureMessageConverters(List)}. * @param messageConverters the list to add the default message converters to */ protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) { StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); stringConverter.setWriteAcceptCharset(false); messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(stringConverter); messageConverters.add(new ResourceHttpMessageConverter()); messageConverters.add(new SourceHttpMessageConverter<Source>()); messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (romePresent) { messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } if (jackson2XmlPresent) { messageConverters.add(new MappingJackson2XmlHttpMessageConverter()); } else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { messageConverters.add(new MappingJackson2HttpMessageConverter()); } else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } }
private static boolean romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", WebMvcConfigurationSupport.class.getClassLoader()); private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", WebMvcConfigurationSupport.class.getClassLoader()); private static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", WebMvcConfigurationSupport.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", WebMvcConfigurationSupport.class.getClassLoader()); private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", WebMvcConfigurationSupport.class.getClassLoader()); private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", WebMvcConfigurationSupport.class.getClassLoader());
if (jackson2XmlPresent) { messageConverters.add(new MappingJackson2XmlHttpMessageConverter()); }
/* Implementation of HttpMessageConverter that can read and write XML using Jackson 2.x extension component for reading and writing XML encoded data . By default, this converter supports application/xml, text/xml, and application/*+xml. This can be overridden by setting the supportedMediaTypes property. The default constructor uses the default configuration provided by Jackson2ObjectMapperBuilder. Compatible with Jackson 2.1 and higher. */ public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2HttpMessageConverter { /** * Construct a new {@code MappingJackson2XmlHttpMessageConverter} using default configuration * provided by {@code Jackson2ObjectMapperBuilder}. */ public MappingJackson2XmlHttpMessageConverter() { this(Jackson2ObjectMapperBuilder.xml().build()); } /** * Construct a new {@code MappingJackson2XmlHttpMessageConverter} with a custom {@link ObjectMapper} * (must be a {@link XmlMapper} instance). * You can use {@link Jackson2ObjectMapperBuilder} to build it easily. * @see Jackson2ObjectMapperBuilder#xml() */ public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) { super(objectMapper, new MediaType("application", "xml", DEFAULT_CHARSET), new MediaType("text", "xml", DEFAULT_CHARSET), new MediaType("application", "*+xml", DEFAULT_CHARSET)); Assert.isAssignable(XmlMapper.class, objectMapper.getClass()); } /** * {@inheritDoc} * The {@code objectMapper} parameter must be a {@link XmlMapper} instance. */ @Override public void setObjectMapper(ObjectMapper objectMapper) { Assert.isAssignable(XmlMapper.class, objectMapper.getClass()); super.setObjectMapper(objectMapper); } }
这时”application/json;”是排在第一个,且 isConcrete()返回true,所以最后返回的就会是json格式,而不再是xml格式了。
从上面的分析种,我们就能猜到以前SpringMVC项目的接口总是默认返回JSON格式的原因:肯定是因为classpath中有“com.fasterxml.jackson.databind.ObjectMapper (属于jackson-databind包)”和“com.fasterxml.jackson.core.JsonGenerator (属于jackson-core包)”这两个接口的实现类。我们查看公司项目的pom文件,确实发现了引用了jackson的两个包:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.6.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.6.2</version> </dependency>
解决办法
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" /> <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean"> <property name="defaultContentType" value="application/json" /> </bean>
@Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.defaultContentType(MediaType.APPLICATION_JSON_UTF8); } }
- https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.html
- https://www.baeldung.com/spring-httpmessageconverter-rest
- https://www.baeldung.com/spring-mvc-content-negotiation-json-xml