Application of Dubbo generalization call in vivo unified configuration system

Author: vivo Internet Server Team - Wang Fei, LinYupan

Dubbo's generalized calling feature can initiate remote calls in scenarios that do not depend on the service interface API package. This feature is especially suitable for framework integration and gateway application development.

Combining with the problems encountered in the actual development process that need to call multiple tripartite systems remotely, this paper expounds how to use Dubbo generalization calls to simplify the development of project practices that reduce system coupling. Finally, the principle of Dubbo generalization calls is discussed. In-depth analysis.

1. Background

The unified configuration platform is a platform that provides file configuration and file distribution capabilities for each module of the terminal device. The module is developed to perform file configuration on the background server, and then the terminal device can obtain the corresponding configuration file according to specific rules, and the file can be distributed according to various The equipment dimension is issued, and the specific project framework can be found in the following figure:

picture

The existing distribution strategies are all configured by the module development in the unified configuration background server to configure the distribution dimension. Whether the file is distributed to the corresponding terminal device is determined by the dimension selected by the user on this platform.

However, other business parties also have the A/B experiment platform within the company to configure and issue rules to use the unified configuration platform to poll the server every day to request new files, but whether the files configured in the unified configuration platform can be issued is determined by A/B The B experiment platform determines that the A/B experiment platform will configure the corresponding rules and the file id corresponding to the unified configuration platform, and then the unified configuration platform needs to call the A/B experiment platform interface for the request to determine whether the file can be delivered.

With the increase of the company's internal experimental platform, there are more and more such docking requirements that the three-party platform decides whether to issue documents. How to better and faster respond to such similar docking requirements is a problem that we need to think deeply about .

Second, the program selection

The original unified configuration distribution logic is to first find all files that can be distributed, and then determine whether a single configuration file meets the device dimension, and if so, it can be distributed. Now after docking with the A/B experimental platform, whether the file can be issued still needs to be determined by the external system. At that time, two schemes were considered in the design:

  • Option One:

Similarly, first find all the files that can be delivered, and then judge whether a single file matches according to ① the device dimension, then ② call the interface of the A/B experiment platform to obtain the file IDs that can be delivered by this device, and then call ③ the grayscale experiment platform Obtain the file IDs that can be delivered by this device, and finally summarize the configuration file IDs obtained in the first three steps to obtain the files that can be delivered, as shown in the following figure.

picture

Scheme 1 breaks the original judgment logic of whether a file can be distributed. In addition to the original judgment logic, additional steps are required to call other systems to append another file that can be distributed. And it is inevitable to connect with other third-party systems in the future. Solution 1 needs to continuously increase the logic of calling the third-party interface to add the file ID that can be issued. In addition, conventional dubbo calls need to introduce second-party libraries and model types of other experimental systems on the provider side, which increases the strong coupling between the unified configuration system and other systems.

  • Option II:

The advanced feature of Dubbo generalization call is used to abstract a delivery dimension (remote call), which is specially used for other scenarios where the three-party experimental system decides whether to issue a file, as shown in the following figure:

picture

Solution 2 abstracts a remote call delivery dimension in a unified manner, which can maintain the original judgment logic, that is, first find all the files in the system that can be delivered, and then match according to the device dimension. If a certain file is configured with remote Call the dimension, then look up the function name, parameter type array and parameter value object array contained in the remote calling dimension, and then call the three-party interface to determine whether the file can be delivered to the device, and finally get the file ID list that can be delivered. .

In addition, by using Dubbo generalization to call advanced features, the caller does not care about the detailed definition of the provider's interface, but only needs to pay attention to which method to call, what parameters to pass, and what return result to receive, thus avoiding the need to rely on the service provider's Second-party libraries and model classifiers can greatly reduce the coupling between the consumer side and the provider side.

Based on the above analysis, we finally determined that the second solution is to use Dubbo generalization calls to abstract a unified dimension. Let's take a look at the specific implementation.

Third, the specific implementation

