SpringCloud OpenFeign(服务调用)

本文共 12,634 字,预计阅读时间 42 分钟

 


1.定义

Feign是一个声明式的Web服务客户端,是面向接口编程的。也就是说使用Feign,只需要创建一个接口并使用注解方式配置它,就可以完成对微服务提供方的接口绑定。

OpenFeign对feign进行进一步的封装,添加了springmvc的一些功能,更加强大。

在使用RestTemplate时,每次调用服务都需要指定服务的具体路径,当在多个地方同时使用时要写多次,显得代码冗余也难以维护,而openfeign就可以避免这种操作。

2.项目实战

源码:https://github.com/zhongyushi-git/cloud-open-feign-demo.git

2.1基础环境搭建

这里使用consul作为服务注册中心。

1)创建一个maven工程名为cloud-open-feign-demo,删除src目录

2)在pom中导入依赖,对SpringBoot和SpringCloud版本进行锁定

复制代码
 <properties>
        <spring.boot.version>2.2.2.RELEASE</spring.boot.version>
        <spring.cloud.version>Hoxton.SR1</spring.cloud.version>
        <lombok.version>1.16.18</lombok.version>
    </properties>

    <!--  依赖管理,父工程锁定版本-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
           <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
复制代码

2.2搭建公共模块

1)新建maven子模块(common-api),导入依赖

复制代码
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
复制代码

2)创建实体对象

复制代码
package com.zys.cloud.entity;

import lombok.Data;

@Data
public class User {

    private String name;

    private String username;

    private Integer age;
}
复制代码

2.3搭建服务提供者

1)新建maven子模块(cloud-provider8001),导入依赖

复制代码
     <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
复制代码

2)新建启动类ProviderMain8001并添加注解

复制代码
package com.zys.cloud;

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

@SpringBootApplication
@EnableDiscoveryClient
public class ProviderMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(ProviderMain8001.class, args);
    }
}
复制代码

3)配置application.yml

复制代码
server:
  port: 8001

spring:
  application:
    name: cloud-consul-provider
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}
复制代码

4)新建controller接口

复制代码
package com.zys.cloud.controller;

import com.zys.cloud.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
public class UserController {

    @Value("${server.port}")
    private String port;

    @GetMapping("/user/get")
    public String get() {
        return "我是服务提供者,端口:" + port;
    }

    @GetMapping("/user/getParam")
    public String getParam(@RequestParam("name") String name) {
        return "我是服务提供者,参数:" + name + ",端口:" + port;
    }

    @PostMapping("/user/postParam")
    public String postParam(@RequestParam("username") String username) {
        return "我是服务提供者,参数:" + username + ",端口:" + port;
    }

    @PostMapping("/user/add")
    public String addUser(@RequestBody User user) {
        return "我是服务提供者,参数:" + user.toString() + ",端口:" + port;
    }


}
复制代码

这里的接口并没有在类上使用@RequestMapping注解,而是把接口路径都写在方法上面,那么在服务调用方进行映射时直接复制其方法名即可,不需要方法体。

5)启动服务,可看到已注册到consul。

6)根据服务提供者ProviderMain8001,再创建ProviderMain8002.

2.4搭建服务消费者

1)新建maven子模块(cloud-consumer80),导入依赖

复制代码
    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.zys.cloud</groupId>
            <artifactId>common-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
复制代码

2)新建启动类ConsumerMain80并添加注解

复制代码
package com.zys.cloud;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
class ConsumerMain80 {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerMain80.class, args);
    }
}
复制代码

要在启动类上启用Feign客户端。

3)配置application.yml

复制代码
server:
  port: 80

spring:
  application:
    name: cloud-consul-consumer
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}
复制代码

3)创建服务接口UserServiceClient,对于服务提供者接口。需要添加注解@FeiginClient,指定微服务的名称

复制代码
package com.zys.cloud.service;

import com.zys.cloud.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

//指定微服务名称
@FeignClient(value = "cloud-consul-provider")
public interface UserServiceClient {

    @GetMapping("/user/get")
    String get();

    @GetMapping("/user/getParam")
    String getParam(@RequestParam("name") String name);

    @PostMapping("/user/postParam")
    String postParam(@RequestParam("username") String username);

    @PostMapping("/user/add")
    String addUser(@RequestBody User user);
}
复制代码

注意:

  •  必须使用注解@FeignClient指定服务提供方的服务名称
  •  在编写接口映射时,可直接复制服务提供方的方法名等信息
  •  get请求传递参数时,要使用@RequestParam注解,其value是参数的名字,需与服务提供者端保持一致
  •  post请求传递参数时,当参数是对象时使用@RequestBody,当参数不是对象时需使用@RequestParam注解

4)创建controller接口,将UserServiceClient注入使用

复制代码
package com.zys.cloud.controller;

import com.zys.cloud.entity.User;
import com.zys.cloud.service.UserServiceClient;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/consumer")
public class TestController {

    @Resource
    private UserServiceClient userServiceClient;

    @GetMapping("/get")
    public String get() {
        return userServiceClient.get();
    }

    @GetMapping("/param")
    public String getParam(String name) {
        return userServiceClient.getParam(name);
    }

    @PostMapping("/post")
    public String postParam(String username) {
        return userServiceClient.postParam(username);
    }

    @PostMapping("/add")
    public String addUser(@RequestBody User user) {
        return userServiceClient.addUser(user);
    }
}
复制代码

