Spring Cloud OpenFeign深入浅出

1. 简单玩法

1.1 一个简单例子

(1)服务端:

@RestController
@RequestMapping("hello")
public class HelloController implements HelloApi {
    @Override
    public String hello(String name) {
        return "Hello, "+name+"!";
    }
}

API声明:

public interface HelloApi {

    @GetMapping("/hello/{name}")
    String hello(@PathVariable("name") String name);

    @GetMapping("/bye/{name}")
    ResponseValue<String> bye(@PathVariable("name") String name);

    @GetMapping(value = "/download")
    byte[] download(HttpServletResponse response);
}

(2)客户端:

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

开启配置 @EnableFeignClients,调用服务的代码:

@FeignClient(name="hello1", url = "127.0.0.1:8080", path = "hello")
public interface HelloApiExp extends HelloApi {
    @GetMapping("/download")
    Response download();
}

调用时的代码

@RestController
@RequestMapping("client")
public class HelloClient {

    @Autowired
    private HelloApiExp helloApi;

    @GetMapping("/hello/{name}")
    public String hello(@PathVariable("name") String name){
        return helloApi.hello(name);
    }
} 

浏览器访问URL:http://127.0.0.1:8080/client/hello/Mark,页面返回: Hello, Mark!

1.2 @FeignClient的简单用法

 

 

2. 高级玩法

2.1 configuration配置类

通过自定义配置类统一配置Feign的各种功能属性,FeignClientsConfiguration为默认配置:

@FeignClient(name="hello1", url = "127.0.0.1:8080", configuration = FeignClientsConfiguration.class)
public interface HelloApi {
    @GetMapping("/{name}")
    String hello(@PathVariable("name") String name);
}

2.1.1 Decoder feignDecoder
Decoder类,将http返回的Entity字符解码(反序列化)为我们需要的实例,如自定义的POJO对象。一般使用FeignClientsConfiguration默认的feignDecoder就能满足返回String、POJO等绝大多数场景。

@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
    return new OptionalDecoder(
        new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}

2.1.2 Encoder feignEncoder
Encode类对请求参数做编码(序列化)后,发送给http服务端。使用spring cloud默认的feignEncoder可以满足我们绝大多数情况。

使用Feign实现文件上传下载时需要特殊处理,使用feign-form能够方便的实现。这里我们对feign-form在spring cloud中的使用举一个简单的例子。

HelloApi接口声明:

public interface HelloApi {
    @GetMapping(value = "/download")
    byte[] download(HttpServletResponse response);

    @PostMapping(value = "upload", 
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseValue<String> upload(@RequestBody MultipartFile file);
}

服务端代码:

@RestController
@RequestMapping("hello")
public class HelloController implements HelloApi {
    
    @Override
    public byte[] download(HttpServletResponse response) {
        FileInputStream fis = null;
        try{
            File file = new File("E:\\图片\\6f7cc39284868762caaed525.jpg");
            fis = new FileInputStream(file);
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition",
                               "attachment;filename=class.jpg");
            return IOUtils.toByteArray(fis, file.length());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    
    @Override
    public ResponseValue<String> upload(@RequestBody MultipartFile file) {
        File destFile = new File("d:\\1.jpg");
        ResponseValue<String> response = new ResponseValue<>();
        try {
            file.transferTo(destFile);
            return response.ok("上传成功!", null);
        } catch (IOException e) {
            e.printStackTrace();
            return response.fail("上传失败,错误原因:"+e.getMessage());
        }
    }    
}

客户端代码:

pom.xml引入依赖:

<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.8.0</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form-spring</artifactId>
    <version>3.8.0</version>
</dependency>

增加FeignClient配置类:

@Configuration
public class FeignMultipartSupportConfig extends FeignClientsConfiguration {
    @Bean
    public Encoder feignFormEncoder() {
        return new SpringFormEncoder();
    }
}

FeignClient接口声明:

import feign.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name="hello1", url = "127.0.0.1:8080", path = "hello",
        configuration = FeignMultipartSupportConfig.class)
public interface HelloApiExp extends HelloApi {
    @GetMapping("/download")
    Response download();
}

调用端代码:

@RestController
@RequestMapping("client")
public class HelloClient {

