After understanding the service registration and de registration, it's time to go to the service invocation phase.
Before calling the service, you need to assemble the request header and request body, build the client object, and call the remote service through the url address of the service provider. Although this method can realize remote call, users need to understand the underlying call, and it will also bring the problem of repeated coding to developers.
To this end, let's learn how OpenFeign of spring cloud implements remote invocation today
3. Service call
3.1 enable Feign remote call
When we need to use feign to make remote calls, we only need to add @ EnableFeignClients annotation at the entrance
3.2 scan FeignClient annotation interface
@The EnableFeignClients annotation introduces the feignclientsregister configuration class, which will perform the scanning action to scan the interfaces marked by the @ FeignClient annotation in the project
ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class)); Set<String> basePackages = getBasePackages(metadata); for (String basePackage : basePackages) { candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); } Copy code
Key code: scanner. Addincludefilter (new annotation type filter (feignclient. Class));
3.3 create and register FeignClientFactoryBean
Traverse the components scanned in 3.2. Each component creates a FeignClientFactoryBean and injects it into the IOC container. The type of FeignClientFactoryBean is the interface marked with @ FeignClient
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); Class clazz = ClassUtils.resolveClassName(className, null); ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory) registry : null; String contextId = getContextId(beanFactory, attributes); String name = getName(attributes); // 1. Create FeignClientFactoryBean FeignClientFactoryBean factoryBean = new FeignClientFactoryBean(); // 2. Set properties factoryBean.setBeanFactory(beanFactory); factoryBean.setName(name); factoryBean.setContextId(contextId); factoryBean.setType(clazz); BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> { factoryBean.setUrl(getUrl(beanFactory, attributes)); factoryBean.setPath(getPath(beanFactory, attributes)); factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404")))); Object fallback = attributes.get("fallback"); if (fallback != null) { factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback : ClassUtils.resolveClassName(fallback.toString(), null)); } Object fallbackFactory = attributes.get("fallbackFactory"); if (fallbackFactory != null) { factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory : ClassUtils.resolveClassName(fallbackFactory.toString(), null)); } return factoryBean.getObject(); }); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); definition.setLazyInit(true); validate(attributes); AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className); beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean); // has a default, won't be null boolean primary = (Boolean) attributes.get("primary"); beanDefinition.setPrimary(primary); String[] qualifiers = getQualifiers(attributes); if (ObjectUtils.isEmpty(qualifiers)) { qualifiers = new String[] { contextId + "FeignClient" }; } BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers); // 3. Inject into IOC container BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } Copy code
3.4 inject FeignClientFactoryBean
When we call the remote service interface, we often need to inject services through @Autowired annotation, and then call the corresponding method.
@RestController public class OpenfeignConsumerController { @Autowired private IndexClient indexClient; @GetMapping("/index/{name}") public String index(@PathVariable(name = "name") String name) { return indexClient.index(name); } } @FeignClient(value = "openfeign-provider-service") public interface IndexClient { @GetMapping("/index/{name}") String index(@PathVariable(name = "name") String name); } Copy code
IndexClient is annotated with @ FeignClient annotation. According to chapter 3.3, it is actually a FeignClientFactoryBean
FeignClientFactoryBean implements the FactoryBean interface. Therefore, when injecting with the @ Autowired annotation, the object returned by the getObject() method in FeignClientFactoryBean is injected
3.5 FeignContext
From the naming, we can know that FeignContext is the context of feign, which has a Map type context attribute
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>(); Copy code
In other words, each @ FeignClient corresponds to an AnnotationConfigApplicationContext context, which also exists in its own application
Therefore, the parent-child context is formed
3.6 building Feign Builder
Builder builder = this.feign(context); Copy code
protected Builder feign(FeignContext context) { FeignLoggerFactory loggerFactory = (FeignLoggerFactory)this.get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(this.type); Builder builder = ((Builder)this.get(context, Builder.class)).logger(logger).encoder((Encoder)this.get(context, Encoder.class)).decoder((Decoder)this.get(context, Decoder.class)).contract((Contract)this.get(context, Contract.class)); this.configureFeign(context, builder); this.applyBuildCustomizers(context, builder); return builder; } Copy code
Building Feign Builder is to obtain relevant components from the container for setting, and then personalize Feign Builder
3.7 obtaining components from containers
this.get(context, xxxx.class)) method appears frequently in Chapter 3.6. You need to know the specific implementation of this method. Here, take this.get(context, FeignLoggerFactory.class) as an example
protected <T> T get(FeignContext context, Class<T> type) { T instance = context.getInstance(this.contextId, type); if (instance == null) { throw new IllegalStateException("No bean found of type " + type + " for " + this.contextId); } else { return instance; } } Copy code
FeignContext is introduced in Chapter 3.5, which maintains a sub container set. Therefore, first, the sub container with the specified name will be obtained from the sub container set
Since FeignContext maintains a collection of sub containers, you must understand how sub containers are created
protected AnnotationConfigApplicationContext getContext(String name) { if (!this.contexts.containsKey(name)) { synchronized (this.contexts) { if (!this.contexts.containsKey(name)) { this.contexts.put(name, createContext(name)); } } } return this.contexts.get(name); } protected AnnotationConfigApplicationContext createContext(String name) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); if (this.configurations.containsKey(name)) { for (Class<?> configuration : this.configurations.get(name).getConfiguration()) { context.register(configuration); } } for (Map.Entry<String, C> entry : this.configurations.entrySet()) { if (entry.getKey().startsWith("default.")) { for (Class<?> configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } } context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType); context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName, Collections.<String, Object>singletonMap(this.propertyName, name))); if (this.parent != null) { // Uses Environment from parent as well as beans context.setParent(this.parent); // jdk11 issue // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101 context.setClassLoader(this.parent.getClassLoader()); } context.setDisplayName(generateDisplayName(name)); context.refresh(); return context; } Copy code
Each time, it is taken from the collection. If there is no corresponding sub container in the collection, it is created, and then the container is registered with PropertyPlaceholderAutoConfiguration and this.defaultConfigType
Through the constructor of FeignContext, we can understand that this.defaultConfigType is FeignClientsConfiguration
Open FeignClientsConfiguration to see that beans such as Encoder, Decoder and Contract are declared
3.8 summary
Here, I should have a general summary in my mind:
- @FeignClient corresponds to FeignClientFactoryBean
- The interface annotated by @ FeignClient is actually injected with the object returned by getObject() in FeignClientFactoryBean
- FeignContext, as the context of feign, creates a sub container for each @ FeignClient, in which the required beans are declared
- Declaring a sub container for each @ FeignClient isolates @ FeignClient from @ FeignClient's configuration
3.9 create Feign instance
Previously, the related properties were set in chain mode through Feign Builder. Now you can create Feign instances through Feign Builder
public Feign build() { Client client = Capability.enrich(this.client, capabilities); Retryer retryer = Capability.enrich(this.retryer, capabilities); List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream() .map(ri -> Capability.enrich(ri, capabilities)) .collect(Collectors.toList()); Logger logger = Capability.enrich(this.logger, capabilities); Contract contract = Capability.enrich(this.contract, capabilities); Options options = Capability.enrich(this.options, capabilities); Encoder encoder = Capability.enrich(this.encoder, capabilities); Decoder decoder = Capability.enrich(this.decoder, capabilities); // 1.InvocationHandlerFactory is used to create InvocationHandler InvocationHandlerFactory invocationHandlerFactory = Capability.enrich(this.invocationHandlerFactory, capabilities); QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities); // 2.SynchronousMethodHandler is used to create SynchronousMethodHandler SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding); ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); } Copy code
3.10 creating agents
@Override public <T> T newInstance(Target<T> target) { // 1. Traverse the methods in the @ FeignClient annotation interface and create a MethodHandler through SynchronousMethodHandler.Factory 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 { // 2. Build the mapping relationship between Method and MethodHandler for Method routing methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } // 3. Create InvocationHandler through InvocationHandlerFactory InvocationHandler handler = factory.create(target, methodToHandler); // 4. Generate proxy object T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } Copy code
Here we can know
@Autowired private IndexClient indexClient; Copy code
What is injected is a proxy object. When the IndexClient method is called, the invoke method of ReflectiveFeign.FeignInvocationHandler will be called back
3.11 method call
@Override public Object invoke(Object proxy, Method method, 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(); } return dispatch.get(method).invoke(args); } Copy code
As follows, route to the corresponding SynchronousMethodHandler according to the method, then call its invoke().
@Override public Object invoke(Object[] argv) throws Throwable { // 1. Build request template RequestTemplate template = buildTemplateFromArgs.create(argv); Options options = findOptions(argv); Retryer retryer = this.retryer.clone(); while (true) { try { // 2. Initiate a remote call via http and return the result return executeAndDecode(template, options); } catch (RetryableException e) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } } Copy code
So far, the remote service call is realized. You can see that it is very simple to use Openfeign for remote call. You can call the remote service just like calling the local method with relevant annotations
4. Conclusion
After Openfeign is explained in this article, you can also try to use HttpClient with dynamic proxy to implement an RPC framework to meet your sense of achievement