You must understand and can understand microservice series 3: service invocation

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

Keywords: Java Spring

Added by tgh on Sat, 20 Nov 2021 06:22:27 +0200