dubbo Source Resolution Service Exposure Process

Keywords: Java Dubbo Zookeeper Attribute

dubbo Service Exposure Process

Objective: To analyze the service exposure process from the source point of view.

Preface

Originally this article was about asynchronous transformation, but recently I've been thinking about how to write a part of the optimization transformation to make it more understandable to the reader.I think it's necessary to start with the whole call chain, find out when the function happens, say something general, when or what scene the code was executed, then analyze how it was implemented internally, and finally explain the benefits of this transformation.

I rarely mentioned the relationship between call chains in the previous articles, nor do the modules be strung together, and to explain the asynchronous transformation I think it is necessary to understand the process of exposing and referencing services first, so I will use two articles to explain the process of exposing and referencing services.

Service Exposure Process

The process of service exposure can be roughly divided into three parts:

  1. Pre-work, mainly for checking parameters and assembling URL s.
  2. Exporting a service involves exposing the service locally (JVM) and remotely.
  3. Register a service with the registry for service discovery.

Exposure Start Point

Spring has an ApplicationListener interface that defines an onApplicationEvent() method that is triggered when any event occurs within a container.

The ServiceBean class in Dubbo implements this interface and implements the onApplicationEvent method:

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
      // If the service is not exposed and the service is not unexposed, print the log
    if (!isExported() && !isUnexported()) {
        if (logger.isInfoEnabled()) {
            logger.info("The service ready on spring started. service: " + getInterface());
        }
          // export
        export();
    }
}

Services are exposed as long as they are not exposed and services are not unexposed.Execute the export method.The next step is related to the time series diagram of the official website. Look at the process below against the time series diagram.

I will add the subtitle below with the scope of each step of the time series diagram, for example, the preceding work mentioned below is actually 1:export() in the time series diagram, so I will write 1 in the heading brackets.

The eighth step in service exposure is no longer available.

Pre-job (1)

The front-end work mainly consists of two parts, configuration checking and URL assembly.Before exposing the service, Dubbo needs to check that the user's configuration is reasonable or supplement the default configuration for the user.Once the configuration checks are complete, you need to assemble the URLs based on these configurations.URLs play an important role in Dubbo.Dubbo uses URLs as configuration vectors, and all the extensions are to obtain the configuration through URLs.

Configuration Check

After calling the export method, the export method in ServiceConfig is executed.

public synchronized void export() {
    //Check and update the configuration
    checkAndUpdateSubConfigs();

    // If it should not be exposed, end directly
    if (!shouldExport()) {
        return;
    }

    // Exposing services after delaying if delayed loading is used
    if (shouldDelay()) {
        delayExportExecutor.schedule(this::doExport, delay, TimeUnit.MILLISECONDS);
    } else {
        // Exposure Services
        doExport();
    }
}

You can see that the first thing to do is to check and update the configuration, which executes the checkAndUpdateSubConfigs method in ServiceConfig.It then detects whether it should be exposed, or if it should not, ends directly, and then detects whether delayed loading is configured, and if so, uses a timer for the purpose of delayed loading.

checkAndUpdateSubConfigs()
public void checkAndUpdateSubConfigs() {
    // Use default configs defined explicitly on global configs
    // Used to detect if core configuration class objects such as provider, application, etc. are empty.
    // If empty, try to get an instance from another configuration class object.
    completeCompoundConfigs();
    // Config Center should always being started first.
    // Open Configuration Center
    startConfigCenter();
    // Detects if the provider is empty, creates a new one, and initializes it with a system variable
    checkDefault();
    // Check if the application is empty
    checkApplication();
    // Check if the registry is empty
    checkRegistry();
    // Check if protocols are empty
    checkProtocol();
    this.refresh();
    // Check if metadata center configuration is empty
    checkMetadataReport();

    // Service interface name cannot be empty, otherwise an exception is thrown
    if (StringUtils.isEmpty(interfaceName)) {
        throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!");
    }

    // Detect if ref is a generalized service type
    if (ref instanceof GenericService) {
        // Set interfaceClass to GenericService
        interfaceClass = GenericService.class;
        if (StringUtils.isEmpty(generic)) {
            // Set generic = true
            generic = Boolean.TRUE.toString();
        }
    } else {
        try {
            // Get Interface Type
            interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                    .getContextClassLoader());
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        // Check the necessary fields in the interfaceClass and <dubbo:method>tags
        checkInterfaceAndMethods(interfaceClass, methods);
        // Testing ref legality
        checkRef();
        generic = Boolean.FALSE.toString();
    }
    // Stub locals are also configured with local stubs
    if (local != null) {
        if ("true".equals(local)) {
            local = interfaceName + "Local";
        }
        Class<?> localClass;
        try {
            localClass = ClassHelper.forNameWithThreadContextClassLoader(local);
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        if (!interfaceClass.isAssignableFrom(localClass)) {
            throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName);
        }
    }
    if (stub != null) {
        if ("true".equals(stub)) {
            stub = interfaceName + "Stub";
        }
        Class<?> stubClass;
        try {
            stubClass = ClassHelper.forNameWithThreadContextClassLoader(stub);
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        if (!interfaceClass.isAssignableFrom(stubClass)) {
            throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName);
        }
    }
    // Local stub validity check
    checkStubAndLocal(interfaceClass);
    // mock validity check
    checkMock(interfaceClass);
}

As you can see, this method checks various configurations and updates some of them.I will not expand on the details of the checks, because the entire process of service exposure is the focus of this article.

