springcloud之通过openfeign优化服务调用方式

写在前面

源码 。
在前面的文章中我们实际上已经完成了优惠券模块微服务化的改造,但是其中还是有比较多可以优化和增强的地方,本文就先来对服务间的通信方式进行优化,具体就是使用openfeign来替换调原来的webclient。下面我们就开始吧!

1:为什么要替换webclient

使用webclient进行服务间调用的方式可能如下:

webClientBuilder.build()
    // 声明这是一个POST方法
    .post()
    // 声明服务名称和访问路径
    .uri("http://coupon-calculation-serv/calculator/simulate")
    // 传递请求参数的封装
    .bodyValue(order)
    .retrieve()
    // 声明请求返回值的封装类型
    .bodyToMono(SimulationResponse.class)
    // 使用阻塞模式来获取结果
    .block()

这段代码有如下的不足:

1:和业务代码耦合,如请求地址,请求方式这些其实和业务是没有任何关系的,不符合指责隔离的原则
2:每个接口调用都需要写类似的重复代码,编码的效率低

针对以上的问题,springcloud给出的解决方案是openfeign ,可以认为openfeign是一种rpc框架允许我们通过好像调用一个本地的方法一样来调用远端的服务。

2:实战改造

2.1:引入openfeign依赖

首先我们需要在coupon-customer-impl的pom中引入openfeign的基础依赖:



    org.springframework.cloud
    spring-cloud-starter-openfeign

2.2:定义服务的service

我们以调用template服务为例来进行改造,因此首先在coupon-customer-impl模块中定义如下的service:

@FeignClient(value = "coupon-template-serv-feign", path = "/template")
public interface TemplateService {
    // 读取优惠券
    @GetMapping("/getTemplate")
    CouponTemplateInfo getTemplate(@RequestParam("id") Long id);
    
    // 批量获取
    @GetMapping("/getBatch")
    Map<Long, CouponTemplateInfo> getTemplateInBatch(@RequestParam("ids") Collection<Long> ids);
}

在注解@FeignClient中定义了要访问的服务名称以及要web接口的基础路径这样就不用重复在方法上配置了,通过注解@XxxMapping定义的接口的访问路径信息,通过方法的参数来定义入参信息,这样发起服务调用的完整信息就都全了。

2.3:改造接口调用

我们来修改接口/coupon-customer/simulateOrder 来执行试算,当前代码如下:

public SimulationResponse simulateOrderPrice(SimulationOrder order) {
    ...
    return webClientBuilder.build().post()
//                .uri("http://coupon-calculation-serv/calculator/simulate")
            .uri("http://coupon-calculation-serv-feign/calculator/simulate")
            .bodyValue(order)
            .retrieve()
            .bodyToMono(SimulationResponse.class)
            .block();
}            

修改为openfeign后如下:

@Autowired
private CalculationService calculationService;
public SimulationResponse simulateOrderPrice(SimulationOrder order) {
    List couponInfos = Lists.newArrayList();
    ...
    System.out.println("calculate by openfeign...");
    return calculationService.simulate(order);
}

最后还需要在main函数上增加注解@EnableFeignClients(basePackages = { "dongshi.daddy" })来设置需要扫描的openfeign服务接口所在的包路径。具体的大家可自行测试。效果是一样的。

3:openfeign原理分析

实战重要,但原理更重要,所以一起来看一波原理吧!

当我们在main上增加了@EnableFeignClients(basePackages = { "dongshi.daddy" })注解后,就会扫描指定包路径下标注了@FeignClient注解的接口,使用jdk的动态代理技术生成动态代理类,之后会将这个生成的动态代理类放到spring容器中,最后注入到需要的类中,这个过程如下:
springcloud之通过openfeign优化服务调用方式_第1张图片
看到这里不知道你有没有疑问,这个扫描包的过程是怎么开始的,其实秘密藏在@EnableFeignClients注解中,该注解如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    ...
}

注意在注解上使用了@Import注解,spring会调用类FeignClientsRegistrar的registerBeanDefinitions方法,如下:

org.springframework.cloud.openfeign.FeignClientsRegistrar#registerBeanDefinitions
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    ...
    // 注册feign客户端(重要!!!)
    registerFeignClients(metadata, registry);
}

