那天晚上和@FeignClient注解的深度交流


廢話篇

那晚,我和@FeignClient注解的深度交流了一次,爽!

主要還是在技術群里看到有同學在問相關問題,比如: contextId是干嘛的?name相同的多個Client會報錯?

然后覺得有必要寫篇文章聊聊@FeignClient的使用,百忙之中抽時間,寫篇文章不容易啊,記得點贊。

正式篇

Feign基本介紹

首先來個基本的普及,怕有些同學還沒接觸過Spring Cloud。Feign是Netflix開源的一個REST客戶端,通過定義接口,使用注解的方式描述接口的信息,就可以發起接口調用。

GitHub地址:https://github.com/OpenFeign/feign

下面是GitHub主頁上給的一個最基本的使用示列,示列中采用Feign調用GitHub的接口。

interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

  @RequestLine("POST /repos/{owner}/{repo}/issues")
  void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

}

public static class Contributor {
  String login;
  int contributions;
}

public static class Issue {
  String title;
  String body;
  List<String> assignees;
  int milestone;
  List<String> labels;
}

public class MyApp {
  public static void main(String... args) {
    GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");
  
    // Fetch and print a list of the contributors to this library.
    List<Contributor> contributors = github.contributors("OpenFeign", "feign");
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

Spring Cloud OpenFeign介紹

Spring Cloud OpenFeign是Spring Cloud團隊將原生的Feign結合到Spring Cloud中的產物。從上面原生Feign的使用示列來看,用的注解都是Feign中自帶的,但我們在開發中基本上都是基於Spring MVC的注解,不是很方便調用。所以Spring Cloud OpenFeign擴展了對Spring MVC注解的支持,同時還整合了Ribbon和Eureka來提供均衡負載的HTTP客戶端實現。

GitHub地址:https://github.com/spring-cloud/spring-cloud-openfeign

官方提供的使用示列:

@FeignClient("stores")
public interface StoreClient {
    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    List<Store> getStores();

    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);
}

FeignClient注解的使用介紹

value, name

value和name的作用一樣,如果沒有配置url那么配置的值將作為服務名稱,用於服務發現。反之只是一個名稱。

serviceId

serviceId已經廢棄了,直接使用name即可。

contextId

比如我們有個user服務,但user服務中有很多個接口,我們不想將所有的調用接口都定義在一個類中,比如:

Client 1

@FeignClient(name = "optimization-user")
public interface UserRemoteClient {
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id") int id);
}

Client 2

@FeignClient(name = "optimization-user")
public interface UserRemoteClient2 {
	@GetMapping("/user2/get")
	public User getUser(@RequestParam("id") int id);
}

這種情況下啟動就會報錯了,因為Bean的名稱沖突了,具體錯誤如下:

Description:
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

解決方案可以增加下面的配置,作用是允許出現beanName一樣的BeanDefinition。

spring.main.allow-bean-definition-overriding=true

另一種解決方案就是為每個Client手動指定不同的contextId,這樣就不會沖突了。

上面給出了Bean名稱沖突后的解決方案,下面來分析下contextId在Feign Client的作用,在注冊Feign Client Configuration的時候需要一個名稱,名稱是通過getClientName方法獲取的:

String name = getClientName(attributes);

registerClientConfiguration(registry, name,
attributes.get("configuration"));
private String getClientName(Map<String, Object> client) {
    if (client == null) {
      return null;
    }
    String value = (String) client.get("contextId");
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("value");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("name");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("serviceId");
    }
    if (StringUtils.hasText(value)) {
      return value;
    }


    throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
        + FeignClient.class.getSimpleName());
  }

可以看到如果配置了contextId就會用contextId,如果沒有配置就會去value然后是name最后是serviceId。默認都沒有配置,當出現一個服務有多個Feign Client的時候就會報錯了。

其次的作用是在注冊FeignClient中,contextId會作為Client 別名的一部分,如果配置了qualifier優先用qualifier作為別名。

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientFactoryBean.class);
    validate(attributes);
    definition.addPropertyValue("url", getUrl(attributes));
    definition.addPropertyValue("path", getPath(attributes));
    String name = getName(attributes);
    definition.addPropertyValue("name", name);
    String contextId = getContextId(attributes);
    definition.addPropertyValue("contextId", contextId);
    definition.addPropertyValue("type", className);
    definition.addPropertyValue("decode404", attributes.get("decode404"));
    definition.addPropertyValue("fallback", attributes.get("fallback"));
    definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

    // 拼接別名
    String alias = contextId + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();


    boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
                                // null


    beanDefinition.setPrimary(primary);

    // 配置了qualifier優先用qualifier
    String qualifier = getQualifier(attributes);
    if (StringUtils.hasText(qualifier)) {
      alias = qualifier;
    }


    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
        new String[] { alias });
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
  }

