SpringCloud微服務服務間調用之OpenFeign介紹


開發微服務,免不了需要服務間調用。Spring Cloud框架提供了RestTemplate和FeignClient兩個方式完成服務間調用,本文簡要介紹如何使用OpenFeign完成服務間調用。

OpenFeign思維導圖

在此奉上我整理的OpenFeign相關的知識點思維導圖。

基礎配置使用例子

(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!

@FeignClient的簡單用法

屬性名稱 屬性說明 默認值
name/value 作為serviceId,bean name
contextId 作為bean name,代替name/value的值
qualifier 限定詞
url http的URL前綴(不包括協議名):主機名和端口號
decode404 請求遇到404則拋出FeignExceptions false
path 服務前綴,等同於ContextPath
primary whether to mark the feign proxy as a primary bean true

高級配置——使用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);
}

Decoder feignDecoder

Decoder類,將http返回的Entity字符解碼(反序列化)為我們需要的實例,如自定義的POJO對象。一般使用FeignClientsConfiguration默認的feignDecoder就能滿足返回String、POJO等絕大多數場景。

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

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);
    }    
}

Retryer feignRetryer

請求重試策略類,默認不重試,可配置成feign.Retryer.Default,啟用重試,默認間隔100毫秒重試一次,最大間隔時間限制為1秒,最大重試次數5次。

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

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。

FeignLoggerFactory feignLoggerFactory

設置LoggerFactory類,默認為Slf4j。

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();
   }
}

OpenFeign+consul使用示例

背景介紹

本示例使用consul作為服務注冊中心,基於SpringCloud框架開發兩個微服務,一個user-service(服務提供方),一個feignusercommodity-service(服務調用方),具體版本信息如下

軟件/框架 版本
consul v1.2.0
Spring Boot 2.0.1.RELEASE
Spring Cloud Finchley.RELEASE

openFeign使用默認版本的,也就是spring-cloud-starter-openfeign 2.0.0版本。

主要代碼

核心代碼主要包括兩點,
1, 對應接口添加@FeignClient,並完成對應服務提供者的requestMapping映射。
2,在啟動類加上@EnableFeignClients(basePackages = {"com.yq.client"}), 我的serviceClieng位於com.yq.client包。

提供方的主要接口如下:

userSvc001.png

ServiceClient類的主要實現如下.
注意:User 類在兩個服務中是一樣,實際項目中我們可以把它放到公共依賴包中。

@FeignClient(value = "user-service", fallbackFactory = UserServiceFallbackFactory.class)
public interface UserServiceClient {
    
    @RequestMapping(value="/v1/users/{userId}", method= RequestMethod.GET, produces = "application/json;charset=UTF-8")
    public User getUser(@PathVariable(value = "userId") String userId);

    @RequestMapping(value="/v1/users/queryById", method= RequestMethod.GET, produces = "application/json;charset=UTF-8")
    public User getUserByQueryParam(@RequestParam("userId") String userId);

    @RequestMapping(value="/v1/users", method= RequestMethod.POST, produces = "application/json;charset=UTF-8")
    public String createUser();
}

完整代碼看 user-servciefeignusercommodity-service,里面的pom文件,serviceClient都是完整的可以運行的。 歡迎加星,fork。

效果截圖

第一張截圖,兩個服務都正常在consul上注冊,完成服務間調用

consul001OK_FeignCall.png

第二張截圖,兩個服務都正常在consul上注冊,完成服務間調用, 這是consul down了,服務間調用可以繼續,因為feignusercommodity-service服務緩存了user-service服務的服務提供地址信息

consul002_shutdownConsul_CallOK.png

第三張截圖,feignusercommodity-service服務正常在consul上注冊,但是user-service沒有注冊,系統給出了“com.netflix.client.ClientException: Load balancer does not have available server for client: user-service”

consul003_userSvcOff.png

第四張截圖,user-service提供方的對應方法報異常,服務調用能正常獲取到該異常並顯示。

consul004_svcCallOriginalSvcException.png

故障轉移

使用Feign可以完成服務間調用,但是總存在一種情況:服務提供方沒有注冊到注冊中心、服務提供方還沒開發完成(因為也就無法調用)等等。此時如果我們需要完成服務間調用該如何做呢?

Feign提供了fallback機制,也就是當對方服務還沒ready(一般情況是服務提供方在注冊中心上沒有可用的實例),可以返回信息供服務進行下,也就是服務降級

故障轉移機制,如果@FeignClient指定了fallbackfallbackFactory屬性,http請求調用失敗時會路由到fallback處理類的相同方法中。

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("上傳文件出錯,請聯系管理員。");
    }
}

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();
  }
}

Feign結合Hystrix可以實現服務降級

主要使用consul 1.2.0, Spring Boot 1.5.12, Spring Cloud Edgware.RELEASE。

需要引入Hystrix依賴並在啟動類和配置文件中啟用Hystrix

pom文件增加如下依賴

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>

啟動類加上@EnableHystrix@EnableHystrixDashboard@EnableFeignClients

@SpringBootApplication
@EnableHystrix
@EnableHystrixDashboard
@EnableDiscoveryClient
@EnableCircuitBreaker
//@EnableTurbine
@EnableFeignClients(basePackages = {"com.yq.client"})
public class HystrixDemoApplication {
    private static final Logger logger = LoggerFactory.getLogger(HystrixDemoApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(HystrixDemoApplication.class, args);
        logger.info("HystrixDemoApplication Start done.");
    }
}

配置文件中feign啟用hystrix

feign.hystrix.enabled=true

實現自己的fallback服務

feignClient類

@FeignClient(value = "user-service", fallback = UserServiceClientFallbackFactory.class)
@Component
public interface UserServiceClient {
    @RequestMapping(value = "/v1/users/{userId}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    String getUserDetail(@PathVariable("userId") String userId);
}

自定義的fallback類

@Component
@Slf4j
public class UserServiceClientFallbackFactory implements UserServiceClient{
    @Override
    public String getUserDetail(String userId) {
        log.error("Fallback2, userId={}", userId);
        return "user-service not available2 when query '" + userId + "'";
    }
}

效果截圖

第一張截圖

雖然我們創建了fallback類,也引入了Hystrix,但是沒有啟用feign.hystrix.enabled=true,所以無法實現服務降級,服務間調用還是直接報異常。

feign001_NoEnableHystrix.png

第二張截圖

我們創建了fallback類,也引入了Hystrix,同時啟用feign.hystrix.enabled=true,所以當user-service不可用時,順利實現服務降級。

feign002_EnableHystrix.png

第三張, user-service服務正常, fallback不影響原有服務間調用正常進行。

feign003_userServiceON.png

參考文檔

官方文檔在這里: http://cloud.spring.io/spring-cloud-openfeign/single/spring-cloud-openfeign.html

fallback官方文檔:http://cloud.spring.io/spring-cloud-openfeign/single/spring-cloud-openfeign.html#spring-cloud-feign-hystrix-fallback


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM