省流助手
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]
意思是没有一个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);
}
被调用接口在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);
}
}
比较疑惑的是,在联调这两个服务之前,已经调通了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("<script>alert('uri为 /error, 可能原因为:1.请求方法错误 2.参数格式解析错误');</script>");
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("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.dumbzarro.top/login.html'</script>");
return false;
}
}
在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 {
}
}
用户未登录
虽然不会报转换异常,但是还会返回“用户未登录”。
可以确保的是我在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);
}
这时候启动服务,开始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;
}
}
在application.yml配置打印日志
logging:
level:
feign.Logger: debug
log4j定义了8个级别的log,优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。log4j默认的优先级为ERROR。Log4j建议只使用ERROR、WARN、INFO、DEBUG这四个级别(优先级从高到低)。如果将log level设置在某一个级别上,那么比此级别优先级高的log都能打印出来。
- ALL:最低等级的,用于打开所有日志记录。
- TRACE:很低的日志级别,一般不会使用。
- DEBUG:指出细粒度信息事件对调试应用程序是非常有帮助的,主要用于开发过程中打印一些运行信息。
- INFO:消息在粗粒度级别上突出强调应用程序的运行过程。打印一些你感兴趣的或者重要的信息,这个可以用于生产环境中输出程序运行的一些重要信息,但是不能滥用,避免打印过多的日志。
- WARN:表明会出现潜在错误的情形,有些信息不是错误信息,但是也要给程序员的一些提示。
- ERROR:打印错误和异常信息,指出虽然发生错误事件,但仍然不影响系统的继续运行。
- FATAL:指出每个严重的错误事件将会导致应用程序的退出。重大错误,这种级别可以直接停止程序了。
- 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;
}
}
查看日志,发现请求已经成功带上了cookie
按道理来说两个请求应该是一个cookie和session的,但是这里却发现两个session不一致。
大概是登陆超时了,过期了,从新登陆一下就好了。
成功返回了消息。文章来源:https://www.toymoban.com/news/detail-570780.html
为什么之前的微服务不会出现问题?
之前调通了auth-server和third-party 以及 auth-server 和 member,都没有出现类似的问题。
前者的原因是third-party没有登陆拦截器,因此auth-server 调用third-party的时候不会返回text/html的内容,因此能正常解析。既然没有登陆拦截器,那么有无cookie也不影响远程调用。
后者的原因是虽然member有登陆拦截器,但是因为auth-server请求的接口是放行的(详细见上面的代码),所以也不会返回text/html的返回值,因此也能正常解析。同时有因为接口不需要登陆认证的cookie,fegin请求头的cookie丢失了也不影响。文章来源地址https://www.toymoban.com/news/detail-570780.html
到了这里,关于【Feign请求头丢失问题】no suitable HttpMessageConverter found for response type的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!