【软件架构】

本文章主要记录软件开发过程中遇到的相关术语和技术名词并对技术名词和相关术语做语言解释


jwt常见问题回复与简介

  • 什么是JWT(JSON Web Token)?
    • JWT是一种用于身份验证和授权的开放标准(RFC 7519),它是基于JSON格式的轻量级安全令牌。JWT由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。通常,JWT被用于在不同的系统之间传递安全性的声明信息,以便用户在跨域应用中进行身份验证。
  • JWT有什么好处,能干啥?

    JWT的主要优点包括:

    • 轻量级:JWT是基于JSON格式的,相比于传统的XML格式,它更加轻巧且易于解析。
    • 自包含:JWT中包含了用户的一些声明信息,因此无需查询数据库来验证用户身份,有效降低了服务器的负担。
    • 无状态性:JWT本身是无状态的,所有的信息都被包含在令牌中,服务器端无需保存任何状态信息,使得系统易于扩展和维护。
    • 安全性:JWT中使用签名进行验证,防止数据被篡改,确保了数据的完整性和安全性。
  • JWT能够实现的功能包括:
    • 用户认证:JWT可以用于用户身份验证,客户端通过携带有效的JWT令牌来请求受限资源。
    • 单点登录(SSO):用户在一个应用登录后,可以获取JWT令牌,然后在其他相互信任的应用中使用该令牌来免登录。
    • 授权信息传递:JWT中的载荷可以包含用户的角色、权限等信息,服务器可以根据这些信息做出授权决策(私密信息不要使用JWT 可以被人解析)。
  1. JWT的组成:
    JWT由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
  • 头部(Header):通常由两部分组成,令牌类型(typ)和使用的签名算法(alg)。示例:{"alg": "HS256", "typ": "JWT"}
  • 载荷(Payload):包含用户的声明信息,例如过期时间用户ID、角色、权限等。可以自定义其他声明信息。示例:{"sub": "1234567890", "name": "John Doe", "admin": true}
  • 签名(Signature):通过对头部和载荷进行签名,保证数据的完整性和安全性。签名的生成通常使用密钥进行加密,只有持有密钥的服务器才能验证签名的有效性。
  1. 如何使用JWT?
    使用JWT通常包括以下步骤:
  • 用户登录:用户提供用户名和密码进行登录验证。
  • 服务器验证:服务器验证用户提供的信息是否正确,并生成JWT令牌。
  • 令牌传递:服务器将JWT令牌发送回客户端。
  • 客户端使用:客户端在后续请求中携带JWT令牌,发送给服务器进行身份验证和授权。
  1. 系统中的JWT是如何使用的,在什么时候使用的?
    在一个系统中,JWT通常用于实现用户身份验证和授权。当用户登录成功后,服务器会生成一个JWT令牌并发送给客户端,客户端保存该令牌,并在后续的请求中将令牌添加到请求头或请求参数中。服务器在收到请求时,会解析JWT令牌并验证其合法性和有效性,从而判断用户是否已经登录以及是否有权限访问请求的资源。

    JWT的使用可以简化系统的身份验证和授权流程,提高系统的安全性和性能。同时,由于JWT本身是无状态的,不需要在服务器端保存会话信息,使得系统更易于扩展和维护。引入jwt,并使用生成token

使用步骤

