简介

Flyway 是一个开源的数据库版本管理工具,并且支持市面上大多数数据库
企业开发中,一般会使用 Git 来做代码版本管理,而数据库同样也是有版本的
例如新增表、增加索引、修改表结构等,为了确保项目多个环境的一致性
也可以避免版本发布时,由于忘发或漏发的情况导致 bug 的产生

Spring Boot Demo

这里使用一个 Spring Boot 项目作为演示,项目代码见github

项目版本

  • spring boot 3.1.2
  • jdk 17
  • mysql 8.0

整个项目结构如下

├── pom.xml                                             //  maven pom文件
└── src
└── main
├── java
│   └── com
│   └── example
│   └── flywayboot3
│   └── FlywayBoot3Application.java // SpringBoot 启动文件
└── resources
├── application.yml // SpringBoot 配置文件
└── db
└── migration // 迁移脚本
├── R__inc_user.sql // 可重复迁移脚本
├── V1__test_ddl.sql // v1 脚本
└── V2__user_ddl.sql // v2 脚本

maven 依赖如下

 <dependencies>
<!-- web 容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- jdbc连接, flyway自动配置需要用到 JDBC连接 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.1.2</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

配置 MySQL 数据库连接

application.yml

spring:
datasource:
url: jdbc:mysql://192.168.1.83:3306/flyway?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver

接着启动项目,就可以到 flyway 的一些日志信息, 如下

控制台输出

也可以看到数据库新增一张表

flyway表

schame history 表

现在整个数据库之后一张 flyway_schame_history 的表,如果我们需要新增一张表,要怎么操作呢

因为我们使用的 MySQL 数据库,所以新增表脚本如下

V1.0__user_ddl.sql

CREATE TABLE `user`  (
`id` bigint NOT NULL AUTO_INCREMENT ,
`name` varchar(32) NULL ,
PRIMARY KEY (`id`) USING BTREE
);

我们在项目的 resources 新增 db/migration/V1.0__user_dll.sql
然后启动项目
可以看到表 flyway_schame_history 增加一条版本为1.0的记录,同时数据库新增一张user表

flyway表

简单说明各个字段的含义

字段 描述
install_rank 主键,递增
version 数据库版本
description 描述
type 操作类型
script 脚本文件名称
checksum 校验哈希
installed_by 执行用户
installed_on 执行日期
execution_time 执行耗时
success 是否已执行

可以看到我们的脚本名称被分隔成几部分 V1.0__user_dll.sql

以 V 开头,双下划线结束,最后文件,格式如下
V{version}__{description}.{type}
flyway 会忽略低于

flyway 默认会在从 classpath:db/migration 查找脚本

flyway 一些常用配置

在 spring-boot 中已经帮我们自动配置了 flyway, 并抽出一些配置项
让我们可以自己管理
这里只对比较常用的配置项目说明
详见 org.springframework.boot.autoconfigure.flyway.FlywayProperties

@ConfigurationProperties(prefix = "spring.flyway")
public class FlywayProperties {
/**
* 是否开启flyway, 默认开启
*/
private boolean enabled = true;

/**
* sql脚本的位置,默认在db/migration目录下
*/
private List<String> locations = new ArrayList<>(Collections.singletonList("classpath:db/migration"));

/**
* flyway history表名, 默认是flyway_schema_history
* 支持占位符
*/
private String table = "flyway_schema_history";

/**
* 基线版本,默认是1
* 即从V1开始,低于1的版本会忽略(前提是数据库已经存在schame)
*/
private String baselineVersion = "1";

/**
* 基线版本的描述,用于插入基线行的 description 字段
*/
private String baselineDescription = "<< Flyway Baseline >>";

/**
* 当 schame 非空时,是否自动调用 baseline
* 如果配置为了 false,发现非空schame且找不到 flyway table 会直接报错
*/
private boolean baselineOnMigrate;

/**
* 是否禁用清除数据库,默认禁用
*/
private boolean cleanDisabled = true;

/**
* 是否允许不按顺序迁移
*/
private boolean outOfOrder;

}

迁移

在 flyway 中,所有数据库的变化都叫迁移( Migrations )
迁移类型有两种,分别是版本迁移(Versioned migrations),或可重复迁移(Repeatable migrations)
还有一种撤销迁移(Undo migrations)是企业版,这里不做说明

版本迁移(Versioned migrations)有一个 version, 一个 description 和 一个 checksum。
首先 version 必须是唯一的,description 方便用户了解迁移脚本的内容,checksum 是用于校验文件是否更改,
程序启动之后,如果发现 Versioned migrations 的文件内容发生更改,会抛出异常

