Feign extension - in-process call

1. Background

There is no need to describe how popular microservices are at present. The most direct result is that some software products with monolithic architecture can perform better in some major application scenarios, and follow the fashion and choose microservice architecture. So a series of nightmares ensued.

We will not discuss what these nightmares are here, and interested readers can refer to the reference links at the bottom. The main purpose of this article is to alleviate the problem in a relatively smooth way, to change the implementation of feign calls from the default "use http request to realize inter-process interaction", and provide an additional implementation through extension - intra-process interaction .
insert image description here

Doing so yields the following benefits:

  1. Leave the previous microservice version unchanged. It is suitable for microservice application scenarios such as high concurrency and high traffic.
  2. At the same time, a product package of the single deployment version is provided, which is suitable for most small application scenarios.
  3. During the whole process, the upper layer has no perception, and the backward compatibility is maximized.

2. Realize

2.1 Ideas

Thanks to reading feign-related source code-1 and feign-related source code-2 in the past , the whole process is relatively smooth, and some key ideas are given below.

  1. Referenced from the implementation of hystrix or sentinel ( SentinelFeign) , provide a custom implementation in the process of Feign.Builderoverriding the implementation method .build()InvocationHandler
  2. In the custom InvocationHandlerimplementation, based on SpringMVC's processing logic and feign's analysis result container for feign's defined interface, after performing corresponding query matching, find out the corresponding called springmvc controller method, use reflection to align and call, and then enter the normal original processing logic.

2.2 Key code

Let's start with an overview of the relevant key types.

key class explain
FeignInvokeInProcess.Builder Learn from SentinelFeign.Builderor HystrixFeign.Builder, through the custom InvocationHandlerFactoryintervention feign main logic, generate a custom InvocationHandlerimplementation class for each feign interface
InvokeInProcessInwardInvocationHandler Custom InvocationHandlerimplementation class. It is used to override feign's default "http method to call the corresponding service", and instead use the in-process scheduling corresponding service.
NeedCallByRemoteWhenFeignInvokeInProcess This is a note. It can be applied to classes and methods to indicate that the current method still needs to use feign's default http method to call services.
  1. The first is Feign.Builderthe implementation of , to embed the custom implementation InvocationHandlerinto feign's main body execution logic.
public class FeignInvokeInProcess {
    
    

	public static Builder builder() {
    
    
		return new Builder();
	}

	public static final class Builder extends Feign.Builder implements ApplicationContextAware {
    
    
		private Contract contract = new Contract.Default();
		private ApplicationContext applicationContext;
		private FeignContext feignContext;

		@Override
		public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
    
    
			throw new UnsupportedOperationException();
		}

		@Override
		public Builder contract(Contract contract) {
    
    
			this.contract = contract;
			return this;
		}