当使用JWT进行用户身份验证和授权时,通常包含以下步骤:

  1. 用户登录认证:

    • 用户提供用户名和密码进行登录。
    • 服务器接收用户提交的用户名和密码,通过UserService类中的authenticateUser方法验证用户名和密码是否正确。
    • 如果用户名和密码验证通过,认为用户登录成功,可以继续下一步生成JWT令牌。
  2. 生成JWT令牌:

    • 在登录成功后,服务器使用JwtUtils工具类中的generateJwtToken方法生成JWT令牌。
    • generateJwtToken方法接收三个参数:
      • subject:即载荷(Payload)中的"sub"字段,通常用于标识用户的唯一标识,例如用户ID、用户名等。
      • expirationMillis:令牌的有效期,以毫秒为单位。令牌过期后将无效,需要重新登录获取新的令牌。
      • SECRET_KEY:用于签名的密钥,确保密钥保密,不要泄露给他人。
    • generateJwtToken方法会使用Jwts类的builder方法构建JWT令牌,并设置令牌的头部(Header)、载荷(Payload)、过期时间(Expiration)等信息。
    • 最后,使用指定的签名算法(此处使用HS256)对令牌进行签名,生成签名(Signature)部分,得到最终的JWT令牌。
  3. 返回JWT令牌:

    • 服务器将生成的JWT令牌返回给客户端,通常通过将令牌添加到响应的Header或Body中。
  4. 客户端携带JWT令牌:

    • 在后续的请求中,客户端需要在请求头或请求参数中携带JWT令牌,通常使用"Authorization"字段来传递令牌。
    • 例如,可以将JWT令牌添加到请求头的"Authorization"字段中,格式为"Bearer <JWT_Token>“,其中”<JWT_Token>"为生成的JWT令牌。
  5. 解析和验证JWT令牌:

    • 服务器接收到带有JWT令牌的请求后,需要对令牌进行解析和验证,以验证用户身份和授权信息。
    • 使用JwtUtils工具类中的parseJwtToken方法对JWT令牌进行解析和验证。
    • parseJwtToken方法接收一个JWT令牌作为参数,并使用之前设置的密钥SECRET_KEY对令牌进行解析。
    • 如果解析和验证成功,parseJwtToken方法将返回载荷(Payload)中的"sub"字段的值,即之前设置的用户唯一标识。
    • 如果解析和验证失败,可能是令牌过期、签名验证不通过等,服务器将拒绝请求,要求客户端重新登录获取新的令牌。
  6. 用户访问控制:

    • 在服务器端验证JWT令牌成功后,可以根据令牌中携带的用户信息(例如用户角色、权限等)进行访问控制和授权决策。
    • 服务器可以根据令牌中的信息判断用户是否有权限访问请求的资源,并执行相应的业务逻辑。
//客户端生成令牌:
public void jwtBuilderTest(){
        JwtBuilder builder = Jwts.builder();
        JwtBuilder jwtBuilder = builder
                // 头部
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                // 载荷数据/过期时间
                .claim("id", 10001)
                .claim("nickName", "老王")
                // 设置过期时间,这里设置为当前时间加上一小时builder = {DefaultJwtBuilder@14248}
                .setExpiration(new Date(System.currentTimeMillis() + 1000*60*60));
    }

微服务架构java多模块项目,微服务之间相互调用方法

本篇主要记录市面上常用的微服务调用方法


1. HTTP/RESTful API调用

  • 每个微服务模块提供自己的RESTful API,其他微服务可以通过发送HTTP请求来调用服务。这种方式是最常见和简单的实现方式之一。在Spring Boot应用中,可以使用Spring MVC或Spring WebFlux等框架来快速构建RESTful API。

2. RPC(远程过程调用)

  • 使用RPC框架实现微服务之间的直接方法调用,而不需要通过HTTP协议。一些常用的Java RPC框架包括gRPC、Apache Thrift、Dubbo等。通过定义接口和实现类,可以在微服务之间进行远程方法调用。

3. 消息队列

  • 使用消息队列作为微服务之间的通信中介,通过发送和接收消息来进行通信。常用的消息队列包括RabbitMQ、Apache Kafka等。通过消息队列可以实现微服务之间的解耦合和异步通信,适用于需要处理大量消息的场景。

4. 服务代理

  • 使用服务代理或API网关作为微服务的统一入口,对外提供统一的API,并负责将请求转发给后端微服务。常见的服务代理包括Netflix Zuul、Spring Cloud Gateway等。服务代理可以实现路由、负载均衡、安全认证等功能,简化了微服务之间的通信。

5. 事件驱动

  • 使用事件驱动的方式实现微服务之间的通信,例如使用事件总线或事件流处理平台。一个微服务可以发布事件,其他微服务订阅并响应这些事件,实现服务之间的解耦合和异步通信。

6. Feign

  • 适用场景:适用于基于HTTP的微服务架构,希望简化服务调用过程、提高开发效率的场景。特别适合于与Spring Cloud等微服务框架集成的项目。

这些方式可以根据具体项目需求和技术选型来选择。在Java多模块项目中,可以将不同的微服务模块实现为独立的模块,每个模块负责实现一个特定的业务功能,通过以上方式来实现微服务之间的相互调用。

Feign调用的实现原理及代码举例

