第三方接口回调转发到不同环境

背景:完成完整的业务,需要第三方异步回调系统的接口。从而更新业务状态。但是第三方系统经常只能配置一个回调接口。但是我们系统有4个环境。常常只能在一个环境测试,切换环境测试需要去第三方修改回调接口。

目标:接口回调回来后,允许接口转发到其他环境

实现思路:由于回调接口中是没有参数能标识发到哪个环境,因此无法在nginx层面解决。就在具体服务上做:

  1. 接收请求,找到需要转发到哪些环境

  1. 封装请求,将回调请求转发过去(header带上标识:x-forward-from)

  1. 如果请求带有x-forward-from,则不转发

  1. 同时本服务也接收该请求并处理

这些逻辑都是和接口逻辑不相干的,因此考虑切面等方式处理,最后采用filter处理。逻辑在filter中处理

以下是遇到的相关问题:项目采取SpringMVC

Q:WebApplicationContext获取HttpClient的bean失败

call其他环境用封装的HttpClient的bean,它是一个带线程池的,filter通过在web.xml进行配置生效。如下:


<filter>
    <filter-name>NotifyMessageForwardFilter</filter-name>
    <filter-class>com.lenovo.ofp.payment.front.webapp.filter.NotifyMessageForwardFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>NotifyMessageForwardFilter</filter-name>
    <url-pattern>/notify/*</url-pattern>
</filter-mapping>

这种方式,可以看作是new NotifyMessageForwardFilter()。无法通过@Auwired 注入bean,会报错。当然这个报错具体原因是因为filter调用时,bean对象还没初始化好:web.xml中各个标签初始化的顺序如下:contetxt-param -> listener -> filter -> servlet


<context-param> 用来加载你配置的文件信息
<listener> 配置你的监听服务
<filter> 过滤器配置你单独的一些操作
<servlet> 容器初始化
加载顺序为:context-param -> listener -> filter -> servlet    加载的顺序不受在web.xml中配置的位置影响

springmvc.xml中定义component-scan;而springmvc.xml在servlet中加载;所以filter中找不到servlet才生成的bean

其次是WebApplicationContext获取的applicationContext,它id是:org.springframework.web.context.WebApplicationContext:/payment-front-webapp,通过BeanDefinitionNames()这个方法发现,没有HttpClient的bean,连Controller的bean都没有。因此才认为,至少还有一个其他的ApplicationContext

而servlet的applicationContext的id是:org.springframework.web.context.WebApplicationContext:/payment-front-webapp/dispatcher

发现了吧,ApplicationContext不是一个,所以呀,我们要用下面那个。

A:解决方法:通过上面的分析,我们也知道ApplicationContext除了WebApplicationUtils获取的那个外,还存在至少一个。所以用ApplicationContextAware来获取。毕竟这个接口是在服务启动完成后执行的内容。获取的Context更为完整。


@Component
public class ApplicationContextUtils implements ApplicationContextAware {
    public static ApplicationContext APPLICATION_CONTEXT = null;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        log.info("init ApplicationContextUtils success");
        ApplicationContextUtils.APPLICATION_CONTEXT = applicationContext;
    }
}

Q1:org.apache.http.ProtocolException: Content-Length header already present

A:content-length是根据内容的长度动态计算的。因此在请求转发的时候,要删掉

Q2:requestBody无法被重复读取

请求转发到不同环境,这意味着我们需要封装http请求,需要读取body。而body读取一次后就会报错。

A:body无法被重复读取原因是body内容放在InputStream中,这个流是只能读一次的,根据HttpServletRequestWrapper包装一下就行了

2023年8月31日 更新:出新问题啦,原来的封装不支持表单多次读取,所以更新了

public class HttpMultiReadServletRequestWrapper extends HttpServletRequestWrapper {

    BufferedReader bufferedReader = null;
    String requestBody = "";
    String contentType;
    Map parameterMap;

    public HttpMultiReadServletRequestWrapper(HttpServletRequest request) {
        super(request);
        // OFP-57616 form表单数据和body公用一个流;因此这里需要支持在form表单场景下多次读取
        this.contentType = request.getContentType();
        if (contentType.contains("application/x-www-form-urlencoded")) {
            this.parameterMap = request.getParameterMap();
            this.requestBody = (String) parameterMap.entrySet().stream()
                    .flatMap(obj -> {
                        Entry entry = ((Entry) obj);
                        String name = entry.getKey().toString();
                        String[] values = getParameterValues(name);
                        return Arrays.stream(values).map(value -> name + "=" + value);
                    }).collect(Collectors.joining("&"));
        } else {
            // 用原始的request获取一次body,然后缓存起来
            this.requestBody = HttpRequestUtils.getRequestBody(request);
        }
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        String characterEncoding = getCharacterEncoding();
        characterEncoding = StringUtils.isBlank(characterEncoding) ? "UTF-8" : characterEncoding;
        return new DelegatingServletInputStream(
                new ByteArrayInputStream(requestBody.getBytes(characterEncoding)));
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (bufferedReader == null) {
            // 需要搭配StringReader才能反复读。用inputStream只能读一次
            bufferedReader = new BufferedReader(new StringReader(requestBody));
            bufferedReader.mark(requestBody.length() + 1);
        } else {
            // 重置之后,bufferedReader就又可以读了
            bufferedReader.reset();
        }
        return bufferedReader;
    }

    public String getRequestBody() {
        return this.requestBody;
    }

    // OFP-57616 form表单数据和body公用一个流;因此这里需要支持在form表单场景下多次读取
    @Override
    public Enumeration getParameterNames() {
        return parameterMap != null ? new IteratorEnumeration(parameterMap.keySet().iterator())
                : super.getParameterNames();
    }

    @Override
    public Map getParameterMap() {
        if (parameterMap != null) {
            return parameterMap;
        } else {
            return super.getParameterMap();
        }
    }

    @Override
    public String getParameter(String name) {
        Optional<Object> firstEle = Optional.ofNullable(parameterMap)
                .map(e -> e.get(name))
                .filter(Objects::nonNull)
                .map(e -> e.getClass().isArray() ? (Object[]) e : new Object[]{e})
                .filter(arr -> arr.length > 0)
                .map(e -> e[0])
                .filter(Objects::nonNull);
        if (firstEle.isPresent()) {
            return firstEle.get().toString();
        } else {
            return super.getParameter(name);
        }
    }

    @Override
    public String[] getParameterValues(String name) {
        Optional<Object[]> arr = Optional.ofNullable(parameterMap)
                .map(e -> e.get(name))
                .filter(Objects::nonNull)
                .map(e -> e.getClass().isArray() ? (Object[]) e : new Object[]{e});
        if (arr.isPresent()) {
            Object[] tempRes = arr.get();
            return Arrays.copyOf(tempRes, tempRes.length, String[].class);
        }
        return super.getParameterValues(name);
    }
}

HttpRequestUtils的getRequestBody内容如下
public static String getRequestBody(HttpServletRequest request) {
    StringBuilder sb = new StringBuilder();
    try {
        BufferedReader br = request.getReader();
        String line;
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
    } catch (Exception e) {
        log.error("Read notify request body", e);
    }
    return sb.toString();
}

Q3:@Autowired request是代理类,无法加载为了解决requestBody而生成RequestWrapper

A:其实就是一个实现逻辑出错。我们生成RequestWrapper后,应当在filterChain里面传入RequestWrapper而不是原来的request。其次是:RequestContextHolder,这个是在filter之前就设置了Request,所以有必要,需要更新里面的Request

Q4:301 Moved Permanently

请求第三方时出现301问题。但是本地使用apifox是可以直接调用的。接口完全一致。因此是header出现了问题。本文进行转发时调用其他环境的请求中header有如下内容(在本地测试环境下)


user-agent: apifox/1.0.0 (https://www.apifox.cn)
content-type: application/json
accept: */**
host: localhost:8082
accept-encoding: gzip,deflate,br
connection: keep-alive
x-forward-from: local

通过控制变量法测试:当存在host时,会出现301错误,当host不存在,且accept存在时,会报如下错误(错误内容有截取)

The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值