registerFeignClients方法如下:

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 最终存储所有openfeign的接口
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        ...
        Set<String> basePackages = getBasePackages(metadata);
        for (String basePackage : basePackages) {
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }
    else {
        ...
    }

    for (BeanDefinition candidateComponent : candidateComponents) {
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            // verify annotated class is an interface
            ...
            // 注册feign客户端
            registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

registerFeignClients方法如下:

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
        Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    ...
    FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
    ...
    BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
        ...
        // 获取基于jdk的动态代理类
        return factoryBean.getObject();
    });
    ...
}

factoryBean.getObject方法最终调用到如下方法:

feign.ReflectiveFeign#newInstance
public <T> T newInstance(Target<T> target) {
    // 解析openfeign方法为MethodHandler,作为方法代理
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      ...
    }
    // 封装methodToHandler创建动态代理要使用的InvocationHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
    // 生成动态代理
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    ...
    // 返回动态代理
    return proxy;
  }

到这里就成功获取动态代理类了。总结这个过程如下:

1:项目加载:在项目的启动阶段,EnableFeignClients 注解扮演了“启动开关”的角色,它使用 Spring 框架的 Import 注解导入了 FeignClientsRegistrar 类,开始了 OpenFeign 组件的加载过程。
2:扫包:FeignClientsRegistrar 负责 FeignClient 接口的加载,它会在指定的包路径下扫描所有的 FeignClients 类,并构造 FeignClientFactoryBean 对象来解析 FeignClient 接口。
3:解析 FeignClient 注解:FeignClientFactoryBean 有两个重要的功能,一个是解析 FeignClient 接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中,动态代理构造是由更下一层的 ReflectiveFeign 完成的。
4:构建动态代理对象:ReflectiveFeign 包含了 OpenFeign 动态代理的核心逻辑,它主要负责创建出 FeignClient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个重要任务,一个是解析 FeignClient 接口上各个方法级别的注解,将其中的远程接口 URL、接口类型(GET、POST 等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要任务是将这些 MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实现了 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到 FeignClient 接口上。这样一来,所有发生在 FeignClient 接口上的调用,最终都会由它背后的动态代理对象来承接。

最后上述流程中解析接口中方法和注解信息为MethodHandler的过程在如下方法中完成:

// org.springframework.cloud.openfeign.support.SpringMvcContract#processAnnotationOnMethod
// 解析FeignClient接口方法级别上的RequestMapping注解
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
   // 省略部分代码...
   
   // 如果方法上没有使用RequestMapping注解,则不进行解析
   // 其实GetMapping、PostMapping等注解都属于RequestMapping注解
   if (!RequestMapping.class.isInstance(methodAnnotation)
         && !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
      return;
   }

   // 获取RequestMapping注解实例
   RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
   // 解析Http Method定义,即注解中的GET、POST、PUT、DELETE方法类型
   RequestMethod[] methods = methodMapping.method();
   // 如果没有定义methods属性则默认当前方法是个GET方法
   if (methods.length == 0) {
      methods = new RequestMethod[] { RequestMethod.GET };
   }
   checkOne(method, methods, "method");
   data.template().method(Request.HttpMethod.valueOf(methods[0].name()));

   // 解析Path属性,即方法上写明的请求路径
   checkAtMostOne(method, methodMapping.value(), "value");
   if (methodMapping.value().length > 0) {
      String pathValue = emptyToNull(methodMapping.value()[0]);
      if (pathValue != null) {
         pathValue = resolve(pathValue);
         // 如果path没有以斜杠开头,则补上/
         if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
            pathValue = "/" + pathValue;
         }
         data.template().uri(pathValue, true);
         if (data.template().decodeSlash() != decodeSlash) {
            data.template().decodeSlash(decodeSlash);
         }
      }
   }

   // 解析RequestMapping中定义的produces属性
   parseProduces(data, method, methodMapping);

   // 解析RequestMapping中定义的consumer属性
   parseConsumes(data, method, methodMapping);

   // 解析RequestMapping中定义的headers属性
   parseHeaders(data, method, methodMapping);
   data.indexToExpander(new LinkedHashMap<>());
}

写在后面

参考文章列表

你可能感兴趣的:(springcloud,spring,cloud,spring,openfeign)