Feign调用的工作原理是通过动态代理技术将接口方法的调用转换为HTTP请求,并发送到远程服务。下面是Feign调用的一般工作流程:

  1. 定义Feign客户端接口
    • 在客户端项目中定义一个Java接口,该接口用于描述要调用的远程服务的API。接口的方法对应了远程服务的不同接口或资源路径。
      // 指定要调用的微服务名称 service-pm-service
      @FeignClient(name = "service-pm-service", fallbackFactory = PostEntryConfigFallbackFactory.class)
      public interface PostEntryConfigFeign extends PostEntryConfigApi {
      
      }
      
      public interface PostEntryConfigApi {
      
          /**
          * 分页查询
          * @param params 分页查询参数
          * @return PageR
          */
          @PostMapping("/postEntryConfig/queryPage")// 指定远程服务的URL路径
          PageR queryPage(@RequestParam Map<String, Object> params);// 定义要调用的远程方法
      }
  1. 创建Feign客户端
    • 使用@FeignClient注解标注接口,指定要调用的远程服务的名称或URL。Feign根据该注解创建一个动态代理对象,该代理对象实现了接口定义的方法。
          @Resource
          private PostEntryConfigFeign postEntryConfigFeign;// 注入Feign客户端接口
  2. 调用远程服务方法
    • 在客户端代码中直接调用Feign客户端接口的方法,就像调用本地方法一样。Feign客户端会拦截这些方法调用,并将它们转换为HTTP请求。
          /**
           * 分页查询
           *
           * @param params 分页查询参数
           * @return R
           */
          @GetMapping("/list")
          @RequiresPermissions("pm:postentryconfig:list")
          public R list(@RequestParam Map<String, Object> params) {
              PageR page = postEntryConfigFeign.queryPage(params);// 调用远程服务的方法
              AssertUtils.isNull(page);
              return R.ok().put("page", page);
          }
  3. HTTP请求转换
    • 当调用Feign客户端接口的方法时,Feign会根据方法的注解(如@GetMapping@PostMapping等)以及方法的参数,构建对应的HTTP请求。例如,根据@GetMapping注解指定的路径和方法参数,生成一个GET请求。

名词解释:

什么是RESTful API?

        要弄清楚什么是RESTful API,首先要弄清楚什么是REST。REST 全称:REpresentational State Transfer,英文翻译过来就是“表现层状态转化”。用一句话通俗解释一下。 RESTful:用URL定位资源用HTTP动词(GET、POST、PUT、DELETE)描述操作。只要记住这句话也就不难理解了

     Resource:资源,即数据。
     Representational:某种表现形式,比如用JSON,XML,JPEG等;
     State Transfer:状态变化。通过HTTP动词实现

        URI(统一资源标识符):可以唯一标识一个资源
        URL(统一资源定位符):可以提供找到某个资源的路径,比如平时最常见的网址:

一般一个URL也是一个URI,比如上面的网址,即URL可以看做是URI的子集,在图书领域中一本书都有唯一的一个isbn编号,这个编号其实也是URI。

RESTful API由后台也就是SERVER来提供前端来调用。前端调用API向后台发起HTTP请求,后台响应请求将处理结果反馈给前端。也就是说RESTful 是典型的基于HTTP的协议。那么RESTful API有哪些设计原则和规范呢?

     1、资源。首先是弄清楚资源的概念。资源就是网络上的一个实体,一段文本,一张图片或者一首歌曲。资源总是要通过一种载体来反应它的内容。文本可以用TXT,也可以用HTML或者XML、图片可以用JPG格式或者PNG格式,JSON是现在最常用的资源表现形式。

     2、统一接口。RESTful风格的数据元操CRUD(create,read,update,delete)分别对应HTTP方法:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,这样就统一了数据操作的接口。

     3、URI。可以用一个URI(统一资源定位符)指向资源,即每个URI都对应一个特定的资源。要获取这个资源访问它的URI就可以,因此URI就成了每一个资源的地址或识别符。一般的,每个资源至少有一个URI与之对应,最典型的URI就是URL。

     4、有/无状态。所谓无状态即所有的资源都可以URI定位,而且这个定位与其他资源无关,也不会因为其他资源的变化而变化。有状态和无状态的区别,举个例子说明一下,例如要查询员工工资的步骤为第一步:登录系统。第二步:进入查询工资的页面。第三步:搜索该员工。第四步:点击姓名查看工资。这样的操作流程就是有状态的,查询工资的每一个步骤都依赖于前一个步骤,只要前置操作不成功,后续操作就无法执行。如果输入一个URL就可以得到指定员工的工资,则这种情况就是无状态的,因为获取工资不依赖于其他资源或状态,且这种情况下,员工工资是一个资源,由一个URL与之对应可以通过HTTP中的GET方法得到资源,这就是典型的RESTful风格。

说了这么多,到底RESTful长什么样子的呢?下面我们举一些例子。

GET:http://www.xxx.com/source/id 获取指定ID的某一类资源。

GET:http://www.xxx.com/friends/123表示获取ID为123的用户的好友列表。如果不加id就表示获取所有用户的好友列表。

POST:http://www.xxx.com/friends/123表示为指定ID为123的用户新增好友。其他的操作类似就不举例了。

在具体项目中我遇到的RESTful API 调用方式:

无状态调用:

