distinct返回null报错_突发!接口返回的JSON突然变成了字符串!

d40c70c1c9cf4ea761b855d10b5ddc3e.png

背景

在后端定义接口的时候,后端接口返回一般都是Content-Type=application/json格式的json串。

最近碰到一个问题,负责的一个系统在升级组里的一个公共依赖包common的版本之后,所有的接口返回的Content-Type都由application/json变成了text/html,导致接收端调用解析报错。

把版本修改回去则接口返回的Content-Type即又恢复正常。

排查

这个问题的排查思路其实很简单,首先查看common包做了什么变动。

common包中只增加了一个配置类:

@Configuration
public class JacksonConfig {

    @Bean
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper objectMapper() {
        return JacksonUtils.objectMapper().propsIgnorable().build();
    }

    @Bean
    @ConditionalOnMissingBean(JsonViewSupportFactoryBean.class)
    public JsonViewSupportFactoryBean views() {
        return new JsonViewSupportFactoryBean(objectMapper());
    }

}

初看过去觉得没啥问题,不知道为什么增加这么一个配置类就会影响接口返回的格式。

那么就换一个思路,来看看SpringBoot框架是怎么判断接口最终应该返回什么格式的Content-Type

通过对SpringBoot框架源码调试,最终发现SpringBoot框架是在AbstractMessageConverterMethodProcessor类中的writeWithMessageConverters()方法中实现判断返回格式的。

(如果不知道怎么找到这个地方,可以在Controller接口的最后一句加断点,等程序执行到断点处在F7快捷键Step Into,进入到框架源码内部调试)

@SuppressWarnings({"rawtypes", "unchecked"})
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
  ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
  throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

 Object body;
 Class<?> valueType;
 Type targetType;

 // ... 部分省略代码

 MediaType selectedMediaType = null;
 MediaType contentType = outputMessage.getHeaders().getContentType();
 boolean isContentTypePreset = contentType != null && contentType.isConcrete();
 if (isContentTypePreset) {
  if (logger.isDebugEnabled()) {
   logger.debug("Found 'Content-Type:" + contentType + "' in response");
  }
  selectedMediaType = contentType;
 }
 else {
  HttpServletRequest request = inputMessage.getServletRequest();
  // 获取调用方能接受什么类型的MediaType
  List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
  // 获取服务提供方能产生哪些类型的MediaType
  List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);

  if (body != null && producibleTypes.isEmpty()) {
   throw new HttpMessageNotWritableException(
     "No converter found for return value of type: " + valueType);
  }
  // 综合请求方和服务提供方的MediaType情况,计算最终能够返回哪些MediaType
  List<MediaType> mediaTypesToUse = new ArrayList<>();
  for (MediaType requestedType : acceptableTypes) {
   for (MediaType producibleType : producibleTypes) {
    if (requestedType.isCompatibleWith(producibleType)) {
     mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
    }
   }
  }
  if (mediaTypesToUse.isEmpty()) {
   if (body != null) {
    throw new HttpMediaTypeNotAcceptableException(producibleTypes);
   }
   if (logger.isDebugEnabled()) {
    logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
   }
   return;
  }

  // 对所有最终可返回的MediaType进行排序
  MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

  // 计算最终选择返回哪个MediaType,按照先后顺序,只要有一个符合条件,则直接返回,忽略剩余其他的可满足条件的MediaType
  for (MediaType mediaType : mediaTypesToUse) {
   if (mediaType.isConcrete()) {
    selectedMediaType = mediaType;
    break;
   }
   else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
    selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
    break;
   }
  }

  if (logger.isDebugEnabled()) {
   logger.debug("Using '" + selectedMediaType + "', given " +
     acceptableTypes + " and supported " + producibleTypes);
  }
 }

 // ...其他省略代码,包括最终的结果的返回

}

writeWithMessageConverters()方法主要作用就是把接口返回的结果经过合适的Converter处理之后再返回。

这就要求首先判断应该返回什么类型的MediaType

writeWithMessageConverters()方法判断使用什么类型的MediaType逻辑如下:

首先调用getAcceptableMediaTypes(request)判断接收方能接受哪些类型的MediaType

如果没有设置的话,则按照默认的来。默认的MediaTypeMEDIA_TYPE_ALL_LIST