1. GenericService is a generalization interface provided by Dubbo for generalization calls. Only one $invoke method is provided, and the three entry parameters are the function name, the parameter type array and the parameter value object array.

package com.alibaba.dubbo.rpc.service;
 
/**
 * Generic service interface
 *
 * @export
 */
public interface GenericService {
 
    /**
     * Generic invocation
     *
     * @param method         Method name, e.g. findPerson. If there are overridden methods, parameter info is
     *                       required, e.g. findPerson(java.lang.String)
     * @param parameterTypes Parameter types
     * @param args           Arguments
     * @return invocation return value
     * @throws Throwable potential exception thrown from the invocation
     */
    Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;

2. Create a service reference configuration object ReferenceConfig.

private ReferenceConfig<GenericService> buildReferenceConfig(RemoteDubboRestrictionConfig config) {
    ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
    referenceConfig.setApplication(applicationConfig);
    referenceConfig.setRegistry(registryConfig);
    referenceConfig.setInterface(config.getInterfaceName());
    referenceConfig.setVersion(config.getVersion());
    referenceConfig.setGeneric(Boolean.TRUE.toString());
    referenceConfig.setCheck(false);
    referenceConfig.setTimeout(DUBBO_INVOKE_TIMEOUT);
    referenceConfig.setRetries(DUBBO_INVOKE_RETRIES);
    return referenceConfig;
}

3. Set the request parameters and service call, here the service call can be made by using the complete method name, parameter type array and parameter value array configured in the background.

public List<Integer> invoke(RemoteDubboRestrictionConfig config, ConfigFileListQuery listQuery) {
    //由于ReferenceConfig很重量,里面封装了所有与注册中心及服务提供方连接,所以这里做了缓存
    GenericService genericService = prepareGenericService(config);
 
    //构建参数
    Map<String, Object> params = buildParams(listQuery);
    String method = config.getMethod();
    String[] parameterTypeArray = new String[]{Map.class.getName()};
    Object[] parameterValueArray = new Object[]{params};
 
    long begin = System.currentTimeMillis();
    Assert.notNull(genericService, "cannot find genericService");
    //具体调用
    Object result = genericService.$invoke(method, parameterTypeArray, parameterValueArray);
 
    if (logger.isDebugEnabled()) {
        long duration = System.currentTimeMillis() - begin;
        logger.debug("Dubbo调用结果:{}, 耗时: {}", result, duration);
    }
 
    return result == null ? Collections.emptyList() : (List<Integer>) result;
}

So why does the caller involved in the Dubbo generalization call not care about the detailed definition of the provider's interface, but only need to pay attention to which method to call, what parameters to pass, and what return result to receive?

Before explaining the implementation principle of generalized invocation, let's briefly describe the principle of direct invocation.

Fourth, Dubbo direct call related principles

The principle of direct invocation of Dubbo involves two aspects: the principle of Dubbo service exposure and the principle of Dubbo service consumption

4.1 Dubbo Service Exposure Principle

4.1.1 The overall process of remote exposure of services

picture

On the whole, the service exposure of the Dubbo framework is divided into two parts. The first step is to convert the held service instance into an Invoker through a proxy. The second step is to convert the Invoker into an Exporter through a specific protocol (such as Dubbo). This layer of abstraction also greatly facilitates function expansion.

The Invoker here can be simply understood as a real service object instance, which is the entity domain of the Dubbo framework. All models will move closer to it, and invoke calls can be made to it. It may be a local implementation, it may be a remote implementation, it may be a cluster implementation.

The source code is as follows:

if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
 