url

url用於配置指定服務的地址,相當於直接請求這個服務,不經過Ribbon的服務選擇。像調試等場景可以使用。

使用示列

@FeignClient(name = "optimization-user", url = "http://localhost:8085")
public interface UserRemoteClient {
	
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id") int id);
}

decode404

當調用請求發生404錯誤時,decode404的值為true,那么會執行decoder解碼,否則拋出異常。

解碼也就是會返回固定的數據格式給你:

{"timestamp":"2020-01-05T09:18:13.154+0000","status":404,"error":"Not Found","message":"No message available","path":"/user/get11"}

拋異常的話就是異常信息了,如果配置了fallback那么就會執行回退邏輯:

configuration

configuration是配置Feign配置類,在配置類中可以自定義Feign的Encoder、Decoder、LogLevel、Contract等。

configuration定義

public class FeignConfiguration {
	@Bean
	public Logger.Level getLoggerLevel() {
		return Logger.Level.FULL;
	}
	@Bean
	public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
		return new BasicAuthRequestInterceptor("user", "password");
	}
	
	@Bean
	public CustomRequestInterceptor customRequestInterceptor() {
		return new CustomRequestInterceptor();
	}
	// Contract,feignDecoder,feignEncoder.....
}

使用示列

@FeignClient(value = "optimization-user", configuration = FeignConfiguration.class)
public interface UserRemoteClient {
	
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id")int id);
	
}

fallback

定義容錯的處理類,也就是回退邏輯,fallback的類必須實現Feign Client的接口,無法知道熔斷的異常信息。

fallback定義

@Component
public class UserRemoteClientFallback implements UserRemoteClient {
	@Override
	public User getUser(int id) {
		return new User(0, "默認fallback");
	}
	
}

使用示列

@FeignClient(value = "optimization-user", fallback = UserRemoteClientFallback.class)
public interface UserRemoteClient {
	
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id")int id);
	
}

fallbackFactory

也是容錯的處理,可以知道熔斷的異常信息。

fallbackFactory定義

@Component
public class UserRemoteClientFallbackFactory implements FallbackFactory<UserRemoteClient> {
	private Logger logger = LoggerFactory.getLogger(UserRemoteClientFallbackFactory.class);
	
	@Override
	public UserRemoteClient create(Throwable cause) {
		return new UserRemoteClient() {
			@Override
			public User getUser(int id) {
				logger.error("UserRemoteClient.getUser異常", cause);
				return new User(0, "默認");
			}
		};
	}
}

使用示列

@FeignClient(value = "optimization-user", fallbackFactory = UserRemoteClientFallbackFactory.class)
public interface UserRemoteClient {
	
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id")int id);
	
}

path

path定義當前FeignClient訪問接口時的統一前綴,比如接口地址是/user/get, 如果你定義了前綴是user, 那么具體方法上的路徑就只需要寫/get 即可。

使用示列

@FeignClient(name = "optimization-user", path="user")
public interface UserRemoteClient {
	
	@GetMapping("/get")
	public User getUser(@RequestParam("id") int id);
}

primary

primary對應的是@Primary注解,默認為true,官方這樣設置也是有原因的。當我們的Feign實現了fallback后,也就意味着Feign Client有多個相同的Bean在Spring容器中,當我們在使用@Autowired進行注入的時候,不知道注入哪個,所以我們需要設置一個優先級高的,@Primary注解就是干這件事情的。

qualifier

qualifier對應的是@Qualifier注解,使用場景跟上面的primary關系很淡,一般場景直接@Autowired直接注入就可以了。

如果我們的Feign Client有fallback實現,默認@FeignClient注解的primary=true, 意味着我們使用@Autowired注入是沒有問題的,會優先注入你的Feign Client。

如果你鬼斧神差的把primary設置成false了,直接用@Autowired注入的地方就會報錯,不知道要注入哪個對象。

解決方案很明顯,你可以將primary設置成true即可,如果由於某些特殊原因,你必須得去掉primary=true的設置,這種情況下我們怎么進行注入,我們可以配置一個qualifier,然后使用@Qualifier注解進行注入,示列如下:

Feign Client定義

@FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
public interface UserRemoteClient {
	
	@GetMapping("/get")
	public User getUser(@RequestParam("id") int id);
}

Feign Client注入

@Autowired
@Qualifier("userRemoteClient")
private UserRemoteClient userRemoteClient;


免責聲明!

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



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