Could not extract response: no suitable HttpMessageConverter

news2024/9/23 15:27:07

版本:spring-cloud-openfeign-core-2.1.1.RELEASE.jar,spring-webmvc-5.1.14.RELEASE.jar,jetty-server-9.4.41.v20210516.jar,tomcat-embed-core-9.0.48.jar

问题背景

生产服务请求下游服务时偶发抛出下面的异常,下游服务已经很久没有人发布并且没有修改任何配置,而且是偶发,这个问题很奇怪,服务使用的Spring cloud openfeign,由于不熟悉Spring cloud与openfeign,先梳理学习Spring cloud openfeign bean的初始化与定义

feign.codec.DecodeException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.....master.models.APIResponse] and content type [application/xhtml+xml;charset=UTF-8]
    at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:180)
    at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:140)
    at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78)
    at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)
    at com.sun.proxy.$Proxy401.sendEmail(Unknown Source)
    at ......
    Caused by: org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.......APIResponse] and content type [application/xhtml+xml;charset=UTF-8]
    at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:121)
    at org.springframework.cloud.openfeign.support.SpringDecoder.decode(SpringDecoder.java:59)
    at org.springframework.cloud.openfeign.support.ResponseEntityDecoder.decode(ResponseEntityDecoder.java:62)
    at feign.optionals.OptionalDecoder.decode(OptionalDecoder.java:36)
    at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:176)
    ... 106 more

Spring Cloud openfeign

初始化过程

应用启动SpringBootApplication,引入自动配置sdk,自动配置:org.springframework.cloud.openfeign.FeignAutoConfiguration

FeignAutoConfiguration

  1. 构建feignContext:org.springframework.cloud.openfeign.FeignContext,FeignContext是Spring应用上下文与feignClient配置org.springframework.cloud.openfeign.FeignClientSpecification的组合
  2. 按需(@ConditionalOnClass(name = “feign.hystrix.HystrixFeign”))构建feignTargeter:HystrixTargeter,默认(@ConditionalOnMissingClass(“feign.hystrix.HystrixFeign”)):DefaultTargeter
  3. 按需构建HttpClientFeignConfiguration,条件见:下方代码
  4. 按需构建OkHttpFeignConfiguration,条件见:下方代码
	@ConditionalOnClass(ApacheHttpClient.class)
	@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
	@ConditionalOnMissingBean(CloseableHttpClient.class)
	@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
	protected static class HttpClientFeignConfiguration {
        ...
    }
	@Configuration
	@ConditionalOnClass(OkHttpClient.class)
	@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
	@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
	@ConditionalOnProperty("feign.okhttp.enabled")
	protected static class OkHttpFeignConfiguration {
        ...
    }

查看注解:@EnableFeignClients(basePackages = {“…”}),客户端注入过程切入点,该注解会通过Spring Import注解导入bean定义,注解指定导入bean定义类:org.springframework.cloud.openfeign.FeignClientsRegistrar

注册BeanDefinitions流程

  1. registerDefaultConfiguration
  2. registerFeignClients

registerDefaultConfiguration

  1. 获取EnableFeignClients注解默认属性配置
  2. 如果默认配置包含(defaultConfiguration),则将默认配置注册为bean(FeignClientSpecification)。beanName:default…notification.Application

registerFeignClients

  1. 使用ClassPathScanningCandidateComponentProvider扫描器按照EnableFeignClients配置的basePackages扫描当前资源resourceLoader,扫描过滤器AnnotationTypeFilter筛选出FeignClient注解的AnnotatedBeanDefinition
  2. 根据FeignClient注解的属性(configuration)定义构建FeignClientSpecification的BeanDefinition,注册客户端配置方法代码如下
	private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
			Object configuration) {
		BeanDefinitionBuilder builder = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientSpecification.class);
		builder.addConstructorArgValue(name);
		builder.addConstructorArgValue(configuration);
		registry.registerBeanDefinition(
				name + "." + FeignClientSpecification.class.getSimpleName(),
				builder.getBeanDefinition());
	}

