公司几乎所有项目都会加上一个BFF,没有为什么,就是想要一个BFF,经历过几个项目,无一例外,BFF都成了一坨屎。包括不限于以下问题:

  1. request和response明明都不需要做任何修改,但就是要重新写一遍给客户端的API和调用上游的Client
  2. 需要把返回包上code/data/message,然后在API层面调用无数次的ResponseData.ok(……)
  3. 背锅侠,任何问题都会被第一时间找到,需要找各种日志自证清白

在最近的一个项目中,我在客户的限制下搭了一套不太完善的BFF架子,目的就是避免上面的部分问题,尽可能减少一些模板代码。

客户限制:必须要Java+Spring Boot,而且必须要WebMVC,不能是Webflux/Reactor

全局

最终成品如下

Diagram.drawio.svg

从上往下

  • Sleuth Tracing 这是一个servlet filter,用来给req加transactionId
  • 自定义的Log Filter,用来记录所有的Request和Response
  • Reverse Proxy Filter,会处理特定的path,直接进入转发流程。
  • Spring Mvc的Filter,进入Spring的处理范围。

Proxy

对于Proxy部分,会有一串Interceptor,这个interceptor chain是金字塔形状的,

  1. 如果对request做操作,order越优先,越接近原始,越往后,越接近转发给上游的状态
  2. 如果对response做操作,order越优先,越接近返回给客户端的状态,越往后,越接近从上游拿到的原始状态

所以

  1. Logger的优先级放最低,拿到发给上游和从上游拿到的结果。
  2. ErrorCatch放次低,在第二顺位拿到上游的错误,包装后抛出
  3. 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;
    }
}