专栏目录
37. 实现异步的客户端封装配置管理的意义与设计 44.避免链路信息丢失做的设计(2) 45. 实现公共日志记录 42.SpringCloudGateway 现有的可供分析的请求日志以及缺陷 41. SpringCloudGateway 基本流程讲解(1) 4.maven依赖回顾以及项目框架结构 39. 改造 resilience4j 粘合 WebClient 13.UnderTow 核心配置 5.所有项目的parent与spring-framework-common说明 30. FeignClient 实现重试 35. 验证线程隔离正确性 11.Log4j2 监控相关 19.Eureka的服务端设计与配置 23.订制Spring Cloud LoadBalancer 38. 实现自定义 WebClient 的 NamedContextFactory 28.OpenFeign的生命周期-进行调用 14.UnderTow AccessLog 配置介绍 41. SpringCloudGateway 基本流程讲解(2) 21.Spring Cloud LoadBalancer简介 40. spock 单元测试封装的 WebClient(下) 20. 启动一个 Eureka Server 集群 17.Eureka的实例配置 3.Eureka Server 与 API 网关要考虑的问题 15.UnderTow 订制 18.Eureka的客户端核心设计和配置 22.Spring Cloud LoadBalancer核心源码 44.避免链路信息丢失做的设计(1) 16.Eureka架构和核心概念 40. spock 单元测试封装的 WebClient(上) 8.理解 NamedContextFactory 9.如何理解并定制一个Spring Cloud组件 26.OpenFeign的组件 33. 实现重试、断路器以及线程隔离源码 27.OpenFeign的生命周期-创建代理 10.使用Log4j2以及一些核心配置 43.为何 SpringCloudGateway 中会有链路信息丢失 29.Spring Cloud OpenFeign 的解析(1) 24.测试Spring Cloud LoadBalancer 1. 背景 25.OpenFeign简介与使用 12.UnderTow 简介与内部原理 7.从Bean到SpringCloud 2.微服务框架需要考虑的问题 6.微服务特性相关的依赖说明 32. 改进负载均衡算法 34.验证重试配置正确性 31. FeignClient 实现断路器以及线程隔离限流的思路 36. 验证断路器正确性

29.Spring Cloud OpenFeign 的解析(1)

unknown
• 阅读 697

29.Spring Cloud OpenFeign 的解析(1)

本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent

在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务映射,通过健康检查接口判断实例健康状态,然后直接使用 OpenFeign 生成对应域名的 Feign Client。Spring Cloud 生态中,对 OpenFeign 进行了封装,其中的 Feign Client 的各个组件,也是做了一定的定制化,可以实现在 OpenFeign Client 中集成服务发现与负载均衡。在此基础上,我们还结合了 Resilience4J 组件,实现了微服务实例级别的线程隔离,微服务方法级别的断路器以及重试。

我们先来分析下 Spring Cloud OpenFeign

Spring Cloud OpenFeign 解析

从 NamedContextFactory 入手

Spring Cloud OpenFeign 的 github 地址:https://github.com/spring-cloud/spring-cloud-openfeign

首先,根据我们之前分析 spring-cloud-loadbalancer 的流程,我们先从继承 NamedContextFactory 的类入手,这里是 FeignContext,通过其构造函数,得到其中的默认配置类:

FeignContext.java

public FeignContext() {
    super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}

从构造方法可以看出,默认的配置类是:FeignClientsConfiguration。我们接下来详细分析这个配置类中的元素,并与我们之前分析的 OpenFeign 的组件结合起来。

负责解析类元数据的 Contract,与 spring-web 的 HTTP 注解相结合

为了开发人员更好上手使用和理解,最好能实现使用 spring-web 的 HTTP 注解(例如 @RequestMapping@GetMapping 等等)去定义 FeignClient 接口。在 FeignClientsConfiguration 中就是这么做的:

FeignClientsConfiguration.java

@Autowired(required = false)
private FeignClientProperties feignClientProperties;

@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();

@Autowired(required = false)
private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();

@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
    boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
    return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);
}

@Bean
public FormattingConversionService feignConversionService() {
    FormattingConversionService conversionService = new DefaultFormattingConversionService();
    for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
        feignFormatterRegistrar.registerFormatters(conversionService);
    }
    return conversionService;
}

其核心提供的 Feign 的 Contract 就是 SpringMvcContractSpringMvcContract 主要包含两部分核心逻辑:

  • 定义 Feign Client 专用的 Formatter 与 Converter 注册
  • 使用 AnnotatedParameterProcessor 来解析 SpringMVC 注解以及我们自定义的注解

定义 Feign Client 专用的 Formatter 与 Converter 注册

