The author | Spring Cloud Alibaba senior development engineer, night From the public, alibaba middleware

Some time ago, Hystrix announced that it would no longer maintain the product. Where is Spring Cloud going? Feign, as a component that is heavily dependent on Hystrix, must be concerned about its future use.

As the fuse Sentinel in Spring Cloud Alibaba system, Sentinel is currently integrated with Feign. This paper summarizes the integration process and welcomes everyone to discuss and use it.

What is Feign?

Feign is an Http client implemented in Java to simplify Restful calls.

Feign doesn’t have the same concept as OkHttp or HttpClient. Feign emphasizes the definition of an interface, in which a method corresponds to an Http request, calling a method sends an Http request; OkHttp or HttpClient sends Http requests in a procedural manner. Feign’s low-level implementation of sending requests can be integrated with OkHttp or HttpClient.

To integrate Feign, you need to first understand how Feign is used and implemented, and then see how Sentinel fits in.

The use of Feign

There are two steps:

1, use,@EnableFeignClientsAnnotation turns Feign on

@SpringBootApplication
@EnableFeignClients // Enable Feign
public class MyApplication {... }Copy the code

@enableFeignClients attribute description

Value :String[] Package path. For example, org.my. PKG scans the @FeignClient annotated classes in the package path and processes them.

BasePackages :String[]

basePackageClasses:Class<? >[] basePackages is a String array; basePackageClasses is a Class array;

defaultConfiguration:Class
[] Default configuration classes. For all Feign Clients, the configuration in these classes will apply to them. Feign. Codec. Decoder, Feign. Codec. Encoder, feign.

clients:Class
[] @feignClient; A collection of classes modified by an annotation. If this property is specified, the properties associated with the scan function are invalidated. Such as Value, basePackages, and basePackageClasses;

2, use,@FeignClientAnnotations modify the interface, which generates proxy classes based on the interface

@FeignClient(name = "service-provider")
public interface EchoService {
  @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
  String echo(@PathVariable("str") String str);
}
Copy the code

Simply ensure that the interface modified by the @FeignClient annotation can be scanned by the @EnableFeignClients annotation, and a Proxy class is generated based on java.lang.Reflect.Proxy based on this interface.

Once the proxy class is generated, it is injected into ApplicationContext and can be used directly by AutoWired. When used, calling the Echo method is equivalent to making a Restful request.

@feignClient:

Value :String Service name. For example, service-provider, http://service-provider. For example, if value=service-provider is configured in EchoService, the URL for invoking echo is http://service-provider/echo. If value=https://service-provider is set, the url for calling the echo method is https://service-provider/divide

ServiceId :String This attribute has expired but is still available. Name :String has the same effect as value

Qualifier :String Sets the @qualifier annotation to FeignClient

Url :String Absolute path, used to replace the service name. Priority is higher than service name. For example, if the URL =aaa is configured in EchoService, the url for calling echo is http://aaa/echo. If the URL =https://aaa is set, the url for calling echo is https://aaa/divide

Decode404: Boolean default is false, indicates whether to decode a request with HTTP status code404, default is not decode, as an exception. If set to true, a 404 response will still parse the body

configuration:Class
[] works the same way as the @enableFeignClients attribute defaultConfiguration, but for a single FeignClient configuration, The @enableFeignClients defaultConfiguration property is globally scoped for all FeignClients

fallback:Class
The default value is void. Class, which represents the fallback class. The corresponding FeignClient interface method needs to be implemented.

If the fallback property is configured, the fallback class is wrapped in a Default FallbackFactory implementation class, fallBackFactory.default. Instead of using the fallbackFactory implementation class corresponding to the fallbackFactory property

fallbackFactory:Class
the default value is void. The class, said fallback class Factory production, can realize feign. Hystrix. FallbackFactory interface, The FallbackFactory internally returns a Fallback class for a Throwable exception

Path :String Request path. Between the service name or URL and the requestPath

Primary: Boolean Defaults to true, indicating whether the current feignClient-generated bean is primary.

So if you haveApplicationContextThere is an implementation inEchoServiceInterface Bean, but it is not used for injection because the Bean generated by FeignClient is primary

Feign execution process

Now that we know how to use Feign, let’s look at how Feign constructs a Client.

The @enableFeignClients annotation shows that the entry is in the FeignClientsRegistrar class on the @enableFeignClients annotation and the link looks like this:

From this link we can get several information:

The interface modified by 1.@FeignClient annotations will eventually be converted to FeignClientFactoryBean this FactoryBean, and the getObject method inside the FactoryBean will eventually return a Proxy

2. In the process of constructing Proxy based on org. Springframework. Cloud. Openfeign. Targeter to construct the target of the interface methods. If the hystrix switch is enabled (feign.hystrix.enabled=true), HystrixTargeter is used, otherwise the default DefaultTargeter is used

Feign. Feign.Builder will call its build method to construct feign.Feign instance (default only subclass ReflectiveFeign).

If launched hystrix switch (feign. Hystrix. Enabled = true), will use feign. Hystrix. HystrixFeign. Builder, or use the default feign. Feign. Builder

4. After creating feign.Feign instance, call newInstance to return a Proxy

A quick look at the logic inside this newInstance method:

public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if(Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else{ methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); }}// Use the InvocationHandlerFactory to construct the InvocationHandler from the interface's method information and target object
    InvocationHandler handler = factory.create(target, methodToHandler);
    // Construct the proxy
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), newClass<? >[]{target.type()}, handler);for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }
Copy the code