registerFeignClient

  1. 构建客户端bean:FeignClientFactoryBean
  2. 将bean封装为:BeanDefinitionHolder注册至工厂
    1. bean名称为className
    2. bean别名读取注解属性(qualifier),如果qualifier不存在则兜底使用contextId+FeignClient

代码如下

	private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		validate(attributes);
		definition.addPropertyValue("url", getUrl(attributes));
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		definition.addPropertyValue("name", name);
		String contextId = getContextId(attributes);
		definition.addPropertyValue("contextId", contextId);
		definition.addPropertyValue("type", className);
		definition.addPropertyValue("decode404", attributes.get("decode404"));
		definition.addPropertyValue("fallback", attributes.get("fallback"));
		definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		String alias = contextId + "FeignClient";
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
		boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
																// null
		beanDefinition.setPrimary(primary);
		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

FeignClientFactoryBean

  1. 应用上下文中获取FeignContext
  2. 创建Feign.Builder:org.springframework.cloud.openfeign.FeignClientFactoryBean#feign
    1. 从FeignContext上下文获取构造者feign.Feign.Builder
    2. 为构建者设置encoder,从FeignContext上下文获取feign.codec.Encoder:org.springframework.cloud.openfeign.support.PageableSpringEncoder->SpringEncoder->SpringFormEncoder->feign.codec.Encoder.Default
    3. 为构建者设置decoder,从FeignContext上下文获取feign.codec.Decoder:feign.optionals.OptionalDecoder->org.springframework.cloud.openfeign.support.ResponseEntityDecoder->SpringDecoder
    4. 为构建者设置contract,从FeignContext上下文获取feign.Contract:使用feign.hystrix.HystrixDelegatingContract#HystrixDelegatingContract代理包装自定义实现OpenFeignSpringMvcContract(继承自feign.OpenFeignBaseContract)
  3. 如果不存在url配置:org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance
  4. 否则
    1. 获取Client:org.springframework.cloud.openfeign.FeignClientFactoryBean#getOptional。如果client不为空并且是LoadBalancerFeignClient类型则获取其代理的实际Client
    2. 获取Targeter:org.springframework.cloud.openfeign.FeignClientFactoryBean#get
    3. 构建目标bean:org.springframework.cloud.openfeign.Targeter#target(feign.Target.HardCodedTarget)

当前案例默认无url,即步骤3:org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance

  1. 获取Client:org.springframework.cloud.openfeign.FeignClientFactoryBean#getOptional。常用实现:ApacheHttpClient,OkHttpClient,feign.Client.Default。当前案例使用自定义封装ReWriteHeaderFeignClient客户端
  2. 获取Targeter:org.springframework.cloud.openfeign.FeignClientFactoryBean#get
  3. 构建目标bean:org.springframework.cloud.openfeign.Targeter#target(feign.Target.HardCodedTarget)

构建目标bean

构建Feign:feign.hystrix.HystrixFeign.Builder#build(feign.hystrix.FallbackFactory<?>)

  1. 实现InvocationHandler:feign.hystrix.HystrixInvocationHandler
  2. 包装contract:feign.hystrix.HystrixDelegatingContract#HystrixDelegatingContract
  3. 调用父类build方法构建Feign:feign.Feign.Builder#build
public Feign build() {
  SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
      new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
          logLevel, decode404, closeAfterDecode, propagationPolicy);
  ParseHandlersByName handlersByName =
      new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
          errorDecoder, synchronousMethodHandlerFactory);
  return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}