            // export to local if the config is not remote (export to remote only when config is remote)
            if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
                exportLocal(url);
            }
            // export to remote if the config is not local (export to local only when config is local)
            if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
                if (logger.isInfoEnabled()) {
                    logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
                }
                if (registryURLs != null && !registryURLs.isEmpty()) {
                    for (URL registryURL : registryURLs) {
                        url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
                        URL monitorUrl = loadMonitor(registryURL);
                        if (monitorUrl != null) {
                            url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
                        }
                        if (logger.isInfoEnabled()) {
                            logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
                        }
 
                        // For providers, this is used to enable custom proxy to generate invoker
                        String proxy = url.getParameter(Constants.PROXY_KEY);
                        if (StringUtils.isNotEmpty(proxy)) {
                            registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
                        }
 
                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                        DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
                        //向注册中心注册服务信息
                        Exporter<?> exporter = protocol.export(wrapperInvoker);
                        exporters.add(exporter);
                    }
                } else {
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
                    DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
 
                    Exporter<?> exporter = protocol.export(wrapperInvoker);
                    exporters.add(exporter);
                }
            }

First, encapsulate the implementation class ref as an Invoker, then convert the invoker into an exporter, and finally put the exporter into the cached Listexporters.

4.1.2 Details of service exposure

4.1.2.1 Encapsulate the implementation class ref as Invoker

Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

picture

① The entry of dubbo remote exposure is in the export() method of ServiceBean. Since servicebean' inherits the serviceconfig class, the actual exposed logic is the doExport() method of serviceconfig.

② Dubbo supports the same service to expose multiple protocols, such as exposing Dubbo and REST protocols at the same time, and also supports multiple registries, such as zookeeper and nacos, the framework will make a service exposure to the used protocols in turn, and each protocol registers metadata Will be written to multiple registries, specifically by executing doExportUrlsFor1Protocol.

③ Then create an Invoker object through a dynamic proxy, generate an AbstractProxylnvoker instance on the server side, all real method calls will be delegated to the proxy, and then forwarded to the service implementer for ref calls; dynamic proxies generally include: JavassistProxyFactory and JdkProxyFactory. The JavassistProxyFactory chosen here.

4.1.2.2 Convert invoker to exporter

Exporter exporter= protocol.export(wrapperInvoker);

Exporter<?> exporter = protocol.export(wrapperInvoker);

After the service instance ref is converted into an Invoker, the service exposure process begins.

picture

There will be a series of filter links here, and finally more fine-grained control will be performed through RegistryProtocol#export, such as first exposing the service and then registering the service metadata. The registry does the following things in sequence when exposing the service:

  1. Delegate a specific protocol (Dubbo) to expose services, create NettyServer listening ports and save service instances.
  2. Create a registry object and create a TCP connection with the registry.
  3. Register service metadata to the registry.
  4. Subscribe to the configurers node and listen for service dynamic property change events.
  5. The service destroys the finishing work, such as closing the port, deregistering the service information, etc.
@Override
    public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        //export invoker
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
 
        URL registryUrl = getRegistryUrl(originInvoker);
 
        //registry provider
        final Registry registry = getRegistry(originInvoker);
        final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);
 
        //to judge to delay publish whether or not
        boolean register = registeredProviderUrl.getParameter("register", true);
 
        ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);
 
        if (register) {
            //TODO 注册服务元数据
            register(registryUrl, registeredProviderUrl);
            ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
        }
 
        // Subscribe the override data
        // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call the same service. Because the subscribed is cached key with the name of the service, it causes the subscription information to cover.
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        //Ensure that a new exporter instance is returned every time export
        return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
    }

Here we focus on the process doLocalExport(final InvokeroriginInvoker) of entrusting specific protocols to expose services.

private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) {
        String key = getCacheKey(originInvoker);
        ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
        if (exporter == null) {
            synchronized (bounds) {
                exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
                if (exporter == null) {
                    final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
                    exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
                    bounds.put(key, exporter);
                }
            }
        }
        return exporter;
    }

(Exporter) The protocol.export(invokerDelegete) method will go through a series of interceptors and finally call the export method of DubboProtocol.

picture

@Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        URL url = invoker.getUrl();
 
        // export service.
        String key = serviceKey(url);
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
        exporterMap.put(key, exporter);
 
        //export an stub service for dispatching event
        Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
        Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
        if (isStubSupportEvent && !isCallbackservice) {
            String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
            if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
                if (logger.isWarnEnabled()) {
                    logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
                            "], has set stubproxy support event ,but no stub methods founded."));
                }
            } else {
                stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
            }
        }
 
        openServer(url);
        optimizeSerialization(url);
        return exporter;
    }