After shouldExport() and shouldDelay() method detection, the doExport() method of ServiceConfig is executed

doExport()
protected synchronized void doExport() {
    // The unexported value is true if an unexposed method is called
    if (unexported) {
        throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
    }
    // If the service is exposed, it ends directly
    if (exported) {
        return;
    }
    // Settings Exposed
    exported = true;

    // Assign interface name if path is empty
    if (StringUtils.isEmpty(path)) {
        path = interfaceName;
    }
    // Multi-Protocol Multi-Registry Exposure Service
    doExportUrls();
}

This method is to support multi-protocol, multi-registry exposure of services by performing a check and then the doExportUrls() method of ServiceConfig.

doExportUrls()
private void doExportUrls() {
    // Load Registry Link
    List<URL> registryURLs = loadRegistries(true);
    // Traverse protocols and expose services under each protocol
    for (ProtocolConfig protocolConfig : protocols) {
        // Use path, group, version as key for service uniqueness determination
        String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
        ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
        ApplicationModel.initProviderModel(pathKey, providerModel);
        // Assembly URL
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}

You can see from this method:

  • The loadRegistries() method is to load the registry link.
  • The uniqueness of a service is determined by a combination of path, group, and version.
  • The doExportUrlsFor1Protocol() method starts assembling the URL.
loadRegistries()
protected List<URL> loadRegistries(boolean provider) {
    // check && override if necessary
    List<URL> registryList = new ArrayList<URL>();
    // If registries are empty, return the empty collection directly
    if (CollectionUtils.isNotEmpty(registries)) {
        // Traversing the registry configuration collection registries
        for (RegistryConfig config : registries) {
            // Get Address
            String address = config.getAddress();
            // Set to 0.0.0.0 if address is empty
            if (StringUtils.isEmpty(address)) {
                address = Constants.ANYHOST_VALUE;
            }
            // If the address is N/A, skip
            if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
                Map<String, String> map = new HashMap<String, String>();
                // Add field information from ApplicationConfig to map
                appendParameters(map, application);
                // Add RegistryConfig field information to map
                appendParameters(map, config);
                // Add path
                map.put(Constants.PATH_KEY, RegistryService.class.getName());
                // Add protocol version, release version, timestamp, etc. to map
                appendRuntimeParameters(map);
                // If there is no protocol in the map, the default is to use the dubbo protocol
                if (!map.containsKey(Constants.PROTOCOL_KEY)) {
                    map.put(Constants.PROTOCOL_KEY, Constants.DUBBO_PROTOCOL);
                }
                // Resolves to a list of URLs, address may contain more than one registry ip, so it resolves to a list of URLs
                List<URL> urls = UrlUtils.parseURLs(address, map);

                // Traverse the list of URL s
                for (URL url : urls) {
                    // Set URL protocol header to registry
                    url = URLBuilder.from(url)
                            .addParameter(Constants.REGISTRY_KEY, url.getProtocol())
                            .setProtocol(Constants.REGISTRY_PROTOCOL)
                            .build();
                    // By judging the criteria, you decide whether to add a url to the registryList under the following conditions:
                    // If it's a service provider, it's a registry service or a consumer side, and it's a subscription service
                    // Join the registryList
                    if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
                            || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
                        registryList.add(url);
                    }
                }
            }
        }
    }
    return registryList;
}

Assembly URL

As I mentioned in previous articles, dubbo uses URLs to carry various configurations throughout the call chain, which is the carrier of configurations.This is where service configurations are assembled into URLs. Traversing through each protocol configuration mentioned above, exposing the service under each protocol, executes the doExportUrlsFor1 Protocol () method of ServiceConfig, which implements the logic of assembling URLs in the first half and exposing dubbo services in the second half, separated by a splitter line.

