All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.dubbo.metadata.MetadataInfo Maven / Gradle / Ivy

There is a newer version: 3.3.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.metadata;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.url.component.URLParam;
import org.apache.dubbo.common.utils.ArrayUtils;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.dubbo.common.utils.JsonUtils;
import org.apache.dubbo.common.utils.StringUtils;

import java.beans.Transient;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.apache.dubbo.common.constants.CommonConstants.DOT_SEPARATOR;
import static org.apache.dubbo.common.constants.CommonConstants.GROUP_CHAR_SEPARATOR;
import static org.apache.dubbo.common.constants.CommonConstants.METHODS_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.TIMESTAMP_KEY;
import static org.apache.dubbo.metadata.RevisionResolver.EMPTY_REVISION;

public class MetadataInfo implements Serializable {
    public static final MetadataInfo EMPTY = new MetadataInfo();
    private static final Logger logger = LoggerFactory.getLogger(MetadataInfo.class);

    private String app;
    // revision that will report to registry or remote meta center, must always update together with rawMetadataInfo, check {@link this#calAndGetRevision}
    private volatile String revision;
    // key format is '{group}/{interface name}:{version}:{protocol}'
    private final Map services;

    /* used at runtime */
    private transient AtomicBoolean initiated = new AtomicBoolean(false);
    // Json formatted metadata that will report to remote meta center, must always update together with revision, check {@link this#calAndGetRevision}
    private transient volatile String rawMetadataInfo;
    // key format is '{group}/{interface name}:{version}'
    private transient Map> subscribedServices;
    private transient final Map extendParams;
    private transient final Map instanceParams;
    protected transient volatile boolean updated = false;
    private transient ConcurrentNavigableMap> subscribedServiceURLs;
    private transient ConcurrentNavigableMap> exportedServiceURLs;
    private transient ExtensionLoader loader;

    public MetadataInfo() {
        this(null);
    }

    public MetadataInfo(String app) {
        this(app, null, null);
    }

    public MetadataInfo(String app, String revision, Map services) {
        this.app = app;
        this.revision = revision;
        this.services = services == null ? new ConcurrentHashMap<>() : services;
        this.extendParams = new ConcurrentHashMap<>();
        this.instanceParams = new ConcurrentHashMap<>();
    }

    private MetadataInfo(String app, String revision, Map services, AtomicBoolean initiated,
                        Map extendParams, Map instanceParams, boolean updated,
                        ConcurrentNavigableMap> subscribedServiceURLs,
                        ConcurrentNavigableMap> exportedServiceURLs,
                        ExtensionLoader loader) {
        this.app = app;
        this.revision = revision;
        this.services = new ConcurrentHashMap<>(services);
        this.initiated = new AtomicBoolean(initiated.get());
        this.extendParams = new ConcurrentHashMap<>(extendParams);
        this.instanceParams = new ConcurrentHashMap<>(instanceParams);
        this.updated = updated;
        this.subscribedServiceURLs = subscribedServiceURLs == null ? null : new ConcurrentSkipListMap<>(subscribedServiceURLs);
        this.exportedServiceURLs = exportedServiceURLs == null ? null : new ConcurrentSkipListMap<>(exportedServiceURLs);
        this.loader = loader;
    }

    /**
     * Initialize is needed when MetadataInfo is created from deserialization on the consumer side before being used for RPC call.
     */
    public void init() {
        if (!initiated.compareAndSet(false, true)) {
            return;
        }
        if (CollectionUtils.isNotEmptyMap(services)) {
            services.forEach((_k, serviceInfo) -> {
                serviceInfo.init();
                // create duplicate serviceKey(without protocol)->serviceInfo mapping to support metadata search when protocol is not specified on consumer side.
                if (subscribedServices == null) {
                    subscribedServices = new HashMap<>();
                }
                Set serviceInfos = subscribedServices.computeIfAbsent(serviceInfo.getServiceKey(), _key -> new HashSet<>());
                serviceInfos.add(serviceInfo);
            });
        }
    }