List<MediaType> MEDIA_TYPE_ALL_LIST = Collections.singletonList(MediaType.ALL);

/**
 * Public constant media type that includes all media ranges (i.e. "&#42;/&#42;").
 */
public static final MediaType ALL;

/**
 * A String equivalent of {@link MediaType#ALL}.
 */
public static final String ALL_VALUE = "*/*";

接着调用getProducibleMediaTypes()方法来计算当前接口能产生哪些类型的MediaType

/**
 * Returns the media types that can be produced. The resulting media types are:
 * <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>
 * @since 4.2
 */
@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(
  HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {

 // 如果request中已经指定了MediaType,则直接使用指定的
 Set<MediaType> mediaTypes =
   (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
 if (!CollectionUtils.isEmpty(mediaTypes)) {
  return new ArrayList<>(mediaTypes);
 }
 else if (!this.allSupportedMediaTypes.isEmpty()) {
  List<MediaType> result = new ArrayList<>();
  // 依次遍历当前系统中所有的HttpMessageConverte列表,只要能够支持写入指定的targetType,即认为可生成converter支持的MediaType
  for (HttpMessageConverter<?> converter : this.messageConverters) {
   if (converter instanceof GenericHttpMessageConverter && targetType != null) {
    if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
     result.addAll(converter.getSupportedMediaTypes());
    }
   }
   else if (converter.canWrite(valueClass, null)) {
    result.addAll(converter.getSupportedMediaTypes());
   }
  }
  return result;
 }
 else {
  return Collections.singletonList(MediaType.ALL);
 }
}

getProducibleMediaTypes()方法首先判断request中有没有指定特定的MediaType,如果有的话则直接使用指定的,如果没有的话,则依次遍历当前系统中所有的HttpMessageConverter,只要对应ConvertercanWrite()方法返回true,则把对应Converter所支持的所有的MediaType加入返回列表中。

当前系统中支的所有HttpMessageConverter列表如下:

f4bea53a788f2a51d5cf2353894800f9.png

执行完成发现第一个canWrite()返回true的ConverterBczRequestConfig$HtmlJsonMessageConverter

执行完成之后的 result的结果如下图所示:

0040315d8f3d88fed24cd148dbfdad8e.png

在计算得到所有可以生成的MediaType之后,又会依次判断这些可以生成的MediaType是否兼容acceptableTypes,由于本次请求中acceptableTypes为默认值,则默认兼容。

之后会把上一步中得到的所有的MediaType按照各自的qualityValue(每个MediaType都会有一个值)进行从小到大排序。

本系统中没有做任何特殊的设置,默认值都是1,所有MediaType顺序保持不变。

做完上述操作之后,从上一步中处理之后的所有MediaType中选择第一个确定的MediaType(所谓的确定的MediaType是指该MediaType对应的typesubtype都是具体的,不存在通配符的)作为该次请求应该返回的MediaType

for (MediaType mediaType : mediaTypesToUse) {
 if (mediaType.isConcrete()) {
  selectedMediaType = mediaType;
  break;
 }
 else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
  selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
  break;
 }
}

位于列表中第一个的MediaTypetext/html,符合条件,所以text/html即被认为只接口请求最终返回的MediaType

现在接口返回的Content-Type格式变成了text/html的原因已经找到了,那么为什么升级common包引入JacksonConfig配置类之后就会变成这样呢?

来看看升级版本之前的MediaType:

26769e7647b3404eb2ba3b82dd92e68d.png

可以看到引入JacksonConfig之前,HttpMessageConverter列表中排在BczRequestConfig$HtmlJsonMessageConverter前面有两个MappingJackson2HttpMessageConverter,计算出的可以生成的MediaType集合中排在第一个的是application/json,所以接口返回的是Content-Type为JSONapplication/json

现在重新来看看JacksonConfig:

@Bean
@ConditionalOnMissingBean(JsonViewSupportFactoryBean.class)
public JsonViewSupportFactoryBean views() {
    return new JsonViewSupportFactoryBean(objectMapper());      // 在全局objectMapper的基础上通过convert修改
}