doExportUrlsFor1Protocol()
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
    // Get Protocol Name
    String name = protocolConfig.getName();
    // If empty, the default dubbo
    if (StringUtils.isEmpty(name)) {
        name = Constants.DUBBO;
    }

    Map<String, String> map = new HashMap<String, String>();
    // Set up a service provider book
    map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE);

    // Add protocol version, release version, timestamp, etc. to map
    appendRuntimeParameters(map);
    // Add all the information for metrics, application, module, provider, protocol s to the map
    appendParameters(map, metrics);
    appendParameters(map, application);
    appendParameters(map, module);
    appendParameters(map, provider, Constants.DEFAULT_KEY);
    appendParameters(map, protocolConfig);
    appendParameters(map, this);
    // If the configuration list for method is not empty
    if (CollectionUtils.isNotEmpty(methods)) {
        // Traversing the method configuration list
        for (MethodConfig method : methods) {
            // Add method name to map
            appendParameters(map, method, method.getName());
            // Add the field information of the MethodConfig object to the map, key = method name. property name.
            // For example, store <dubbo:method name="sayHello" retries="2">corresponding MethodConfig,
            // Key= sayHello.retries, map = {"sayHello.retries": 2, "xxx": "yyy"}
            String retryKey = method.getName() + ".retry";
            if (map.containsKey(retryKey)) {
                String retryValue = map.remove(retryKey);
                // If retryValue is false, do not retry, set value to 0
                if ("false".equals(retryValue)) {
                    map.put(method.getName() + ".retries", "0");
                }
            }
            // Get the ArgumentConfig list
            List<ArgumentConfig> arguments = method.getArguments();
            if (CollectionUtils.isNotEmpty(arguments)) {
                // Traverse the ArgumentConfig list
                for (ArgumentConfig argument : arguments) {
                    // convert argument type
                    // //Detect whether the type property is empty or an empty string
                    if (argument.getType() != null && argument.getType().length() > 0) {
                        // Get all collection of methods for the service using reflection
                        Method[] methods = interfaceClass.getMethods();
                        // visit all methods
                        if (methods != null && methods.length > 0) {
                            // Traverse all methods
                            for (int i = 0; i < methods.length; i++) {
                                // Get method name
                                String methodName = methods[i].getName();
                                // target the method, and get its signature
                                // Find the target method
                                if (methodName.equals(method.getName())) {
                                    // Array of parameter types for the target method by reflection
                                    Class<?>[] argtypes = methods[i].getParameterTypes();
                                    // one callback in the method
                                    // If the subscript is -1
                                    if (argument.getIndex() != -1) {
                                        // Detect if the argType name matches the type attribute in ArgumentConfig
                                        if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
                                            //  Add ArgumentConfig field information to map
                                            appendParameters(map, argument, method.getName() + "." + argument.getIndex());
                                        } else {
                                            // Inconsistency throws an exception
                                            throw new IllegalArgumentException("Argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
                                        }
                                    } else {
                                        // multiple callbacks in the method
                                        // Traverse through the parameter type array argtypes to find parameters of type argument.type
                                        for (int j = 0; j < argtypes.length; j++) {
                                            Class<?> argclazz = argtypes[j];
                                            if (argclazz.getName().equals(argument.getType())) {
                                                // If found, add ArgumentConfig field information to map
                                                appendParameters(map, argument, method.getName() + "." + j);
                                                if (argument.getIndex() != -1 && argument.getIndex() != j) {
                                                    throw new IllegalArgumentException("Argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    } else if (argument.getIndex() != -1) {
                        // The user does not configure the type attribute, but configures the index attribute, and the index!= -1 is added directly to the map
                        appendParameters(map, argument, method.getName() + "." + argument.getIndex());
                    } else {
                        // throw
                        throw new IllegalArgumentException("Argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
                    }

                }
            }
        } // end of methods for
    }

    // If it is a generalization call, set generic and methods in the map
    if (ProtocolUtils.isGeneric(generic)) {
        map.put(Constants.GENERIC_KEY, generic);
        map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
    } else {
        // Get Version Number
        String revision = Version.getVersion(interfaceClass, version);
        // Put in map
        if (revision != null && revision.length() > 0) {
            map.put(Constants.REVISION_KEY, revision);
        }

        // Get a collection of methods
        String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
        // Alert if empty
        if (methods.length == 0) {
            logger.warn("No method found in service interface " + interfaceClass.getName());
            // Set method to *
            map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
        } else {
            // Otherwise join the method collection
            map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
        }
    }
    // Add the token value to the map
    if (!ConfigUtils.isEmpty(token)) {
        if (ConfigUtils.isDefault(token)) {
            map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
        } else {
            map.put(Constants.TOKEN_KEY, token);
        }
    }
    // export service
    // Get Address
    String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
    // Get the port number
    Integer port = this.findConfigedPorts(protocolConfig, name, map);
    // Generate URL s
    URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);

    // _________161


    // Load the ConfiguratorFactory and generate an instance of Configurator to determine if an implementation of the protocol exists
    if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
            .hasExtension(url.getProtocol())) {
        // Configuring URLs through instances
        url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
    }

    String scope = url.getParameter(Constants.SCOPE_KEY);
    // don't export when none is configured
    // //If scope = none, do nothing
    if (!Constants.SCOPE_NONE.equalsIgnoreCase(scope)) {

        // export to local if the config is not remote (export to remote only when config is remote)
        // // scope!= remote, exposed locally
        if (!Constants.SCOPE_REMOTE.equalsIgnoreCase(scope)) {
            // Exposure to Local
            exportLocal(url);
        }
        // export to remote if the config is not local (export to local only when config is local)
        // // scope!= local, export to remote
        if (!Constants.SCOPE_LOCAL.equalsIgnoreCase(scope)) {
            if (logger.isInfoEnabled()) {
                logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
            }
            // If the registry link collection is not empty
            if (CollectionUtils.isNotEmpty(registryURLs)) {
                // Traverse Registry
                for (URL registryURL : registryURLs) {
                    // Add dynamic configuration
                    url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
                    // Load Monitor Link
                    URL monitorUrl = loadMonitor(registryURL);
                    if (monitorUrl != null) {
                        // Add Monitor Configuration
                        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
                    // Get Agent Method
                    String proxy = url.getParameter(Constants.PROXY_KEY);
                    if (StringUtils.isNotEmpty(proxy)) {
                        // Add proxy to registry to url
                        registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
                    }

                    // Generate Invoker for Service Provider Class (ref)
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                    // DelegateProviderMetaDataInvoker is used to hold Invoker and ServiceConfig
                    DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                    // Expose the service and generate the Exporter
                    Exporter<?> exporter = protocol.export(wrapperInvoker);
                    // Join the Exposer Collection
                    exporters.add(exporter);
                }
            } else {
                // No registry exists, only services are exposed, no address exposure is recorded
                Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
                DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                Exporter<?> exporter = protocol.export(wrapperInvoker);
                exporters.add(exporter);
            }
            /**
             * @since 2.7.0
             * ServiceData Store
             */
            MetadataReportService metadataReportService = null;
            // If the Metadata Center service is not empty, publish the service, that is, record the partial configuration in the metadata center's url
            if ((metadataReportService = getMetadataReportService()) != null) {
                metadataReportService.publishProvider(url);
            }
        }
    }
    this.urls.add(url);
}

Looking at the part on the split line first, that is, the whole process of assembling the URL, I think it can be roughly divided into the following steps:

  1. It puts all the configurations of metrics, application, module, provider, protocol into the map.
  2. For each method configuration, a signature check is made first to find out if there is a method for configuring the service. Then, if there is a parameter for the method signature, the method configuration is checked successfully before adding the method configuration to the map.
  3. Add information such as generalized calls, version numbers, method s, token s, etc. to the map
  4. Obtain the service exposure address and port number, and assemble the URL using the data in the map.

Create invoker (2,3)

Exposed to remote source directly look at the lower half of the line split by the doExportUrlsFor1Protocol() method.When an exposer is generated, the service is exposed, and the internal process of exposing is analyzed in detail.You can find that invokers are created through a proxy factory whether exposed locally or remotely.This brings you to the ProxyFactory of the time series diagram above.I mentioned invoker in this article: dubbo Source Parsing (19) Remote Calls - Beginning First, let's see how invoker was born.Invoker was created by ProxyFactory, and the default ProxyFactory implementation class for Dubbo is JavassistProxyFactory.There is a getInvoker() method in JavassistProxyFactory.

Get invoker method

getInvoker()
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
    // //Create a Wrapper for the target class
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    // Create anonymous Invoker class objects and implement the doInvoke method.
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName,
                                  Class<?>[] parameterTypes,
                                  Object[] arguments) throws Throwable {
            // Invoke Wrapper's invokeMethod method, which eventually calls the target method
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}

You can see that this method creates an anonymous Invoker class object and calls the wrapper.invokeMethod() method in the doInvoke() method.Wrapper is an abstract class that can only be subclassed by the getWrapper(Class) method.During the creation of a Wrapper subclass, the subclass code generation logic parses the Class object passed in by the getWrapper method to get information such as class methods, class member variables, and so on.And generate invokeMethod method code and some other method code.Once the code is generated, the Class object is generated through Javassist, and the Wrapper instance is created through reflection.So let's first look at the getWrapper() method:

getWrapper()
public static Wrapper getWrapper(Class<?> c) {
    while (ClassGenerator.isDynamicClass(c)) // can not wrapper on dynamic class.
    {
        // Returns the superclass of the object
        c = c.getSuperclass();
    }

    // Returns the subclass Wrapper if the superclass is Object
    if (c == Object.class) {
        return OBJECT_WRAPPER;
    }

    // Get an instance of Wrapper from the cache
    Wrapper ret = WRAPPER_MAP.get(c);
    // Create Wrapper if no hits
    if (ret == null) {
        // Create Wrapper
        ret = makeWrapper(c);
        // Write Cache
        WRAPPER_MAP.put(c, ret);
    }
    return ret;
}

This method only caches Wrapper.The main logic is makeWrapper().

makeWrapper()
    // Check if c is a base type and throw an exception if it is
    if (c.isPrimitive()) {
        throw new IllegalArgumentException("Can not create wrapper for primitive type: " + c);
    }

    // Get Class Name
    String name = c.getName();
    // Get Class Loader
    ClassLoader cl = ClassHelper.getClassLoader(c);

    // c1 is used to store setPropertyValue method code
    StringBuilder c1 = new StringBuilder("public void setPropertyValue(Object o, String n, Object v){ ");
    // c2 is used to store getPropertyValue method code
    StringBuilder c2 = new StringBuilder("public Object getPropertyValue(Object o, String n){ ");
    // c3 is used to store invokeMethod method code
    StringBuilder c3 = new StringBuilder("public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws " + InvocationTargetException.class.getName() + "{ ");

    // Generate type conversion code and exception capture code, such as:
    //   DemoService w; try { w = ((DemoServcie) $1); }}catch(Throwable e){ throw new IllegalArgumentException(e); }
    c1.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }");
    c2.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }");
    c3.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }");

    // pts is used to store member variable names and types
    Map<String, Class<?>> pts = new HashMap<>(); // <property name, property types>
    // ms is used to store Method description information (which can be understood as Method signatures) and Method instances
    Map<String, Method> ms = new LinkedHashMap<>(); // <method desc, Method instance>
    // mns is a list of method names
    List<String> mns = new ArrayList<>(); // method names.
    // dmns are used to store the name of the method defined in the current class
    List<String> dmns = new ArrayList<>(); // declaring method names.

    // get all public field.
    // Gets the fields of the public access level and generates conditional judgment statements for all fields
    for (Field f : c.getFields()) {
        String fn = f.getName();
        Class<?> ft = f.getType();
        if (Modifier.isStatic(f.getModifiers()) || Modifier.isTransient(f.getModifiers())) {
            // Ignore keyword static or transient ly modified variables
            continue;
        }
        // Generate conditional judgments and assignment statements, such as:
        // if( $2.equals("name") ) { w.name = (java.lang.String) $3; return;}
        // if( $2.equals("age") ) { w.age = ((Number) $3).intValue(); return;}
        c1.append(" if( $2.equals(\"").append(fn).append("\") ){ w.").append(fn).append("=").append(arg(ft, "$3")).append("; return; }");
        // Generate conditional judgments and return statements, such as:
        // if( $2.equals("name") ) { return ($w)w.name; }
        c2.append(" if( $2.equals(\"").append(fn).append("\") ){ return ($w)w.").append(fn).append("; }");
        // Store <field name, field type>key-value pairs in pts
        pts.put(fn, ft);
    }

    // Get all methods of class c
    Method[] methods = c.getMethods();
    // get all public method.
    // Methods that detect whether c contains declarations in the current class
    boolean hasMethod = hasMethods(methods);
    // If Included
    if (hasMethod) {
        c3.append(" try{");
        for (Method m : methods) {
            //ignore Object's method.
            // Ignore methods defined in Object
            if (m.getDeclaringClass() == Object.class) {
                continue;
            }

            // Get the name of the method
            String mn = m.getName();
            // Generate method name judgment statements, such as:
            // if ( "sayHello".equals( $2 )
            c3.append(" if( \"").append(mn).append("\".equals( $2 ) ");
            int len = m.getParameterTypes().length;
            // Generate a "Number of parameters passed in at runtime and length of method parameter list" judgment statement, such as:
            // && $3.length == 2
            c3.append(" && ").append(" $3.length == ").append(len);

            boolean override = false;
            for (Method m2 : methods) {
                // Detect whether there is overload in the method if the method object is different & the method name is the same
                if (m != m2 && m.getName().equals(m2.getName())) {
                    override = true;
                    break;
                }
            }
            // To handle overloaded methods, consider the following:
            //    1. void sayHello(Integer, String)
            //    2. void sayHello(Integer, Integer)
            // The method names are the same and the parameter lists are the same length, so you can't just tell if the two methods are equal.
            // The parameter type of the method needs to be further determined
            if (override) {
                if (len > 0) {
                    for (int l = 0; l < len; l++) {
                        c3.append(" && ").append(" $3[").append(l).append("].getName().equals(\"")
                                .append(m.getParameterTypes()[l].getName()).append("\")");
                    }
                }
            }
            // Add) {, complete the method judgment statement, and the generated code may be as follows (formatted):
            // if ("sayHello".equals($2)
            //     && $3.length == 2
            //     && $3[0].getName().equals("java.lang.Integer")
            //     && $3[1].getName().equals("java.lang.String")) {
            c3.append(" ) { ");

            // Generate target method call statements based on return value type
            if (m.getReturnType() == Void.TYPE) {
                // w.sayHello((java.lang.Integer)$4[0], (java.lang.String)$4[1]); return null;
                c3.append(" w.").append(mn).append('(').append(args(m.getParameterTypes(), "$4")).append(");").append(" return null;");
            } else {
                // return w.sayHello((java.lang.Integer)$4[0], (java.lang.String)$4[1]);
                c3.append(" return ($w)w.").append(mn).append('(').append(args(m.getParameterTypes(), "$4")).append(");");
            }

            c3.append(" }");

            // Add method name to mns collection
            mns.add(mn);
            // Detects if the current method is declared in c
            if (m.getDeclaringClass() == c) {
                // If so, add the current method name to the dmns
                dmns.add(mn);
            }
            ms.put(ReflectUtils.getDesc(m), m);
        }
        // Add exception capture statement
        c3.append(" } catch(Throwable e) { ");
        c3.append("     throw new java.lang.reflect.InvocationTargetException(e); ");
        c3.append(" }");
    }

    // Add NoSuchMethodException exception
    c3.append(" throw new " + NoSuchMethodException.class.getName() + "(\"Not found method \\\"\"+$2+\"\\\" in class " + c.getName() + ".\"); }");

    // deal with get/set method.
    Matcher matcher;
    // Processing get/set methods
    for (Map.Entry<String, Method> entry : ms.entrySet()) {
        // Get method name
        String md = entry.getKey();
        // Get Method Method
        Method method = entry.getValue();
        // If the method starts with get
        if ((matcher = ReflectUtils.GETTER_METHOD_DESC_PATTERN.matcher(md)).matches()) {
            // Get Property Name
            String pn = propertyName(matcher.group(1));
            // Generate attribute judgments and return statements as follows:
            // if( $2.equals("name") ) { return ($w).w.getName(); }
            c2.append(" if( $2.equals(\"").append(pn).append("\") ){ return ($w)w.").append(method.getName()).append("(); }");
            pts.put(pn, method.getReturnType());
        } else if ((matcher = ReflectUtils.IS_HAS_CAN_METHOD_DESC_PATTERN.matcher(md)).matches()) {
            String pn = propertyName(matcher.group(1));
            c2.append(" if( $2.equals(\"").append(pn).append("\") ){ return ($w)w.").append(method.getName()).append("(); }");
            // Store property names and return types to pts
            pts.put(pn, method.getReturnType());
        } else if ((matcher = ReflectUtils.SETTER_METHOD_DESC_PATTERN.matcher(md)).matches()) {
            // If the method starts with set
            // Get the parameter type
            Class<?> pt = method.getParameterTypes()[0];
            // Get property name
            String pn = propertyName(matcher.group(1));
            // Generate attribute judgments and setter call statements as follows:
            // if( $2.equals("name") ) { w.setName((java.lang.String)$3); return; }
            c1.append(" if( $2.equals(\"").append(pn).append("\") ){ w.").append(method.getName()).append("(").append(arg(pt, "$3")).append("); return; }");
            pts.put(pn, pt);
        }
    }
    // Add NoSuchPropertyException exception throw code
    c1.append(" throw new " + NoSuchPropertyException.class.getName() + "(\"Not found property \\\"\"+$2+\"\\\" field or setter method in class " + c.getName() + ".\"); }");
    c2.append(" throw new " + NoSuchPropertyException.class.getName() + "(\"Not found property \\\"\"+$2+\"\\\" field or setter method in class " + c.getName() + ".\"); }");

    // make class
    long id = WRAPPER_CLASS_COUNTER.getAndIncrement();
    // Create Class Generator
    ClassGenerator cc = ClassGenerator.newInstance(cl);
    // Set Class Name and Superclass
    cc.setClassName((Modifier.isPublic(c.getModifiers()) ? Wrapper.class.getName() : c.getName() + "$sw") + id);
    cc.setSuperClass(Wrapper.class);

    // Add default constructor
    cc.addDefaultConstructor();
    // Add Field
    cc.addField("public static String[] pns;"); // property name array.
    cc.addField("public static " + Map.class.getName() + " pts;"); // property type map.
    cc.addField("public static String[] mns;"); // all method name array.
    cc.addField("public static String[] dmns;"); // declared method name array.
    for (int i = 0, len = ms.size(); i < len; i++) {
        cc.addField("public static Class[] mts" + i + ";");
    }

    // Add Method
    cc.addMethod("public String[] getPropertyNames(){ return pns; }");
    cc.addMethod("public boolean hasProperty(String n){ return pts.containsKey($1); }");
    cc.addMethod("public Class getPropertyType(String n){ return (Class)pts.get($1); }");
    cc.addMethod("public String[] getMethodNames(){ return mns; }");
    cc.addMethod("public String[] getDeclaredMethodNames(){ return dmns; }");
    cc.addMethod(c1.toString());
    cc.addMethod(c2.toString());
    cc.addMethod(c3.toString());

    try {
        // Generate Class
        Class<?> wc = cc.toClass();
        // setup static field.
        // Setting field values
        wc.getField("pts").set(null, pts);
        wc.getField("pns").set(null, pts.keySet().toArray(new String[0]));
        wc.getField("mns").set(null, mns.toArray(new String[0]));
        wc.getField("dmns").set(null, dmns.toArray(new String[0]));
        int ix = 0;
        for (Method m : ms.values()) {
            wc.getField("mts" + ix++).set(null, m.getParameterTypes());
        }
        // Create a Wrapper instance
        return (Wrapper) wc.newInstance();
    } catch (RuntimeException e) {
        throw e;
    } catch (Throwable e) {
        throw new RuntimeException(e.getMessage(), e);
    } finally {
        cc.release();
        ms.clear();
        mns.clear();
        dmns.clear();
    }
}

This method is a bit long and can be roughly divided into several steps:

  1. Initialize c1, c2, c3, pts, ms, mns, dmns variables, add method definitions and type conversion codes to c1, c2, c3.
  2. Generate conditional evaluation and assignment codes for public-level fields
  3. Generate judgement statements and method call statements for methods defined in the current class.
  4. Processing getter s, setter s, and methods starting with is/has/can.This is handled by getting the method type (get/set/is/...) and the property name through a regular expression.A judgment statement is then generated for the property name and a call statement for the method.
  5. Class classes are built from ClassGenerator for the code just generated, and objects are created from reflection.ClassGenerator is encapsulated by Dubbo itself, and its core is the overloaded method toClass(ClassLoader, ProtectionDomain) of toClass(), which builds a Class from javassist.

Service Exposure

Service exposure is divided into local exposure (JVM) and remote exposure.The doExportUrlsFor1Protocol() method splits the lower half of the line into the logic of service exposure.The scope s are divided into:

  • scope = none, do not expose service
  • Scope!= remote, exposed locally
  • Scope!= local, exposed to remote

Exposure to Local

Export locally executes the exportLocal() method in ServiceConfig.

exportLocal()(4)
private void exportLocal(URL url) {
    // If the protocol is not injvm
    if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
        // Generate local url s, change protocol to injvm, set host and port, respectively
        URL local = URLBuilder.from(url)
                .setProtocol(Constants.LOCAL_PROTOCOL)
                .setHost(LOCALHOST_VALUE)
                .setPort(0)
                .build();
        // Create invoker through proxy project
        // Call the export method again to expose the service and generate the Exporter
        Exporter<?> exporter = protocol.export(
                proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
        // Add generated exposers to the collection
        exporters.add(exporter);
        logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
    }
}

Local exposure calls the injvm protocol method, the export() method of the InjvmProtocol.

export()(5)
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}

This method only creates one because it is exposed locally and therefore in the same jvm.No other action is required.

Exposure to Remote

The logic of exposing to remote is more complex than local, and it can be roughly divided into two processes: service exposure and service registration.Let's first look at service exposure.We know that dubbo has many protocol implementations. Once an Invoker is generated in the lower half of the split line of the doExportUrlsFor1 Protocol () method, you need to call the export() method of the protocols. Many people will think that export() here is the method in the protocol implementation specified in the configuration, but this is incorrect.Because service registration is required after exposing to remote locations, the export() method of RegistryProtocols implements two processes of service exposure and service registration.So here export() calls export() of RegistryProtocol.

export()
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    // Get the url of the registry
    URL registryUrl = getRegistryUrl(originInvoker);
    // url to export locally
    //Get registered service provider url
    URL providerUrl = getProviderUrl(originInvoker);

    // 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.
    // Get the override subscription URL
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
    // Create an override listener
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    // Add listeners to the collection
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

    // Override the original url according to the override configuration so that the configuration is up-to-date.
    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
    //export invoker
    // Service Exposure
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // url to registry
    // Load Registry implementation classes based on URL s, such as ZookeeperRegistry
    final Registry registry = getRegistry(originInvoker);
    // Return the url registered with the registry and filter the url parameter once
    final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
    // Generate ProviderInvokerWrapper, which saves call addresses and proxy objects for service providers and consumers
    ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
            registryUrl, registeredProviderUrl);


    // __________
    //to judge if we need to delay publish
    // Get register parameter
    boolean register = registeredProviderUrl.getParameter("register", true);
    // If you need to register a service
    if (register) {
        // Register services with the registry
        register(registryUrl, registeredProviderUrl);
        // Setting reg to true indicates service registration
        providerInvokerWrapper.setReg(true);
    }

    // Deprecated! Subscribe to override rules in 2.6.x or before.
    // Subscribe to the registry for override data
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

    // Set registry url
    exporter.setRegisterUrl(registeredProviderUrl);
    // Set url for override data subscription
    exporter.setSubscribeUrl(overrideSubscribeUrl);
    //Ensure that a new exporter instance is returned every time export
    // Create and return DestroyableExporter
    return new DestroyableExporter<>(exporter);
}

