Spring Cloud OpenFeign source code analysis

0. Getting started demo

  • This code is an example code of OpenFeign, which gets all contributors of a Github warehouse and creates an issue. It is recommended to DEBUG and read the source code
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 + ")");
    }
  }
}

Feign.build injection dependency configuration item

    public Feign build() {
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
              logLevel, decode404, closeAfterDecode, propagationPolicy);
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
              errorDecoder, synchronousMethodHandlerFactory);
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }

Calling JDK dynamic proxy to generate interface proxy class

Dynamic proxy generation interface object

public class ReflectiveFeign extends Feign {
	@Override
	public <t> T newInstance(Target<t> target) {
		//Use Contract to parse the methods and annotations on the interface class, and transform the individual MethodHandler processing
		Map<string, methodhandler> nameToHandler = targetToHandlersByName.apply(target);
		// DK dynamic proxy is used to generate proxy objects for the interface. The actual business logic is handed to InvocationHandler for processing, which is actually to call MethodHandler 
		InvocationHandler handler = factory.create(target, methodToHandler);
		T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<!--?-->[]{target.type()}, handler);
		return proxy;
	}
}

Parsing interface method annotation information

  • How to parse the annotation information of Github.contributors method in Demo above. Feign provides a Contract resolution protocol, which is implemented as follows.

Parsing logic is supported by default

class Default extends Contract.BaseContract {
	protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
		Class<!--? extends Annotation--> annotationType = methodAnnotation.annotationType();
		if (annotationType == RequestLine.class) {
			//@RequestLine annotation processing logic
		} else if (annotationType == Body.class) {
			//@Body annotation processing logic
		} else if (annotationType == Headers.class) {
			//@Headers annotation processing logic
		}
	}
	protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
		boolean isHttpAnnotation = false;
		for (Annotation annotation : annotations) {
			Class<!--? extends Annotation--> annotationType = annotation.annotationType();
			if (annotationType == Param.class) {
				Param paramAnnotation = (Param) annotation;
				//@Param annotation processing logic
			} else if (annotationType == QueryMap.class) {
				//@QueryMap annotation processing logic
			} else if (annotationType == HeaderMap.class) {
				//@HeaderMap annotation processing logic
			}
		}
		return isHttpAnnotation;
	}
}

Native common annotations

Annotation Interface Target
@RequestLine Method
@Param Parameter
@Headers Method, Type
@QueryMap Parameter
@HeaderMap Parameter
@Body Method

Spring MVC extension annotation

  • Spring mvccontract supports spring MVC annotation for the extension of spring cloud open feign. Now feign version also supports spring MVC annotation
public class SpringMvcContract  {
	
	// Process @ RequestMapping on class
	@Override
	protected void processAnnotationOnClass(MethodMetadata data, Class<!--?--> clz) {
		if (clz.getInterfaces().length == 0) {
			RequestMapping classAnnotation = findMergedAnnotation(clz,
					RequestMapping.class);
		}
	}
	
	// Processing @ RequestMapping annotation, of course, also supports the processing of derived annotation @ GetMapping @PostMapping, etc
	@Override
	protected void processAnnotationOnMethod(MethodMetadata data,
											 Annotation methodAnnotation, Method method) {
		if (!RequestMapping.class.isInstance(methodAnnotation) &amp;&amp; !methodAnnotation
				.annotationType().isAnnotationPresent(RequestMapping.class)) {
			return;
		}
		RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
		// Get request method
		RequestMethod[] methods = methodMapping.method();
		// produce processing
		parseProduces(data, method, methodMapping);
		// Consumers processing
		parseConsumes(data, method, methodMapping);
		// headers header processing
		parseHeaders(data, method, methodMapping);

		data.indexToExpander(new LinkedHashMap<integer, param.expander>());
	}

	// Processing request parameter spring MVC native annotation
	@Override
	protected boolean processAnnotationsOnParameter(MethodMetadata data,
													Annotation[] annotations, int paramIndex) {
		Param.Expander expander = this.convertingExpanderFactory
				.getExpander(typeDescriptor);
		if (expander != null) {
			data.indexToExpander().put(paramIndex, expander);
		}
		return isHttpAnnotation;
	}
}

