SpringCloud 03 —— 客户端Feign

上一章我们讲解和分析了Ribbon 的功能和作用,提供了分布式架构之前调用的负载均衡策略,使我们分布式架构必须要考虑的,使用了Spring自带的RestTemplate。而RestTemplate使用的是HttpClient发送请求。本章我们将介绍另一个重要的REST客户端Feign

Feign介绍

Feign是GitHub 上的一个开源项目,目的之简化Web Service 客户端的开发,以Java接口注解的方式调用Http请求,而不用像Spring自带的RestTemplate直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。
Spring Cloud 将Feign 整合到了Netflix项目中,当与Eureka、Ribbon整合时,Feign 就具备了负载均衡的能力,在Spring Cloud 的高度整合下,使用该框架调用Spring Cloud集群服务,会大大降低开发工作量。

Feign基础

新建模块cloud-feign,导入maven依赖:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

新建启动类

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

在启动类上加入@EnableFeignClients注解,如果Feign的定义跟启动类不在一个包名下,还需要制定路径,如@EnableFeignClients(basePackages = "group.zhouning.xxx.xxx")

修改application.yml配置指定项目名称和Eureka的地址

server:
  port: 8771
spring:
  application:
    name: cloud-feign
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/

定义Feign 请求的接口

这里我们配合前面Eureka的例子,做一次远程调用

//value 就是服务的名称
@FeignClient(value = "register-eureka-client")
public interface EurekaFeignService {
    //feign中你可以有多个@RequestParam,但只能有不超过一个@RequestBody
    @GetMapping("/hello")
    String hello();
}

注解@FeignClient(value = "register-eureka-client")里面配置的value 就是前面服务的名称

注意

  • 如果你在项目里面设置了统一的请求路径(server.servlet.context-path),需要将@FeignClient注解调整@FeignClient(value = "register-eureka-client",path = "xxx")
  • Feign 里面定义的接口,有多个@RequestParam,但只能有不超过一个@RequestBody
    在定义接口的时候,如果返回的是用户自定义的实体,建议抽取出来,在Controller中实现接口,将抽取出来的接口单独打包,需要调用的项目依赖此包即可,每个项目不用重新定义一遍

定义控制层

@RestController
@Slf4j
public class EurekaFeignController {

    @Resource
    private EurekaFeignService eurekaFeignService;

    @GetMapping("/feignInfo")
    public String feignInfo() {
        String message = eurekaFeignService.hello();
        log.info(message);
        return message;
    }
}

由于需要配合的项目是cloud-client-eureka,因此我们仍然按照之前的方式启动项目,先启动eureka注册,再启动eureka-client,最后启动feign-client。访问127.0.0.1:8771/feignInfo验证,返回hello。

Feign 自定义日志

我们在开发时候一般出现问题,需要将出错信息、异常信息以及正常的输入输出打印出来,以供我们更好的排查和解决问题,比如想看到接口的性能,就需要看Feign 的日志,那么如何让Feign 的日志展示出来呢?

添加日志的配置信息:

新建FeignConfig类,并设置日志级别的输出信息

public class FeignConfig {

    /**
     * 输出的日志级别
     * @return
     */
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}

通过源码可知日志等级有

  • NONE: 不输出日志
  • BASIC: 只输出请求方法的URL 和响应状态码以及接口的请求时间
  • HEADERS :将 BASIC信息和请求头信息输出
  • FULL :输出完 的请求信息

对应的源码如下所示

代码路径:feign.Logger
public static enum Level {
    NONE,
    BASIC,
    HEADERS,
    FULL;

    private Level() {
    }
}

@FeignClient修改配置

之后将配置信息添加到Feign 的接口配置上面

//value 就是服务的名称,相当于调用server名为register-eureka-client的hi,configuration添加配置
@FeignClient(value = "register-eureka-client",configuration = FeignConfig.class)
public interface EurekaFeignService {
    //feign中你可以有多个@RequestParam,但只能有不超过一个@RequestBody
    @GetMapping("/hi")
    String hello();
}

在application.yml中设置日志级别
group.zhouning是设置包路径,在这个路径里面的debug信息都会被捕获到

logging:
  level:
    group.zhouning: debug

重启应用并调用服务接口,再次访问127.0.0.1:8771/feignInfo可以看到在控制台的debug日志输出。

Feign Basic 认证配置

一般我们在调用服务间的接口时,接口上都会设置需要的权限信息,而一般的权限认证有通过token校验的、也有通过用户名密码校验的等方式。比如我们在Feign 请求中我们可以配置Basic 认证,如下

/**
 * 设置Spring Security Basic认证的用户名密码
 * @return
 */
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor(){
    return new BasicAuthRequestInterceptor("user","12345");
}

那Spring Security Basic认证又是什么呢?