From a code point of view, I divide the line into two parts, service exposure and service registration.The logic of this method is roughly divided into the following steps:

  1. Obtain the url of the service provider, reconfigure the url with override data, and execute doLocalExport() to expose the service.
  2. Load the registry implementation class to register the service with the registry.
  3. Subscribe to the registry for override data.
  4. Create and return DestroyableExporter

Service exposure calls the doLocalExport() method of RegistryProtocol first

private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
    String key = getCacheKey(originInvoker);

    // Join Cache
    return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
        // Create Invoker as a delegate class object
        Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
        // export method calling protocol to expose service
        return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
    });
}

The logic here is simple, mainly because different protocol implementations are called here according to different protocol configurations.Implement InjvmProtocol when exposed locally.Let me continue with the following discussion, assuming that the configuration is based on the dubbo protocol.

export() of DubboProtocol

dubbo Source Resolution (24) Remote Call - dubbo Protocol (3) DubboProtocol s have export() related source code analysis, from the source code you can see that some local stub processing has been done, the key is openServer to start the server.

openServer() for DubboProtocol

dubbo Source Resolution (24) Remote Call - dubbo Protocol (3) DubboProtocol s have open Server () related source code analysis, but the code in this article is 2.6.x, and DCL has been added to the latest version.The reset method resets some of the server's configurations.For example, on the same machine (single network card), only one server instance is allowed to start on the same port.If a server instance already exists on a port, the reset method is called to reset some of the server's configuration.The main focus is on the createServer() method.