// 删除客户表数据
termMultiAppServiceClient.dropTable();
termMultiAppServiceClient.createTable();

/**
 * 删除表
  */
@RequestMapping(value = "/termMultiApp/dropTable", method = RequestMethod.POST)
void dropTable();

/**
 * 创建表
 */
@RequestMapping(value = "/termMultiApp/createTable", method = RequestMethod.POST)
void createTable();

什么是RPC?

RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

RPC需要解决的问题

1、Call ID映射

我们怎么告诉远程机器我们要调用funA,而不是funB或者funC呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用funA,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。

所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。

【Note】当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。

2、序列化和反序列化

客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。

但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。

【Note】这时候就需要客户端把参数先转成一个字节流(编码),传给服务端后,再把字节流转成自己能读取的格式(解码)。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。

3、网络传输

远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。

【Note】网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。

所以,要实现一个RPC框架,其实只需要把以上三点实现了就基本完成了。Call ID映射可以直接使用函数字符串,也可以使用整数ID。映射表一般就是一个哈希表。序列化反序列化可以自己写,也可以使用Protobuf或者FlatBuffers之类的。网络传输库可以自己写socket,或者用asio,ZeroMQ,Netty之类。

4、RPC的调用流程图

常用的RPC框架
  • gRPC是Google公布的开源软件,基于最新的HTTP2.0协议,并支持常见的众多编程语言。我们知道HTTP2.0是基于二进制的HTTP协议升级版本,目前各大浏览器都在快马加鞭的加以支持。这个RPC框架是基于HTTP协议实现的,底层使用到了Netty框架的支持。
  • Thrift是Facebook的一个开源项目,主要是一个跨语言的服务开发框架。它有一个代码生成器来对它所定义的IDL定义文件自动生成服务代码框架。用户只要在其之前进行二次开发就行,对于底层的RPC通讯等都是透明的。不过这个对于用户来说的话需要学习特定领域语言这个特性,还是有一定成本的。
  • Dubbo是阿里集团开源的一个极为出名的RPC框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是及其鲜明的特色。同样的远程接口是基于Java Interface,并且依托于spring框架方便开发。可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致。

OpenFeign核心原理

第一部分,脱离于SpringCloud,原始的Feign是什么样的?

日常开发中,使用Feign调用的步骤有

第一步:引入依赖

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
     <version>2.2.5.RELEASE</version>
</dependency>

第二步:在启动引导类加上@EnableFeignClients注解

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableConfigurationProperties
@EnableDiscoveryClient
@EnableFeignClients    //调用feign 
public class ManagerApp extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(ManagerApp.class, args);
    }

    /**
     * 如此配置打包后可以war包才可在tomcat下使用
     *
     * @param application
     * @return
     */
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(ManagerApp.class);
    }

}

第三步:写个FeignClient接口

@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderApiClient {

    @GetMapping
    Order queryOrder(@RequestParam("orderId") Long orderId);
}

// 或者 这种写法的,这种的方法是使用静态代理方式

@FeignClient(name = "service-plat-service", fallbackFactory = SysDeptFallbackFactory.class)
public interface SysDeptFeign extends SysDeptApi {

}

public interface SysDeptApi {
    /**
     * 根据参数获取部门列表
     *
     * @param params 参数
     * @return 部门列表
     */
    @GetMapping("/sysDept/queryList")
    List<SysDeptEntity> queryList(@RequestParam Map<String, Object> params);
}

我们要调用的时候只需要注入SysDeptFeign 对象就行了 如图所示:

@RestController
@RequestMapping("/sys/dept")
public class SysDeptController extends AbstractController {
    @Resource
    private SysDeptFeign sysDeptFeign;

}

虽然使用方便,但这并不是Feign最原始的使用方式,而是SpringCloud整合Feign之后的使用方式

Feign本身有自己的使用方式,也有类似Spring MVC相关的的注解

public interface OrderApiClient {

    @RequestLine("GET /order/{orderId}")
    Order queryOrder(@Param("orderId") Long orderId);

}

OrderApiClient对象需要手动通过Feign.builder()来创建

public class FeignDemo {

    public static void main(String[] args) {
        OrderApiClient orderApiClient = Feign.builder()
                .target(OrderApiClient.class, "http://localhost:8088");
        orderApiClient.queryOrder(9527L);
    }

}

第二部分,Feign的本质,Feign的核心组件有哪些,整个执行链路是什么样的?

Feign底层其实是基于JDK动态代理来的。

所以Feign.builder()最终构造的是一个代理对象