A very important point here is to put the exporter in the cache. The key here is in the form of serviceGroup/serviceName:serviceVersion:port. The last thing obtained here is com.alibaba.dubbo.demo.DemoService:20880, and then the DubboExporter is created. The memory cache exporterMap here is a very important attribute, which will be used again when subsequent consumers call the service provider.

So far, the remote exposure process of the server provider is basically introduced.

4.2 Implementation principle of Dubbo service consumption

4.2.1 The overall process of service consumption

picture

On the whole, the service consumption of the Dubbo framework is also divided into two parts. The first step is to generate an Invoker by holding a remote service instance. This Invoker is the core remote proxy object on the client side. The second step will convert the Invoker through the dynamic proxy into a dynamic proxy reference that implements the user interface. The Invoker here carries functions such as network connection, service invocation and retry. On the client side, it may be a remote implementation or a cluster implementation.

The source code is as follows:

public Object getObject() throws Exception {
        return get();
    }
 
    public synchronized T get() {
        if (destroyed) {
            throw new IllegalStateException("Already destroyed!");
        }
        if (ref == null) {
            init();
        }
        return ref;
    }
 
    private void init() {
        ...
        ref = createProxy(map);
    }
 
    private T createProxy(Map<String, String> map) {
        ...
        if (urls.size() == 1) {
            invoker = refprotocol.refer(interfaceClass, urls.get(0));
        }
        ...
        // 创建服务代理
        return (T) proxyFactory.getProxy(invoker);
    }

4.2.2 Details of service consumption

4.2.2.1 Use Protocol to convert interfaceClass to Invoker

invoker = refprotocol.refer(interfaceClass, url);

picture

① The entry point of the service reference is ReferenceBean#getObject. Since Referencebean' inherits the serviceconfig class, the get method of Reference will be called.

② Then generate Invoker according to the referenced interface type that will hold the remote service instance.

③ Through a series of filter chains, the doRefer method of RegistryProtocol is finally called.

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
        RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
        directory.setRegistry(registry);
        directory.setProtocol(protocol);
        // all attributes of REFER_KEY
        Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
        URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);
        if (!Constants.ANY_VALUE.equals(url.getServiceInterface())
                && url.getParameter(Constants.REGISTER_KEY, true)) {
            URL registeredConsumerUrl = getRegisteredConsumerUrl(subscribeUrl, url);
            registry.register(registeredConsumerUrl);
            directory.setRegisteredConsumerUrl(registeredConsumerUrl);
        }
        directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
                Constants.PROVIDERS_CATEGORY
                        + "," + Constants.CONFIGURATORS_CATEGORY
                        + "," + Constants.ROUTERS_CATEGORY));
 
        Invoker invoker = cluster.join(directory);
        ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
        return invoker;
    }

This logic mainly completes the creation of the registry instance, the metadata registration to the registry and the subscription functions.

Where is the specific remote Invoker created? Where is the client call interceptor constructed?

When a subscription is initiated for the first time in directory.subscrib(), a data pull operation will be performed, and the RegistryDirectory#notify method will be triggered at the same time. The notification data here is the full data of a certain category, such as providers and routers category data. The Invoker conversion is done within the RegistryDirectory#toInvokers method when the providers are notified of the data.

picture

private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
        Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>();
        if (urls == null || urls.isEmpty()) {
            return newUrlInvokerMap;
        }
        Set<String> keys = new HashSet<String>();
        String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
        for (URL providerUrl : urls) {
            // If protocol is configured at the reference side, only the matching protocol is selected
           ......
            URL url = mergeUrl(providerUrl);
 
            String key = url.toFullString(); // The parameter urls are sorted
            if (keys.contains(key)) { // Repeated url
                continue;
            }
            keys.add(key);
            // Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
            Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
            Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
            if (invoker == null) { // Not in the cache, refer again
                try {
                    boolean enabled = true;
                    .........
                    if (enabled) {
                        invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
                    }
                } catch (Throwable t) {
                    logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
                }
                if (invoker != null) { // Put new invoker in cache
                    newUrlInvokerMap.put(key, invoker);
                }
            } else {
                newUrlInvokerMap.put(key, invoker);
            }
        }
        keys.clear();
        return newUrlInvokerMap;
    }