createServer() of DubboProtocol

dubbo Source Resolution (24) Remote Call - dubbo Protocol (3) DubboProtocol s have source code analysis related to createServer(), and the latest version of the default remote communication server implementation has been changed to netty4.This method can be roughly divided into the following steps:

  1. Detection of support for server-side remote communication server implementation configuration
  2. Create a server instance, that is, call the bind() method
  3. Detection of support for server-side remote communication client implementation configuration
Exchangers'bind()

Can be referred to dubbo Source Resolution (10) Remote Communication - Exchange Layer (21) Exchangers in.

public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
    if (url == null) {
        throw new IllegalArgumentException("url == null");
    }
    if (handler == null) {
        throw new IllegalArgumentException("handler == null");
    }
    url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
    // Gets Exchanger, which defaults to HeaderExchanger.
    // Next, call HeaderExchanger's bind method to create an ExchangeServer instance
    return getExchanger(url).bind(url, handler);
}
Bid() of HeaderExchanger

Can be referred to dubbo Source Resolution (10) Remote Communication - Exchange Layer (16) HeaderExchanger, in which the bind() method takes roughly the following steps:

  1. Create HeaderExchangeHandler
  2. Create DecodeHandler
  3. Transporters.bind(), create a server instance.
  4. Create HeaderExchangeServer