命名规范如下,前缀V + 版本号(数字+下划线/点)+ 描述 + 分隔符(双下划线) + 文件类型(.sql)
版本迁移

可重复迁移(Repeatable migrations)一个 description 和 一个 checksum,没有version
当checksum改变时(相当于脚本变化),就是插入一条记录,并执行一次

命名规范如下,前缀R + 描述 + 分隔符(双下划线) + 文件类型(.sql)
可重复迁移

详见Migraions

脚本位置

flyway 通过 locations 配置脚本位置

  • 默认配置Java的classpath, classpath:db/migration
  • 支持系统文件,例如 filesystem:/my-project/my-other-folder
  • 支持aws s3, 前缀s3:, 需要依赖 AWS SDK
  • 支持 google gcs, 前缀gcs:, 需要依赖 GCS 的 SDK

基线

baseline 是 flyway 中叫做基线的东西,通过配置 baselineVersion 来设置基线值,
主要是为了兼容非空 schame 的数据库,例如数据库已经存在一些表,可能并不需要执行所有的 Migrate 脚本,
那么,就可以通过设置基线,让 flyway 自动忽略一些脚本

例如,现在有两个脚本

迁移脚本

flyway 的配置如下
设置了 baseline-version = 1, flyway 会自动忽略V1版本(和更小版本)
baseline-on-migrate = true, 在非空 schame 执行一次 baseline

flyway配置

启动项目,flyway_schema_history 表如下

基线初始化

可以看到在非空 schame 的数据库设置 baseline 后,flyway_schema_history会自动生成一条基线数据
并自动忽略对应版本的迁移脚本

dagger 是有 Square 公司开源的一个 JavaAndroid 的依赖注入框架

dagger认为最好的类是那些做实事的类,像 BarcodeDecoderAudioStreamer,而像
BarcodeDecoderFactoryMutableContextWrapper 这些类是最没有用的类,却占用了大量空间, dagger可以替代掉一些工厂类,让你专注于实用类的编写,只需要声明类之间的依赖关系即可

dagger 实现了 JSR-330 的依赖注入标准
dagger1 目前已经停止维护,dagger2 由 google 接手维护了
由于 dagger1 已经废弃了,所以这里只做一个简单介绍,不过多深入分析

使用dagger1

我们以一个打印机服务的例子来演示一下 dagger 依赖注入
假设我们要开发一个打印服务

注入依赖

public class PrintApp {
private final Printer printer; // 打印机
private final PrintDriver printDriver; // 打印机驱动

@Inject // 注入依赖
PrintApp(PrintDriver printDriver, Printer printer) {
this.printDriver = printDriver;
this.printer = printer;
}
}

打印程序依赖 PrinterPrintDriver

public interface Printer {
/**
* 打印
*/
void print(Connection connect);
}

public interface PrintDriver {
/**
* 连接打印机
*/
Connection connect();

}

我们使用 @Inject 注解来通过构造器注入依赖
我们也可以通过字段注入

public class PrintApp {
@Inject
Printer printer;

@Inject
PrintDriver printDriver;
}

注意字段不能是 private
但是 dagger 不支持方法注入

@Inject 有几个限制

  • 无法构造接口类
  • 无法注解第三方类
  • 可配置对象必须配置
    如果配置类没有满足依赖,将无法通过编译

提供依赖

接着我们需要给 PrinterPrintDriver 提供实现

@Module(injects = PrintApp.class, library = true)  // <1> injects表示注入类型, library表示可在外部依赖
public class PrintModule {

@Provides // <2> 方法注解,方法的返回值可用于满足依赖
@Singleton // 单例, 在所有注入的地方dagger都不会重新创建
Printer providerPrinter() {
return new ZebraPrinter();
}

@Provides
PrintDriver providerDriver() {
return new ZebraPrintDriver();
}

}

