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);
    。。。业务代码。。。
}