5)启动测试。先启动服务提供者集群,再启动服务消费者,对四个接口进行测试,服务均可正常调用。

另外对其中一个接口进行多次刷新,会发现服务提供者集群是遵循轮询机制。原因是openfeignl默认已引入了Ribbon,可提供负载均衡策略。

2.5超时控制

为了体现服务快速响应的特点,Feign默认等待1秒,超过后报错。也就是说Feigin客户端只等待一秒,若服务端处理过程超过一秒,会导致客户端会出错,故需设置超时时间,避免出现这样的情况。

1)情景重现

为了看到这种效果,可在服务提供者的接口中设置线程阻塞,让其响应时间超过1S。这里以8001为例,修改UserController的get()方法:

复制代码
    @GetMapping("/user/get")
    public String get() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "我是服务提供者,端口:" + port;
    }
复制代码

截图:

重启后测试这个接口,消费者控制台报错:

2)配置超时时间

在客户端(服务消费者80)的yml进行配置即可。配置有两种方式:

1)方式一:设置Ribbon的负载超时时间

复制代码
#设置ribbon的负载超时时间,单位都是ms
ribbon:
  #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000
复制代码

2)方式二:设置feign的超时时间

复制代码
feign:
  client:
    config:
      #指定全局
      default:
        #连接超时时间
        connectTimeout: 5000
        #服务等待时间
        readTimeout: 5000
复制代码

上述是配置全局的,也可对每个服务设置超时时间:

 

使用上述任意一种配置,配置后重启客户端后测试这个接口,服务正常调用,没有错误。

2.6日志打印

Feign对日志的处理非常灵活,可为每个Feign客户端指定日志记录策略。每个客户端都会创建一个Logger,默认情况下Feign的日志是Debug级别的,故不会显示。

2.6.1日志级别类型

1
2
3
4
NONE:默认的,不显示任何日志
BASIC:仅记录请求方法、URL、响应状态码及执行时间
HEADERS:BASIC信息以及请求和响应的头信息 
FULL:HEADERS信息以及请求和响应的正文和元数据

2.6.2配置日志

配置日志有两种方式,二选一即可。

1)第一种方式:使用配置类+yml配置

第一步:在客户端(服务消费者80)添加配置类LogConfig,指定日志的级别

复制代码
package com.zys.cloud.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//配置feign日志级别
@Configuration
public class LogConfig {
    @Bean
    Logger.Level feignLevel(){
        return Logger.Level.FULL;
    }
}
复制代码

第二步:在yml配置指定调用的客户端的日志级别,必须是Debug级别

logging:
  level:
    com.zys.cloud.service.UserServiceClient: debug

2)第二种方式:全部使用yml配置

配置如下:

复制代码

feign:
client:
config:
#指定全局
default:
loggerLevel: full

logging:
level:
com.zys.cloud.service.UserServiceClient: debug
复制代码

当然,上述使用default指定了全局的级别,也可以对每个服务指定级别,把default换成服务名即可:

配置完成后重启客户端,调用四个接口的任意一个,在控制台可以看出多了很多的打印信息。

2.7文件上传

在使用openfeign进行文件上传时,需要特别注意,不能使用默认方式。

//import org.springframework.http.MediaType;

//文件上传
@PostMapping(value = "/user/fileUpload",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String fileUpload(@RequestPart("file")MultipartFile file);

调用的关键代码如上。必须在请求中指定consumes的值是MediaType.MULTIPART_FORM_DATA_VALUE,另外传递的参数需要使用@RequestPart注解来修饰MultipartFile.

2.8请求通讯连接优化

OpenFeign底层通信组件默认使用的是JDK自带的 URLConnection 对象进行HTTP请求,而这种方式并没有使用连接池,对于性能来说不太友好。

查看feign的源码,在如下包中

通过对源码(feign.SynchronousMethodHandler#executeAndDecode())debug可以看到,默认的通信组件如下图:

而 OpenFeign支持替换通讯组件为专用通信组件,如Apache HttpClientOKHttp ,因为这些专用通信组件自带连接池,能更好地对 HTTP 连接对象进行重用与管理,同时能大大的提升 HTTP 请求的效率。

下面以Apache HttpClient为例进行替换:

先导入依赖,此httpclient是专门适配feign的,因此需要导入
<dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-httpclient</artifactId>
</dependency>
而feign默认已开启使用httpclient
故无需配置即可,重启服务后再次调用,在debug时发现已经替换成功

3.面试题-Feign为什么第一次加载很慢?

Feign的底层也是使用的Ribbon,而ribbon默认是懒加载的,也就是在第一次调用时才会生成ribbon所需的组件。

4.面试题-Feign如何实现认证传递?

实现RequestInterceptor接口,在apply方法中添加请求头需要的认证信息。然后将其注入到拦截器中。
具体代码如下:
复制代码
@Slf4j
@Configuration
public class FeignConfiguration implements RequestInterceptor {



    @Override
    public void apply(RequestTemplate requestTemplate) {
        Map<String,String> headers = getHeaders(getHttpServletRequest());
        for(String headerName : headers.keySet()){
            requestTemplate.header(headerName, getHeaders(getHttpServletRequest()).get(headerName));
        }
    }

    private HttpServletRequest getHttpServletRequest() {
        try {
            return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        while (enumeration.hasMoreElements()) {
            String key = enumeration.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }
}
复制代码
是不是很简单
posted @   钟小嘿  阅读(964)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示