    public synchronized void addService(URL url) {
        // fixme, pass in application mode context during initialization of MetadataInfo.
        if (this.loader == null) {
            this.loader = url.getOrDefaultApplicationModel().getExtensionLoader(MetadataParamsFilter.class);
        }
        List filters = loader.getActivateExtension(url, "params-filter");
        // generate service level metadata
        ServiceInfo serviceInfo = new ServiceInfo(url, filters);
        this.services.put(serviceInfo.getMatchKey(), serviceInfo);
        // extract common instance level params
        extractInstanceParams(url, filters);

        if (exportedServiceURLs == null) {
            exportedServiceURLs = new ConcurrentSkipListMap<>();
        }
        addURL(exportedServiceURLs, url);
        updated = true;
    }

    public synchronized void removeService(URL url) {
        if (url == null) {
            return;
        }
        this.services.remove(url.getProtocolServiceKey());
        if (exportedServiceURLs != null) {
            removeURL(exportedServiceURLs, url);
        }

        updated = true;
    }

    public String getRevision() {
        return revision;
    }

    /**
     * Calculation of this instance's status like revision and modification of the same instance must be synchronized among different threads.
     * 

* Usage of this method is strictly restricted to certain points such as when during registration. Always try to use {@link this#getRevision()} instead. */ public synchronized String calAndGetRevision() { if (revision != null && !updated) { return revision; } updated = false; if (CollectionUtils.isEmptyMap(services)) { this.revision = EMPTY_REVISION; } else { StringBuilder sb = new StringBuilder(); sb.append(app); for (Map.Entry entry : new TreeMap<>(services).entrySet()) { sb.append(entry.getValue().toDescString()); } String tempRevision = RevisionResolver.calRevision(sb.toString()); if (!StringUtils.isEquals(this.revision, tempRevision)) { if (logger.isInfoEnabled()) { logger.info(String.format("metadata revision changed: %s -> %s, app: %s, services: %d", this.revision, tempRevision, this.app, this.services.size())); } this.revision = tempRevision; this.rawMetadataInfo = JsonUtils.getJson().toJson(this); } } return revision; } public void setRevision(String revision) { this.revision = revision; } @Transient public String getContent() { return this.rawMetadataInfo; } public String getApp() { return app; } public void setApp(String app) { this.app = app; } public Map getServices() { return services; } /** * Get service info of an interface with specified group, version and protocol * @param protocolServiceKey key is of format '{group}/{interface name}:{version}:{protocol}' * @return the specific service info related to protocolServiceKey */ public ServiceInfo getServiceInfo(String protocolServiceKey) { return services.get(protocolServiceKey); } /** * Get service infos of an interface with specified group, version. * There may have several service infos of different protocols, this method will simply pick the first one. * * @param serviceKeyWithoutProtocol key is of format '{group}/{interface name}:{version}' * @return the first service info related to serviceKey */ public ServiceInfo getNoProtocolServiceInfo(String serviceKeyWithoutProtocol) { if (CollectionUtils.isEmptyMap(subscribedServices)) { return null; } Set subServices = subscribedServices.get(serviceKeyWithoutProtocol); if (CollectionUtils.isNotEmpty(subServices)) { return subServices.iterator().next(); } return null; } public ServiceInfo getValidServiceInfo(String serviceKey) { ServiceInfo serviceInfo = getServiceInfo(serviceKey); if (serviceInfo == null) { serviceInfo = getNoProtocolServiceInfo(serviceKey); if (serviceInfo == null) { return null; } } return serviceInfo; } public Map getExtendParams() { return extendParams; } public Map getInstanceParams() { return instanceParams; } public String getParameter(String key, String serviceKey) { ServiceInfo serviceInfo = getValidServiceInfo(serviceKey); if (serviceInfo == null) return null; return serviceInfo.getParameter(key); } public Map getParameters(String serviceKey) { ServiceInfo serviceInfo = getValidServiceInfo(serviceKey); if (serviceInfo == null) { return Collections.emptyMap(); } return serviceInfo.getAllParams(); } public String getServiceString(String protocolServiceKey) { if (protocolServiceKey == null) { return null; } ServiceInfo serviceInfo = getValidServiceInfo(protocolServiceKey); if (serviceInfo == null) { return null; } return serviceInfo.toFullString(); } public synchronized void addSubscribedURL(URL url) { if (subscribedServiceURLs == null) { subscribedServiceURLs = new ConcurrentSkipListMap<>(); } addURL(subscribedServiceURLs, url); } public boolean removeSubscribedURL(URL url) { if (subscribedServiceURLs == null) { return true; } return removeURL(subscribedServiceURLs, url); } public ConcurrentNavigableMap> getSubscribedServiceURLs() { return subscribedServiceURLs; } public ConcurrentNavigableMap> getExportedServiceURLs() { return exportedServiceURLs; } private boolean addURL(Map> serviceURLs, URL url) { SortedSet urls = serviceURLs.computeIfAbsent(url.getServiceKey(), this::newSortedURLs); // make sure the parameters of tmpUrl is variable return urls.add(url); } boolean removeURL(Map> serviceURLs, URL url) { String key = url.getServiceKey(); SortedSet urls = serviceURLs.getOrDefault(key, null); if (urls == null) { return true; } boolean r = urls.remove(url); // if it is empty if (urls.isEmpty()) { serviceURLs.remove(key); } return r; } private SortedSet newSortedURLs(String serviceKey) { return new TreeSet<>(URLComparator.INSTANCE); } @Override public int hashCode() { return Objects.hash(app, services); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof MetadataInfo)) { return false; } MetadataInfo other = (MetadataInfo)obj; return Objects.equals(app, other.getApp()) && ((services == null && other.services == null) || (services != null && services.equals(other.services))); } private void extractInstanceParams(URL url, List filters) { if (CollectionUtils.isEmpty(filters)) { return; } String[] included, excluded; if (filters.size() == 1) { MetadataParamsFilter filter = filters.get(0); included = filter.instanceParamsIncluded(); excluded = filter.instanceParamsExcluded(); } else { Set includedList = new HashSet<>(); Set excludedList = new HashSet<>(); filters.forEach(filter -> { if (ArrayUtils.isNotEmpty(filter.instanceParamsIncluded())) { includedList.addAll(Arrays.asList(filter.instanceParamsIncluded())); } if (ArrayUtils.isNotEmpty(filter.instanceParamsExcluded())) { excludedList.addAll(Arrays.asList(filter.instanceParamsExcluded())); } }); included = includedList.toArray(new String[0]); excluded = excludedList.toArray(new String[0]); } Map tmpInstanceParams = new HashMap<>(); if (ArrayUtils.isNotEmpty(included)) { for (String p : included) { String value = url.getParameter(p); if (value != null) { tmpInstanceParams.put(p, value); } } } else if (ArrayUtils.isNotEmpty(excluded)) { tmpInstanceParams.putAll(url.getParameters()); for (String p : excluded) { tmpInstanceParams.remove(p); } } tmpInstanceParams.forEach((key, value) -> { String oldValue = instanceParams.put(key, value); if (!TIMESTAMP_KEY.equals(key) && oldValue != null && !oldValue.equals(value)) { throw new IllegalStateException(String.format("Inconsistent instance metadata found in different services: %s, %s", oldValue, value)); } }); } @Override public String toString() { return "metadata{" + "app='" + app + "'," + "revision='" + revision + "'," + "size=" + (services == null ? 0 : services.size()) + "," + "services=" + getSimplifiedServices(services) + "}"; } public String toFullString() { return "metadata{" + "app='" + app + "'," + "revision='" + revision + "'," + "services=" + services + "}"; } private String getSimplifiedServices(Map services) { if (services == null) { return "[]"; } return services.keySet().toString(); } @Override public synchronized MetadataInfo clone() { return new MetadataInfo(app, revision, services, initiated, extendParams, instanceParams, updated, subscribedServiceURLs, exportedServiceURLs, loader); } private Object readResolve() { // create a new object from the deserialized one, in order to call constructor return new MetadataInfo(this.app, this.revision, this.services); } public static class ServiceInfo implements Serializable { private String name; private String group; private String version; private String protocol; private String path; // most of the time, path is the same with the interface name. private Map params; // params configured on consumer side, private volatile transient Map consumerParams; // cached method params private volatile transient Map> methodParams; private volatile transient Map> consumerMethodParams; // cached numbers private volatile transient Map numbers; private volatile transient Map> methodNumbers; // service + group + version private volatile transient String serviceKey; // service + group + version + protocol private volatile transient String matchKey; private transient URL url; public ServiceInfo() {} public ServiceInfo(URL url, List filters) { this(url.getServiceInterface(), url.getGroup(), url.getVersion(), url.getProtocol(), url.getPath(), null); this.url = url; Map params = extractServiceParams(url, filters); // initialize method params caches. this.methodParams = URLParam.initMethodParameters(params); this.consumerMethodParams = URLParam.initMethodParameters(consumerParams); } public ServiceInfo(String name, String group, String version, String protocol, String path, Map params) { this.name = name; this.group = group; this.version = version; this.protocol = protocol; this.path = path; this.params = params == null ? new ConcurrentHashMap<>() : params; this.serviceKey = buildServiceKey(name, group, version); this.matchKey = buildMatchKey(); } private Map extractServiceParams(URL url, List filters) { Map params = new HashMap<>(); if (CollectionUtils.isEmpty(filters)) { params.putAll(url.getParameters()); this.params = params; return params; } String[] included, excluded; if (filters.size() == 1) { included = filters.get(0).serviceParamsIncluded(); excluded = filters.get(0).serviceParamsExcluded(); } else { Set includedList = new HashSet<>(); Set excludedList = new HashSet<>(); for (MetadataParamsFilter filter : filters) { if (ArrayUtils.isNotEmpty(filter.serviceParamsIncluded())) { includedList.addAll(Arrays.asList(filter.serviceParamsIncluded())); } if (ArrayUtils.isNotEmpty(filter.serviceParamsExcluded())) { excludedList.addAll(Arrays.asList(filter.serviceParamsExcluded())); } } included = includedList.toArray(new String[0]); excluded = excludedList.toArray(new String[0]); } if (ArrayUtils.isNotEmpty(included)) { String[] methods = url.getParameter(METHODS_KEY, (String[]) null); for (String p : included) { String value = url.getParameter(p); if (StringUtils.isNotEmpty(value) && params.get(p) == null) { params.put(p, value); } appendMethodParams(url, params, methods, p); } } else if (ArrayUtils.isNotEmpty(excluded)) { for (Map.Entry entry : url.getParameters().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); boolean shouldAdd = true; for (String excludeKey : excluded) { if (key.equalsIgnoreCase(excludeKey) || key.contains("." + excludeKey)) { shouldAdd = false; break; } } if (shouldAdd) { params.put(key, value); } } } this.params = params; return params; } private void appendMethodParams(URL url, Map params, String[] methods, String p) { if (methods != null) { for (String method : methods) { String mValue = url.getMethodParameterStrict(method, p); if (StringUtils.isNotEmpty(mValue)) { params.put(method + DOT_SEPARATOR + p, mValue); } } } } /** * Initialize necessary caches right after deserialization on the consumer side */ protected void init() { buildMatchKey(); buildServiceKey(name, group, version); // init method params this.methodParams = URLParam.initMethodParameters(params); // Actually, consumer params is empty after deserialized on the consumer side, so no need to initialize. // Check how InstanceAddressURL operates on consumer url for more detail. // this.consumerMethodParams = URLParam.initMethodParameters(consumerParams); // no need to init numbers for it's only for cache purpose } public String getMatchKey() { if (matchKey != null) { return matchKey; } buildMatchKey(); return matchKey; } private String buildMatchKey() { matchKey = getServiceKey(); if (StringUtils.isNotEmpty(protocol)) { matchKey = getServiceKey() + GROUP_CHAR_SEPARATOR + protocol; } return matchKey; } private String buildServiceKey(String name, String group, String version) { this.serviceKey = URL.buildKey(name, group, version); return this.serviceKey; } public String getServiceKey() { if (serviceKey != null) { return serviceKey; } buildServiceKey(name, group, version); return serviceKey; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGroup() { return group; } public void setGroup(String group) { this.group = group; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getProtocol() { return protocol; } public void setProtocol(String protocol) { this.protocol = protocol; } public Map getParams() { if (params == null) { return Collections.emptyMap(); } return params; } public void setParams(Map params) { this.params = params; } @Transient public Map getAllParams() { if (consumerParams != null) { Map allParams = new HashMap<>((int) ((params.size() + consumerParams.size()) / 0.75f + 1)); allParams.putAll(params); allParams.putAll(consumerParams); return allParams; } return params; } public String getParameter(String key) { if (consumerParams != null) { String value = consumerParams.get(key); if (value != null) { return value; } } return params.get(key); } public String getMethodParameter(String method, String key, String defaultValue) { String value = getMethodParameter(method, key, consumerMethodParams); if (value != null) { return value; } value = getMethodParameter(method, key, methodParams); return value == null ? defaultValue : value; } private String getMethodParameter(String method, String key, Map> map) { String value = null; if (map == null) { return value; } Map keyMap = map.get(method); if (keyMap != null) { value = keyMap.get(key); } return value; } public boolean hasMethodParameter(String method, String key) { String value = this.getMethodParameter(method, key, (String) null); return StringUtils.isNotEmpty(value); } public boolean hasMethodParameter(String method) { return (consumerMethodParams != null && consumerMethodParams.containsKey(method)) || (methodParams != null && methodParams.containsKey(method)); } public String toDescString() { return this.getMatchKey() + path + new TreeMap<>(getParams()); } public void addParameter(String key, String value) { if (consumerParams != null) { this.consumerParams.put(key, value); } // refresh method params consumerMethodParams = URLParam.initMethodParameters(consumerParams); } public void addParameterIfAbsent(String key, String value) { if (consumerParams != null) { this.consumerParams.putIfAbsent(key, value); } // refresh method params consumerMethodParams = URLParam.initMethodParameters(consumerParams); } public void addConsumerParams(Map params) { // copy once for one service subscription if (consumerParams == null) { consumerParams = new ConcurrentHashMap<>(params); // init method params consumerMethodParams = URLParam.initMethodParameters(consumerParams); } } public Map getNumbers() { // concurrent initialization is tolerant if (numbers == null) { numbers = new ConcurrentHashMap<>(); } return numbers; } public Map> getMethodNumbers() { if (methodNumbers == null) { // concurrent initialization is tolerant methodNumbers = new ConcurrentHashMap<>(); } return methodNumbers; } public URL getUrl() { return url; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!(obj instanceof ServiceInfo)) { return false; } ServiceInfo serviceInfo = (ServiceInfo) obj; /** * Equals to Objects.equals(this.getMatchKey(), serviceInfo.getMatchKey()), but match key will not get initialized * on json deserialization. */ return Objects.equals(this.getVersion(), serviceInfo.getVersion()) && Objects.equals(this.getGroup(), serviceInfo.getGroup()) && Objects.equals(this.getName(), serviceInfo.getName()) && Objects.equals(this.getProtocol(), serviceInfo.getProtocol()) && this.getParams().equals(serviceInfo.getParams()); } @Override public int hashCode() { return Objects.hash(getVersion(), getGroup(), getName(), getProtocol(), getParams()); } @Override public String toString() { return getMatchKey(); } public String toFullString() { return "service{" + "name='" + name + "'," + "group='" + group + "'," + "version='" + version + "'," + "protocol='" + protocol + "'," + "params=" + params + "," + "}"; } } static class URLComparator implements Comparator { public static final URLComparator INSTANCE = new URLComparator(); @Override public int compare(URL o1, URL o2) { return o1.toFullString().compareTo(o2.toFullString()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy