如何基于 Spring Cloud Feign 实现强类型接口调用RESTful服务

如何基于 Spring Cloud Feign 实现强类型接口调用RESTful服务

背景

编程语言的强弱类型

编程语言有强、弱类型之分,进一步,还有动态、静态之分;比方说 Java、C# 是强类型的,而且是静态语言;那么JavaScript、php是弱类型的,而且是动态语言。

强类型静态语言 常常会被称为类型安全,强类型的语言一般在编译期会做强制类型检查,提前避免这些类型错误。

弱类型动态语言 虽然也有类型的概念,但是比较松散灵活,弱类型动态语言大多是解释型语言,一般没有强制类型检查,类型问题一般要在运行期才会暴露出来。

强弱类型语言各有优劣,相互补充,各有适用的场景;比方说服务器端开发,我们经常用强类型的;前端界面开发,我们经常会用这种 JavaScript 弱类型的,所以他们各有适用的场景。

32.强类型与弱类型接口方案

服务 API 接口的强弱类型

对于服务 API 接口,它也有强弱类型之分,传统的这种RPC服务一般是强类型的,RPC通常采用定制的二进制协议对消息进行编码和解码,采用TCP传输消息

RPC服务通常有严格的契约,也就是 contract 这样一个概念;开发服务前,先要定义 IDL(Interface description language,接口描述语言) ,用 IDL 定义契约,再通过这个契约自动生成强类型的服务器端和客户端的接口,服务调用的时候直接使用强类型客户端,不需要再手动进行消息的编码和解码

GRPC 和 Thrif 是目前主流的两种强类型的 RPC 框架,而现代的 RESTful 服务一般是弱类型的

RESTful 服务 通常采用 JSON 作为传输消息,然后使用 HTTP 作为传输协议。 RESTful 服务一般没有严格的契约的概念,它使用普通的 http client 就可以调用,但是调用方通常需要对这个JSON消息进行手动编码和解码工作

在现实世界当中,大部分服务框架都是弱类型 RESTful 服务框架,比方说 Java 生态当中, Spring Boot 可以认为是目前主流的弱类型 RESTful 框架之一,当然这种区分不是业界标准,只是基于经验总结出来的一种区分的方法。

问题

强类型服务接口的优点: 是接口规范、自动代码生成、自动编码解码、编译期自动类型检查。

强类型服务接口的缺点:

  • 首先,是客户端和服务器端的强耦合,任何一方升级改动可能会造成另一方break;
  • 另外,自动代码生成需要工具支持,而开发这些工具的成本也比较高;
  • 还有,强类型接口开发测试不太友好,一般浏览器、Postman这样的工具无法直接访问这些强类型接口。

弱类型服务接口的优点: 是客户端和服务器端不强耦合,不需要开发特别的代码生成工具,一般的 http client 就可以调用;
开发测试友好,普通的浏览器、Postman可以轻松访问。

弱类型服务接口的缺点: 是需要调用方手动的编码解码消息;没有自动代码生成,没有编译期接口类型检查,代码不容易规范;开发效率相对低,而且容易出现运行期的错误。

问题: 那么我们在服务开发过程中如何设计强类型接口?有没有办法结合强弱类型服务接口各自的好处,同时又规避他们的不足呢?

方案

方案: 在 Spring RESTful 服务弱类型接口的基础上,借助 Spring Cloud Feign 支持的强类型接口调用的特性,实现了强类型 REST 接口调用机制,同时兼备强弱类型服务接口的好处。

Spring Cloud Feign 本质上是一种动态代理机制,你只要给出一个 RESTful API 对应的 Java 接口,它就可以在运行期动态的拼装出对应接口的强类型客户端,这个拼装出来的客户端的简化结构和请求响应的流程,如下图:

33.Spring Feign

客户端请求响应的流程:

  1. 当客户应用发起一个请求,传过来一个请求 Bean(Request Bean),这个请求通过 Java 接口,首先会被动态代理截获,然后通过相应的编码器(Encoder)进行编码,成为 Request JSON;
  2. Request JSON 根据需要,可以经过一些拦截器(Interceptor),做进一步处理,处理完以后,传递给 HTTP Client。由他将这个 Request JSON 通过 HTTP 协议发送到服务器端;
  3. 当服务器端响应回来的时候,相应的 Request JSON 首先会被 HTTP Client 接收到,同样可以经过一些拦截器做进一步的响应处理;
  4. 然后转发给解码器(Decoder)进行解码,解码成 Response Bean,最后这个 Response Bean 再通过 Java 接口返回给调用方。