Feign在构建动态代理的时候,会去解析方法上的注解和参数,获取Http请求需要用到基本参数以及和这些参数和方法参数的对应关系。比如Http请求的url、请求体是方法中的第几个参数、请求头是方法中的第几个参数等等。之后在构建Http请求时,就知道请求路径以及方法的第几个参数对应是Http请求的哪部分数据

当调用动态代理方法的时候,Feign就会将上述解析出来的Http请求基本参数和方法入参组装成一个Http请求。然后发送Http请求,获取响应,再根据响应的内容的类型将响应体的内容转换成对应的类型

这就是Feign的大致原理

在整个Feign动态代理生成和调用过程中,需要依靠Feign的一些核心组件来协调完成,下面是Feign的一些核心组件,这些核心组件可以通过 Feign.builder()进行替换。由于组件太多,我就挑几个核心组件展开来说

public abstract class BaseBuilder<B extends BaseBuilder<B>> {
    private final B thisB;
    protected final List<RequestInterceptor> requestInterceptors = new ArrayList();
    protected ResponseInterceptor responseInterceptor;
    protected Logger.Level logLevel;
    protected Contract contract;
    protected Retryer retryer;
    protected Logger logger;
    protected Encoder encoder;
    protected Decoder decoder;
    protected boolean closeAfterDecode;
    protected QueryMapEncoder queryMapEncoder;
    protected ErrorDecoder errorDecoder;
    protected Request.Options options;
    protected InvocationHandlerFactory invocationHandlerFactory;
    protected boolean dismiss404;
    protected ExceptionPropagationPolicy propagationPolicy;
    protected List<Capability> capabilities;
}

Contract

public interface Contract {
    List<MethodMetadata> parseAndValidateMetadata(Class<?> var1);

前面在说Feign在构建动态代理的时候,会去解析方法上的注解和参数,获取Http请求需要用到基本参数

而这个Contract接口的作用就是用来干解析这件事的

Contract的默认实现是解析Feign自己原生注解的

public static class Default extends DeclarativeContract {
        static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$");

        public Default() {
            super.registerClassAnnotation(Headers.class, (header, data) -> {
                String[] headersOnType = header.value();
                Util.checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.", new Object[]{data.configKey()});
                Map<String, Collection<String>> headers = toMap(headersOnType);
                headers.putAll(data.template().headers());
                data.template().headers((Map)null);
                data.template().headers(headers);
            });
            super.registerMethodAnnotation(RequestLine.class, (ann, data) -> {
                String requestLine = ann.value();
                Util.checkState(Util.emptyToNull(requestLine) != null, "RequestLine annotation was empty on method %s.", new Object[]{data.configKey()});
                Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
                if (!requestLineMatcher.find()) {
                    throw new IllegalStateException(String.format("RequestLine annotation didn't start with an HTTP verb on method %s", data.configKey()));
                } else {
                    data.template().method(HttpMethod.valueOf(requestLineMatcher.group(1)));
                    data.template().uri(requestLineMatcher.group(2));
                    data.template().decodeSlash(ann.decodeSlash());
                    data.template().collectionFormat(ann.collectionFormat());
                }
            });
            super.registerMethodAnnotation(Body.class, (ann, data) -> {
                String body = ann.value();
                Util.checkState(Util.emptyToNull(body) != null, "Body annotation was empty on method %s.", new Object[]{data.configKey()});
                if (body.indexOf(123) == -1) {
                    data.template().body(body);
                } else {
                    data.template().bodyTemplate(body);
                }

            });
            super.registerMethodAnnotation(Headers.class, (header, data) -> {
                String[] headersOnMethod = header.value();
                Util.checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.", new Object[]{data.configKey()});
                data.template().headers(toMap(headersOnMethod));
            });
            super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
                String annotationName = paramAnnotation.value();
                Parameter parameter = data.method().getParameters()[paramIndex];
                String name;
                if (Util.emptyToNull(annotationName) == null && parameter.isNamePresent()) {
                    name = parameter.getName();
                } else {
                    name = annotationName;
                }

                Util.checkState(Util.emptyToNull(name) != null, "Param annotation was empty on param %s.", new Object[]{paramIndex});
                this.nameParam(data, name, paramIndex);
                Class<? extends Param.Expander> expander = paramAnnotation.expander();
                if (expander != Param.ToStringExpander.class) {
                    data.indexToExpanderClass().put(paramIndex, expander);
                }

                if (!data.template().hasRequestVariable(name)) {
                    data.formParams().add(name);
                }

            });
            super.registerParameterAnnotation(QueryMap.class, (queryMap, data, paramIndex) -> {
                Util.checkState(data.queryMapIndex() == null, "QueryMap annotation was present on multiple parameters.", new Object[0]);
                data.queryMapIndex(paramIndex);
            });
            super.registerParameterAnnotation(HeaderMap.class, (queryMap, data, paramIndex) -> {
                Util.checkState(data.headerMapIndex() == null, "HeaderMap annotation was present on multiple parameters.", new Object[0]);
                data.headerMapIndex(paramIndex);
            });
        }

        private static Map<String, Collection<String>> toMap(String[] input) {
            Map<String, Collection<String>> result = new LinkedHashMap(input.length);
            String[] var2 = input;
            int var3 = input.length;

            for(int var4 = 0; var4 < var3; ++var4) {
                String header = var2[var4];
                int colon = header.indexOf(58);
                String name = header.substring(0, colon);
                if (!result.containsKey(name)) {
                    result.put(name, new ArrayList(1));
                }

                ((Collection)result.get(name)).add(header.substring(colon + 1).trim());
            }

            return result;
        }
    }

