公司几乎所有项目都会加上一个BFF,没有为什么,就是想要一个BFF,经历过几个项目,无一例外,BFF都成了一坨屎。包括不限于以下问题:
- request和response明明都不需要做任何修改,但就是要重新写一遍给客户端的API和调用上游的Client
- 需要把返回包上code/data/message,然后在API层面调用无数次的ResponseData.ok(……)
- 背锅侠,任何问题都会被第一时间找到,需要找各种日志自证清白
在最近的一个项目中,我在客户的限制下搭了一套不太完善的BFF架子,目的就是避免上面的部分问题,尽可能减少一些模板代码。
客户限制:必须要Java+Spring Boot,而且必须要WebMVC,不能是Webflux/Reactor
全局
最终成品如下
从上往下
- Sleuth Tracing 这是一个servlet filter,用来给req加transactionId
- 自定义的Log Filter,用来记录所有的Request和Response
- Reverse Proxy Filter,会处理特定的path,直接进入转发流程。
- Spring Mvc的Filter,进入Spring的处理范围。
Proxy
对于Proxy部分,会有一串Interceptor,这个interceptor chain是金字塔形状的,
- 如果对request做操作,order越优先,越接近原始,越往后,越接近转发给上游的状态
- 如果对response做操作,order越优先,越接近返回给客户端的状态,越往后,越接近从上游拿到的原始状态
所以
- Logger的优先级放最低,拿到发给上游和从上游拿到的结果。
- ErrorCatch放次低,在第二顺位拿到上游的错误,包装后抛出
- ErrorHandler放最高,在最外层捕获到所有错误,记录后给客户端友好提示
Spring
ResponseDataBodyAdvice负责对API包内的返回值进行包装,判断如果returnType是由Jackson处理,那就包一层。
@Slf4j
@ControllerAdvice(basePackages = "com.example.bff.api")
public class ResponseDataBodyAdvice<T> implements ResponseBodyAdvice<T> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
}
@Override
@SneakyThrows
public T beforeBodyWrite(T body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
return (T) new CommonResponse(body);
}
}
敢于这么做的原因是,我没见过在API返回不同Code的,基本都是返回ok,非ok的情况都是抛出错误由ErrorHandler来处理。
这样做会有一个小小的问题,那就是spring会把字符串json交给StringHttpMessageConverter
处理,有个很tricky的解决方案,就是调整MappingJackson2HttpMessageConverter
的优先级,使其排在第一个。
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
HttpMessageConverter<?> jsonConverter = converters.stream()
.filter(x -> x instanceof MappingJackson2HttpMessageConverter)
.findFirst()
.orElse(null);
if (jsonConverter != null) {
converters.remove(jsonConverter);
converters.add(0, jsonConverter);
}
}
Feign相关的就是非常简单的加上类似的request/response日志,错误mapping。
关于DTO
选择在所有必须得写的DTO里面只声明“需要”的。一些额外的参数用Jackson @AnySetter,在给到客户端的时候,再用上@AnyGetter,大致就是这样
public class BaseDTO {
@JsonAnySetter
private Map<Object, Object> extra = new HashMap<>();
@JsonAnyGetter
public Map<Object, Object> getExtra() {
return extra;
}
}