整个请求响应流程的关键步骤就是编码和解码

  • 也就是 Java 对象和 JSON 消息的互转,这个过程也被称为序列化和反序列化,另外一个叫法,就是叫 JSON 对象绑定。
  • 对一个服务框架而言,序列化、反序列化器的性能,对框架的性能影响是最大的;可以认为 Encoder 和 Decoder 决定了服务框架的总体的性能。

虽然我们开发出来的服务是弱类型的 RESTful 的服务,但是因为有 Spring Cloud Feign 的支持,我们只要简单的给出一个强类型的 Java API 接口(这个工作量不大的),就自动获得了强类型的客户端

也就是说,利用 Spring Cloud Feign ,我们可以同时获得强弱类型的好处,包括:编译期自动类型检查、不需要手动编码解码、不需要开发代码生成工具、而且客户端和服务器端不强耦合。这样可以同时规范代码风格,提升开发测试效率。

这也就解释了为什么每个微服务都是由两个子模块组成的:一个是api接口模块,一个是服务实现模块,api接口模块里就是强类型的 Java API 接口,包括请求响应这些 DTO ,它可以直接被 Spring Cloud Feign 引用并动态拼装出这个强类型的客户端。

例:account服务,对应的有 account-api 就是接口模块,account-svc就是服务实现模块。并且,account-svc也是依赖account-api的,这种接口和实现分离的组织方式,一方面代码结构比较清晰,另一方面也益于这个接口模块的重用。

当然,这个技术仅限于 Java Spring 开发的客户端,如果是其他语言的客户端,一方面仍然可以采用基本的 HTTP Client 的方式去调用,也可以基于服务接口 Swagger 文档自动生成其他语言的强类型客户端。


采用强类型接口,如何做到既能同时处理正常响应又能处理异常的响应,而且这个接口又是统一的呢?

  • 问题:控制器可能会返回正常的响应,也可能会返回异常响应。
  • 应对:采用了一种特殊的设计方式,我们的响应 Response 对象都是继承自 BaseResponse 对象。

34.强类型接口设计

例:当客户端向服务器端的 Controller 发送请求操作的时候:

  • 如果响应异常,那么他就只返回对应的 BaseResponse 异常 JSON 消息,因为 ListAccountResponse 继承自 BaseResponse ,所以这个异常 JSON 消息也可以序列化自动绑定到 ListAccountResponse 上。只不过绑定后,ListAccountResponse 对象自己扩展的字段为空,没有数据而已,但他还是一个正常的 ListAccountResponse 对象。然后,客户端根据 Response 的错误码指示进行异常处理即可。
  • 如果响应正常,返回 ListAccountResponse 的 JSON 消息就可以被正常的绑定到 ListAccountResponse 对象上。

通过这种简单的继承机制,我们实现了一个强类型接口,可以同时处理正常、异常两种情况。

注意:网上也有种做法,采用的是泛型加组合的方式来设计 Response,也就是说具体响应作为泛型被包含在一个通用的响应中的这种方式。如果用 Spring Cloud Feign 这种泛型设计是不能work的,因为 Java 中的泛型信息在运行期会被擦除。


封装消息+捎带

目前,业界的 RESTful API 的设计,通常采用 HTTP 协议状态码来传递和表达错误语义。

但是我们的设计是将错误码打包在一个正常的 JSON 消息当中,也就是 BaseResponse 当中。这是一种称为封装消息加上捎带的一种设计模式,这样的设计的目标是为了支持强类型的客户端,同时简化和规范这个错误处理。

如果借用 HTTP 协议状态码来传递和表达错误语义,虽然也可以开发对应的强类型客户端,但是内部的调用处理逻辑就会变得比较复杂。你要去处理各种 HTTP 的错误码,开发成本会比较高,没有我们这个简单。

提醒: 除了 BaseReponse 这个继承之外,如果你在建模响应的时候,尽量内部不要再使用其他的继承关系,或者是泛型也尽量的不要采用。除了是那种 collection ,它的泛型是可以的,其他的泛型或者是继承关系,尽量避免。否则还是容易出反序列化问题的。

实践

1.创建项目

创建项目 rpc-feign-rest-demo ,其子项目代码组织如下:

<modules>
    <module>common-lib</module>
    <module>account-api</module>
    <module>account-svc</module>
    <module>demo-web</module>
</modules>
  • common-lib 公共依赖模块
  • account-api account服务接口模块
  • account-svc account服务实现模块
  • demo-web 服务间强类型接口调用验证模块

2.引入依赖

rpc-feign-rest-demo 父pom指定 Spring Cloud 和 Spring Boot 的版本:Greenwich.SR6 + 2.1.18.RELEASE

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring.boot.version>2.1.18.RELEASE</spring.boot.version>
    <spring.cloud.version>Greenwich.SR6</spring.cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- Spring Cloud -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring.cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