HeaderExchangeHandler, DecodeHandler, HeaderExchangeServer are available for reference dubbo Source Resolution (10) Remote Communication - Exchange Layer Explanation in.

Bid() (6) of Transporters
public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException {
    if (url == null) {
        throw new IllegalArgumentException("url == null");
    }
    if (handlers == null || handlers.length == 0) {
        throw new IllegalArgumentException("handlers == null");
    }
    ChannelHandler handler;
    if (handlers.length == 1) {
        handler = handlers[0];
    } else {
        // Create ChannelHandler Distributor if the number of handlers elements is greater than 1
        handler = new ChannelHandlerDispatcher(handlers);
    }
    // Get an adaptive Transporter instance and call the instance method
    return getTransporter().bind(url, handler);
}

The Transporter obtained by the getTransporter() method is dynamically created at runtime with the class name TransporterAdaptive, or Adaptive Extension Class.TransporterAdaptive determines what type of Transporter to load at runtime based on the URL parameters passed in, defaulting to a Netty4-based implementation.Assume NettyTransporter's bind method.

Bid() (6) of NettyTransporter

Can be referred to dubbo Source Parsing (17) Remote Communication - Netty4 (6) NettyTransporter.

public Server bind(URL url, ChannelHandler listener) throws RemotingException {
    // Create NettyServer
    return new NettyServer(url, listener);
}
Construction method of NettyServer (7)

