背景:完成完整的业务,需要第三方异步回调系统的接口。从而更新业务状态。但是第三方系统经常只能配置一个回调接口。但是我们系统有4个环境。常常只能在一个环境测试,切换环境测试需要去第三方修改回调接口。
目标:接口回调回来后,允许接口转发到其他环境
实现思路:由于回调接口中是没有参数能标识发到哪个环境,因此无法在nginx层面解决。就在具体服务上做:
-
接收请求,找到需要转发到哪些环境
-
封装请求,将回调请求转发过去(header带上标识:x-forward-from)
-
如果请求带有x-forward-from,则不转发
-
同时本服务也接收该请求并处理
这些逻辑都是和接口逻辑不相干的,因此考虑切面等方式处理,最后采用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