• 【Feign请求头丢失问题】no suitable HttpMessageConverter found for response type


    省流助手

    HttpMessageConverter 失败的原因是:在项目中使用了一个拦截器拦截请求,部分接口需要登陆才能访问,否则返回一个text/html格式的响应,导致远程服务解析响应失败。
    登陆失败的原因是:Feign发起远程调用的时候会重新生成一个新的请求,带来的问题就是不会携带原来请求的cookie,导致调用需要登陆的远程接口时会失败。解决方法是配置一个Feign的拦截器,在发送请求的时候带上原请求的cookie。
    本文主要内容是围绕这个问题展开的一系列知识点,包括但不限于:

    • http的content type
    • 微服务联调debug
    • 查看Feign日志
    • 登陆拦截器
    • Fegin丢头问题

    问题分析定位

    今天在联调两个微服务的时候发现远程接口总是返回以下报错:

    Could not extract response: no suitable HttpMessageConverter found for response type [class top.dumbzarro.greensource.common.utils.R] and content type [text/html;charset=UTF-8]
    
    • 1

    意思是没有一个HttpMessageConverter 可以将 [text/html;charset=UTF-8]转化为[class top.dumbzarro.greensource.common.utils.R] 。
    其中,R是项目中定义的一个通用的返回对象,所有接口都返回这个对象。

    远程接口在ware服务,详细如下:

    @FeignClient("greensource-member")
    public interface MemberFeignService {
        @GetMapping("/memberreceiveaddress/info/{id}")
        R info(@PathVariable("id") Long id);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    被调用接口在member服务,详细如下:

    @RestController
    @RequestMapping("memberreceiveaddress")
    public class MemberReceiveAddressController {
    	@Autowired
        private MemberReceiveAddressService memberReceiveAddressService;
    
        @GetMapping("/info/{id}")
        //@RequiresPermissions("member:memberreceiveaddress:info")
        public R info(@PathVariable("id") Long id){
    		MemberReceiveAddressEntity memberReceiveAddress = memberReceiveAddressService.getById(id);
    		return R.ok().setData(memberReceiveAddress);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    比较疑惑的是,在联调这两个服务之前,已经调通了auth服务和member服务、auth服务和third-party服务,两个服务之间的Feign远程调用就没有问题。

    网上对于no suitable HttpMessageConverter的解决方案就是添加一个自定义的转换器等等。但是隐约感觉这不是类型转换的问题,不然在没有额外配置的情况下,之前的服务不可能跑的通。

    HTTP Content-type

    Content-type是HTTP协议中的一个字段,Content-Type 标头告诉客户端实际返回的内容的内容类型。
    常见的有:

    • text/html: HTML格式,浏览器在获取到这种文件时会自动调用html的解析器对文件进行渲染的处理。
    • text/plain:将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。
    • application/json: JSON数据格式,浏览器不会对其进行处理。

    TODO Content-type springmvc fegin的默认content-type

    印象里接口都是返回json数据,content-type是application/json,怎么会突然冒出个text/html呢。于是使用全局搜索查了一下。
    在这里插入图片描述
    突然想起在部分需要登陆的业务中都增加了一个拦截器,用于判断用户是否登陆,在判断用户没有登陆的时候会返回一个text/html的响应。详细代码如下。

    @Component
    public class LoginUserInterceptor implements HandlerInterceptor {
    
        public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            String uri = request.getRequestURI();
            if(uri.equals("/error")){
                response.setContentType("text/html;charset=UTF-8");
                PrintWriter out = response.getWriter();
                out.println("");
                return false;
            }
    
            boolean match = new AntPathMatcher().match("/member/**", uri);
            if (match) { // member接口(登陆,注册)可以不用登陆就使用,否则需要登陆
                return true;
            }
    
            HttpSession session = request.getSession();
    
            //获取登录的用户信息
            MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);
    
            if (attribute != null) {
                //把登录后用户的信息放在ThreadLocal里面进行保存
                loginUser.set(attribute);
    
                return true;
            } else {
                //未登录,返回登录页面
                response.setContentType("text/html;charset=UTF-8");
                PrintWriter out = response.getWriter();
                out.println("");
                return false;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    在feign的请求的时候,被判定为没有登陆,所以返回了这个“text/html”格式的数据,而在远程接口处我们使用的是R进行接受,自然就无法成功解析然后就会出现报错。
    正常来说这里应该返回的是一个application对象,由于这个项目是基于谷粒商城修改的,谷粒商城是前后端不分离了,而后续这个项目使用的是前后端分离的结构,所以这里将这个返回值做一个修改,即可解决这个报错了。
    可参考如下代码修改

    @Component
    public class LoginUserInterceptor implements HandlerInterceptor {
    
        public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            String uri = request.getRequestURI();
            if(uri.equals("/error")){
                response.setContentType("application/json; charset=utf-8");
                PrintWriter out = response.getWriter();
    
                out.println(JSONObject.toJSONString(R.error()
                                .put("error","uri为 /error, 可能原因为:1.请求方法错误 2.参数格式解析错误"),
                        SerializerFeature.WriteMapNullValue,
                        SerializerFeature.WriteDateUseDateFormat));
                return false;
            }
    
            boolean match = new AntPathMatcher().match("/member/**", uri);
            if (match) { // member接口(登陆,注册)可以不用登陆就使用,否则需要登陆
                return true;
            }
    
            HttpSession session = request.getSession();
    
            //获取登录的用户信息
            MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);
    
            if (attribute != null) {
                //把登录后用户的信息放在ThreadLocal里面进行保存
                loginUser.set(attribute);
    
                return true;
            } else {
                //未登录
                response.setContentType("application/json; charset=utf-8");
                PrintWriter out = response.getWriter();
                out.println(JSONObject.toJSONString(
                        R.error().put("error","用户未登录"),
                        SerializerFeature.WriteMapNullValue,
                        SerializerFeature.WriteDateUseDateFormat
                        )
                );
                return false;
            }
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    用户未登录

    虽然不会报转换异常,但是还会返回“用户未登录”。
    在这里插入图片描述
    可以确保的是我在swagger已经登陆了,请求的时候带上了cookie了的,但是经过fegin之后就显示没有登陆,而仅仅是ware服务的这个接口报错,而auth和third-party都不会报错。

    微服务联调

    因为单独去测试member服务的时候都没有问题,于是就想看直接请求member服务和从ware服务器请求member的请求有什么不同,于是打算在两个服务都打断点看看。注意,如果你同一个服务有多个实例注册在nacos上,那么要在@FeignClient加入url的参数,去指定到本地的服务,否则请求可能会打到其他的机器上,导致没办法debug到当前的机器上。当然,如果只有一个实例,其实不用加也可以。示例如下:

    //@FeignClient(value="greensource-member")
    @FeignClient(value="greensource-member",url="localhost:7000")// 指定某台机器
    public interface MemberFeignService {
        @GetMapping("/memberreceiveaddress/info/{id}")
        R info(@PathVariable("id") Long id);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这时候启动服务,开始debug,发现程序不会经过接口调用处经过,而是在member的登陆拦截器处被判定为没有登陆,直接返回到ware服务。
    查看请求,发现此时没有session,没有登陆成功。
    在这里插入图片描述

    打开fegin 日志

    我们配置一个FeginConfig,查看fegin的请求响应情况

    @Configuration
    public class FeignConfig {
    
        @Bean
        public feign.Logger logger() {
            return new Slf4jLogger();
        }
        @Bean
        public Logger.Level level() {
            return Logger.Level.FULL;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在application.yml配置打印日志

    logging:
      level:
        feign.Logger: debug
    
    • 1
    • 2
    • 3

    log4j定义了8个级别的log,优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。log4j默认的优先级为ERROR。Log4j建议只使用ERROR、WARN、INFO、DEBUG这四个级别(优先级从高到低)。如果将log level设置在某一个级别上,那么比此级别优先级高的log都能打印出来。

    1. ALL:最低等级的,用于打开所有日志记录。
    2. TRACE:很低的日志级别,一般不会使用。
    3. DEBUG:指出细粒度信息事件对调试应用程序是非常有帮助的,主要用于开发过程中打印一些运行信息。
    4. INFO:消息在粗粒度级别上突出强调应用程序的运行过程。打印一些你感兴趣的或者重要的信息,这个可以用于生产环境中输出程序运行的一些重要信息,但是不能滥用,避免打印过多的日志。
    5. WARN:表明会出现潜在错误的情形,有些信息不是错误信息,但是也要给程序员的一些提示。
    6. ERROR:打印错误和异常信息,指出虽然发生错误事件,但仍然不影响系统的继续运行。
    7. FATAL:指出每个严重的错误事件将会导致应用程序的退出。重大错误,这种级别可以直接停止程序了。
    8. OFF:最高等级的,用于关闭所有日志记录。

    可以看到我们的请求是没有设置cookie的
    在这里插入图片描述
    这就是fegin请求失败的根本原因,所以我们在ware配置fegin发送请求是带上cookie。

    Feign丢失cookie问题

    由于fegin每次请求都会自己发一个新的请求,而不会带上我们之前的请求的cookie,这时候我们就要手动配置一下。在之前设置debug的地方继续添加配置,注入一个拦截器到spring容器中,在Feign请求之前我们设置一下cookie

    @Configuration
    public class FeignConfig {
    
        @Bean
        public feign.Logger logger() {
            return new Slf4jLogger();
        }
        @Bean
        public Logger.Level level() {
            return Logger.Level.FULL;
        }
    
        @Bean("requestInterceptor")
        public RequestInterceptor requestInterceptor() {
    
            RequestInterceptor requestInterceptor = new RequestInterceptor() {
                @Override
                public void apply(RequestTemplate template) {
                    //1、使用RequestContextHolder拿到刚进来的请求数据
                    ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    
                    if (requestAttributes != null) {
                        //老请求
                        HttpServletRequest request = requestAttributes.getRequest();
    
                        //2、同步请求头的数据(主要是cookie)
                        //把老请求的cookie值放到新请求上
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            };
            return requestInterceptor;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    查看日志,发现请求已经成功带上了cookie
    在这里插入图片描述
    按道理来说两个请求应该是一个cookie和session的,但是这里却发现两个session不一致。
    大概是登陆超时了,过期了,从新登陆一下就好了。
    在这里插入图片描述
    成功返回了消息。

    为什么之前的微服务不会出现问题?

    之前调通了auth-server和third-party 以及 auth-server 和 member,都没有出现类似的问题。
    前者的原因是third-party没有登陆拦截器,因此auth-server 调用third-party的时候不会返回text/html的内容,因此能正常解析。既然没有登陆拦截器,那么有无cookie也不影响远程调用。
    后者的原因是虽然member有登陆拦截器,但是因为auth-server请求的接口是放行的(详细见上面的代码),所以也不会返回text/html的返回值,因此也能正常解析。同时有因为接口不需要登陆认证的cookie,fegin请求头的cookie丢失了也不影响。

  • 相关阅读:
    Linux dts list python tool
    【taro react】---- 兼容微信小程序和H5的海报绘制插件
    springboot整合redis-sentinel哨兵模式集群(二)
    不同平台下运行历程代码
    吉利发布新出行科技品牌“礼帽出行” 定位高品质定制化出行
    论文阅读【5】Attention Is All You Need
    vue实现刷新页面随机切换背景图【适用于登陆界面】
    健康饮酒进家庭,国台酒业与碧桂园服务集团达成战略合作
    docker容器镜像管理+compose容器编排(持续更新中)
    mysql数据库,字符串使用双引号““导致报错,使用单引号‘‘不报错,Unknown column ‘user-test‘ in ‘where clause‘
  • 原文地址:https://blog.csdn.net/weixin_45654405/article/details/126768380