common-lib 公共模块引入公用 spring-boot-starter-web 和 spring-cloud-starter-openfeign 依赖 。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

3.实现 RESTful API 服务端

使用 @Validated 注解开启请求接口参数校验,@Valid 指定校验参数。

@RestController
@RequestMapping(AccountConstant.ACCOUNT_V1+"/account")
@Validated
public class AccountController {
    /**
     * 创建新账户
     */
    @PostMapping(path = "/create")
    @Authorize(value = {
            AuthConstant.AUTHORIZATION_SUPPORT_USER,
            AuthConstant.AUTHORIZATION_DEMO_WEB_SERVICE
    })
    public GenericAccountResponse createAccount(@RequestBody @Valid CreateAccountRequest request){
        //...
        GenericAccountResponse response = new GenericAccountResponse(accountDto);
        return response;
    }
    
    //...
}

4.编写强类型客户端接口

通过 url 指定 account 服务调用地址 。

@FeignClient(name = AccountConstant.SERVICE_NAME, path = AccountConstant.ACCOUNT_V1+"/account", url = "${rpc.account-service-endpoint}")
public interface AccountClient {

    /**
     * 创建新账户
     */
    @PostMapping(path = "/create")
    GenericAccountResponse createAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid CreateAccountRequest request);
    
    //...
}

5.客户端调用范例

1) 开启 Feign Client 调用

在调用服务启动类使用 EnableFeignClients 开启 Feign Client 调用,并配置 client 包,可以配置多个。

@EnableFeignClients(basePackages = {"com.chen.solution.rpc.account"})
@SpringBootApplication
public class DemoWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoWebApplication.class, args);
    }
}

2) 启动 account-svc 服务

启动用户服务 AccountApplication 。

3) 服务间调用 Feign Client 范例

@RestController
@RequestMapping("/account/client")
@Validated
public class AccountDemoController {

    @Resource
    private AccountClient accountClient;

    @PostMapping("/create")
    public GenericAccountResponse createAccount(@RequestBody @Valid CreateAccountRequest request){
        GenericAccountResponse resp = null;

        try {
            resp = accountClient.createAccount(AuthConstant.AUTHORIZATION_DEMO_WEB_SERVICE, request);
        } catch (Exception ex) {
            String errMsg = "could not create account";
            handleErrorAndThrowException(log, ex, errMsg);
        }
        if (!resp.isSuccess()) {
            handleErrorAndThrowException(log, resp.getMessage());
        }
        AccountDto account = resp.getAccount();
        GenericAccountResponse response = new GenericAccountResponse(account);
        return response;

    }
    
    //...
}

4) 单元测试范例

@RunWith(SpringRunner.class)
@SpringBootTest
@EnableFeignClients(basePackages = {"com.chen.solution.rpc.account.api.client"})
@Import(TestWebConfig.class)
public class AccountControllerTest {

    @Resource
    private AccountClient accountClient;
    
    @Test
    public void testUpdateAccount(){
        String name = "testAccount";
        String email = "test@sin.com";
        String phoneNumber = "18234523421";
        CreateAccountRequest createAccountRequest = CreateAccountRequest.builder()
                .name(name)
                .email(email)
                .phoneNumber(phoneNumber)
                .build();

        // create one account
        GenericAccountResponse accountResponse = accountClient.createAccount(AuthConstant.AUTHORIZATION_DEMO_WEB_SERVICE, createAccountRequest);
        log.info(accountResponse.toString());
        assertThat(accountResponse.isSuccess()).isTrue();
        
        //...
    }
    //...
}

6.微服务之间调用的权限控制

通过自定义注解 @Authorize 对服务接口进行权限控制,在接口声明时,通过注解控制接口被允许访问的业务服务。

@PostMapping(path = "/create")
@Authorize(value = {
        AuthConstant.AUTHORIZATION_SUPPORT_USER,
        AuthConstant.AUTHORIZATION_DEMO_WEB_SERVICE
})
public GenericAccountResponse createAccount(@RequestBody @Valid CreateAccountRequest request){
    //...
}

通过继承 HandlerInterceptorAdapter 自定义请求拦截 AuthorizeInterceptor ,实现接口访问权限控制。

public class AuthorizeInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(!(handler instanceof HandlerMethod)){
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Authorize authorize = handlerMethod.getMethod().getAnnotation(Authorize.class);
        if(authorize == null){
            // no need to authorize
            return true;
        }
    
        //...
        return true;
    }
}

代码仓库

https://gitee.com/chentian114/solution-springboot

rpc-feign-rest-demo

公众号

知行chen

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值