Can be referred to dubbo Source Parsing (17) Remote Communication - Netty4 (5) NettyServer

public NettyServer(URL url, ChannelHandler handler) throws RemotingException {
    super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME)));
}

Called the parent AbstractServer construction method

Constructing AbstractServer (7)

Can be referred to dubbo Source Parsing (9) Remote Communication - Transport Layer (3) Construction methods in AbstractServer.

When the server instance is created, the server is opened. Is doOpen in AbstractServer an abstract method or a netty4 explanation, that is, the method of doOpen() in NettyServer?

doOpen() of NettyServer

Can be referred to dubbo Source Parsing (17) Remote Communication - Netty4 (5) Source code analysis in NettyServer.When execution is complete here, the server is turned on and the service is exposed.The next step is to explain the content of service registration.

Service Registration (9)

dubbo service registration is not required because dubbo supports direct connections to bypass the registry.Direct connections are often used for service testing.

Go back and look below the split line of the RegistryProtocol export() method.Where service registration calls the register() method first.

register() of RegistryProtocol
public void register(URL registryUrl, URL registeredProviderUrl) {
    // Get Registry
    Registry registry = registryFactory.getRegistry(registryUrl);
    // Registration Services
    registry.register(registeredProviderUrl);
}

Therefore, service registration can be roughly divided into two steps:

  1. Get Registry Instance
  2. Registration Services