core code

invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);

Here will go through a series of filter chains, and then finally call the refer method of DubboProtocol to create a specific invoker.

@Override
    public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
        optimizeSerialization(url);
        // create rpc invoker.
        DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
        invokers.add(invoker);
        return invoker;
    }

The invoker returned here will be used to update the methodInvokerMap property of the RegistryDirectory. Finally, when the consumer method is actually called, the corresponding invoker list will be found according to the method.

private void refreshInvoker(List<URL> invokerUrls) {
        if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
                && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
            this.forbidden = true; // Forbid to access
            this.methodInvokerMap = null; // Set the method invoker map to null
            destroyAllInvokers(); // Close all invokers
        } else {
            this.forbidden = false; // Allow to access
            Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
            if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
                invokerUrls.addAll(this.cachedInvokerUrls);
            } else {
                this.cachedInvokerUrls = new HashSet<URL>();
                this.cachedInvokerUrls.addAll(invokerUrls);//Cached invoker urls, convenient for comparison
            }
            if (invokerUrls.isEmpty()) {
                return;
            }
            Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map
            Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // Change method name to map Invoker Map
            // state change
            // If the calculation is wrong, it is not processed.
            if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
                logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size :0. urls :" + invokerUrls.toString()));
                return;
            }
            this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
            this.urlInvokerMap = newUrlInvokerMap;
            try {
                destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
            } catch (Exception e) {
                logger.warn("destroyUnusedInvokers error. ", e);
            }
        }
    }

4.2.2.2 Using ProxyFactory to create proxies

(T) proxyFactory.getProxy(invoker)

The above proxyFactory is an instance of ProxyFactory$Adaptive, and the final result of getProxy is a JavassistProxyFactory wrapped by StubProxyFactoryWrapper. Look directly at the JavassistProxyFactory.getProxy method.

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

[invoker]: MockClusterInvoker instance

【interfaces】:[interface com.alibaba.dubbo.demo.DemoService, interface com.alibaba.dubbo.rpc.service.EchoService]

The proxy object we finally returned is actually a proxy0 object. When we call its sayHello method, it calls the internal handler.invoke method.

package com.alibaba.dubbo.common.bytecode;
 
import com.alibaba.dubbo.demo.DemoService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
 
public class proxy0 implements DemoService {
    public static Method[] methods;
    private InvocationHandler handler;
 
    public String sayHello(String paramString) {
        Object[] arrayOfObject = new Object[1];
        arrayOfObject[0] = paramString;
        Object localObject = this.handler.invoke(this, methods[0], arrayOfObject);
        return (String) localObject;
    }
 
    public proxy0() {
    }
 
    public proxy0(InvocationHandler paramInvocationHandler) {
        this.handler = paramInvocationHandler;
    }
}

Dubbo generalization calls are generally in the form of direct exposure on the server provider and service generalization calls on the consumer side, so here we focus on the difference between Dubbo generalization calls and direct calls on the consumer side of service references and initiating consumption. connect.

5. The difference and connection between Dubbo generalization call and direct call

5.1 Generate Invoker by holding remote service instance

private T createProxy(Map<String, String> map) {
        ...
        if (urls.size() == 1) {
            invoker = refprotocol.refer(interfaceClass, urls.get(0));
        }
        ...
        // 创建服务代理
        return (T) proxyFactory.getProxy(invoker);
    }

The source of the interfaceClass here is different. createProxy(Mapmap) is called in the init() method of ReferenceConfig. The specific interfaceClass will be different depending on whether it is a return call or not. See the following code for details:

private void init() {
        ...
        if (ProtocolUtils.isGeneric(getGeneric())) {
            interfaceClass = GenericService.class;
        } else {
            try {
                interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                        .getContextClassLoader());
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            checkInterfaceAndMethods(interfaceClass, methods);
        }
        ...
        ref = createProxy(map);
    }