SynchronousMethodHandler.Factory工厂相关属性

  1. client:常用实现:ApacheHttpClient,OkHttpClient,feign.Client.Default。当前案例使用自定义封装ReWriteHeaderFeignClient客户端
  2. retryer:feign.Retryer.Default#Default()
  3. requestInterceptors
    1. 自定义:OpenFeignRequestInterceptor
    2. 自定义:GrayPatternMatcherRequestInterceptor
    3. 自定义:GzipFeignAcceptGzipEncodingInterceptor继承FeignAcceptGzipEncodingInterceptor,如果配置feign.compression.response.enabled=true,则启用该拦截器,追加header Accept-Encoding -> gzip
    4. 自定义:FeignEnvFlagInterceptor
  4. decode404:false
  5. closeAfterDecode:true
  6. propagationPolicy:feign.Feign.Builder#propagationPolicy(NONE)

ParseHandlersByName相关属性

  1. options:feign.Request.Options#Options(),connectTimeoutMillis默认10s,readTimeoutMillis默认60s
  2. encoder,decoder,contract:同Feign.Builder
  3. queryMapEncoder:feign.QueryMapEncoder.Default
  4. errorDecoder:自定义实现new ErrorDecoder()

ReflectiveFeign相关属性

  1. invocationHandlerFactory:feign.InvocationHandlerFactory.Default
  2. queryMapEncoder:同上
  3. handlersByName:ParseHandlersByName

创建目标实例
feign.ReflectiveFeign#newInstance

  1. 获取nameToHandler映射:feign.ReflectiveFeign.ParseHandlersByName#apply
    1. 解析与校验元数据:feign.Contract#parseAndValidatateMetadata,当前案例:自定义实现重写父类方法feign.OpenFeignBaseContract#parseAndValidateMetadata
    2. processAnnotationOnClass
    3. processAnnotationOnMethod
    4. processAnnotationsOnParameter
    5. checkState
    6. checkMapString:HeaderMap
    7. checkMapKeys:QueryMap
    8. 回调handler:feign.FeignClientMethodMetadataParseHandler#parsed
  2. 根据default与nameToHandler获取methodToHandler映射
  3. 创建InvocationHandler:feign.InvocationHandlerFactory.Default#create
  4. 创建目标实例代理:java.lang.reflect.Proxy#newProxyInstance(java.lang.ClassLoader, java.lang.Class<?>[], java.lang.reflect.InvocationHandler)
  5. 如果是default方法:feign.Util#isDefault,绑定feign.DefaultMethodHandler至动态代理
  6. 返回代理对象

isDefault默认方法定义

	// Default methods are public non-abstract, non-synthetic, and non-static instance methods
    // declared in an interface.
    // method.isDefault() is not sufficient for our usage as it does not check
    // for synthetic methods. As a result, it picks up overridden methods as well as actual default
    // methods.

动态代理InvocationHandler

  static final class Default implements InvocationHandlerFactory {

    @Override
    public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
      return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
    }
  }

请求过程

关键对象属性

FeignInvocationHandler

  1. target:HardCodedTarget
    1. type:目标对象class,例如:MyTestAPI
    2. name:my-feign-client
    3. url:http://my-feign-client
  2. dispatch:key代理对象方法,例如:MyTestAPI.helloWorld(),value:feign.SynchronousMethodHandler

SynchronousMethodHandler

  1. 与SynchronousMethodHandler.Factory相同部分不再重复,搜索上方关键字:SynchronousMethodHandler.Factory
  2. target:HardCodedTarget
  3. decoder,options,errorDecoder:同ParseHandlersByName.decoder
  4. metadata:feign.MethodMetadata
  5. buildTemplateFromArgs:feign.ReflectiveFeign.BuildEncodedTemplateFromArgs#BuildEncodedTemplateFromArgs/BuildTemplateByResolvingArgs/BuildFormEncodedTemplateFromArgs

