浅谈【Feign Client】 开发规范化

背景

随着公司业务的快速发展,项目微服务化已经成为了一个普遍常态。一个项目涉及到的微服务的少说三五个,多则几十个甚至几十个不等,而服务之间的调用所使用的技术进几年来也是层出不穷,例如我们熟知常见的 Http Restful、dubbo、motan、grpc、还有Spring 的Feign等。每个公司都有自己的技术栈,而我们公司大部分Java服务普遍使用的都是Spring的Feign调用,由于公司Java微服务众多,每个部门甚至每个团队都有自己负责的维护的微服务,由于feign Client的开发还没有统一的规范,因此每个项目的实现也是层出不穷,因此给整个Feign Client的维护也带来了诸多的不变。

一、现状

  • 部分服务提供Client依赖包,部分服务没有完善Client包甚至没有。
  • 每个微服务定义Client包的方式不进相同。
  • 每个服务都调用其他的微服务都有自己的熔断,导致Client升级新增了接口的时候,调用方必须更新自己的业务代码,重写熔断接口采能避免编译报错。
  • 每个服务的调用方不明确,也就是说一个微服务上线时间久了,随着业务的快速发展,到了后期不清楚自己的服务有哪些系统在调用,导致重构或者升级过程中对风险的评估不准确带来潜在风险。

    相信做Java开发的大佬们都隐隐约约的遇到了以上提到的难题,那么针对以上问题,我们应该怎么来规范是定义Client或者解决以上问题呢?

二、思考

1.为何要做Feign Hystrix。
什么是服务熔断,我这里在百度百科上找到定义如下


服务熔断按照层级来划分也分为很多种,比如服务之间调用的熔断,网关调用的熔断等。从定义上来看可能比较难理解,我们举个例子。

假设A服务的某个接口依赖B服务的某个接口,当某时刻A服务的这个接口流量成倍数增长,假设这个时候A服务依赖的B服务这个接口因为mysql、或者缓存 或其他原因发生异常,试想会发生什么?很显然此时B服务将会接收大量 的无效请求,那么就会占用B服务的系统资源从而造成资源浪费。
那么有了熔断会怎么处理呢?其实如果A服务对B服务做了熔断处理,那么当大量请求B服务的时候,当第一次请求B服务异常后,那么后续的其他请求将在一段时间内将不会连续再次请求B服务了,这样就给B服务节省了大量的资源。除此之外,在A服务对B服务做熔断的过程中在业务允许范围内也可以对异常响应做mock default处理。
2.统一规范Feign Client会带来哪些好处。
大家都晓得在对接一些三方服务的时候都会有对应开发语言的一些SDK,例如我们熟知的腾讯微信有自己的微信相关SDK、腾讯对象存储COS有自己的 SDK、阿里相关功能也都会有自己的sdk。其实 Feign Client包从某种意义上来说也是属于我们给下游系统提供的sdk。
那么对于下游系统来说会带来哪些好处呢?服务接入将会变得简单,原来没有一sdk可能需要自己要写大量的http请求相关代码,如果你的请求需要有签名、加密相关要求,那么每个客户端都需要重复的开发一套逻辑去进行对接,如果对你们的服务接口进行升级,那么对下游的系统将会影响巨大,甚至可能还需要重新写一份。
那么对于服务端会带来哪些好处呢?这里好处不再一一列举,例如可以对所有的客户端进行统一的升级维护,可以规范下游系统调用服务的规范,例如加固定的请求头信息,统一的签名处理,统一的加密处理等,这样既方便的自己,也给下游系统带来了便利,何乐而不为呢?

三、实现

包结构说明
└─plan
├─client
├─config
│ ├─fallback
│ │ └─plan
│ ├─interceptor
│ └─log
├─request
└─response
client:接口定义,定义提供服务的接口。
config: 配置定义
fallback: 定义熔断支持。
interceptor:定义拦截器,处理统一的请求头、签名、加密等。
log:定义统一的日志搜集打印。
request: 定义统一的请求dto参数定义。
response:定义统一的响应体参数定义。
Client接口定义