Call directly: interfaceClass→com.alibaba.dubbo.demo.DemoService

Generalization call: interfaceClass→com.alibaba.dubbo.rpc.service.GenericService

The final invoker obtained is also different

Call directly:

interface com.alibaba.dubbo.demo.DemoService -> dubbo://xx.xx.xx.xx:20881/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-consumer&bean.name=com.alibaba.dubbo.demo.DemoService&check=false&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=24932&qos.port=33333®ister.ip=xx.xx.xx.xx&remote.timestamp=1640744945905&side=consumer×tamp=1640745033688

Generalized call:

interface com.alibaba.dubbo.rpc.service.GenericService -> dubbo://xx.xx.xx.xx:20881/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=test&bean.name=com.alibaba.dubbo.demo.DemoService&check=false&dubbo=2.0.2&generic=true&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=27952&qos.port=33333®ister.ip=xx.xx.xx.xx&remote.timestamp=1640748337173&side=consumer×tamp=1640748368427

5.2 Service Initiation Consumption Process

In 4.2.2 Service Consumer Initiating Request Details The first step is to encapsulate the request parameters (method name, method parameter type, method parameter value, service name, additional parameters) into an Invocation.

The directly called RpcInvoaction is as follows:

RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[world], attachments={}]

The RpcInvoaction of the generalization call is as follows:

RpcInvocation [methodName=$invoke, parameterTypes=[class java.lang.String, class [Ljava.lang.String;, class [Ljava.lang.Object;], arguments=[sayHello, [Ljava.lang.String;@22c1bff0, [Ljava.lang.Object;@30ae230f], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

We can find that the RpcInvocation objects generated here are different, but the services exposed by the service provider will not change, so there must be a conversion process here. The key to parameter conversion here lies in the GenericImplFilter class on the service provider side.

@Activate(group = Constants.PROVIDER, order = -20000)
public class GenericFilter implements Filter {
 
    protected final Logger logger = LoggerFactory.getLogger(getClass());
 
    @Override
    public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
        logger.info("----------------GenericFilter-------------------------");
        if (inv.getMethodName().equals(Constants.$INVOKE)
                && inv.getArguments() != null
                && inv.getArguments().length == 3
                && !invoker.getInterface().equals(GenericService.class)) {
            String name = ((String) inv.getArguments()[0]).trim();
            String[] types = (String[]) inv.getArguments()[1];
            Object[] args = (Object[]) inv.getArguments()[2];
            try {
                Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
                Class<?>[] params = method.getParameterTypes();
                if (args == null) {
                    args = new Object[params.length];
                }
                String generic = inv.getAttachment(Constants.GENERIC_KEY);
 
                if (StringUtils.isBlank(generic)) {
                    generic = RpcContext.getContext().getAttachment(Constants.GENERIC_KEY);
                }
 
                if (StringUtils.isEmpty(generic)
                        || ProtocolUtils.isDefaultGenericSerialization(generic)) {
                    args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
                } else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
                    ...
                } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
                    ...
                }
                Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));
                if (result.hasException()
                        && !(result.getException() instanceof GenericException)) {
                    return new RpcResult(new GenericException(result.getException()));
                }
                RpcResult rpcResult;
                if (ProtocolUtils.isJavaGenericSerialization(generic)) {
                    ...
                } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
                    ...
                } else {
                    rpcResult = new RpcResult(PojoUtils.generalize(result.getValue()));
                }
                rpcResult.setAttachments(result.getAttachments());
                return rpcResult;
            } catch (NoSuchMethodException e) {
                throw new RpcException(e.getMessage(), e);
            } catch (ClassNotFoundException e) {
                throw new RpcException(e.getMessage(), e);
            }
        }
        return invoker.invoke(inv);
    }
 
}

Core process:

① Whether it is a generalization call judgment