请求过程

  1. 动态代理调用:feign.ReflectiveFeign.FeignInvocationHandler#invoke
  2. 调用代理方法:feign.SynchronousMethodHandler#invoke
  3. 创建RequestTemplate:feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create
    1. 解析参数构建EncodedTemplate对象:feign.ReflectiveFeign.BuildEncodedTemplateFromArgs#resolve
    2. encode request body:org.springframework.cloud.openfeign.support.PageableSpringEncoder#encode->org.springframework.cloud.openfeign.support.SpringEncoder#encode
    3. org.springframework.http.converter.json.MappingJackson2HttpMessageConverter->org.springframework.http.converter.AbstractHttpMessageConverter#write将request body写入org.springframework.cloud.openfeign.support.SpringEncoder.FeignOutputMessage
    4. 如果请求没有Content-Type header默认值设置为:application/json;charset=UTF-8
    5. 写入encoded body,并追加Content-Length header
  4. 克隆Retryer:feign.Retryer.Default#clone
  5. 发起请求并解析响应结果:feign.SynchronousMethodHandler#executeAndDecode
  6. 构建请求:feign.SynchronousMethodHandler#targetRequest
    1. 回调拦截器处理请求:feign.RequestInterceptor
    2. 创建request请求:feign.Target.HardCodedTarget#apply-》feign.RequestTemplate#request
  7. 客户端执行请求:feign.SynchronousMethodHandler#client#execute(request, options):org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#execute
  8. 负载均衡,重构Server请求URI:com.netflix.client.AbstractLoadBalancerAwareClient#executeWithLoadBalancer(S, com.netflix.client.config.IClientConfig)-》com.netflix.loadbalancer.LoadBalancerContext#reconstructURIWithServer
  9. 完成请求返回响应结果feign.Response
  10. decode response
  11. org.springframework.web.client.HttpMessageConverterExtractor.extractData
  12. 如果转换器类型为GenericHttpMessageConverter,根据responseType+contentType获取转换器GenericHttpMessageConverter
  13. 否则根据responseClass+contentType获取转换器
  14. HttpMessageConverter将response转换为目标方法返回值类型

问题分析

客户端

ReWriteHeaderFeignClient自定义封装调用链路

  1. feign.SynchronousMethodHandler#invoke->executeAndDecode->自定义实现ReWriteHeaderFeignClient#execute ->org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#execute->org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#lbClient->自定义实现EncodeHeaderCachingSpringLoadBalancerFactory#create->FeignLoadBalancer(如果存在重试工厂loadBalancedRetryFactory则使用RetryableFeignLoadBalancer)->自定义实现EncodeHeaderCachingSpringLoadBalancerFactory.EncodeFeignLoadBalancer#executeWithLoadBalancer->LoadBalancerCommand.submit->异步调用org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute-》org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer.RibbonRequest.client()->自定义实现ResetTimeoutFeignClient#execute-》自定义实现JettyHttpClient#execute-〉org.eclipse.jetty.client.HttpRequest#send(org.eclipse.jetty.client.api.Response.CompleteListener)-》请求完成回调org.eclipse.jetty.client.util.FutureResponseListener#onComplete-》将org.eclipse.jetty.client.HttpResponse封装为org.eclipse.jetty.client.HttpContentResponse#HttpContentResponse->自定义实现JettyHttpClient#toFeignResponse(代码见下方)-》org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute将Response封装为RibbonResponse->feign.SynchronousMethodHandler#decode
return Response.builder().status(status).reason(reason).headers(headers).request(feignRequest).body(body).build();

查看所有自定义实现的类没有重写Content-Type header的类,故而排除

服务端