Spring Security 是Spring官方提供的安全框架,是一种比较重的权限校验框架,当然还有一种比较轻量型的框架shiro。关于Spring Security 的使用,我们后面会专门开一个模块来讲解。

接着上面说,如果我不是Basic认证又该怎么办?
那当然是自定义我们的拦截器了,请请求之前做认证操作,然后往请求头中设置认证之后的信息,下面通过实现RequestInterceptor接口可以自定义自己的认证。

@NoArgsConstructor
public class FeignAuthRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //编写自己的业务逻辑...
    }
}

然后在FeignConfig配置中添加Bean

/**
 * 自定义认证逻辑
 * @return
 */
@Bean
public FeignAuthRequestInterceptor basicAuthRequestInterceptor(){
    return new FeignAuthRequestInterceptor();
}

Feign 的超时配置

服务的请求见,肯定需要设置服务请求之间的超时时间,不可能一直在哪里等待,那feign 的超时时间如何配置?

类型备注默认值
connectTimeout连接超时时间默认 2000毫秒
readTimeout读取超时时间默认 5000毫秒

代码路径com.netflix.client.config.DefaultClientConfigImpl

方式一:

添加代码配置,但是这个不能注册到全局,需要在FeignClient上指定

/**
 * 设置连接超时时间和响应超时时间,默认值是10000和60000
 * @return
 */
@Bean
public Request.Options options(){
    return new Request.Options(5000,1000);
}

方式二:

添加application.yml的配置

需要注意的是connectTimeout和readTimeout必须同时配置,要不然不会生效,这种方式可以全局配置,至于为什么请看后面。

feign:
  client:
    config:
      default:
        connectTimeout: 10000
        readTimeout: 10000
      service-name:
        connectTimeout: 10000
        readTimeout: 10000
#  或者
#ribbon:
#  ReadTimeout: 60000
#  ConnectTimeout: 60000
#service-name:
#  ribbon:
#    ReadTimeout: 30000
#    ConnectTimeout: 30000

以上配置default是全局的配置,service-name是配置具体服务的配置

配置生效的原因

当然有人会说,我直接配置Ribbon 不也是可以的吗?毕竟Feign 集成了Ribbon,但是在Feign 的实现中,Feign的配置如果不是默认的是优先Ribbon 生效的。

public class LoadBalancerFeignClient implements Client {

IClientConfig getClientConfig(Options options, String clientName) {
    Object requestConfig;
   //只要配置不是默认的就会走Feign里面配置的
    if (options == DEFAULT_OPTIONS) {
        requestConfig = this.clientFactory.getClientConfig(clientName);
    } else {
        requestConfig = new LoadBalancerFeignClient.FeignOptionsClientConfig(options);
    }

    return (IClientConfig)requestConfig;
}

为什么connectTimeout和readTimeout必须同时配置?

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
        ApplicationContextAware {
        
    protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) {
        if (config == null) {
            return;
        }
        if (config.getLoggerLevel() != null) {
            builder.logLevel(config.getLoggerLevel());
        }
        //此处,必须俩值都不为null才会替换新options
        if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
            builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout()));
        } 

重试配置

方式一(代码配置):

在config添加重试的Bean,Feign 模式是不重试的,如果通过Retryer.Default()开启的话,默认是5次重试,可以自定义修改重试次数

@Bean
    public Retryer feignRetryer() {
//        return new Retryer.Default();
        //100毫秒到1000毫秒间重试4次  自定义
        return  new Retryer.Default(100, 1000, 4);
    }

重试配置源码:
可以看到默认的配置是100毫秒到1秒之间,重试5次

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

调整配置
修改application.yml的配置,设施超时时间

feign:
  client:
    config:
      default:
	#也就是一秒超时
        connectTimeout: 1000
        readTimeout: 1000

修改register-eureka-client中UserController添加随机的睡眠时间

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

    @GetMapping("/hi")
    public String hello() throws InterruptedException {

        int millis = new Random().nextInt(3000);
        System.out.println("client线程休眠时间:"+millis);
        Thread.sleep(millis);
        return "hello:"+serverPort;
    }

重启应用,调用接口127.0.0.1:8771/feignInfo,可以看到重试了3次。

方式二 (文件配置):

#重试
ribbon:
  #配置首台服务器重试1次
  MaxAutoRetries: 1
  #配置其他服务器重试两次
  MaxAutoRetriesNextServer: 2
  #链接超时时间
  ConnectTimeout: 500
  #请求处理时间
  ReadTimeout: 500
  #每个操作都开启重试机制
  OkToRetryOnAllOperations: true
#配置断路器超时时间,默认是1000(1秒)
feign:
  hystrix:
    enabled: true
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2100

UserController和上面一样的添加休眠。

重启,测试。

feign请求和响应的压缩配置

Spring Cloud feign支持对请求和响应进行gzip压缩,以减少通信过程中的性能损耗