    @GetMapping(value = "/download")
    public byte[] download(HttpServletResponse response){
        response.setHeader("Content-Disposition",
                           "attachment;filename=class.jpg");
        //response.setHeader("Content-Type","application/octet-stream");

        Response resp = helloApi.download();
        Response.Body body = resp.body();
        try(InputStream is = body.asInputStream()) {
            return IOUtils.toByteArray(is, resp.body().length());
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    @PostMapping(value = "upload", 
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseValue<String> upload(@RequestBody MultipartFile file){
        return helloApi.upload(file);
    }    
}

2.1.3 Retryer feignRetryer

请求重试策略类,默认不重试,可配置成feign.Retryer.Default,启用重试,默认间隔100毫秒重试一次,最大间隔时间限制为1秒,最大重试次数5次。

@Configuration
public class FeignRetryConfig extends FeignClientsConfiguration {
    @Bean
    @Override
    public Retryer feignRetryer() {
        return new Retryer.Default();
    }
}

2.1.4 Feign.Builder feignBuilder

FeignClient的Builder,我们可以通过他使用代码的方式设置相关属性,代替@FeignClient的注解过的接口,如下面的代码:

@GetMapping("/hello/{name}")
public String hello(@PathVariable("name") String name){
    String response = feignBuilder
        .client(new OkHttpClient())
        .encoder(new SpringFormEncoder())
        .requestInterceptor(new ForwardedForInterceptor())
        .logger(new Slf4jLogger())
        .logLevel(Logger.Level.FULL)
        .target(String.class, "http://127.0.0.1:8080");

    return response;
    //return helloApi.hello(name);
}

其实@FeignClient生成的代理类也是通过它构建的。代码中的feignBuilder.client()可以使用RibbonClient,就集成了Ribben。

2.1.5 FeignLoggerFactory feignLoggerFactory
设置LoggerFactory类,默认为Slf4j。

2.1.6 Feign.Builder feignHystrixBuilder
配置Hystrix,从下面的配置类可以看出,@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) ,如果引用了Hystrix的相关依赖,并且属性feign.hystrix.enabled为true,则构建@FeignClient代理类时使用的FeignBuilder会使用feignHystrixBuilder。Feign通过这种方式集成了Hystrix。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {

   @Bean
   @Scope("prototype")
   @ConditionalOnMissingBean
   @ConditionalOnProperty(name = "feign.hystrix.enabled")
   public Feign.Builder feignHystrixBuilder() {
      return HystrixFeign.builder();
   }
}

2.2 故障转移

故障转移机制,如果@FeignClient指定了fallback或fallbackFactory属性,http请求调用失败时会路由到fallback处理类的相同方法中。

2.2.1 fallback

@FeignClient声明:

@FeignClient(name="hello1", url = "127.0.0.1:8080", path = "hello",
        configuration = FeignMultipartSupportConfig.class,
        fallback = HelloApiFallback.class)
public interface HelloApiExp extends HelloApi {
    @GetMapping("/download")
    Response download();
}

HelloApiFallback代码需要实现HelloApiExp接口(包括父接口)的所有方法:

@Slf4j
public class HelloApiFallback implements HelloApiExp {
    @Override
    public Response download() {
        log.error("下载文件出错。");
        return null;
    }

    @Override
    public String hello(String name) {
        log.error("调用hello接口出错。");
        return "调用hello接口出错,请联系管理员。";
    }

    @Override
    public ResponseValue<String> bye(String name) {
        log.error("调用bye接口出错。");
        ResponseValue<String> response = new ResponseValue<>();
        return response.fail("调用hello接口出错,请联系管理员。");
    }

    @Override
    public byte[] download(HttpServletResponse response) {
        log.error("调用bye接口出错。");
        return new byte[0];
    }

    @Override
    public ResponseValue<String> upload(MultipartFile file) {
        log.error("调用上传文件接口出错。");
        ResponseValue<String> response = new ResponseValue<>();
        return response.fail("上传文件出错,请联系管理员。");
    }
}

2.2.2 fallbackFactory

为@FeignClient接口所有方法指定统一的故障处理方法。

@FeignClient(name="hello1", url = "127.0.0.1:8080", path = "hello",
        configuration = FeignMultipartSupportConfig.class,
        fallbackFactory = FallbackFactory.Default.class)
public interface HelloApiExp extends HelloApi {

    @GetMapping("/download")
    Response download();
}

FallbackFactory.Default实现如下,请求失败后,统一路由到create(Throwable cause)方法。

/** Returns a constant fallback after logging the cause to FINE level. */
final class Default<T> implements FallbackFactory<T> {
  // jul to not add a dependency
  final Logger logger;
  final T constant;

  public Default(T constant) {
    this(constant, Logger.getLogger(Default.class.getName()));
  }

  Default(T constant, Logger logger) {
    this.constant = checkNotNull(constant, "fallback");
    this.logger = checkNotNull(logger, "logger");
  }

  @Override
  public T create(Throwable cause) {
    if (logger.isLoggable(Level.FINE)) {
      logger.log(Level.FINE, "fallback due to: " + cause.getMessage(), cause);
    }
    return constant;
  }

  @Override
  public String toString() {
    return constant.toString();
  }
}

3. OpenFeign思维导图

在此奉上我整理的OpenFeign相关的知识点思维导图。

 

posted @ 2020-06-15 22:57  King-DA  阅读(1131)  评论(0编辑  收藏  举报