1. 排查流程
今天在升级数字化平台时,测试业务过程中发现老的业务系统无法通过 OpenFeign 调用数字化平台的基础组件。经过排查,发现了一个异常,提示:“Content-Type 'application/x-www-form-urlencoded;charset=UTF-8' is not supported”。看来是由于此次升级对接口进行了变更,导致业务服务与新版本不兼容。
16:41:10,794 WARN c.c.c.r.c.b.w.GlobalExceptionHandler [http-nio-8089-exec-2] uri: /post/addUserPostRel , remote:172.22.127.235 Content-Type 'application/x-www-form-urlencoded;charset=UTF-8' is not supported client error
org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/x-www-form-urlencoded;charset=UTF-8' is not supported
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179) ~[spring-web-6.0.13.jar!/:6.0.13]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122) ~[spring-web-6.0.13.jar!/:6.0.13]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:136) ~[spring-webmvc-6.0.13.jar!/:6.0.13]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:163) ~[spring-webmvc-6.0.13.jar!/:6.0.13]
我们现在来检查一下业务服务的接口。从代码来看,这个接口更像是处理前端表单提交的内容,因此 Content-Type 显然是 application/x-www-form-urlencoded
。如果是 @RequestBody
类型的请求,那么 Content-Type 则会是 application/json
。
@ApiOperation(value = "根据手机号查用户信息")
@RequestMapping(value = "/getUserInfoByMobile", method = {RequestMethod.POST})
public CommonResponse<BizRippleUserInfoVo> getUserInfoByMobile(@RequestParam("mobile") String mobile, @RequestParam(name = "userRepositoryCode", required = false) String userRepositoryCode) {
return userInfoService.getUserInfoByMobile(mobile, userRepositoryCode);
}
进入内部代码后,通过查看依赖关系,发现 mgtService.addPostUser(baseRippleUserInfo, userInfo)
这一行是通过 OpenFeign 远程调用数字化平台的基础组件。对方返回了 Content-Type is not supported
的错误。为了进一步排查,我决定加点代码,检查上下文中的 RequestContextHolder
中的头信息。经过分析,应该是因为 Controller 中的接口没有使用 @RequestBody
注解,导致请求的 Content-Type
被默认为 application/x-www-form-urlencoded
。这个请求通过 OpenFeign 传递给数字化平台时,出现了兼容性问题。
private BizRippleUserInfoVo makeNewUserInfoVo(boolean isAnonymous, String mobile, String openId) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 打印请求的头信息
System.out.println("=== 请求头信息 ===");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
System.out.println(headerName + ": " + headerValue); // 输出每个请求头的字段名和对应的值
}
System.out.println("====================");
com.crpharm.hvlp.common.biz.entity.UserInfo userInfo = saveUserInfo(isAnonymous, mobile, openId);
BizRippleUserInfoVo baseUserInfo = new BizRippleUserInfoVo();
baseUserInfo.setUserId(userInfo.getRippleUserId());
baseUserInfo.setLoginName(userInfo.getLoginName()); //登录名用户名一致
baseUserInfo.setPhone(userInfo.getPhone());
baseUserInfo.setUserName(userInfo.getLoginName());//登录名用户名一致
//绑定默认学习岗位
UserCenterRippleUserInfoVo baseRippleUserInfo = new UserCenterRippleUserInfoVo();
BeanUtils.copyProperties(baseUserInfo, baseRippleUserInfo);
mgtService.addPostUser(baseRippleUserInfo, userInfo);
return baseUserInfo;
}
=== 请求头信息 ===
sw8: 1-YjYxZDliNTIyNjAxNDU0ZTg4NmJiMmI1Y2QxMGYwNjIuOTMuMTc0NDE4NTQwNjkzNTY4NTc=-MThlMWZhZmQ2Y2NlNDJkNTk0MzRkYmM5MjU1ZTk4ZjQuNzYuMTc0NDE4NTQwODM2NzAwMTA=-3-dXNlci1jZW50ZXItcHJveHk=-N2UzZWQ1ZGMwZTM0NDJmMjlhODE0NzcxMjc2YTRjZTJAMTcyLjIyLjEyMS4yNTE=-UE9TVDovdXNlci1jZW50ZXIvZ2V0VXNlckluZm9CeU1vYmlsZQ==-MTcyLjIyLjEyNy4yMDI6OTgwNA==
sw8-correlation:
sw8-x: 0-
accept: */*
user-agent: Java/21.0.2
host: 172.22.127.202:9804
connection: keep-alive
content-type: application/x-www-form-urlencoded
content-length: 0
====================
2. 解决方式
问题已经非常明确了,数字化基础组件在更新后不再支持旧的 application/x-www-form-urlencoded
类型的请求。解决方案是,在 OpenFeign 发起调用之前,先将 Content-Type
修改为 application/json
。为了实现这一点,我可以写一个工具类,利用 RequestContextHolder
来修改请求头,这样在 OpenFeign 调用时就不会将错误的 Content-Type
传递过去了。幸运的是,业务系统中调用 Feign 接口的次数不多,这种方式还是比较合理的。
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
public class RequestContextHeaderModifier {
/**
* 修改多个请求头
*
* @param headers 包含多个头部键值对的 Map
*/
public void modifyHeaders(Map<String, String> headers) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 创建一个新的 HttpServletRequestWrapper 来修改多个请求头
HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(request) {
@Override
public String getHeader(String name) {
// 遍历修改后的头部键值对
if (headers.containsKey(name)) {
return headers.get(name);
}
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaderNames() {
// 返回所有修改后的头部信息
return Collections.enumeration(headers.keySet());
}
@Override
public Enumeration<String> getHeaders(String name) {
// 返回修改后的头部内容
if (headers.containsKey(name)) {
return Collections.enumeration(Collections.singletonList(headers.get(name)));
}
return super.getHeaders(name);
}
};
// 更新 RequestContextHolder 上下文,使用修改后的请求对象
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(requestWrapper));
}
}
private BizRippleUserInfoVo makeNewUserInfoVo(boolean isAnonymous, String mobile, String openId) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json; charset=utf-8");
RequestContextHeaderModifier modifier = new RequestContextHeaderModifier();
modifier.modifyHeaders(headers);
。。。业务代码。。。
}