JacksonConfig主要就是生成一个工厂BeanJsonViewSupportFactoryBean对象,进入到JsonViewSupportFactoryBean类源码中,发现JsonViewSupportFactoryBean实现了InitializingBean接口,

@Override
public void afterPropertiesSet() throws Exception {
  List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(adapter.getReturnValueHandlers());

 // 把当前系统中已配置的JacksonConverter全部删掉,然后再把新生成的JsonViewMessageConverter加入到转换器列表的末尾
  List<HttpMessageConverter<?>> converters = removeJacksonConverters(adapter.getMessageConverters());
  converters.add(converter);
  adapter.setMessageConverters(converters);

  decorateHandlers(handlers);
  adapter.setReturnValueHandlers(handlers);
}

removeJacksonConverters()方法实现如下:

protected List<HttpMessageConverter<?>> removeJacksonConverters(List<HttpMessageConverter<?>> converters) {
  List<HttpMessageConverter<?>> copy = new ArrayList<>(converters);
  Iterator<HttpMessageConverter<?>> iter = copy.iterator();
  while(iter.hasNext()) {
    HttpMessageConverter<?> next = iter.next();
  // 把所有类名义MappingJackson2开始的转换前全部删掉
    if (next.getClass().getSimpleName().startsWith("MappingJackson2")) {
      log.debug("Removing {} as it interferes with us", next.getClass().getName());
      iter.remove();
    }
  }
  return copy;
}

removeJacksonConverters()方法会把当前所有类名以MappingJackson2Converter全部删掉。

执行删除前后的转换器列表如下:

ad5d6e315cbe3a126909a45cfb255cf5.png

综上:

升级common版本引入JacksonConfig类之后,JacksonConfig中引入了JsonViewSupportFactoryBean工厂对象,JsonViewSupportFactoryBean对象的afterPropertiesSet()方法中会把当前系统中所有类名以MappingJackson2开头的Converter删掉,然后在列表最末尾增加JsonViewMessageConverter

SpringBoot框架在计算接口返回MediaType的时候会选择第一个符合要求的MediaType。由于JsonViewSupportFactoryBean打乱了HttpMessageConverter列表的顺序,导致text/html格式的MediaType排在application/json的前面,所以最终接口返回变成了text/html

解决

不通过JsonViewSupportFactoryBean引入JsonViewMessageConverter,改成手动生成,且JsonViewMessageConverter排在列表最前面即可。

@Override
 public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
   converters.add(0, new JsonViewMessageConverter(mObjectMapper));
   converters.add(new HtmlJsonMessageConverter(mObjectMapper));
 }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
优化这条sql: select distinct (select product_name from t_product from where id = #{productId} and mark = 1 and status = 1) as productName, (select count(0) from t_clue a where a.distribution_status != 4 and a.mark = 1 and a.product_id = #{productId} and a.status in(1,2,3,31,32,33)) as clueCount, (select count(0) from t_clue a left join t_clue_appendix b on a.clue_code = b.clue_code where a.distribution_status != 4 and a.mark = 1 and b.file_url is not null and a.product_id = #{productId} and a.status in (3,31,32,33)) as intentionCount, (select count(0) from t_clue a where a.status in (4,5,7,8) and a.distribution_status != 4 and a.mark = 1 and a.product_id = #{productId} and a.status = 4) as incomingCount, (select count(0) from t_clue a where a.status in (5,7,8) and a.distribution_status != 4 and a.mark = 1 and a.product_id = #{productId} and a.status in (5,7,8)) as approvedCount, (select count(0) from t_clue a where a.status = 6 and a.distribution_status != 4 and a.mark = 1 and a.product_id = #{productId} and a.status = 6) as rejectionCount, (select count(0) from t_clue a where a.status in (7,8) and a.distribution_status != 4 and a.mark = 1 and a.product_id = #{productId}) as loanCount, (select count(0) from t_clue a where a.status = 8 and a.distribution_status != 4 and a.mark = 1 and a.product_id = #{productId}) as swipeCount, (select sum(a.loan_amount) from t_clue a where a.distribution_status != 4 and a.mark = 1 and a.product_id = #{productId}) as loanMoney, (select sum(a.use_amount) from t_clue a where a.distribution_status != 4 and a.mark = 1 and a.product_id = #{productId}) as swipeMoney
最新发布
06-06

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值