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

背景:完成完整的业务,需要第三方异步回调系统的接口。从而更新业务状态。但是第三方系统经常只能配置一个回调接口。但是我们系统有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包装一下就行了

public class HttpMultiReadServletRequestWrapper extends HttpServletRequestWrapper {
    BufferedReader bufferedReader = null;
    String requestBody = "";
    public HttpMultiReadServletRequestWrapper(HttpServletRequest request) {
        super(request);
        // 用原始的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;
    }
}

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


版权声明:本文为whylackgoodname原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。