添加配置信息

mime-types:配置压缩的类型
min-request-size:最小压缩值的标准
#GZIP 压缩配置
feign:
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048
    response:
      enabled: true

调用postman访问127.0.0.1:8771/feignInfo,查看size大小。如果返回的是乱码,可以用ResponseEntity<byte[]>处理二进制数据

Feign 客户端组件替换

Feign 中默认使用的是HttpClient 来进行接口调用,我们现在使用OkHttp替换掉HttpClient

okhttp 是由 square 公司开源的一个 http 客户端。是一款高效的HTTP客户端,支持连接同一地址的链接共享同一个socket,通过连接池来减小响应延迟

okhttp 的设计初衷就是简单和高效,这也是我们选择它的重要原因之一。它的优势如下:

  • 支持 HTTP/2 协议。
  • 允许连接到同一个主机地址的所有请求,提高请求效率。
  • 共享Socket,减少对服务器的请求次数。
  • 通过连接池,减少了请求延迟。
  • 缓存响应数据来减少重复的网络请求。
  • 减少了对数据流量的消耗。
  • 自动处理GZip压缩。

新建模块cloud-feign-okhttp用于将HttpClient替换OkHttp,代码里的内容和cloud-feign-client内容一样,拷贝过来即可。

添加pom依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>

添加OkHttpLogInterceptor类记录连接的url日志

@Slf4j
public class OkHttpLogInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        log.info("OkHttpUrl : " + chain.request().url());
        return chain.proceed(chain.request());
    }
}

这里就不需要HttpClient的之前的config配置了。下面的配置主要是一个demo性质的,实际项目中可以修改为读取application.yml 或application.properties里面的内容。

@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
    @Bean
    public okhttp3.OkHttpClient okHttpClient(){
        return new okhttp3.OkHttpClient.Builder()
                .readTimeout(60, TimeUnit.SECONDS)  //设置读取超时时间
                .connectTimeout(60, TimeUnit.SECONDS) //设置连接超时时间
                .writeTimeout(120, TimeUnit.SECONDS) //设置写入超时时间
                .connectionPool(new ConnectionPool())
                .addInterceptor(new OkHttpLogInterceptor())
                .build();
    }
}

重启项目,访问测试等到与之前一样的结果

Feign 文件上传

新建上传模块cloud-upload,后续的项目上传都可以走此公共上传服务.

引入maven模块:

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

创建启动类:

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

控制类
下面我们需要提供一个统一的上传接口,默认的上传路径是D:/myCloud/file/

@RestController
@Slf4j
public class UploadController {
    @PostMapping(value = "/uploadFile")
    public String uploadFile(MultipartFile file) throws Exception {
        log.info("upload file name : {}", file.getName());
        //上传文件
        file.transferTo(new File("D:/myCloud/file/" + file.getOriginalFilename()));
        return file.getOriginalFilename();
    }
}

添加项目配置文件

server:
  port: 8871
spring:
  application:
    name: cloud-upload
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka

启动,使用postman上传测试

调整Feign项目配置

这里需要在Feign 中调用上传服务,跟刚才的直接调用上传服务是有区别的,需要设置一些配置文件,有哪些配置,请继续往下看。首先我们需要修改maven依赖,添加Feign 上传需要的依赖包。

需要注意的是有些低版本的包不适合高版本的Spring Cloud ,需要调试最合适你的版本

我们修改之前的cloud-feign-client,添加feign上传所需依赖

修改后的cloud-feign-client依赖:

<properties>
        <form.version>3.8.0</form.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- Feign文件上传依赖-->
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form</artifactId>
            <version>${form.version}</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form-spring</artifactId>
            <version>${form.version}</version>
        </dependency>

    </dependencies>

添加新的配置类

@Configuration
public class FeignMultipartSupportConfiguration {

    /**
     * Feign Spring 表单编码器
     * @return 表单编码器
     */
    @Bean
    @Primary
    @Scope("prototype")
    public Encoder multipartEncoder() {
        return new SpringFormEncoder();
    }
}

新建Feign调用接口并引入配置文件

上一步的配置上传编码配置主要是为了在当前Feign接口中配置configuration = FeignMultipartSupportConfiguration.class,否则调用上传服务会失败。注意区分@RequestPart和RequestParam,不要将 @RequestPart(value = "file") 写成@RequestParam(value = "file")

@RequestPart与@RequestParam的区别

  • @RequestPart这个注解用在multipart/form-data表单提交请求的方法上。
  • @RequestPart支持的请求方法的方式MultipartFile,属于Spring的MultipartResolver类。这个请求是通过http协议传输的。
  • @RequestParam也同样支持multipart/form-data请求。
  • 他们最大的不同是,当请求方法的请求参数类型不再是String类型的时候。
  • @RequestParam适用于name-valueString类型的请求域, @RequestPart适用于复杂的请求域(像JSON,XML等)