解析时,会为每个方法生成一个MethodMetadata对象

public final class MethodMetadata implements Serializable {
    private static final long serialVersionUID = 1L;
    private String configKey;
    private transient Type returnType;    //方法返回值类型
    private Integer urlIndex;
    private Integer bodyIndex;            //请求体对应第几个方法参数
    private Integer headerMapIndex;       //请求头对应的第几个方法参数
    private Integer queryMapIndex;
    private boolean alwaysEncodeBody;
    private transient Type bodyType;      //请求体的方法参数类型
    private final RequestTemplate template = new RequestTemplate();

MethodMetadata就封装了Http请求需要用到基本参数以及这些参数和方法参数的对应关系

SpringCloud在整合Feign的时候,为了让Feign能够识别Spring MVC的注解,所以就自己实现了Contract接口

public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
    private static final Log LOG = LogFactory.getLog(SpringMvcContract.class);
    private static final String ACCEPT = "Accept";
    private static final String CONTENT_TYPE = "Content-Type";
    private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
    private static final TypeDescriptor ITERABLE_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Iterable.class);
    private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
    private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;
    private final Map<String, Method> processedMethods;
    private final ConversionService conversionService;
    private final ConvertingExpanderFactory convertingExpanderFactory;
    private ResourceLoader resourceLoader;
    private final boolean decodeSlash;

Encoder

public interface Encoder {
    Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;

    void encode(Object var1, Type var2, RequestTemplate var3) throws EncodeException;

    public static class Default implements Encoder {
        public Default() {
        }
        public void encode(Object object, Type bodyType, RequestTemplate template) {
            if (bodyType == String.class) {
                template.body(object.toString());
            } else if (bodyType == byte[].class) {
                template.body((byte[])((byte[])object), (Charset)null);
            } else if (object != null) {
                throw new EncodeException(String.format("%s is not a type supported by this encoder.", object.getClass()));
            }
        }
    }
}

通过名字也可以看出来,这个其实用来编码的,具体的作用就是将请求体对应的方法参数序列化成字节数组

  1. Feign默认的Encoder实现只支持请求体对应的方法参数类型为String和字节数组

    public interface Encoder {
        Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
    
        void encode(Object var1, Type var2, RequestTemplate var3) throws EncodeException;
    
        public static class Default implements Encoder {
            public Default() {
            }
    
            public void encode(Object object, Type bodyType, RequestTemplate template) {
                if (bodyType == String.class) {
                    template.body(object.toString());
                } else if (bodyType == byte[].class) {
                    template.body((byte[])((byte[])object), (Charset)null);
                } else if (object != null) {
                    throw new EncodeException(String.format("%s is not a type supported by this encoder.", object.getClass()));
                }
            }
        }
    }
    

    如果是其它类型,比如说请求体对应的方法参数类型为AddOrderRequest.class类型,此时就无法对AddOrderRequest对象进行序列化

    这就导致默认情况下,这个Encoder的实现很难用,于是乎,Spring就实现了Encoder接口

    public class SpringEncoder implements Encoder {
        private static final Log log = LogFactory.getLog(SpringEncoder.class);
        private final SpringFormEncoder springFormEncoder;
        private final ObjectFactory<HttpMessageConverters> messageConverters;
        private final FeignEncoderProperties encoderProperties;
        private final ObjectProvider<HttpMessageConverterCustomizer> customizers;
    
        public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
            this(new SpringFormEncoder(), messageConverters);
        }

    可以将任意请求体对应的方法参数类型对象序列化成字节数组

Decoder

public interface Decoder {
    Object decode(Response var1, Type var2) throws IOException, DecodeException, FeignException;