处理流程

  1. 接收派发请求:org.springframework.web.servlet.DispatcherServlet#doDispatch
  2. 根据请求查找处理句柄:org.springframework.web.servlet.DispatcherServlet#getHandler
  3. 查找Handler适配器(即目标方法适配器):org.springframework.web.servlet.DispatcherServlet#getHandlerAdapter
  4. 预处理句柄回调:org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle-》org.springframework.web.servlet.HandlerInterceptor#preHandle
  5. 实际执行处理句柄:org.springframework.web.servlet.HandlerAdapter#handle
  6. 回处理完成后置调拦截器:org.springframework.web.servlet.HandlerInterceptor#postHandle
  7. 处理派发请求处理结果:org.springframework.web.servlet.DispatcherServlet#processDispatchResult
    1. 如果存在异常则处理:org.springframework.web.servlet.DispatcherServlet#processHandlerException
    2. 如果存在ModelAndView,则渲染mv:org.springframework.web.servlet.DispatcherServlet#render
    3. 如果是并发异步处理则返回
    4. 否则回调拦截器:org.springframework.web.servlet.HandlerInterceptor#afterCompletion
  8. 如果是并发异步处理则回调拦截器:org.springframework.web.servlet.AsyncHandlerInterceptor#afterConcurrentHandlingStarted
  9. 如果是multipart请求清理multipart:org.springframework.web.servlet.DispatcherServlet#cleanupMultipart
  10. 发布事件:org.springframework.web.servlet.FrameworkServlet#publishRequestHandledEvent
  11. javax.servlet.FilterChain

实际执行处理句柄

org.springframework.web.servlet.DispatcherServlet#doDispatch->org.springframework.web.servlet.HandlerAdapter#handle

  1. org.springframework.web.servlet.HandlerAdapter#handle,当前案例实现类:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter-》org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
  2. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal
  3. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
    1. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDataBinderFactory
    2. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getModelFactory
    3. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#createInvocableHandlerMethod
    4. 设置argumentResolvers(见图1)
    5. 设置returnValueHandlers(见图2)
    6. 设置parameterNameDiscoverer:org.springframework.core.DefaultParameterNameDiscoverer
    7. 设置ignoreDefaultModelOnRedirect,默认true
    8. 设置asyncRequestTimeout,默认null
    9. 设置TaskExecutor:new SimpleAsyncTaskExecutor(“MvcAsync”)
    10. 注册callableInterceptors,默认空数组:CallableProcessingInterceptor
    11. 注册deferredResultInterceptors,默认空数组:DeferredResultProcessingInterceptor
    12. 执行目标方法:org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
    13. 反射调用目标方法,得到返回值
    14. 如果返回值为空或者responseStatusReason不为空设置setRequestHandled后返回
    15. 选择支持返回值类型的处理句柄:org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite#selectHandler,当前案例:org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
    16. 回调返回值拦截器:org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite#handleReturnValue,当前案例:org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#handleReturnValue
    17. 创建输入输出消息对象:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#createInputMessage,org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#createOutputMessage
    18. 写入数据:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)
    19. 如果是isResourceType,写入header:Accept-Ranges,存在异常则写入header:Content-Range,当前案例:否
    20. 如果outputMessage存在Content-Type则使用,当前案例:不存在
    21. 否则获取request请求的Accept类型org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#getAcceptableMediaTypes
    22. 获取请求producible类型(对应注解produces属性:@PostMapping(value = “/hello”, produces = “application/json;charset=utf8”))rg.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#getProducibleMediaTypes(javax.servlet.http.HttpServletRequest, java.lang.Class<?>, java.lang.reflect.Type):当前案例请求中org.springframework.web.servlet.HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE参数不存在,走兜底:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#allSupportedMediaTypes
    23. 根据请求方acceptableTypes获取可兼容的producibleTypes作为可使用的Content-Type MediaType列表:mediaTypesToUse,当前案例:Content-Type列表见:注释1
    24. 排序可兼容的mediaTypes:org.springframework.http.MediaType#sortBySpecificityAndQuality,排序结果见:注释2
    25. 选择第一个具体的MediaType:org.springframework.util.MimeType#isConcrete
    26. 获取匹配的转换器:org.springframework.http.converter.HttpMessageConverter,当前案例:org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
    27. 写入body前置通知回调:org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice#beforeBodyWrite
    28. 如果存在则写入Response header:Content-Disposition
    29. 写入body数据:MappingJackson2HttpMessageConverter-》org.springframework.http.converter.AbstractGenericHttpMessageConverter#write
    30. 添加默认header:org.springframework.http.converter.AbstractHttpMessageConverter#addDefaultHeaders,Content-Type,Content-Length(当前案例均为null不写入:org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#getContentLength)
    31. 向outputMessage写入body数据:org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#writeInternal
    32. 构建ModelAndView:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getModelAndView
  4. 请求完成后置处理:org.springframework.web.context.request.AbstractRequestAttributes#requestCompleted
  5. 如果Response不包含header(Cache-Control),并且org.springframework.web.method.annotation.SessionAttributesHandler#hasSessionAttributes,处理cache:org.springframework.web.servlet.support.WebContentGenerator#applyCacheSeconds(javax.servlet.http.HttpServletResponse, int)
  6. 否则准备Response:org.springframework.web.servlet.support.WebContentGenerator#prepareResponse
    1. cacheControl不为空,则设置header(Cache-Control),或header(Pragma),或header(Expires)
    2. 否则设置header(Cache-Control),或header(Pragma),或header(Expires)为指定值,例如:no-cache,no-store,1L
    3. 如果varyByRequestHeaders不为空,设置header(Vary)