首先,Spring 提供了类型转换机制,其中单向的类型转换为实现 Converter 接口;在 web 应用中,我们经常需要将前端传入的字符串类型的数据转换成指定格式或者指定数据类型来满足我们调用需求,同样的,后端开发也需要将返回数据调整成指定格式或者指定类型返回到前端页面(在 Spring Boot 中已经帮我们做了从 json 解析和返回对象转化为 json,但是某些特殊情况下,比如兼容老项目接口,我们还可能使用到),这个是通过实现 Formatter 接口实现。举一个简单的例子:

定义一个类型:

@Data
@AllArgsConstructor
public class Student {
    private final Long id;
    private final String name;
}

我们定义可以通过字符串解析出这个类的对象的 Converter,例如 "1,zhx" 就代表 id = 1 并且 name = zhx:

public class StringToStudentConverter implements Converter<String, Student> {
    @Override
    public Student convert(String from) {
        String[] split = from.split(",");
        return new Student(
                Long.parseLong(split[0]),
                split[1]);
    }
}

然后将这个 Converter 注册:

@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToStudentConverter());
    }
}

编写一个测试接口:

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/string-to-student")
    public Student stringToStudent(@RequestParam("student") Student student) {
        return student;
    }
}

调用 /test/string-to-student?student=1,zhx,可以看到返回:

{
    "id": 1,
    "name": "zhx"
}

同样的,我们也可以通过 Formatter 实现:

public class StudentFormatter implements Formatter<Student> {
    @Override
    public Student parse(String text, Locale locale) throws ParseException {
        String[] split = text.split(",");
        return new Student(
                Long.parseLong(split[0]),
                split[1]);
    }

    @Override
    public String print(Student object, Locale locale) {
        return object.getId() + "," + object.getName();
    }
}

然后将这个 Formatter 注册:

@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new StudentFormatter());
    }
}

Feign 也提供了这个注册机制,为了和 spring-webmvc 的注册机制区分开,使用了 FeignFormatterRegistrar 继承了 FormatterRegistrar 接口。然后通过定义 FormattingConversionService 这个 Bean 实现 Formatter 和 Converter 的注册。例如:

假设我们有另一个微服务需要通过 FeignClient 调用上面这个接口,那么就需要定义一个 FeignFormatterRegistrar 将 Formatter 注册进去:

@Bean
public FeignFormatterRegistrar getFeignFormatterRegistrar() {
    return registry -> {
        registry.addFormatter(new StudentFormatter());
    };
}

之后我们定义 FeignClient:

@FeignClient(name = "test-server", contextId = "test-server")
public interface TestClient {
    @GetMapping("/test/string-to-student")
    Student get(@RequestParam("student") Student student);
}

在调用 get 方法时,会调用 StudentFormatter 的 print 将 Student 对象输出为格式化的字符串,例如 {"id": 1,"name": "zhx"} 会变成 1,zhx

AnnotatedParameterProcessor 来解析 SpringMVC 注解以及我们自定义的注解

AnnotatedParameterProcessor 是用来将注解解析成 AnnotatedParameterContext 的 Bean,AnnotatedParameterContext 包含了 Feign 的请求定义,包括例如前面提到的 Feign 的 MethodMetadata 即方法元数据。默认的 AnnotatedParameterProcessor 包括所有 SpringMVC 对于 HTTP 方法定义的注解对应的解析,例如 @RequestParam 注解对应的 RequestParamParameterProcessor

RequestParamParameterProcessor.java

public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
    //获取当前参数属于方法的第几个
    int parameterIndex = context.getParameterIndex();
    //获取参数类型
    Class<?> parameterType = method.getParameterTypes()[parameterIndex];
    //要保存的解析的方法元数据 MethodMetadata
    MethodMetadata data = context.getMethodMetadata();

    //如果是 Map,则指定 queryMap 下标,直接返回
    //这代表一旦使用 Map 作为 RequestParam,则其他的 RequestParam 就会被忽略,直接解析 Map 中的参数作为 RequestParam
    if (Map.class.isAssignableFrom(parameterType)) {
        checkState(data.queryMapIndex() == null, "Query map can only be present once.");
        data.queryMapIndex(parameterIndex);
        //返回解析成功
        return true;
    }
    RequestParam requestParam = ANNOTATION.cast(annotation);
    String name = requestParam.value();
    //RequestParam 的名字不能是空
    checkState(emptyToNull(name) != null, "RequestParam.value() was empty on parameter %s", parameterIndex);
    context.setParameterName(name);

    Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
    //将 RequestParam 放入 方法元数据 MethodMetadata
    data.template().query(name, query);
    //返回解析成功
    return true;
}

我们也可以实现 AnnotatedParameterProcessor 来自定义我们的注解,配合 SpringMVC 的注解一起使用去定义 FeignClient

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

29.Spring Cloud OpenFeign 的解析(1)

点赞
收藏
评论区
推荐文章

暂无数据

unknown
unknown
Lv1
男 · rrrr · rrrrrrrr
rrrrr
文章
0
粉丝
17
获赞
0
热门文章

暂无数据