我们新建一个打印模块 PrintModule, 注解 @Module 用于声明该是一个 dagger 依赖模块
为了让dagger知道我们的模块要注入到那个依赖中,需要声明 injects 的属性,这个会在编译期验证
我们在里面提供可选的依赖,例如在<2>中 我们提供了打印机的实现(ZebraPrinter

public class ZebraPrinter implements Printer {
@Override
public void print(Connection connect) {
if(connect.connected) {
System.out.println("开始打印");
} else {
throw new RuntimeException("打印机未连接");
}
}
}

还有斑马打印驱动

public class ZebraPrintDriver implements PrintDriver {
@Override
public Connection connect() {
// 随机失败
if(System.currentTimeMillis() % 2 > 0) {
System.out.println("打印机连接失败");
return new Connection(false);
}
System.out.println("打印机连接成功");
return new Connection(true);
}
}

构建依赖图

注入 @inject 和提供 @provides 结合可以表达成一个依赖注入图,我们通过 ObjectGraph 来创建

// 通过打印模块来构造一个依赖图
ObjectGraph objectGraph = ObjectGraph.create(new PrintModule());

然后就可以通过get方法实现对PrintApp打印程序的依赖注入

// 通过打印模块来构造一个依赖图
ObjectGraph objectGraph = ObjectGraph.create(new PrintModule());
PrintApp printApp = objectGraph.get(PrintApp.class);

实现打印

public class PrintApp {
@Inject Printer printer;
@Inject PrintDriver printDriver;

void print() {
Connection connect = printDriver.connect();
printer.print(connect);
}

public static void main(String[] args) {
// 通过打印模块来构造一个依赖图
ObjectGraph objectGraph = ObjectGraph.create(new PrintModule());
PrintApp printApp = objectGraph.get(PrintApp.class);
printApp.print();
}
}

运行输出

打印机连接成功
开始打印

结语

dagger1 还支持懒加载,但是由于是过时的技术,这里只做简单的演示

演示代码上传至 github 仓库 dagger1-learn

feign-core 迁移到 maven 的 pom.xml 文件

parent

parent 模块的 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.netflix.feign</groupId>
<artifactId>feign-parent</artifactId>
<version>${revision}</version>

<packaging>pom</packaging>

<name>feign</name>
<description>feign</description>


<properties>
<revision>1.0</revision>

<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<modules>
<module>feign-core</module>
<module>feign-ribbon</module>
</modules>

</project>

feign-core

feign-core 模块的 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-parent</artifactId>
<version>${revision}</version>
</parent>

<packaging>jar</packaging>

<artifactId>feign-core</artifactId>
<name>feign-ribbon</name>
<description>feign-ribbon</description>

<dependencies>
<dependency>
<groupId>com.squareup.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.8.1</version>
</dependency>
<dependency>
<groupId>com.google.mockwebserver</groupId>
<artifactId>mockwebserver</artifactId>
<version>20130505</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>14.0.1</version>
</dependency>
<dependency>
<groupId>com.squareup.dagger</groupId>
<artifactId>dagger</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>

</project>

feign-ribbon

feign-ribbon 模块的 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-parent</artifactId>
<version>${revision}</version>
</parent>

<packaging>jar</packaging>

<artifactId>feign-ribbon</artifactId>
<name>feign-ribbon</name>
<description>feign-ribbon</description>

<dependencies>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>com.squareup.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.8.1</version>
</dependency>
<dependency>
<groupId>com.google.mockwebserver</groupId>
<artifactId>mockwebserver</artifactId>
<version>20130505</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>14.0.1</version>
</dependency>
<dependency>
<groupId>com.squareup.dagger</groupId>
<artifactId>dagger</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</project>

feign-example-cli

feign-example-cli 模块的 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>feign</groupId>
<artifactId>feign-example-cli</artifactId>
<version>${revision}</version>

<packaging>jar</packaging>

<name>feign-example-cli</name>
<description>feign-example-cli</description>

<properties>
<revision>1.0</revision>
</properties>

<dependencies>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>com.squareup.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>1.0.1</version>
</dependency>
</dependencies>
</project>

什么是 Feign

Feign 是一个声明式的、模板化的 HTTP 客户端库,它简化了通过 HTTP 协议进行服务间通信的开发
Feign 提供了一种简洁的编程方式,使得开发者可以定义一个接口,通过注解来描述接口的方法以及与之对应的远程服务的调用。
Feign 底层使用了基于 HTTP 的 RESTful 服务,并提供了一些默认的编码器和解码器,使得开发者无需手动编写大量的网络请求代码,而只需要关注业务逻辑即可

以上来自 chatGPT

简单来说

Feign 的使用非常简单,只要定义接口,就可以实现 http 请求

让我们来学习一下 Feign 的底层是如何实现的吧

前置知识点

dagger

dagger 是一个快速的依赖注入 Java 和 Android 框架

在 feign 的 v1 版本是使用 dagger1 来管理类的依赖和注入的

简单来说就是通过注解 @Inject@Provides@Module 这三个注解声明依赖关系

其中 @Inject 表示实例由框架注入,

@Module 注解表名该类为依赖模块类,@Provides 表示方法可以满足注入依赖的关系

@Inject 用法类似Spring框架的 @Autowired
@Provides 用法类似Spring框架的 @Component

可以通过 dagger 1 简单教程学习一下

获取 Feign 源码

注意:这里的 FeignOpenFeign 开源的 feign 项目, 而不是 Spring-Cloud 开源 spring-cloud-openfeign 项目

先 clone 项目

git clone git@github.com:OpenFeign/feign.git

项目当前版本是 12.4-SNAPSHOT
可以看到有很多模块,且项目代码量已经来到 5w+ 了

为了方便学习,我们从1.x切出一个本地分支 v1,下文的代码来自 v1

git checkout -b v1 origin/1.x   

项目结构如下

.
|-- LICENSE // Apache2.0许可
|-- build.gradle // gradle构建
|-- codequality // checkstyle(忽略)
|-- examples // 演示项目
|-- feign-core // feign核心表(重点学习)
|-- feign-ribbon // ribbon支持
|-- gradle // 一些gradle脚本
|-- gradle.properties // 一些gradle配置
`-- settings.gradle // gradle聚合项目配置

由于 feign 是16年才转成 Maven 管理依赖

Maven 改造(可选)

gradle 用得不多,为了方便编译测试,所以改成了 maven

篇幅问题,pom文件查看访问 feign-core 迁移到maven的pom文件

改造之后,结构如下

|-- examples             
| `-- feign-example-cli
|-- feign-core
| |-- pom.xml
| |-- src
|-- feign-ribbon
| |-- pom.xml
| |-- src
|-- pom.xml

Feign 简单例子

在进入源码之前,我们通过官方一个 demo 学习一下

假设,我们想要查看某个 github 项目的贡献者有哪些

Github 已经提供了 REST API ,可以直接请求,如下图

获取Github仓库贡献者

我们使用 Feign 方式来完成这次请求

新建 Github 类

public interface GitHub {
@GET
@Path("/repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
}

新建Contributor类

public class Contributor {
public String login;
public int contributions;
}

新建GitHubExample类

public class GitHubExample {

public static void main(String... args) {
// <1> 新建代理接口
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());

// <2> 调用代理方法(获取netflix的feign项目的贡献者列表)
List<Contributor> contributors = github.contributors("netflix", "feign");
for (Contributor contributor : contributors) {
// 打印输出
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}

@Module(overrides = true, library = true)
static class GsonModule { // <3> GsonModule 模块
@Provides // 方法的返回可作为注入实例
@Singleton // 单例,全局唯一
Map<String, Decoder> decoders() { // 提供http响应解码器
return ImmutableMap.of("GitHub", jsonDecoder);
}

// gson实现的json解码器
final Decoder jsonDecoder = new Decoder() {
Gson gson = new Gson();

@Override
public Object decode(String methodKey, Reader reader, TypeToken<?> type) {
return gson.fromJson(reader, type.getType());
}
};
}
}

输出

贡献者列表响应

可以看到,我们像调用 Java 方法一样就可以实现远程调用

下面分析代码

<1> 中,通过 Feign 创建了 Github 代理类,下文都将将参数(如Github.class)叫做代理类,返回对象叫做代理实例或 feign 实例
,指定了代理类的请求域名地址(https://api.github.com),这样就可以不用在方法声明地址参数 Uri
并提供了自定义模块 GsonModule

<2>中,github.contributors 方法的底层是执行了Feign的动态代理类的方法,所以可以通过 http 发送了请求并将 json 响应解析到 List<Contributor>

<3>GsonModule 类声明为一个 dagger module,并提供了一个 可处理 json 响应的 Decoder 覆盖 feign 提供的默认解码器,由于使用这个 dagger module,不在同一个包下,所以配置 library = true, 而
overrides = true 则允许覆盖其他 @Module 的方法

feign-core 的结构

feign-core 项目(v1)现在还比较小巧,代码只有3千行不到,但是各个核心划分都比较成熟

看看结构

|-- main
| `-- java
| `-- feign
| |-- Client.java // http客户端接口(内部类默认实现)
| |-- Contract.java // contract协议层
| |-- Feign.java // Feign抽象类,接口代理入口
| |-- FeignException.java // 异常
| |-- MethodHandler.java // 类方法处理器(代理方法的调用)
| |-- MethodMetadata.java // 方法元数据(地址、参数、返回类型等)
| |-- ReflectiveFeign.java // Feign的反射实现(唯一实现)
| |-- Request.java // http请求封装
| |-- RequestTemplate.java // http请求模板(用于构建请求Request)
| |-- Response.java // http响应封装
| |-- RetryableException.java // 重试异常
| |-- Retryer.java // 重试器
| |-- Target.java // 代理对象Target
| |-- Wire.java // 日志封装
| `-- codec // 编码和解码
| |-- BodyEncoder.java // http body编码器
| |-- Decoder.java // 解码器
| |-- Decoders.java // 匹配解码器实现
| |-- ErrorDecoder.java // 错误解码器
| |-- FormEncoder.java // 表单解码器
| |-- SAXDecoder.java // SAX解码器
| `-- ToStringDecoder.java // 字符串解码器

梳理一下

feign-core 架构图

feign-core架构图

这里我只是按照我自己的理解将 feign 画出分层架构(只是为了更好理解和行文,如果有不同意见,欢迎评论区讨论)

tip: 下面源码解析都是围绕架构图来进行的,建议边阅读边回顾 feign-core 架构图

feign 接口层

feign 接口层有两个组件,TargetFeign

Feign 是一个抽象接口,主要作用就创建 Feign实例,可以理解是一个 Builder 工厂,把内部复杂的逻辑封装起来,暴露几个简单的方法,让用户可以很轻易就构建 Feign 实例

public abstract class Feign {

/**
* 新建代理实例
*/
public abstract <T> T newInstance(Target<T> target);

/**
* 创建一个代理类实例
* @param apiType 实例类型
* @param url 域名地址(直接合方法上的地址拼接)
* @param modules dagger模块(使用门槛还是比较高,提供了 GsonModule
*/
public static <T> T create(Class<T> apiType, String url, Object... modules) {
return create(new HardCodedTarget<T>(apiType, url), modules);
}

public static <T> T create(Target<T> target, Object... modules) {
return create(modules).newInstance(target);
}

public static Feign create(Object... modules) {
Object[] modulesForGraph = ImmutableList.builder()
.add(new Defaults()) // 默认模块实现
.add(new ReflectiveFeign.Module()) // 反射实现模块
.add(Optional.fromNullable(modules).or(new Object[]{}))
.build().toArray();
return ObjectGraph.create(modulesForGraph).get(Feign.class); // dagger 对象依赖图
}
}

在前面的例子中, 我们调用了 create(Class<T>, String, Object) 方法,

最终来到了 create(Object... modules)方法,通过 dagger 框架获取 Feign 的实例,

可以看到,feign 给我们提供了两个默认的模块,Defaults 模块都是一些简单的实现

@dagger.Module(complete = false, injects = Feign.class, library = true)
public static class Defaults {

@Provides SSLSocketFactory sslSocketFactory() {
return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault());
}

@Provides Client httpClient(Client.Default client) { return client; }

@Provides Retryer retryer() { return new Retryer.Default(); }

@Provides Wire noOp() { return new NoOpWire(); }

@Provides Map<String, Options> noOptions() { return ImmutableMap.of(); }

@Provides Map<String, BodyEncoder> noBodyEncoders() { return ImmutableMap.of(); }

@Provides Map<String, FormEncoder> noFormEncoders() { return ImmutableMap.of(); }

@Provides Map<String, Decoder> noDecoders() { return ImmutableMap.of(); }

@Provides Map<String, ErrorDecoder> noErrorDecoders() { return ImmutableMap.of();}
}

基本都是空实现,所以需要我们的自定义模块 GsonModule

另一个 ReflectiveFeign.Module 静态类就提供了 Feign 实例依赖

public class ReflectiveFeign extends Feign {

@dagger.Module(complete = false,// Config
injects = Feign.class, library = true// provides Feign
)
public static class Module {

@Provides Feign provideFeign(ReflectiveFeign in) {
return in;
}
}
}

可以看到 ReflectiveFeign 提供了自身做为 Feign 的实例

Target 作为 newInstance(Target<T> target)方法的参数,其实就只是保存了代理类类型而已
方便后面解析代理类

public interface Target<T> extends Function<RequestTemplate, Request> {
/**
* 保存代理类类型
*/
Class<T> type();

/* 配置实例的名字,无特殊要求 */
String name();

/* 返回实例对象的http url */
String url();

/** 从请求模板生成一个请求 */
@Override
public Request apply(RequestTemplate input);

// 内部类,硬编码Target,只是保存了代理类类型
public static class HardCodedTarget<T> implements Target<T> {
private final Class<T> type;
private final String name;
private final String url;

public HardCodedTarget(Class<T> type, String url) {
this(type, url, url);
}

public HardCodedTarget(Class<T> type, String name, String url) {
this.type = checkNotNull(type, "type");
this.name = checkNotNull(Strings.emptyToNull(name), "name");
this.url = checkNotNull(Strings.emptyToNull(url), "url");
}

@Override
public Class<T> type() {
return type;
}

@Override
public String name() {
return name;
}

@Override
public String url() {
return url;
}

@Override
public Request apply(RequestTemplate input) {
if (input.url().indexOf("http") != 0)
input.insert(0, url());
return input.request();
}
}
}

contract 层

contract 层就一个组件 Contract,单独把它划分一个层次是因为感觉它比较独立,

它实现了 JSR-311 部分注解的功能,例如 @Path@Get@Post@PathParam

JSR-311

Contract 的主要作用是将类方法解析成请求方法元数据 MethodMetadata

public final class Contract {   
// 解析method
public static MethodMetadata parseAndValidatateMetadata(Method method) {
MethodMetadata data = new MethodMetadata();
// 保存方法返回的泛型类型
data.returnType(TypeToken.of(method.getGenericReturnType()));
// 解析方法的javadoc方法串
data.configKey(Feign.configKey(method));

// 方法解析
for (Annotation methodAnnotation : method.getAnnotations()) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
if (http != null) {
data.template().method(http.value());
} else if (annotationType == RequestTemplate.Body.class) {
String body = RequestTemplate.Body.class.cast(methodAnnotation).value();
if (body.indexOf('{') == -1) {
data.template().body(body);
} else {
data.template().bodyTemplate(body); // 请求body模板
}
} else if (annotationType == Path.class) {
// http 请求path, 追加到url
data.template().append(Path.class.cast(methodAnnotation).value());
} else if (annotationType == Produces.class) {
// 解析http请求头 Content-Type
data.template().header(CONTENT_TYPE, Joiner.on(',').join(((Produces) methodAnnotation).value()));
} else if (annotationType == Consumes.class) {
// 解析http请求头 Accept
data.template().header(ACCEPT, Joiner.on(',').join(((Consumes) methodAnnotation).value()));
}
}

Class<?>[] parameterTypes = method.getParameterTypes();

// 参数注解
Annotation[][] parameterAnnotationArrays = method.getParameterAnnotations();
int count = parameterAnnotationArrays.length; // 参数个数
for (int i = 0; i < count; i++) {
boolean hasHttpAnnotation = false;

Class<?> parameterType = parameterTypes[i];
Annotation[] parameterAnnotations = parameterAnnotationArrays[i];
if (parameterAnnotations != null) {
for (Annotation parameterAnnotation : parameterAnnotations) {
// 省略处理参数注解的代码
}

}

if (parameterType == URI.class) {
data.urlIndex(i); // 表示url参数的下标(方法的url参数优先级最高)
} else if (!hasHttpAnnotation) {
data.bodyIndex(i); // json body的参数下标
}
}

return data;
}
}

MethodMetadata 只是一个简单的 pojo,作用是将contract 层和方法处理层隔离来

减少核心业务逻辑的耦合,代码如下(主要是以下字段组成)

public final class MethodMetadata implements Serializable {
MethodMetadata() {}

private String configKey; // 方法全限定名称

private transient TypeToken<?> returnType; // 方法的返回类型

private Integer urlIndex; // url参数的下标,没有则为空

private Integer bodyIndex; // json实体类的下标,没有则为空

private RequestTemplate template = new RequestTemplate(); // 请求模板

private List<String> formParams = Lists.newArrayList(); //表单参数字段名

/**
* 存储请求方法的一些参数的位置下标(顺序)和名称,蚕例如path或query
* key表示位置下标,value表示 PathParam 的name 或 PathParam 的name
*/
private SetMultimap<Integer, String> indexToName = LinkedHashMultimap.create();

// 省略getter setter
}

方法处理层

这一层有三个组件,其中 MethodHandler 是整个框架的核心,它聚合了许多其他组件来完成方法的执行

final class MethodHandler {
private final MethodMetadata metadata; // 方法元数据,通过Contract层解析得到
private final Target<?> target; // 代理类实例
private final Client client; // http客户端接口
/**
* 重试组件,因为组件的实现是有状态的,每次请求都需要重新生成,
* 所以使用Provider方式注入
*/
private final Provider<Retryer> retryer;
private final Wire wire; // 日志扩展
private final Function<Object[], RequestTemplate> buildTemplateFromArgs; // 处理方法参数
private final Options options; // http请求配置
private final Decoder decoder; // 解码器
private final ErrorDecoder errorDecoder; // 错误解码器
}

MethodHandler 包含了这么多东西,都是通过构造方法传入的

而且外部的入参则是通过依赖注入框架来获取

MethodHandler 对象实例的构造过程如下( ReflectiveFeign.ParseHandlersByName 的 apply 方法)

/**
* 将代理类解析成MethodHandler映射
*/
@Override
public Map<String, MethodHandler> apply(Target key) {
// 将代理类的方法解析成MethodMetadata元数据
Set<MethodMetadata> metadata = parseAndValidatateMetadata(key.type());
ImmutableMap.Builder<String, MethodHandler> builder = ImmutableMap.builder();
// 将MethodMetadata转换成MethodHandler
for (MethodMetadata md : metadata) {
// options 是 http 一些配置
Options options = forMethodOrClass(this.options, md.configKey());
if (options == null) {
options = new Options();
}
// 解码器
Decoder decoder = forMethodOrClass(decoders, md.configKey());
if (decoder == null
&& (md.returnType().getRawType() == void.class
|| md.returnType().getRawType() == Response.class)) {
// 方法返回类型是 Void或者Response,使用默认ToStringDecoder解析器
decoder = new ToStringDecoder();
}
if (decoder == null) {
throw noConfig(md.configKey(), Decoder.class);
}
// 错误解码器
ErrorDecoder errorDecoder = forMethodOrClass(errorDecoders, md.configKey());
if (errorDecoder == null) {
errorDecoder = ErrorDecoder.DEFAULT;
}
Function<Object[], RequestTemplate> buildTemplateFromArgs;
if (!md.formParams().isEmpty() && !md.template().bodyTemplate().isPresent()) {
FormEncoder formEncoder = forMethodOrClass(formEncoders, md.configKey());
if (formEncoder == null) {
throw noConfig(md.configKey(), FormEncoder.class);
}
// 表单编码模板
buildTemplateFromArgs = new BuildFormEncodedTemplateFromArgs(md, formEncoder);
} else if (md.bodyIndex() != null) {
BodyEncoder bodyEncoder = forMethodOrClass(bodyEncoders, md.configKey());
if (bodyEncoder == null) {
throw noConfig(md.configKey(), BodyEncoder.class);
}
// body编码模板
buildTemplateFromArgs = new BuildBodyEncodedTemplateFromArgs(md, bodyEncoder);
} else {
buildTemplateFromArgs = new BuildTemplateFromArgs(md);
}
// 用工程创建MethodHandler
builder.put(md.configKey(),
factory.create(key, md, buildTemplateFromArgs, options, decoder, errorDecoder));
}
return builder.build();
}

其中的 decoders、errorDecoders、options 等都是使用注入方式构造

可由内部提供默认的实例或外部 Module 注入

static final class ParseHandlersByName {
@Inject
ParseHandlersByName(Map<String, Options> options,
Map<String, BodyEncoder> bodyEncoders,
Map<String, FormEncoder> formEncoders,
Map<String, Decoder> decoders,
Map<String, ErrorDecoder> errorDecoders,
Factory factory) {
this.options = options;
this.bodyEncoders = bodyEncoders;
this.formEncoders = formEncoders;
this.decoders = decoders;
this.factory = factory;
this.errorDecoders = errorDecoders;
}
}

首先通过 JDK 动态代理,让代理类最终执行方法走 MethodHandlerinvoke 方法

public Object invoke(Object[] argv) throws Throwable {
// 通过方法参数构建请求模板
RequestTemplate template = buildTemplateFromArgs.apply(argv);
// 重试器是有状态的,所以这里需要重新get (内部是new)
Retryer retryer = this.retryer.get();
while (true) {
try {
// 执行http请求并解码响应
return executeAndDecode(metadata.configKey(), template, metadata.returnType());
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
continue;
}
}
}

其中,比较重要的 executeAndDecode 方法,直接看代码

/**
* 执行请求并解码
*/
public Object executeAndDecode(String configKey, RequestTemplate template, TypeToken<?> returnType)
throws Throwable {
// 经过target包一层,其实RequestTemplate就内置一个Request
Request request = target.apply(new RequestTemplate(template));
// 日志扩展
wire.wireRequest(target, request);
// http 请求
Response response = execute(request);
try {
response = wire.wireAndRebufferResponse(target, response);
// 正常http响应
if (response.status() >= 200 && response.status() < 300) {
if (returnType.getRawType().equals(Response.class)) {
// 直接返回原生
return response;
} else if (returnType.getRawType() == URI.class && !response.body().isPresent()) {
ImmutableList<String> location = response.headers().get(LOCATION);
if (!location.isEmpty())
return URI.create(location.get(0));
} else if (returnType.getRawType() == void.class) {
return null;
}
// 解码器
return decoder.decode(configKey, response, returnType);
} else {
// 错误解码器
return errorDecoder.decode(configKey, response, returnType);
}
} catch (Throwable e) {
ensureBodyClosed(response); // 关闭响应流
if (IOException.class.isInstance(e))
throw errorReading(request, response, IOException.class.cast(e));
throw e;
}
}

通过依赖下层http处理层和 RetryerDecoder 的能力来完成方法调用

这里的组件都是可以接口形式,所以扩展性都比价好

http 处理层

http 处理层主要是对 HTTP 请求做了一层简单的封装

对外提供了一个简单的接口

public interface Client {
/**
* 执行http请求
*/
Response execute(Request request, Options options) throws IOException;
}

Request 是一个 pojo 类,定义了 http 请求的基本字段

public final class Request {
private final String method; // 请求方法, 例如 GET/POST/PUT/DELETE 等
private final String url; // 请求地址, 完整链接,包含参数等
private final ImmutableListMultimap<String, String> headers; // http 请求头
private final Optional<String> body; // http request body
}

Options 可以配置 http 的请求超时参数

public static class Options {
private final int connectTimeoutMillis; // 连接超时
private final int readTimeoutMillis; // 响应超时
}

在 v1 版本的 Client 是 jdk net 包来实现 http 请求功能

第一步先是初始化连接实例,配置一些请求参数

HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
// 连接实例
final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
sslCon.setSSLSocketFactory(sslContextFactory.get()); // http 支持
}
// 一些http请求参数
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod(request.method());