问题原因

响应体如果没有指定Content-Type,那么就会从兼容请求体Accept的Content-Type中选择一个进行响应,也就是说请求方使用了非json格式的Accept引起,为了验证问题,我们查看线上pinpoint(开源的链路监控平台)监控,出现问题的请求的Accept header如下,与报错原因也就对应上了

accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

解决方法

  1. 请求方使用适配的Accept类型,例如:application/json;charset=utf8或*/*
  2. 接口PostMapping/RequestMapping等注解增加produces配置:org.springframework.web.bind.annotation.PostMapping#produces
  3. 实现接口org.springframework.http.converter.HttpMessageConverter#write写入OutMessage Content-Type,例如:open-feign在编译时写入header:org.springframework.cloud.openfeign.support.SpringEncoder#encode-》write,服务端是在将return type写入outputMessage时处理(Writes the given return type to the given output message):org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)
  4. 为OutputMessage设置适配的Content-Type header org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#createOutputMessage
    1. 增加自定义ServletHandler,例如:xxl-job中的方法,代码见附件3
    2. 实现预处理handle拦截器处理:org.springframework.web.servlet.HandlerInterceptor#preHandle

注意:方法2-4要根据实际业务场景是否可以使用,客户端期望xhtml,但是实际响应json,是否有潜在的风险?
Q&A

  1. 接口PostMapping/RequestMapping等注解增加headers配置是否可以?org.springframework.web.bind.annotation.PostMapping#headers
    1. 不可以,因为该header是添加再Request请求中,需要添加在Response中的header才可以

注释

注释1

0 = {MediaType@22077} "application/octet-stream"
1 = {MediaType@22078} "text/plain"
2 = {MediaType@22079} "application/xml"
3 = {MediaType@22080} "text/xml"
4 = {MediaType@22081} "application/x-www-form-urlencoded"
5 = {MediaType@22082} "application/cbor"
6 = {MediaType@22083} "application/*+xml"
7 = {MediaType@22084} "multipart/form-data"
8 = {MediaType@22085} "application/json"
9 = {MediaType@22086} "application/*+json"
10 = {MediaType@22057} "*/*"

注释2

0 = {MediaType@22085} "application/json"
1 = {MediaType@22085} "application/json"
2 = {MediaType@22082} "application/cbor"
3 = {MediaType@22293} "application/xml"
4 = {MediaType@22296} "application/xml"
5 = {MediaType@22086} "application/*+json"
6 = {MediaType@22292} "application/*+json"
7 = {MediaType@22294} "text/xml"
8 = {MediaType@22295} "application/*+xml"
9 = {MediaType@22297} "text/xml"
10 = {MediaType@22298} "application/*+xml"

附件