MethodHandler request processing logic

MethodHandler routing

As shown in the figure above, routing to different MethodHandler implementations according to different request methods

final class SynchronousMethodHandler implements MethodHandler {
	@Override
	public Object invoke(Object[] argv) throws Throwable {
		// Get request template
		RequestTemplate template = buildTemplateFromArgs.create(argv);
		// Parameter processing
		Options options = findOptions(argv);
		// Default retrier
		Retryer retryer = this.retryer.clone();
		while (true) {
			try {
				// Execution request interceptor
				Request request = targetRequest(template);
				// Output request message
				if (logLevel != Logger.Level.NONE) {
					logger.logRequest(metadata.configKey(), logLevel, request);
				}
				Response response = client.execute(request, options);
				// Decode according to the returned status code
				...
				return response;
			} catch (RetryableException e) {
				// Related logic of retry
			}
		}
	}
}

Build request template according to different parameters

  • Form submission or direct body submission

Execute Request interceptor to generate final Request

// Get all request interceptors and execute one by one
  Request targetRequest(RequestTemplate template) {
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
    return target.apply(template);
  }

Request log processing

  • Log output level, configuring
public enum Level {
	/**
	 * No output
	 */
	NONE,
	/**
	 * Only the output Http method, URL, status code and execution time are recorded
	 */
	BASIC,
	/**
	 * Output request header and Http method, URL, status code, execution time
	 */
	HEADERS,
	/**
	 * Output request header, report style and Http method, URL, status code, execution time
	 */
	FULL
}

Client performs the final Requst request

default processing

  • Through the java.net package of JDK, the connection implementation will be created if there is no request. High performance implementation of HttpClient or OKHttp can be configured
class Default implements Client {

	private final SSLSocketFactory sslContextFactory;
	private final HostnameVerifier hostnameVerifier;
	
	@Override
	public Response execute(Request request, Request.Options options) throws IOException {
		HttpURLConnection connection = convertAndSend(request, options);
		return convertResponse(connection, request);
	}
」

Load balancing of Spring Cloud

// Client implementation of Spring Cloud
public class FeignBlockingLoadBalancerClient {
	@Override
	public Response execute(Request request, Request.Options options) throws IOException {
		// For example, request: http://pig-auth-server/token/info
		final URI originalUri = URI.create(request.url());
		// Intercept to serviceid: Pig auth server
		String serviceId = originalUri.getHost();
		// Call loadBalancer API to get available service instances
		ServiceInstance instance = loadBalancerClient.choose(serviceId);
		// Build the real request URL http://172.17.0.110:8763/token/info
		String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri)
				.toString();
		// Create request and execute
		Request newRequest = Request.create(request.httpMethod(), reconstructedUrl,
				request.headers(), request.requestBody());
		return delegate.execute(newRequest, options);
	}
}

Return message Decoder processing

  • Default handling
  class Default implements Encoder {

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
      if (bodyType == String.class) {
        template.body(object.toString());
      } else if (bodyType == byte[].class) {
        template.body((byte[]) object, null);
      } else if (object != null) {
        throw new EncodeException(
            format("%s is not a type supported by this encoder.", object.getClass()));
      }
    }
  }

  • In case of return message error handling
  public static class Default implements ErrorDecoder {

    private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder();

    @Override
    public Exception decode(String methodKey, Response response) {
      FeignException exception = errorStatus(methodKey, response);
      Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
      if (retryAfter != null) {
        return new RetryableException(
            response.status(),
            exception.getMessage(),
            response.request().httpMethod(),
            exception,
            retryAfter,
            response.request());
      }
      return exception;
    }

    private <t> T firstOrNull(Map<string, collection<t>&gt; map, String key) {
      if (map.containsKey(key) &amp;&amp; !map.get(key).isEmpty()) {
        return map.get(key).iterator().next();
      }
      return null;
    }
  }
}

Injection of custom ErrorDecoder is common.

The above content is the request processing flow of open feign. The following is how to initialize and run the extended content spring cloud open feign?

[Extension] Spring Cloud OpenFeign