定义Client接口指定fallback熔断配置,指定configuration 配置(包含log日志打印,拦截器配置、参数类型转换器配置、请求统一签名、加密处理等);
熔断配置
整体熔断相关包接口定义如下

AbstractFallbackFactory:为抽象的熔断工厂。
由于一个Client SDK可能存在有多个API Client接口定义,如 XxxAClient、XxxBClient....等,而每个Client都有自己的熔断定义,所以这里在fallback包下又为每个Client单独定义一个包名,如这里的plan就是为PlanClient定义的熔断配置。
XxxFallback 熔断处理接口定义需要继承XxxClient接口,例如这里的 PurchasePlanFallback定义如下。

public interface PurchasePlanFallback extends PurchasePlanClient {

}

AbstractFallbackFactory抽样工厂配置

@Slf4j
public abstract class AbstractFallbackFactory implements FallbackFactory {

    private static final String SERVICE_NAME = "plan-service";

    @Override
    public T create(Throwable error) {
        log.error("[{}]服务调用异常", SERVICE_NAME, error);
        T fallbackInstance = getFallbackInstance();
        log.info("Purchase Plan Service Feign FallBack Instance[{}]", fallbackInstance);
        return fallbackInstance;
    }

    /**
     * 获取熔断处理实例
     *
     * @return 熔断处理实例
     */
    public abstract T getFallbackInstance();
}

XxxFallbackFactory具体的熔断工厂配置类,主要负责定义和实现具体的熔断实例UML图如下

具体实现如下

@Slf4j
@Component
public class PurchasePlanFallbackFactory extends AbstractFallbackFactory {
    @Autowired
    private PurchasePlanFallback fallbackInstance;

    /**
     * PurchasePlanClient 默认熔断配置
     */
    @Bean
    @ConditionalOnMissingBean
    public PurchasePlanFallback getPurchasePlanFallback() {
        return new DefaultPurchasePlanFallback();
    }

    @Override
    public PurchasePlanClient getFallbackInstance() {
        return fallbackInstance;
    }
}

DefaultPurchasePlanFallback 为默认的熔断处理配置。如想自定义熔断异常处理可以有如下方案
1.如果想自定义全部Client接口熔断可以直接定义类实现 PurchasePlanFallback 接口全部实现类即可。

//实例化到IOC中
@Component
public class MyFallBack implements PurchasePlanFallback{
    @Override
    public ... A(... request) {
        你的实现
    }

    @Override
    public ... B(... request) {
    }
}

2.如果想自定义部分接口熔断,可直接继承 DefaultPurchasePlanFallback 类,重写你需要自定义的方法接口即可。
例如:

//实例化到IOC中
@Component
public class myFallBack extends DefaultPurchasePlanFallback{
        @Override
        public ... A(... request) {
            你的实现
        }
}

默认熔断定义

public class DefaultPurchasePlanFallback implements PurchasePlanFallback {

    private static final String SERVICE_NAME = "plan-service";

    @Override
    public ResponseBaseVo cancelPurchasePlan(CancelPlanRequest request) {
        ResponseBaseVo responseBaseVo = new ResponseBaseVo<>();
        responseBaseVo.setCode(-1);
        responseBaseVo.setData(false);
        responseBaseVo.setMsg(String.format("[%s]服务[cancelPurchasePlan]接口调用异常", SERVICE_NAME));
        return responseBaseVo;
    }
}

拦截器配置:添加调用来源

@Slf4j
public class PurchasePlanFeignInterceptor implements RequestInterceptor {

    @Value("${app.name:}")
    private String appId;

    private static final String HEADER_APP_SOURCE_ID = "App-Source-Id";

    @Override
    public void apply(RequestTemplate template) {
        //处理定义请求头添加 调用方应用名称
        template.header(HEADER_APP_SOURCE_ID, appId);
    }
}

四、参考
https://docs.spring.io/spring-cloud-openfeign/docs/2.2.5.RELEASE/reference/html/#spring-cloud-feign

你可能感兴趣的:(浅谈【Feign Client】 开发规范化)