    public static class Default extends StringDecoder {
        public Default() {
        }

Decoder的作用恰恰是跟Encoder相反,Encoder是将请求体对应的方法参数序列化成字节数组

而Decoder其实就是将响应体由字节流反序列化成方法返回值类型的对象

Decoder默认情况下跟Encoder的默认情况是一样的,只支持反序列化成字节数组或者是String,所以,Spring也同样实现了Decoder,扩展它的功能,可以将响应体对应的字节流反序列化成任意返回值类型对象

public class SpringDecoder implements Decoder {
    private final ObjectFactory<HttpMessageConverters> messageConverters;
    private final ObjectProvider<HttpMessageConverterCustomizer> customizers;

    /** @deprecated */
    @Deprecated
    public SpringDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        this(messageConverters, new EmptyObjectProvider());
    }

    public SpringDecoder(ObjectFactory<HttpMessageConverters> messageConverters, ObjectProvider<HttpMessageConverterCustomizer> customizers) {
        this.messageConverters = messageConverters;
        this.customizers = customizers;
    }

    public Object decode(final Response response, Type type) throws IOException, FeignException {
        if (!(type instanceof Class) && !(type instanceof ParameterizedType) && !(type instanceof WildcardType)) {
            throw new DecodeException(response.status(), "type is not an instance of Class or ParameterizedType: " + type, response.request());
        } else {
            List<HttpMessageConverter<?>> converters = ((HttpMessageConverters)this.messageConverters.getObject()).getConverters();
            this.customizers.forEach((customizer) -> {
                customizer.accept(converters);
            });
            HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type, converters);
            return extractor.extractData(new FeignResponseAdapter(response));
        }
    }

Client

public interface Client {
    Response execute(Request var1, Request.Options var2) throws IOException;

从接口方法的参数和返回值其实可以看出,这其实就是动态代理对象最终用来执行Http请求的组件

默认实现就是通过JDK提供的HttpURLConnection来的

    public static class Default implements Client {
        private final SSLSocketFactory sslContextFactory;
        private final HostnameVerifier hostnameVerifier;
        private final boolean disableRequestBuffering;

        public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
            this.sslContextFactory = sslContextFactory;
            this.hostnameVerifier = hostnameVerifier;
            this.disableRequestBuffering = true;
        }

        public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier, boolean disableRequestBuffering) {
            this.sslContextFactory = sslContextFactory;
            this.hostnameVerifier = hostnameVerifier;
            this.disableRequestBuffering = disableRequestBuffering;
        }

        public Response execute(Request request, Request.Options options) throws IOException {
            HttpURLConnection connection = this.convertAndSend(request, options);
            return this.convertResponse(connection, request);
        }

除了这个默认的,Feign还提供了基于HttpClient和OkHttp实现的,

在项目中,要想替换默认的实现,只需要引入相应的依赖,在构建Feign.builder()时设置一下就行了

SpringCloud环境底下会根据引入的依赖自动进行设置

除了上述的三个实现,最最重要的当然是属于它基于负载均衡的实现

如下是OpenFeign用来整合Ribbon的核心实现

public class LoadBalancerFeignClient implements client {

    static final Request.Options DEFAULT_0PTIONS = new Request.Options();

    private final Client delegate;

    private CachingSpringLoadBalancerFactory lbclientFactory;

    private SpringclientFactory clientFactory;

    public LoadBalancerFeignclient(Client delegate,
                                   CachingSpringLoadBalancerFactory lbclientFactory,
                                   SpringClientFactory clientFactory) {
        this.delegate = delegate;
        this.lbClientFactory = lbClientFactory;
        this.clientFactory = clientFactory;
    }
}

InvocationHandlerFactory

对于JDK动态代理来说,必须得实现InvocationHandler才能创建动态代理,里面的invoke方法实现就是动态代理走的核心逻辑,而InvocationHandlerFactory其实就是创建InvocationHandler的工厂

public interface InvocationHandlerFactory {
    InvocationHandler create(Target var1, Map<Method, MethodHandler> var2);

所以,这里就可以猜到,通过InvocationHandlerFactory创建的InvocationHandler应该就是Feign动态代理执行的核心逻辑,InvocationHandlerFactory默认实现是下面这个

public static final class Default implements InvocationHandlerFactory {
        public Default() {
        }

        public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
            return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
        }
    }

SpringCloud环境下默认也是使用它的这个默认实现,所以,我们直接去看看InvocationHandler的实现类FeignInvocationHandler

    static class FeignInvocationHandler implements InvocationHandler {
        private final Target target;
        private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;