Analysis of EnableFeignClients

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    
}
  • When we add @ EnableFeignClients annotation to the Main method, the related functions of spring cloud open feign are enabled.
  • Import (feignclientsregister. Class) import feignclientsregister, scan @ FeignClient to inject into the container

FeignClientsRegistrar

class FeignClientsRegistrar {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
										BeanDefinitionRegistry registry) {
		registerFeignClients(metadata, registry);
	}
	
	public void registerFeignClients(AnnotationMetadata metadata,
									 BeanDefinitionRegistry registry) {
		
		// Scan @ FeignClient in configuration scope in configuration annotation
		for (String basePackage : basePackages) {
			// Fill IOC container
			registerClientConfiguration(registry, name,
							attributes.get("configuration"));
		}
	}
	
	//Feignclient & lt; -- & gt; bean construction
	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);
		...

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

Default condition

public class FeignAutoConfiguration {
    // If feign hystrix module is not introduced, defaulttarget will be injected
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
    protected static class DefaultFeignTargeterConfiguration {
    
    	@Bean
    	@ConditionalOnMissingBean
    	public Targeter feignTargeter() {
    		return new DefaultTargeter();
    	}
    }
}

If feign hystrix is not introduced, the above process is the same as the original process. When we call feignclient.method, the dynamic agent will be triggered to execute the logic of MethodHandler

HystrixFeign

  • First of all, does the introduction of hystrix feign mean that the logic has changed

At the beginning, 0. Get started with Demo Feign.builder(), which becomes HystrixFeign.builder()

public final class HystrixFeign {
	public static Builder builder() {
		return new Builder();
	}
	public static final class Builder extends Feign.Builder {
		
		// Implementation of injection into HystrixInvocationHandler
		Feign build(final FallbackFactory<!--?--> nullableFallbackFactory) {
			super.invocationHandlerFactory(new InvocationHandlerFactory() {
				@Override
				public InvocationHandler create(Target target,
												Map<method, methodhandler> dispatch) {
					return new HystrixInvocationHandler(target, dispatch, setterFactory,
							nullableFallbackFactory);
				}
			});
			super.contract(new HystrixDelegatingContract(contract));
			return super.build();
		}
		
	}
}

  • Inject the implementation of HystrixInvocationHandler, wrap it with HystrixCommand, and finally use methodhandler to call the final interface
final class HystrixInvocationHandler implements InvocationHandler {
	
	@Override
	public Object invoke(final Object proxy, final Method method, final Object[] args)
			throws Throwable {

		// Wrapping with HystrixCommand
		HystrixCommand<object> hystrixCommand =
				new HystrixCommand<object>(setterMethodMap.get(method)) {
					@Override
					protected Object run() throws Exception {
						try {
							// Call methodhandler to process the final request
							return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
						} catch (Exception e) {
							throw e;
						} catch (Throwable t) {
							throw (Error) t;
						}
					}
				};


		return hystrixCommand.execute();
	}
}

SentinelFeign

  • First, look at the class annotation like {@link HystrixFeign.Builder}, "learn from" HystrixFeign
/**
 * {@link Feign.Builder} like {@link HystrixFeign.Builder}.
 */
public final class SentinelFeign {
}
  • Inject the implementation of SentinelInvocationHandler, use Sentinel wrapper, and finally use methodhandler to call the final interface
public class SentinelInvocationHandler implements InvocationHandler {
	@Override
	public Object invoke(final Object proxy, final Method method, final Object[] args)
			throws Throwable {
		// Wrapping requests with sentinel
		try {
			ContextUtil.enter(resourceName);
			entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
			result = methodHandler.invoke(args);
		}
		catch (Throwable ex) {
			// fallback logic
		}
		finally {
			ContextUtil.exit();
		}
		return result;
	}
}

Summary sequence diagram

Follow up plan

Welcome to follow me, and update Ribbon, Hystrix, Sentinel, Nacos and other components source text analysis. >Project recommendation: Welcome to RBAC permission management system of Spring Cloud and Spring Security OAuth2 </object></object></method,></string,></string,></t></integer,></string,></t></t></contributor></string></string></contributor>

Keywords: Programming Spring github JDK Java

Added by timbo6585 on Tue, 18 Feb 2020 06:19:49 +0200