图1
argumentResolvers.png
图2
returnValueHandlers.png
附件3
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/372879.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

git入门

目录 1. git简介 1.1 git是什么 1.2 git与svn的区别 2. github 2.1 创建仓库 2.2 删除仓库 2.3 新建文件及文件夹 3. git的基本操作 3.1 配置账户及邮箱 3.2 git文件状态与工作区域 3.3 常用命令 3.4 克隆&#xff08;clone&#xff09; 3.5 查看git仓库的状态 3.…

[音视频] BMP 图片格式分析

BMP 格式是什么 BMP&#xff08;Bitmap&#xff09;是一种常见的无损位图图像文件格式&#xff0c;是Windows操作系统中最早使用的图像格式之一&#xff0c;也是目前很多应用程序所使用的标准图像格式之一。 整体结构图&#xff0c;如下图所示 格式 BMP文件格式有多个版本&a…

QT中级(5)多线程读取一个文件,并在另一个文件夹中合成这个文件(1)

1 先实现一个简单程序 1.1 功能 用户可以输入一个源文件的路径和目标路径点击开始&#xff0c;程序启动读取和合成合成进度可见、合成步骤可见 1.2 思路 一个线程顺序读取文件&#xff0c;达到设定的缓存块就发给另一个合成线程&#xff0c;主线程用来进行数据传递、显示进…

电子技术——A类输出阶

电子技术——A类输出阶 因为射极跟随器具有较低的输出阻抗&#xff0c;射极跟随器是A类输出阶的典型代表。我们之前已经学习过射极跟随器的小信号模型&#xff0c;本节我们讨论其大信号模型。 传输特性 下图展示了一个射极跟随器的原理图&#xff1a; 其中 Q1Q_1Q1​ 为射极…

并发编程-学习总结(下)

目录 1、Future 1.1、Callable和Runnable的不同 1.2、Future的主要功能 1.3、常用方法 1.4、Future使用注意事项 1.5、CompletableFuture(旅游平台问题) 1.5.1、需求 1.5.2、解决方案1&#xff1a;串行 1.5.3、解决方案2&#xff1a;线程池 1.5.4、解决方案3&#xf…

Prometheus本地存储和VictoriaMetrics远端存储

文章目录Prometheus本地存储简介blockWAL本地存储配置参数VictoriaMetrics简介单机版部署使用安装VictoriaMetrics配置Prometheus使用Victoriametrics配置Grafana以Victoriametrics作为数据源集群版部署使用部署vmstorage部署vmselect部署vminsert配置Prometheus使用vminsert配…

LearnOpenGL-入门-你好,三角形

本人刚学OpenGL不久且自学&#xff0c;文中定有代码、术语等错误&#xff0c;欢迎指正 我写的项目地址&#xff1a;https://github.com/liujianjie/LearnOpenGLProject LearnOpenGL中文官网&#xff1a;https://learnopengl-cn.github.io/ 文章目录图形渲染管线基本介绍着色器…

文献计量三大定律之一---洛特卡定律及普赖斯定律

科学生产率是洛特卡定律的基础&#xff0c;科学生产率”(Scientific Productivity)&#xff09;是指科学家&#xff08;科研人员&#xff09;在科学上所表现出的能力和工作效率&#xff0c;通常用其生产的科学文献的数量来衡量。 1926年&#xff0c;洛特卡在一篇论文中提出了科…

Windows作为操作系统的典型特征和主要功能

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天我们来重新审视一下Windows这个我们熟悉的不能再熟悉的系统。我们每天都在用Windows操作系统&#xff0c;但是其实我们每天直接在打交道的并不是Windows操作系统的内核&#xff0c;而是Windows操作系统的…

Docker部署Springboot项目(含MySQL+Redis)

使用Docker部署之前写的一个博客项目&#xff0c;主要用到了MySQL和Redis&#xff0c;Redis作网站访问量统计。下面会对具体的部署方式作详细讲解 一、服务器安装Docker 1、删除docker旧版本 sudo yum remove docker \docker-client \docker-client-latest \docker-common \…