Here the InvocationHandlerFactory is passed in when constructing Feign:

  • Using the native DefaultTargeter: So will use feign. InvocationHandlerFactory. The factory Default, And constructed InvocationHandler is feign. ReflectiveFeign. FeignInvocationHandler

  • Using hystrix HystrixTargeter: so in feign hystrix. HystrixFeign. Builder# build (feign. Hystrix. FallbackFactory
    the invocationHandlerFactory method that calls the parent class passes in an anonymous invocationHandlerFactory implementation class, The InvocationHandler constructed inside this class is HystrixInvocationHandler

Sentinel integrated Feign

After understanding Feign’s implementation, Sentinel wants to integrate Feign with Hystrix’s implementation:

1.❌ implements the Targeter interface SentinelTargeter. Unfortunately, the Targeter interface is a package-level interface and cannot be used in external packages. It doesn’t matter, we can use the default HystrixTargeter(actually DefaultTargeter, as explained in Note below).

2.✅ FeignClientFactoryBean internally constructs Targeter and feign.feign.Builder from FeignContext. So we stick with the default of DefaultTargeter while internally using feign.feign.Builder, which is not a package-level class and can be used externally

  • Create SentinelFeign.Builder to inherit feign.Feign.Builder to construct feign

  • Internally, the SentinelFeign.Builder needs to fetch properties in the FeignClientFactoryBean for processing, such as fallback, Name, and fallbackFactory.

Unfortunately,FeignClientFactoryBeanThis class is also package-level. It doesn’t matter. We know it existsApplicationContextThe beanName, after get bean according to reflection just retrieve attributes (at the time of initialization of the process, not at the time of call, so will not affect performance)

  • SentinelFeign.BuildercallbuildMethods to constructFeignWe don’t need to implement a new oneFeignSame as HystrixReflectiveFeignCall the parent class as you followfeign.Feign.BuilderSome methods can be modified, such asinvocationHandlerFactoryMethods set upInvocationHandlerFactorycontractThe call

3. ✅ like hystrix implementing SentinelInvocationHandler custom InvocationHandler interface used for processing method call

4. ✅ SentinelInvocationHandler internal use Sentinel, involves the resource name for this time. Internal SentinelInvocationHandler feign. Target can get service name information, Feign. InvocationHandlerFactory. MethodHandler implementation class feign. To get the corresponding SynchronousMethodHandler request path information.

Unfortunately, feign. SynchronousMethodHandler this class’s class package level. it doesn’t matter We can customize a Feign. Contract implementation class SentinelContractHolder to store the metadata as it processes MethodMetadata The method is parsed and validated as the Builder constructs Feign.

Contract is set by calling the SentinelContract. Builder, and the SentinelContractHolder holds a contract internally using the delegate method without affecting the original contract process

Note: Spring-cloud-starter-OpenFeign dependencies include feign-Hystrix internally. HystrixTargeter: HystrixTargeter: HystrixTargeter: HystrixTargeter: HystrixTargeter: HystrixTargeter: HystrixTargeter: HystrixTargeter: HystrixTargeter: HystrixTargeter

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget
       
         target)
        {
  if(! (feigninstanceof feign.hystrix.HystrixFeign.Builder)) {
    / / if the Builder is not feign. Hystrix. HystrixFeign. Builder, use the Builder for processing
    // We built the sentinelfeign. Builder by default. What's wrong with using the feign-hystrix dependency
    returnfeign.target(target); } feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign; . }Copy the code

Inside the SentinelInvocationHandler we handling strategy of resource name is: HTTP methods: protocol: / / service name/request path as parameters

Take this TestService for example:

@FeignClient(name = "test-service")
public interface TestService {
  @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
  String echo(@PathVariable("str") String str);

  @RequestMapping(value = "/divide", method = RequestMethod.GET)
  String divide(@RequestParam("a") Integer a, @RequestParam("b") Integer b);
}
Copy the code
  • echoMethod corresponding to the resource name:GET:http://test-service/echo/{str}
  • divideMethod corresponding to the resource name:GET:http://test-service/divide

conclusion

1. Many of Feign’s internal classes are package-level, and some classes cannot be referenced by the external package. In this case, we have to find ways around them, such as using reflection

2. The current implementation runs the risk of changing the code in case the Feign related classes used inside starter become package level. So it might be more appropriate to put Sentinel implementation in Feign and give it official PR

3.Feign’s process is fairly clear and easy to integrate once we understand the design principles

Welcome everyone to discuss the integration plan, and can give the unreasonable place, of course, can mention PR to solve the unreasonable place is better.

Sentinel Starter’s code for integrating Feign is currently available in the Github repository, but not yet in release. The release is expected by the end of the month. If you want to use it now, you can either import Spring SNAPSHOT’s Repository in poM or download the source code and compile it yourself.

Finally, a Feign example using Nacos for service discovery and Sentinel for stream limiting is included.

Github.com/spring-clou…

Follow wechat public account: Java technology stack, reply to cloud in the background of public account, get the Spring Cloud series tutorial of stack length sorting, all of which are practical dry products.

  • High availability setup of Spring Cloud configuration center
  • How to choose multiple Versions of Spring Cloud
  • What is Spring Cloud, compared to Dubbo
  • Spring Cloud registry high availability setup
  • Spring Cloud Eureka self-protection mechanism