Getting the registry first executes the getRegistry() method of AbstractRegistryFactory

getRegistry() of AbstractRegistryFactory

Can be referred to dubbo Source Parsing (3) Registry - Beginning (7) Source parsing in AbstractRegistryFactory under the support package.The general logic is to fetch from the cache first and create a registry instance if no hits are made, where createRegistry() is an abstract method and the implementation logic is done by subclasses. Assuming zookeeper is used as the registry here, the createRegistry() of ZookeeperRegistryFactory is called.

createRegistry() of ZookeeperRegistryFactory
public Registry createRegistry(URL url) {
    return new ZookeeperRegistry(url, zookeeperTransporter);
}

It creates a ZookeeperRegistry and executes the construction method of ZookeeperRegistry.

Construction method of ZookeeperRegistry

Can be referred to dubbo Source Resolution (7) Registry - zookeeper (1) Source code analysis in ZookeeperRegistry.The general logic can be divided into the following steps:

  1. Create zookeeper client
  2. Add listener

Look primarily at ZookeeperTransporter's connect method, because when the connect method is executed, the registry creation process ends.First, the connect method of AbstractZookeeperTransporter is executed.

connect() of AbstractZookeeperTransporter
public ZookeeperClient connect(URL url) {
    ZookeeperClient zookeeperClient;
    // Get all url addresses
    List<String> addressList = getURLBackupAddress(url);
    // The field define the zookeeper server , including protocol, host, port, username, password
    // Find available clients from the cache and return directly if any
    if ((zookeeperClient = fetchAndUpdateZookeeperClientCache(addressList)) != null && zookeeperClient.isConnected()) {
        logger.info("find valid zookeeper client from the cache for address: " + url);
        return zookeeperClient;
    }
    // avoid creating too many connections, so add lock
    synchronized (zookeeperClientMap) {
        if ((zookeeperClient = fetchAndUpdateZookeeperClientCache(addressList)) != null && zookeeperClient.isConnected()) {
            logger.info("find valid zookeeper client from the cache for address: " + url);
            return zookeeperClient;
        }

        // Create Client
        zookeeperClient = createZookeeperClient(toClientURL(url));
        logger.info("No valid zookeeper client found from cache, therefore create a new client for url. " + url);
        // Join Cache
        writeToClientMap(addressList, zookeeperClient);
    }
    return zookeeperClient;
}

Looking at the source code above, the createZookeeperClient() method, which is an abstract method implemented by a subclass, is primarily executed by the createZookeeperClient() of CuratorZookeeperTransporter.

createZookeeperClient() of CuratorZookeeperTransporter
public ZookeeperClient createZookeeperClient(URL url) {
    return new CuratorZookeeperClient(url);
}

Here's how CuratorZookeeperClient is constructed.

Construction method of CuratorZookeeperClient

Can be referred to dubbo Source Resolution (18) Remote Communication - Zookeeper (4) Source code analysis in Curator ZookeeperClient, where logic is mainly used to create and launch CuratorFramework instances, basically the API that calls the Curator Framework.

Once we have created an instance of the registry, we are ready to register.That is, the register() method of FailbackRegistry is called.

FailbackRegistry's register()

Can be referred to dubbo Source Parsing (3) Registry - Beginning (6) Source code analysis in FailbackRegistry under support package.You can see that the key is to execute the doRegister() method, which is an abstract method and is done by subclasses.Here, because zookeeper is assumed, the doRegister () of ZookeeperRegistry is executed.

ZookeeperRegistry's doRegister()

Can be referred to dubbo Source Resolution (7) Registry - zookeeper (1) Source code in ZookeeperRegistry, you can see that the logic is to call the Zookeeper client to create a service node.Node paths are generated by the toUrlPath method.The create method here executes the create() method of AbstractZookeeperClient

create() of AbstractZookeeperClient

Can be referred to dubbo Source Parsing (18) Remote Communication - Zookeeper (2) Source code analysis in AbstractZookeeperClient.createEphemeral() and createPersistent() are abstract methods that are implemented by subclasses, the CuratorZookeeperClient class.The code logic is simple.I won't go into that again.Up to this point, the service is registered.

The rules for subscribing to override data to the registry have changed considerably in the latest version, unlike 2.6.x and before.So this part is explained in the new features.

Postnote

Refer to official documents: https://dubbo.apache.org/zh-c...

This article explains dubbo's service exposure process, but also paves the way for the following 2.7 new features, the next section explains the service reference process.

Posted by mattd123 on Thu, 02 May 2019 04:10:37 -0700