(三十五)大白话MySQL一个事务多次查询一条数据读到的都是不同的值,这就是不可重复读?

上一讲我们说完了多个事务并发执行时候&#xff0c;对MySQL的缓存页里的同一行数据同时进行更新或者查询的时候&#xff0c;可能发生的脏写和脏读的问题 我们也都理解了&#xff0c;之所以会发生脏写和脏读&#xff0c;最关键的&#xff0c;其实是因为你一个事务写或者查的是人…

黑盒测试的常用方法

这里我们先设置一个示例,后面的文章中会根据示例来进行讲解 假设有一个程序是判断一个整形数字是否属于1-100 目录 1.等价类法 2.边界值法 3.判定表法 4.场景设计法 5.错误猜测法 6.正交法 1.等价类法 概念:系统性的确定要输入的测试条件的方法可以看出概念非常抽象,那…

命令执行漏洞 | iwebsec

文章目录1 靶场环境2 命令执行漏洞介绍3 靶场练习01-命令执行漏洞02-命令执行漏洞空格绕过03-命令执行漏洞关键命令绕过04-命令执行漏洞通配符绕过05-命令执行漏洞base64编码绕过4 命令执行漏洞危害01-读写系统文件02-执行系统命令03-种植恶意木马04-反弹shellpython反弹shellp…

Android 基础知识4-3.4 ImageView(图像视图)详解

一、ImageView简介 ImageView是Android开发中最常用的组件之一&#xff0c;主要用于显示图片&#xff0c;但是它不只是能显示图片&#xff0c;任何Drawable对象都可以使用它来显示。 二、ImageView 的继承关系 ImageView的继承关系 如下&#xff1a; java.lang.Object 《-- …

生成式语言大模型压缩技术思考——以ChatGPT为例

ChatGPT引领了生成式语言大模型的应用与技术热潮&#xff0c;首先简单回顾ChatGPT应用范式&#xff1a;将其应用于指定的下游任务时&#xff08;如知识问答、翻译、编码&#xff09;&#xff0c;ChatGPT需要经历三个阶段的训练&#xff08;增强人类语境的猜想&#xff09;&…

基于nodejs+vue的平面设计课程管理系统vscode

后台由管理员&#xff0c;教师和学生三个角色&#xff0c;其主要功能包括首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;教师管理&#xff0c;课程类型管理&#xff0c;课程学习管理&#xff0c;试题讲解管理&#xff0c;作业信息管理&#xff0c;作业提交管理&…

Flutter3引用原生播放器-IOS(Swift)篇

前言由于Flutter项目中需要使用到播放器功能&#xff0c;因此对flutter中各种播放器解决方案进行了一番研究和比对&#xff0c;最后决定还是自己通过Plugin的方法去引用原生播放器符合自己的需求&#xff0c;本篇文章会对各种解决方案做一个简单的比较&#xff0c;以及讲解一下…

STM32—DMA

什么是DMA&#xff1f; DMA(Direct Memory Access&#xff0c;直接存储器访问) 提供在外设与内存、存储器和存储器、外设与外设之间的高速数据传输使用。它允许不同速度的硬件装置来沟通&#xff0c;而不需要依赖于CPU&#xff0c;在这个时间中&#xff0c;CPU对于内存的工作来…

Leetcode 剑指 Offer II 016. 不含重复字符的最长子字符串

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer&#xff08;专项突击版&#xff09;系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的最长…

软考之操作系统知识

目录 1.进程管理-进程的概念 2.进程的三态图和五态图 3.进程的同步与互斥 4.PV操作应用 5.死锁问题 6.银行家算法 7.存储管理 8.段式存储组织 9.段页式存储组织 10.页面置换算法 11.磁盘管理 12.作业管理 13.索引文件结构 14.树型目录结构 15.空闲存储空间管理 …