		@Override
		public Feign build() {
    
    
			// Refer To HystrixFeign.java
			super.invocationHandlerFactory(new InvocationHandlerFactory() {
    
    
				@SneakyThrows
				@Override
				public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
    
    
					// 注解取值以避免循环依赖的问题
					FeignClient feignClient = AnnotationUtils.findAnnotation(target.type(), FeignClient.class);
					Class fallback = feignClient.fallback();
					Class fallbackFactory = feignClient.fallbackFactory();
					String contextId = feignClient.contextId();

					if (!StringUtils.hasText(contextId)) {
    
    
						contextId = feignClient.name();
					}

					RequestMappingHandlerMapping rmhm = feignContext.getInstance(contextId,
							RequestMappingHandlerMapping.class);
					ServletContext sc = feignContext.getInstance(contextId, ServletContext.class);
					DispatcherServlet dispatcherServlet = feignContext.getInstance(contextId, DispatcherServlet.class);
					RequestMappingHandlerAdapter handlerAdapter = feignContext.getInstance(contextId,
							RequestMappingHandlerAdapter.class);
					Object fallbackInstance;
					FallbackFactory fallbackFactoryInstance;

					// 判断fallback类型
					if (void.class != fallback) {
    
    
						fallbackInstance = getFromContext(contextId, "fallback", fallback, target.type());
						InvocationHandler SentinelInvocationHandler = null; //new SentinelInvocationHandler(
						//								target, dispatch, new FallbackFactory.Default(fallbackInstance));
						return new InvokeInProcessInwardInvocationHandler(target, dispatch, rmhm, sc,
								dispatcherServlet, handlerAdapter, SentinelInvocationHandler);
					}
					if (void.class != fallbackFactory) {
    
    
						fallbackFactoryInstance = (FallbackFactory) getFromContext(contextId, "fallbackFactory",
								fallbackFactory, FallbackFactory.class);
						InvocationHandler SentinelInvocationHandler = null;// new SentinelInvocationHandler(
						//target, dispatch, fallbackFactoryInstance);
						return new InvokeInProcessInwardInvocationHandler(target, dispatch, rmhm, sc,
								dispatcherServlet, handlerAdapter, SentinelInvocationHandler);
					}
					// 默认fallbackFactory
					FallbackFactory FallbackFactory = new FallbackFactory(target);
					InvocationHandler SentinelInvocationHandler = null; //new SentinelInvocationHandler(
					//target, dispatch, FallbackFactory);
					return new InvokeInProcessInwardInvocationHandler(target, dispatch, rmhm, sc,
							dispatcherServlet, handlerAdapter, SentinelInvocationHandler);
				}

				private Object getFromContext(String name, String type, Class fallbackType, Class targetType) {
    
    
					Object fallbackInstance = feignContext.getInstance(name, fallbackType);
					if (fallbackInstance == null) {
    
    
						throw new IllegalStateException(String.format(
								"No %s instance of type %s found for feign client %s", type, fallbackType, name));
					}

					if (!targetType.isAssignableFrom(fallbackType)) {
    
    
						throw new IllegalStateException(String.format(
								"Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
								type, fallbackType, targetType, name));
					}
					return fallbackInstance;
				}
			});
			super.contract(contract);
			return super.build();
		}

		@Override
		public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
			this.applicationContext = applicationContext;
			feignContext = this.applicationContext.getBean(FeignContext.class);
		}
	}

}
  1. Then when the client initiates a feign call, we need to schedule the request to be completed in the process.
public class InvokeInProcessInwardInvocationHandler implements InvocationHandler {
    
    

	private final Target<?> target;

	private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;

	private final RequestMappingHandlerMapping requestMappingHandlerMapping;

	private final ServletContext sc;

	private final DispatcherServlet ds;

	private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;

	@Nullable
	private InvocationHandler fallback;

	public InvokeInProcessInwardInvocationHandler(Target<?> target,
			Map<Method, InvocationHandlerFactory.MethodHandler> dispatch,
			RequestMappingHandlerMapping requestMappingHandlerMapping, ServletContext sc, DispatcherServlet ds,
			RequestMappingHandlerAdapter requestMappingHandlerAdapter, InvocationHandler fallback) {
    
    
		this.target = checkNotNull(target, "target");
		this.dispatch = checkNotNull(dispatch, "dispatch");
		this.requestMappingHandlerMapping = checkNotNull(requestMappingHandlerMapping, "requestMappingHandlerMapping");
		this.sc = checkNotNull(sc, "sc");
		this.ds = checkNotNull(ds, "ds");
		this.requestMappingHandlerAdapter = checkNotNull(requestMappingHandlerAdapter, "requestMappingHandlerAdapter");
		this.fallback = checkNotNull(fallback, "fallback");		
	}

	@Override
	public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
    
    
		if ("equals".equals(method.getName())) {
    
    
			try {
    
    
				Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
				return equals(otherHandler);
			} catch (IllegalArgumentException e) {
    
    
				return false;
			}
		} else if ("hashCode".equals(method.getName())) {
    
    
			return hashCode();
		} else if ("toString".equals(method.getName())) {
    
    
			return toString();
		}

		InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method);
		Object result = null;
		if (isNeedRemoteCall(methodHandler)) {
    
    
			result = methodHandler.invoke(args);
		} else {
    
    
			// 进程内调用
			Object buildTemplateFromArgs = ReflectUtil.getFieldValue(methodHandler, "buildTemplateFromArgs");
			Method createMethod = ReflectUtil.getMethod(buildTemplateFromArgs.getClass(), "create",
					new Object[] {
    
    }.getClass());
			RequestTemplate template = ReflectUtil.invoke(buildTemplateFromArgs, createMethod, args);

			Request request = ReflectUtil.invoke(methodHandler, "targetRequest", template);

			final HttpServletRequest httpServletRequest = transfer(request);
			HandlerExecutionChain handler = requestMappingHandlerMapping.getHandler(httpServletRequest);
			if (Objects.isNull(handler)) {
    
    
				return null;
			}
			final HandlerMethod handlerMethod = Convert.convert(HandlerMethod.class, handler.getHandler());
			if (Objects.isNull(handlerMethod)) {
    
    
				// 托底
				return fallback.invoke(proxy, method, args);
			}

			// 以下两种选其一即可, 效果一样的
			//result = invokeByReflect(httpServletRequest, handlerMethod);
			result = invokeByCompleteRequestFlow(httpServletRequest);
		}

		return result;
	}

	private boolean isNeedRemoteCall(InvocationHandlerFactory.MethodHandler methodHandler) {
    
    
		if (methodHandler.getClass().getSimpleName().contains("SynchronousMethodHandler")) {
    
    
			final MethodMetadata mm = Convert.convert(feign.MethodMetadata.class,
					ReflectUtil.getFieldValue(methodHandler, "metadata"));

			final Method currentMethod = mm.method();
			final Class<?> declaringClass = currentMethod.getDeclaringClass();
			// NeedCallByRemoteWhenFeignInvokeInProcess注解: 标识当前被标注者不要进行进程内调用, 继续执行跨进程的http方式调用
			if (AnnotationUtils.findAnnotation(currentMethod, NeedCallByRemoteWhenFeignInvokeInProcess.class) != null
					|| AnnotationUtils.findAnnotation(declaringClass,
							NeedCallByRemoteWhenFeignInvokeInProcess.class) != null) {
    
    
				return true;
			}

		}
		return false;
	}

	private HttpServletRequest transfer(Request request) {
    
    
		HttpMethod httpMethodOfFeign = request.httpMethod();
		org.springframework.http.HttpMethod resolve = org.springframework.http.HttpMethod
				.resolve(httpMethodOfFeign.name());

		Map<String, Collection<String>> headers = request.headers();
		final HttpHeaders headersContianer = new HttpHeaders();
		headers.forEach((k, vals) -> {
    
    
			headersContianer.addAll(k, java.util.Arrays.asList(vals.toArray(new String[0])));
		});
		return MockMvcRequestBuilders.request(resolve, URLUtil.toURI(request.url()))//
				.headers(headersContianer)//
				.content(request.body()) //
				.buildRequest(sc);
	}

	private Object invokeByReflect(final HttpServletRequest httpServletRequest, final HandlerMethod handlerMethod)
			throws Exception {
    
    
		MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
		// 最终调用: ServletInvocableHandlerMethod.java
		ModelAndView handle = requestMappingHandlerAdapter.handle(httpServletRequest, mockHttpServletResponse,
				handlerMethod);
		Assert.isNull(handle);
		return mockHttpServletResponse.getContentAsString();
	}

	private Object invokeByCompleteRequestFlow(final HttpServletRequest httpServletRequest)
			throws ServletException, IOException {
    
    
		MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
		// 这里以触发SpringMVC的一系列扩展, 例如ResponseBodyAdvice<T>等
		ds.service(httpServletRequest, mockHttpServletResponse);
		return mockHttpServletResponse.getContentAsString();
	}

	....}
  1. For more details, please refer to the gitee warehouse address given at the bottom.

3. Optimization

It can be seen that the entire implementation is still quite rough, and it needs running-in and a lot of brutal testing to be applied to the production version. In addition, the optimization points that can be considered at the moment:

  1. Compatibility for special cases. For example, modules are gradually merged, so some request calls still have to use the default http request mode. (This is already reflected in the code above)
  2. There are a series of extensions in the processing flow of springmvc. How should these be compatible under this implementation? The above invokeByCompleteRequestFlowsimple tests are sufficient, but more detailed compatibility needs to be tested comprehensively.
  3. The above methods obviously require all service calls to use the feign method, but this cannot be guaranteed if there is no inspection mechanism for historical codes. So this piece needs to be checked and refactored.
  4. There may also be some problems with module source level merging, such as introducing dependency conflicts and duplication of package naming.
  5. Optimize performance.

4. Reference

  1. Gitee - feignInvokeInProcess
  2. feign source code analysis - initialization
  3. Feign source code analysis - runtime
  4. Microservices are lonely
  5. The start of microservices
  6. Why so much emphasis on CICD

Guess you like

Origin blog.csdn.net/lqzkcx3/article/details/129381980