if (inv.getMethodName().equals(Constants.$INVOKE)
                && inv.getArguments() != null
                && inv.getArguments().length == 3
                && !invoker.getInterface().equals(GenericService.class)) {

② Extraction of parameters

String name = ((String) inv.getArguments()[0]).trim();
            String[] types = (String[]) inv.getArguments()[1];
            Object[] args = (Object[]) inv.getArguments()[2];

③ Serialization of parameters, and then construct a new RpcInvocation object

Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
                Class<?>[] params = method.getParameterTypes();
if (StringUtils.isEmpty(generic)
                        || ProtocolUtils.isDefaultGenericSerialization(generic)) {
                    args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
                }
...
 
Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));

RpcInvocation object before serialization:

RpcInvocation [methodName=$invoke, parameterTypes=[class java.lang.String, class [Ljava.lang.String;, class [Ljava.lang.Object;], arguments=[sayHello, [Ljava.lang.String;@22c1bff0, [Ljava.lang.Object;@30ae230f], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

RpcInvocation object after serialization:

RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[world], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

The following call logic is consistent with the direct call. For example, from the local cache, get the List<invoker> whose key is sayHello (specified method name) from the Map<string, list<invoker>> methodInvokerMap in the local cache, and then proceed to subsequent calls.

So when will the invoke method of GenericFilter be triggered? This is actually related to the establishment of the call chain of the filter. From the annotation on the GenericFilter class, we can see @Activate(group = Constants.PROVIDER, order = -20000), The description takes effect on the service provider side.

In addition, how does the service provider know whether the call is a direct call or a generalized call? Here, the GenericImplFilter class on the consumer side corresponding to the GenericFilter on the service provider side is involved. The code is as follows:

/**
 * GenericImplInvokerFilter
 */
@Activate(group = Constants.CONSUMER, value = Constants.GENERIC_KEY, order = 20000)
public class GenericImplFilter implements Filter {
 
    private static final Logger logger = LoggerFactory.getLogger(GenericImplFilter.class);
 
    private static final Class<?>[] GENERIC_PARAMETER_TYPES = new Class<?>[]{String.class, String[].class, Object[].class};
 
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String generic = invoker.getUrl().getParameter(Constants.GENERIC_KEY);
        if (ProtocolUtils.isGeneric(generic)
                && !Constants.$INVOKE.equals(invocation.getMethodName())
                && invocation instanceof RpcInvocation) {
        ...
        }
 
        if (invocation.getMethodName().equals(Constants.$INVOKE)
                && invocation.getArguments() != null
                && invocation.getArguments().length == 3
                && ProtocolUtils.isGeneric(generic)) {
 
            Object[] args = (Object[]) invocation.getArguments()[2];
            if (ProtocolUtils.isJavaGenericSerialization(generic)) {
 
                for (Object arg : args) {
                    if (!(byte[].class == arg.getClass())) {
                        error(generic, byte[].class.getName(), arg.getClass().getName());
                    }
                }
            } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
                for (Object arg : args) {
                    if (!(arg instanceof JavaBeanDescriptor)) {
                        error(generic, JavaBeanDescriptor.class.getName(), arg.getClass().getName());
                    }
                }
            }
 
            ((RpcInvocation) invocation).setAttachment(
                    Constants.GENERIC_KEY, invoker.getUrl().getParameter(Constants.GENERIC_KEY));
        }
        return invoker.invoke(invocation);
    }

5.3 Overall flow chart of generalization call

 

picture

6. Summary

High cohesion and low coupling is an important goal of our architecture design, and Dubbo's generalized calling feature only needs to know the complete interface path, request parameter type and request parameter value of the service to directly call and obtain the request result, which can avoid dependencies. A specific third-party jar package, thereby reducing the coupling of the system. In the process of daily learning and development, we need to pay attention to some advanced features in addition to the conventional use of a technology, so as to make a more appropriate architecture design.

References:

  1. "In-depth understanding of Apache Dubbo and actual combat" Yi Ji, Lin Lin

  2. Dubbo source code analysis - generalization call

  3. Dubbo generalization call usage and principle analysis

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/vivotech/blog/5580732