@FeignClient(value = "cloud-upload", configuration = FeignMultipartSupportConfiguration.class)
public interface FileUploadFeignService {

    /***
     * 1.produces,consumes必填
     * 2.注意区分@RequestPart和RequestParam,不要将: @RequestPart(value = "file") 写成@RequestParam(value = "file")
     */
    @RequestMapping(value = "/uploadFile", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String uploadFile(@RequestPart(value = "file") MultipartFile file);
}

在controller中添加对应的接口

    @Autowired
    private FileUploadFeignService fileUploadFeignService;
    
    @PostMapping(value = "/upload")
    public String upload(MultipartFile file){
        return fileUploadFeignService.uploadFile(file);
    }

重启项目,按前面的方法测试127.0.0.1:8771/upload

Feign 原生配置

在Spring Cloud 中通过使用已经封装好的Fiegn 可以很方便的调用其它服务接口,这是因为Spring Cloud 在底层做了很多工作,像集成Eureka、Ribbon、Hystrix、Sring MVC 注解等,假设你的项目不是Spring Cloud的,想用Feign就只能使用原生配置了.

创建新的项目模块cloud-feign-without-spring,然后进行maven配置。

这里我们不引用Spring Cloud 提供的Feign包,只使用Feign自己的包,Feign的GitHub地址,有需要的可以拉源码看看。feign-gson的作用是什么,feign-gson包含了一个编码器和一个解码器,这个可以被用于JSON格式的AP,添加 GsonEncoder 以及 GsonDecoder 到你的 Feign.Builder 中I。
除了Gson 还有一下可以使用

  • Jackson:添加 JacksonEncoder 以及 JacksonDecoder 到你的Feign.Builder 中
  • Sax:SaxDecoder用于解析XML,并兼容普通JVM和Android
  • JAXB: 添加 JAXBEncoder 以及 JAXBDecoder 到你的 Feign.Builder 中

<properties>
   <feign-core.version>10.4.0</feign-core.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-core</artifactId>
        <version>${feign-core.version}</version>
    </dependency>
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-gson</artifactId>
        <version>${feign-core.version}</version>
    </dependency>
</dependencies>

创建启动类

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

应用配置

server:
  port: 8864

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    /**
     * 主键Id
     */
    private long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 昵称
     */
    private String rolename;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 备注
     */
    private String remark;
}

Feign请求接口

public interface HelloClient {

    @RequestLine("GET /hello")
    String hello();

    @RequestLine("GET /{id:\\d+}")
    User getUserById(@Param("id") Long id);

    @RequestLine("POST /getUsers")
    List<User> getUsers();
}

控制层

用于对外测试Feign 接口,注意这里因为没用Spring Cloud 提供的Eureka注册中心,需要我们提供自己的服务地址。

@RestController
public class UserController {

    private static final String REMOTE_ADDRESS = "http://xunfei:12345@127.0.0.1:8762/";

    /**
     * 测试请求
     * @return
     */
    @GetMapping("/hello")
    public String hello(){
        //这里需要自己写地址
        HelloClient hello = Feign.builder().target(HelloClient.class, REMOTE_ADDRESS);
        return hello.hello();
    }

    /**
     * 测试请求根据id获取用户
     * @return
     */
    @GetMapping("/{id:\\d+}")
    public User getUserById(@PathVariable Long id){
        HelloClient hello = Feign.builder().decoder(new GsonDecoder()).target(HelloClient.class, REMOTE_ADDRESS);
        return hello.getUserById(id);
    }

    /**
     * 测试请求根据id获取用户
     * @return
     */
    @PostMapping("/getUsers")
    public List<User> getUsers(){
        HelloClient hello = Feign.builder().decoder(new GsonDecoder()).target(HelloClient.class, REMOTE_ADDRESS);
        return hello.getUsers();
    }
}

启动程序,通过postman调用127.0.0.1:8864的各个接口。验证

Feign帮我们实现了服务之间的调用,实际上它是帮我们动态生成代理类,Feign使用的是JDK动态代理,生成的代理会将请求的信息封装,交给Feign.Client接口发送请求,接口的默认实现类最终会使用java.net.HttpURLConnection来发送Http请求。
很多人可能看到了decoder(new GsonDecoder()),这其实是为了解密返回的JSON字符串转成我们需要的对象,因为我们引入的Gson的包,Feign把请求的数据encode编码之后我们需要decode进行解码。

前面说过Spring Cloud对Feign 的封装已经可以满足我们的日常需要,并且提供了现成的API供我们使用。Ribbon 在整个分布式项目架构中占着举足轻重的作用,同样Feign 的作用比Ribbon 还重要。

更新时间:2020-04-15 21:05:32

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/springcloud03客户端feign
最后更新:2020-04-15 21:05:32

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×