In-depth communication with @FeignClient notes that night

Keywords: Programming github Spring REST JSON

Bullshit

That night, I had a deep exchange with the @FeignClient comment, cool!

Mainly in the technical group, I see some students asking related questions, such as: What is contextId for?Multiple Client s with the same name will fail?

Then I think it's necessary to write an article to chat about the use of @FeignClient. It's not easy to write an article when you're busy. Remember to praise it.

Official Text

Introduction to Feign

Let's start with a basic popularization, in case some of your classmates haven't touched Spring Cloud yet.Feign is an open source REST client for Netflix that initiates interface calls by defining interfaces and describing their information in annotations.

GitHub address: https://github.com/OpenFeign/feign

Below is a basic usage column on the GitHub home page that uses Feign's interface to call 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 + ")");
    }
  }
}

Introduction to Spring Cloud OpenFeign

Spring Cloud OpenFeign is a product of the Spring Cloud team's incorporation of native Feign into Spring Cloud.From the above list of native Feign usage, the annotations used are native to Feign, but we are basically based on Spring MVC annotations in our development, which are not very convenient to call.So Spring Cloud OpenFeign extends support for Spring MVC annotations and also integrates Ribbon and Eureka to provide load-balanced HTTP client implementations.

GitHub address: https://github.com/spring-cloud/spring-cloud-openfeign

Official usage lists:

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

Introduction to the use of FeignClient notes

value, name

Value and name work the same way, if url is not configured, then the configured value will be used as the service name for service discovery.The opposite is just a name.

serviceId

Service Id is obsolete, just use name.

contextId

For example, we have a user service, but there are many interfaces in the user service. We don't want to define all the calling interfaces in one class, for example:

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

In this case, the startup error will occur because the Bean has a conflicting name, with the following specific errors:

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

The solution can add the following configuration to allow BeanDefinition like beanName to appear.

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

Another solution is to manually specify a different contextId for each Client so that there is no conflict.

The Bean Name Conflict Resolution is given above, and the contextId's role in the Feign Client is analyzed below. A name is required to register the Feign Client Configuration. The name is obtained by the getClientName method:

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

You can see that if contextId is configured, contextId will be used, if not, value will be removed, then name will be followed by serviceId.None is configured by default, and an error will be reported when a service has multiple Feign Client s.

The second effect is that in registering a FeignClient, contextId is part of the Client alias, and if qualifier is configured, it takes precedence over 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);

    // Stitching Alias
    String alias = contextId + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();


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


    beanDefinition.setPrimary(primary);

    // Configured qualifier to take precedence over 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

The url is used to configure the address of the specified service, which is equivalent to requesting the service directly without Ribbon's service selection.Scenarios like debugging can be used.

Use Columns

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

decode404

When a call request has a 404 error and decode404 has a value of true, decoder decoding is performed or an exception is thrown.

Decoding returns a fixed data format for you:

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

If an exception is thrown, it is the exception information, and fallback logic is performed if a fallback is configured:

configuration

Configuration is a configuration Feign configuration class in which you can customize Feign's Encoder, Decoder, LogLevel, Contract, and so on.

ConfigurationDefinition

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.....
}

Use Columns

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

fallback

Defines a fault-tolerant processing class, known as fallback logic, which must implement the Feign Client's interface without knowing the fused exception information.

fallback definition

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

Use Columns

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

fallbackFactory

It is also fault-tolerant and knows the exception information of the fuse.

fallbackFactory definition

@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 abnormal", cause);
				return new User(0, "default");
			}
		};
	}
}

Use Columns

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

path

Path defines a uniform prefix for the current FeignClient access interface, such as interface address / user/get. If you define a prefix as user, then the path on the method only needs to be written/get.

Use Columns

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

primary

Primary corresponds to the @Primary comment, which defaults to true, and there is a reason for the official setting.When our Feign implements fallback, it means that Feign Client has multiple identical beans in the Spring container, and when we use @Autowired for injection, we don't know which to inject, so we need to set a higher priority, @Primary annotation is what to do.

qualifier

The qualifier corresponds to the @Qualifier annotation, and the use scenario has a very weak relationship with the primary above. In general, scenarios can be injected directly @Autowired.

If our Feign Client has a fallback implementation, the default @FeignClient comment primary=true means that it is OK for us to use @Autowired injection, and your Feign Client will be injected first.

If you false ly set primary, you'll get an error where you inject it directly with @Autowired, and you don't know which object to inject.

The solution is obvious. You can set the primary to true. If for some special reason you have to remove the setting primary=true, how do we inject in this case? We can configure a qualifier and then use the @Qualifier annotation for injection as follows:

Feign Client Definition

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

Feign Client Injection

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

If you are interested, you can follow my WeChat Ape World and read more technical articles for the first time.My GitHub also has some open source code https://github.com/yinjihuan

Posted by vincente on Tue, 07 Jan 2020 02:11:31 -0800