Spring Cloud 入门 (六): 用声明式 REST 客户端 Feign 调用远端 HTTP 服务

本贴最后更新于 2002 天前,其中的信息可能已经斗转星移

要做一件事, 需要知道三个要素,where, what, how。即在哪里( where)用什么办法(how)做什么(what)。什么时候做(when)我们纳入 how 的范畴。

1)编程式实现: 每一个要素(where,what,how)都需要用具体代码实现来表示。传统的方式一般都是编程式实现,业务开发者需要关心每一处逻辑

2)声明式实现: 只需要声明在哪里(where )做什么(what),而无需关心如何实现(how)。Spring 的 AOP 就是一种声明式实现,比如网站检查是否登录,开发页面逻辑的时候,只需要通过 AOP 配置声明加载页面(where)需要做检查用户是否登录(what),而无需关心如何检查用户是否登录(how)。如何检查这个逻辑由 AOP 机制去实现, 而 AOP 的登录检查实现机制与正在开发页面的逻辑本身是无关的。

在 Spring Cloud Netflix 栈中,各个微服务都是以 HTTP 接口的形式暴露自身服务的,因此在调用远程服务时就必须使用 HTTP 客户端。Feign 就是 Spring Cloud 提供的一种声明式 REST 客户端。可以通过 Feign 访问调用远端微服务提供的 REST 接口。现在我们就用 Feign 来调用 SERVICE-HELLOWORLD 暴露的 REST 接口,以获取到“Hello World”信息。在使用 Feign 时,Spring Cloud 集成了 Ribbon 和 Eureka 来提供 HTTP 客户端的负载均衡。

采用 Feign 的方式来调用 Hello World 服务集群。

1. 创建 Maven 工程,加入 spring-cloud-starter-feign 依赖

	<dependency>
      <groupId>org.springframework.cloudgroupId>
	   <artifactId>spring-cloud-starter-feignartifactId>
	<dependency>

完整的 pom 文件如下:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.chry</groupId>
    <artifactId>springcloud.helloworld.feign.client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>springcloud.helloworld.feign.client</name>
    <description>Demo Feigh client application</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.RC1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
</project>

2. 创建启动类

需加上 @EnableFeignClients 注解以使用 Feign, 使用 @EnableDiscoveryClient 开启服务自动发现

package springcloud.helloworld.feign.service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceFeignApplication.class, args);
    }
}

3. 添加配置文件 application.yml, 使用端口 8902, 名字定义为 service-feign, 并注册到 eureka 服务中心

eureka:
   client:
      serviceUrl:
        defaultZone: http://localhost:8761/eureka/
server:
   port: 8902
spring:
   application:
      name: service-feign

4. 定义 Feign:一个用 @FeignClient 注解的接口类

@FeignClient 用于通知 Feign 组件对该接口进行代理(不需要编写接口实现),使用者可直接通过 @Autowired 注入; 该接口通过 value 定义了需要调用的 SERVICE-HELLOWORLD 服务(通过服务中心自动发现机制会定位具体 URL); @RequestMapping 定义了 Feign 需要访问的 SERVICE-HELLOWORLD 服务的 URL(本例中为根“/”)

package springcloud.helloworld.feign.service;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient(value = "SERVICE-HELLOWORLD")
public interface HelloWorldService {
    @RequestMapping(value = "/",method = RequestMethod.GET)
    String sayHello();
}

Spring Cloud 应用在启动时,Feign 会扫描标有 @FeignClient 注解的接口,生成代理,并注册到 Spring 容器中。生成代理时 Feign 会为每个接口方法创建一个 RequetTemplate 对象,该对象封装了 HTTP 请求需要的全部信息,请求参数名、请求方法等信息都是在这个过程中确定的,Feign 的模板化就体现在这里

5. 定义一个 WebController

注入之前通过 @FeignClient 定义生成的 bean,

sayHello()映射到 http://localhost:8902/hello, 在这里,我修改了 Hello World 服务的映射,将根“/”, 修改成了“/hello”

package springcloud.helloworld.feign.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WebController {
    @Autowired HelloWorldService helloWorldFeignService;
    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    public String sayHello(){
        return helloWorldFeignService.sayHello();
    }
}

动 Feign 应用,访问 http://localhost:8902/hello, 多次刷新,可以看到和前一章 Ribbon 里面的应用一样, 两个 Hello World 服务的输出交替出现。说明通过 Feign 访问服务, Spring Cloud 已经缺省使用了 Ribbon 负载均衡。

imagepng imagepng

6. 在 Feign 中使用 Apache HTTP Client

Feign 在默认情况下使用的是 JDK 原生的 URLConnection 发送 HTTP 请求,没有连接池,但是对每个地址 gwai 会保持一个长连接,即利用 HTTP 的 persistence connection 。我们可以用 Apache 的 HTTP Client 替换 Feign 原始的 http client, 从而获取连接池、超时时间等与性能息息相关的控制能力。Spring Cloud 从 Brixtion.SR5 版本开始支持这种替换,首先在项目中声明 Apache HTTP Client 和 feign-httpclient 依赖:

<!-- 使用Apache HttpClient替换Feign原生httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>${feign-httpclient}</version>
</dependency>

然后在 application.properties 中添加:

feign.httpclient.enabled=true

7. Feign 的 Encoder、Decoder 和 ErrorDecoder

Feign 将方法签名中方法参数对象序列化为请求参数放到 HTTP 请求中的过程,是由编码器(Encoder)完成的。同理,将 HTTP 响应数据反序列化为 Java 对象是由解码器(Decoder)完成的。默认情况下,Feign 会将标有 @RequestParam 注解的参数转换成字符串添加到 URL 中,将没有注解的参数通过 Jackson 转换成 json 放到请求体中。注意,如果在 @RequetMapping 中的 method 将请求方式指定为 POST,那么所有未标注解的参数将会被忽略,例如:

@RequestMapping(value = "/group/{groupId}", method = RequestMethod.GET) 
void update(@PathVariable("groupId") Integer groupId, @RequestParam("groupName") String groupName, DataObject obj);

此时因为声明的是 GET 请求没有请求体,所以 obj 参数就会被忽略。

在 Spring Cloud 环境下,Feign 的 Encoder 只会用来编码没有添加注解的参数。如果你自定义了 Encoder, 那么只有在编码 obj 参数时才会调用你的 Encoder。对于 Decoder, 默认会委托给 SpringMVC 中的 MappingJackson2HttpMessageConverter 类进行解码。只有当状态码不在 200 ~ 300 之间时 ErrorDecoder 才会被调用。ErrorDecoder 的作用是可以根据 HTTP 响应信息返回一个异常,该异常可以在调用 Feign 接口的地方被捕获到。我们目前就通过 ErrorDecoder 来使 Feign 接口抛出业务异常以供调用者处理。

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    941 引用 • 1458 回帖 • 155 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...