        FeignInvocationHandler(Target target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
            this.target = (Target)Util.checkNotNull(target, "target", new Object[0]);
            this.dispatch = (Map)Util.checkNotNull(dispatch, "dispatch for %s", new Object[]{target});
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (!"equals".equals(method.getName())) {
                if ("hashCode".equals(method.getName())) {
                    return this.hashCode();
                } else {
                    return "toString".equals(method.getName()) ? this.toString() : ((InvocationHandlerFactory.MethodHandler)this.dispatch.get(method)).invoke(args);
                }
            } else {
                try {
                    Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                    return this.equals(otherHandler);
                } catch (IllegalArgumentException var5) {
                    return false;
                }
            }
        }

        public boolean equals(Object obj) {
            if (obj instanceof FeignInvocationHandler) {
                FeignInvocationHandler other = (FeignInvocationHandler)obj;
                return this.target.equals(other.target);
            } else {
                return false;
            }
        }

        public int hashCode() {
            return this.target.hashCode();
        }

        public String toString() {
            return this.target.toString();
        }
    }

从实现可以看出,除了Object类的一些方法,最终会调用方法对应的MethodHandler的invoke方法

所以注意注意,这个MethodHandler就封装了Feign执行Http调用的核心逻辑,很重要,后面还会提到

虽然说默认情况下SpringCloud使用是默认实现,最终使用FeignInvocationHandler

但是当其它框架整合SpringCloud生态的时候,为了适配OpenFeign,有时会自己实现InvocationHandler

比如常见的限流熔断框架Hystrix和Sentinel都实现了自己的InvocationHandler

RequestInterceptor

public interface RequestInterceptor {
    void apply(RequestTemplate var1);
}

RequestInterceptor它其实是一个在发送请求前的一个拦截接口,通过这个接口,在发送Http请求之前再对Http请求的内容进行修改,比如我们可以设置一些接口需要的公共参数,如鉴权token之类的

Retryer

在SpringCloud下,使用的是下面这个实现,SpringCloud下默认是不会进行重试


    Retryer NEVER_RETRY = new Retryer() {
        public void continueOrPropagate(RetryableException e) {
            throw e;
        }

        public Retryer clone() {
            return this;
        }
    };

这是一个重试的组件,默认实现如下

    public static class Default implements Retryer {
        private final int maxAttempts;
        private final long period;
        private final long maxPeriod;
        int attempt;
        long sleptForMillis;

        public Default() {
            this(100L, TimeUnit.SECONDS.toMillis(1L), 5);
        }

        public Default(long period, long maxPeriod, int maxAttempts) {
            this.period = period;
            this.maxPeriod = maxPeriod;
            this.maxAttempts = maxAttempts;
            this.attempt = 1;
        }

默认情况下,最大重试5次

小总结

这一节主要是介绍了7个Feign的核心组件以及Spring对应的扩展实现

接口作用Feign默认实现Spring实现
Contract解析方法注解和参数,将Http请求参数和方法参数对应Contract.DefaultSpringMvcContract
Encoder将请求体对应的方法参数序列化成字节数组Encoder.DefaultSpringEncoder
Decoder将响应体的字节流反序列化成方法返回值类型对象Decoder.DefaultSpringDecoder
Client发送Http请求Client.DefaultLoadBalancerFeignClient
InvocationHandlerFactoryInvocationHandler工厂,动态代理核心逻辑InvocationHandlerFactory.Default
RequestInterceptor在发送Http请求之前,再对Http请求的内容进行拦截修改
Retryer重试组件Retryer.Default

除了这些之外,还有一些其它组件这里就没有说了,比如日志级别Logger.Level,日志输出Logger,有兴趣的可以自己查看

第三部分,SpringCloud是如何把Feign融入到自己的生态的?

SpringCloud在整合Feign的时候,主要是分为两部分

  • 核心组件重新实现,支持更多SpringCloud生态相关的功能

  • 将接口动态代理对象注入到Spring容器中

第一部分核心组件重新实现前面已经都说过了,这里就不再重复了

至于第二部分我们就来好好讲一讲,Spring是如何将接口动态代理对象注入到Spring容器中的

将FeignClient接口注册到Spring中

使用OpenFeign时,必须加上@EnableFeignClients,这个注解就是OpenFeign的发动机

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
    String[] value() default {};

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] defaultConfiguration() default {};

    Class<?>[] clients() default {};
}

@EnableFeignClients最后通过@Import注解导入了一个FeignClientsRegistrar

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    private ResourceLoader resourceLoader;
    private Environment environment;

    FeignClientsRegistrar() {
    }

FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar,所以最终Spring在启动的时候会调用registerBeanDefinitions方法实现

第四部分,OpenFeign有几种配置方式,各种配置方式的优先级是什么样的?

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值