Integer contentLength = null;
for (Entry<String, String> header : request.headers().entries()) {
if (header.getKey().equals(CONTENT_LENGTH))
contentLength = Integer.valueOf(header.getValue());
connection.addRequestProperty(header.getKey(), header.getValue());
}

if (request.body().isPresent()) {
if (contentLength != null) {
connection.setFixedLengthStreamingMode(contentLength);
} else {
connection.setChunkedStreamingMode(8196);
}
connection.setDoOutput(true);
// 将输出流写入到body中
new ByteSink() {
public OutputStream openStream() throws IOException {
return connection.getOutputStream();
}
}.asCharSink(UTF_8).write(request.body().get());
}
return connection;
}

然后发送请求

Response convertResponse(HttpURLConnection connection) throws IOException {
int status = connection.getResponseCode();
String reason = connection.getResponseMessage();

ImmutableListMultimap.Builder<String, String> headers = ImmutableListMultimap.builder();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// response message
if (field.getKey() != null)
headers.putAll(field.getKey(), field.getValue());
}

Integer length = connection.getContentLength();
if (length == -1)
length = null;
InputStream stream;
if (status >= 400) {
stream = connection.getErrorStream();
} else {
// 获取响应输入流
stream = connection.getInputStream();
}
Reader body = stream != null ? new InputStreamReader(stream) : null;
// 从reader中创建Response
return Response.create(status, reason, headers.build(), body, length);
}

至此 http 请求完成,返回的 Response 由外部决定由什么 Decoder 来解码处理

在上面的例子,我们使用 Gson 将响应 body 解析成 返回类型

结语

通过阅读源码,我们了解到 feign-core 就是通过 JDK 的动态代理和 Java 反射实现了远程过程调用,主要流程就是类方法解析、构造请求模板、生成请求、执行 http 方法、http 响应解码等过程,当然更重要是学习如何编写一个扩展性更好的软件,在软件迭代初期,怎么去更好得架构和设计

就我个人的理解,应该还是得从简洁出发(最小实现),当然简洁不意味着简单,而是让功能恰到好处,在封装和复杂之间做出取舍,在实现和定义之间也要慎重衡量

feign-core 第一版本已经是10年前的事情了,目前的版本已经来到 12.4-SNAPSHOT
而且 spring-cloud-feign 也变得非常复杂,源码解析需要花费更多时间

日拱一卒,功不唐捐

0%