diff --git a/dubbo-all/pom.xml b/dubbo-all/pom.xml index be5a2e3fd776..6ae3fbf0909c 100644 --- a/dubbo-all/pom.xml +++ b/dubbo-all/pom.xml @@ -500,6 +500,23 @@ true + + + org.apache.dubbo + dubbo-event + ${project.version} + compile + true + + + + org.apache.dubbo + dubbo-metadata + ${project.version} + compile + true + + org.springframework @@ -631,6 +648,10 @@ org.apache.dubbo:dubbo-metadata-report-nacos org.apache.dubbo:dubbo-serialization-native-hession org.apache.dubbo:dubbo-rpc-native-thrift + + + org.apache.dubbo:dubbo-event + org.apache.dubbo:dubbo-metadata @@ -756,6 +777,19 @@ META-INF/dubbo/internal/org.apache.dubbo.metadata.store.MetadataReportFactory + + + META-INF/dubbo/internal/org.apache.dubbo.event.EventDispatcher + + + + META-INF/dubbo/internal/org.apache.dubbo.metadata.export.MetadataServiceExporter + + + + META-INF/dubbo/internal/org.apache.dubbo.metadata.LocalMetadataService + + diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/function/ThrowableConsumer.java b/dubbo-common/src/main/java/org/apache/dubbo/common/function/ThrowableConsumer.java new file mode 100644 index 000000000000..317d04569982 --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/function/ThrowableConsumer.java @@ -0,0 +1,66 @@ +/* + * 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.common.function; + +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * {@link Consumer} with {@link Throwable} + * + * @param the source type + * @see Function + * @see Throwable + * @since 2.7.2 + */ +@FunctionalInterface +public interface ThrowableConsumer { + + /** + * Applies this function to the given argument. + * + * @param t the function argument + * @throws Throwable if met with any error + */ + void accept(T t) throws Throwable; + + /** + * Executes {@link ThrowableConsumer} + * + * @param t the function argument + * @throws RuntimeException wrappers {@link Throwable} + */ + default void execute(T t) throws RuntimeException { + try { + accept(t); + } catch (Throwable e) { + throw new RuntimeException(e.getMessage(), e.getCause()); + } + } + + /** + * Executes {@link ThrowableConsumer} + * + * @param t the function argument + * @param consumer {@link ThrowableConsumer} + * @param the source type + * @return the result after execution + */ + static void execute(T t, ThrowableConsumer consumer) { + consumer.execute(t); + } +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/function/ThrowableFunction.java b/dubbo-common/src/main/java/org/apache/dubbo/common/function/ThrowableFunction.java new file mode 100644 index 000000000000..1acd3fa137fd --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/function/ThrowableFunction.java @@ -0,0 +1,71 @@ +/* + * 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.common.function; + +import java.util.function.Function; + +/** + * {@link Function} with {@link Throwable} + * + * @param the source type + * @param the return type + * @see Function + * @see Throwable + * @since 2.7.2 + */ +@FunctionalInterface +public interface ThrowableFunction { + + /** + * Applies this function to the given argument. + * + * @param t the function argument + * @return the function result + * @throws Throwable if met with any error + */ + R apply(T t) throws Throwable; + + /** + * Executes {@link ThrowableFunction} + * + * @param t the function argument + * @return the function result + * @throws RuntimeException wrappers {@link Throwable} + */ + default R execute(T t) throws RuntimeException { + R result = null; + try { + result = apply(t); + } catch (Throwable e) { + throw new RuntimeException(e.getCause()); + } + return result; + } + + /** + * Executes {@link ThrowableFunction} + * + * @param t the function argument + * @param function {@link ThrowableFunction} + * @param the source type + * @param the return type + * @return the result after execution + */ + static R execute(T t, ThrowableFunction function) { + return function.execute(t); + } +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/DefaultPage.java b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/DefaultPage.java new file mode 100644 index 000000000000..71dd2afe8954 --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/DefaultPage.java @@ -0,0 +1,71 @@ +/* + * 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.common.utils; + +import java.io.Serializable; +import java.util.List; + +/** + * The default implementation of {@link Page} + * + * @since 2.7.2 + */ +public class DefaultPage implements Page, Serializable { + + private static final long serialVersionUID = 1099331838954070419L; + + private final int requestOffset; + + private final int requestSize; + + private int totalSize; + + private List data; + + public DefaultPage(int requestOffset, int requestSize) { + this.requestOffset = requestOffset; + this.requestSize = requestSize; + } + + @Override + public int getRequestOffset() { + return requestOffset; + } + + @Override + public int getRequestSize() { + return requestSize; + } + + @Override + public int getTotalSize() { + return totalSize; + } + + public void setTotalSize(int totalSize) { + this.totalSize = totalSize; + } + + @Override + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/Page.java b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/Page.java new file mode 100644 index 000000000000..58a4249195d1 --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/Page.java @@ -0,0 +1,73 @@ +/* + * 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.common.utils; + +import java.util.List; + +/** + * The model class of pagination + * + * @since 2.7.2 + */ +public interface Page { + + /** + * Gets the offset of request + * + * @return positive integer + */ + int getRequestOffset(); + + /** + * Gets the size of request for query + * + * @return positive integer + */ + int getRequestSize(); + + /** + * Returns the total amount of elements. + * + * @return the total amount of elements + */ + int getTotalSize(); + + /** + * The data of current page + * + * @return non-null {@link List} + */ + List getData(); + + /** + * The size of {@link #getData() data} + * + * @return positive integer + */ + default int getDataSize() { + return getData().size(); + } + + /** + * Returns whether the page has data at all. + * + * @return + */ + default boolean hasData() { + return getDataSize() > 0; + } +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/ReflectUtils.java b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/ReflectUtils.java index ccec6875ca83..4d1c95e70b8f 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/ReflectUtils.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/ReflectUtils.java @@ -28,19 +28,28 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.net.URL; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableSet; /** * ReflectUtils @@ -1096,4 +1105,61 @@ public static Map getBeanPropertyReadMethods(Class cl) { return properties; } + + /** + * Find the {@link Set} of {@link ParameterizedType} + * + * @param sourceClass the source {@link Class class} + * @return non-null read-only {@link Set} + * @since 2.7.2 + */ + public static Set findParameterizedTypes(Class sourceClass) { + // Add Generic Interfaces + List genericTypes = new LinkedList<>(asList(sourceClass.getGenericInterfaces())); + // Add Generic Super Class + genericTypes.add(sourceClass.getGenericSuperclass()); + + Set parameterizedTypes = genericTypes.stream() + .filter(type -> type instanceof ParameterizedType)// filter ParameterizedType + .map(type -> ParameterizedType.class.cast(type)) // cast to ParameterizedType + .collect(Collectors.toSet()); + + if (parameterizedTypes.isEmpty()) { // If not found, try to search super types recursively + genericTypes.stream() + .filter(type -> type instanceof Class) + .map(type -> Class.class.cast(type)) + .forEach(superClass -> { + parameterizedTypes.addAll(findParameterizedTypes(superClass)); + }); + } + + return unmodifiableSet(parameterizedTypes); // build as a Set + + } + + /** + * Find the hierarchical types form the source {@link Class class} by specified {@link Class type}. + * + * @param sourceClass the source {@link Class class} + * @param matchType the type to match + * @param the type to match + * @return non-null read-only {@link Set} + * @since 2.7.2 + */ + public static Set> findHierarchicalTypes(Class sourceClass, Class matchType) { + if (sourceClass == null) { + return Collections.emptySet(); + } + + Set> hierarchicalTypes = new LinkedHashSet<>(); + + if (matchType.isAssignableFrom(sourceClass)) { + hierarchicalTypes.add((Class) sourceClass); + } + + // Find all super classes + hierarchicalTypes.addAll(findHierarchicalTypes(sourceClass.getSuperclass(), matchType)); + + return unmodifiableSet(hierarchicalTypes); + } } \ No newline at end of file diff --git a/dubbo-config/dubbo-config-api/pom.xml b/dubbo-config/dubbo-config-api/pom.xml index 3a4584d90c3b..0b145d8cb22c 100644 --- a/dubbo-config/dubbo-config-api/pom.xml +++ b/dubbo-config/dubbo-config-api/pom.xml @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - + 4.0.0 org.apache.dubbo @@ -64,6 +65,13 @@ dubbo-filter-cache ${project.parent.version} + + + org.apache.dubbo + dubbo-metadata + ${project.parent.version} + + org.apache.dubbo diff --git a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ReferenceConfig.java b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ReferenceConfig.java index 45f24cf0657f..b5b7c0f8f3ae 100644 --- a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ReferenceConfig.java +++ b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ReferenceConfig.java @@ -27,7 +27,11 @@ import org.apache.dubbo.common.utils.StringUtils; import org.apache.dubbo.config.annotation.Reference; import org.apache.dubbo.config.context.ConfigManager; +import org.apache.dubbo.config.event.ReferenceConfigDestroyedEvent; +import org.apache.dubbo.config.event.ReferenceConfigInitializedEvent; import org.apache.dubbo.config.support.Parameter; +import org.apache.dubbo.event.Event; +import org.apache.dubbo.event.EventDispatcher; import org.apache.dubbo.metadata.integration.MetadataReportService; import org.apache.dubbo.remoting.Constants; import org.apache.dubbo.rpc.Invoker; @@ -62,18 +66,18 @@ import static org.apache.dubbo.common.constants.CommonConstants.INTERFACE_KEY; import static org.apache.dubbo.common.constants.CommonConstants.LOCALHOST_VALUE; import static org.apache.dubbo.common.constants.CommonConstants.METHODS_KEY; +import static org.apache.dubbo.common.constants.CommonConstants.MONITOR_KEY; import static org.apache.dubbo.common.constants.CommonConstants.REVISION_KEY; import static org.apache.dubbo.common.constants.CommonConstants.SEMICOLON_SPLIT_PATTERN; import static org.apache.dubbo.common.constants.CommonConstants.SIDE_KEY; import static org.apache.dubbo.common.constants.ConfigConstants.CLUSTER_KEY; -import static org.apache.dubbo.config.Constants.DUBBO_IP_TO_REGISTRY; -import static org.apache.dubbo.common.constants.RegistryConstants.REFER_KEY; -import static org.apache.dubbo.registry.Constants.REGISTER_IP_KEY; -import static org.apache.dubbo.common.constants.CommonConstants.MONITOR_KEY; import static org.apache.dubbo.common.constants.RegistryConstants.CONSUMER_PROTOCOL; +import static org.apache.dubbo.common.constants.RegistryConstants.REFER_KEY; import static org.apache.dubbo.common.constants.RegistryConstants.REGISTRY_PROTOCOL; -import static org.apache.dubbo.rpc.Constants.LOCAL_PROTOCOL; import static org.apache.dubbo.common.utils.NetUtils.isInvalidLocalHost; +import static org.apache.dubbo.config.Constants.DUBBO_IP_TO_REGISTRY; +import static org.apache.dubbo.registry.Constants.REGISTER_IP_KEY; +import static org.apache.dubbo.rpc.Constants.LOCAL_PROTOCOL; /** * ReferenceConfig @@ -117,6 +121,11 @@ public class ReferenceConfig extends AbstractReferenceConfig { */ private final List urls = new ArrayList(); + /** + * The {@link EventDispatcher} + */ + private final EventDispatcher eventDispatcher = EventDispatcher.getDefaultExtension(); + /** * The interface name of the reference service */ @@ -267,6 +276,9 @@ public synchronized void destroy() { } invoker = null; ref = null; + + // dispatch a ReferenceConfigDestroyedEvent since 2.7.2 + dispatch(new ReferenceConfigDestroyedEvent(this)); } private void init() { @@ -331,6 +343,9 @@ private void init() { String serviceKey = URL.buildKey(interfaceName, group, version); ApplicationModel.initConsumerModel(serviceKey, buildConsumerModel(serviceKey, attributes)); initialized = true; + + // dispatch a ReferenceConfigInitializedEvent since 2.7.2 + dispatch(new ReferenceConfigInitializedEvent(this, invoker)); } private ConsumerModel buildConsumerModel(String serviceKey, Map attributes) { @@ -373,7 +388,7 @@ private T createProxy(Map map) { } } else { // assemble URL from register center's configuration // if protocols not injvm checkRegistry - if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())){ + if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) { checkRegistry(); List us = loadRegistries(false); if (CollectionUtils.isNotEmpty(us)) { @@ -660,4 +675,14 @@ private void resolveFile() { } } } + + /** + * Dispatch an {@link Event event} + * + * @param event an {@link Event event} + * @since 2.7.2 + */ + protected void dispatch(Event event) { + eventDispatcher.dispatch(event); + } } diff --git a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ServiceConfig.java b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ServiceConfig.java index d485be702464..fe26082b17ca 100644 --- a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ServiceConfig.java +++ b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ServiceConfig.java @@ -29,8 +29,12 @@ import org.apache.dubbo.common.utils.StringUtils; import org.apache.dubbo.config.annotation.Service; import org.apache.dubbo.config.context.ConfigManager; +import org.apache.dubbo.config.event.ServiceConfigExportedEvent; +import org.apache.dubbo.config.event.ServiceConfigUnexportedEvent; import org.apache.dubbo.config.invoker.DelegateProviderMetaDataInvoker; import org.apache.dubbo.config.support.Parameter; +import org.apache.dubbo.event.Event; +import org.apache.dubbo.event.EventDispatcher; import org.apache.dubbo.metadata.integration.MetadataReportService; import org.apache.dubbo.remoting.Constants; import org.apache.dubbo.rpc.Exporter; @@ -67,31 +71,31 @@ import static org.apache.dubbo.common.constants.CommonConstants.DUBBO; import static org.apache.dubbo.common.constants.CommonConstants.LOCALHOST_VALUE; import static org.apache.dubbo.common.constants.CommonConstants.METHODS_KEY; +import static org.apache.dubbo.common.constants.CommonConstants.MONITOR_KEY; import static org.apache.dubbo.common.constants.CommonConstants.PATH_KEY; import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER_SIDE; import static org.apache.dubbo.common.constants.CommonConstants.REVISION_KEY; import static org.apache.dubbo.common.constants.CommonConstants.SIDE_KEY; import static org.apache.dubbo.common.constants.ConfigConstants.DUBBO_IP_TO_BIND; +import static org.apache.dubbo.common.constants.RegistryConstants.DYNAMIC_KEY; +import static org.apache.dubbo.common.constants.RegistryConstants.EXPORT_KEY; +import static org.apache.dubbo.common.utils.NetUtils.getAvailablePort; +import static org.apache.dubbo.common.utils.NetUtils.getLocalHost; +import static org.apache.dubbo.common.utils.NetUtils.isInvalidLocalHost; +import static org.apache.dubbo.common.utils.NetUtils.isInvalidPort; import static org.apache.dubbo.config.Constants.DUBBO_IP_TO_REGISTRY; import static org.apache.dubbo.config.Constants.DUBBO_PORT_TO_BIND; import static org.apache.dubbo.config.Constants.DUBBO_PORT_TO_REGISTRY; -import static org.apache.dubbo.common.constants.RegistryConstants.EXPORT_KEY; import static org.apache.dubbo.config.Constants.MULTICAST; import static org.apache.dubbo.config.Constants.PROTOCOLS_SUFFIX; -import static org.apache.dubbo.rpc.Constants.SCOPE_KEY; -import static org.apache.dubbo.rpc.Constants.SCOPE_LOCAL; import static org.apache.dubbo.config.Constants.SCOPE_NONE; -import static org.apache.dubbo.rpc.Constants.SCOPE_REMOTE; -import static org.apache.dubbo.common.constants.CommonConstants.MONITOR_KEY; -import static org.apache.dubbo.common.constants.RegistryConstants.DYNAMIC_KEY; import static org.apache.dubbo.rpc.Constants.GENERIC_KEY; import static org.apache.dubbo.rpc.Constants.LOCAL_PROTOCOL; import static org.apache.dubbo.rpc.Constants.PROXY_KEY; +import static org.apache.dubbo.rpc.Constants.SCOPE_KEY; +import static org.apache.dubbo.rpc.Constants.SCOPE_LOCAL; +import static org.apache.dubbo.rpc.Constants.SCOPE_REMOTE; import static org.apache.dubbo.rpc.Constants.TOKEN_KEY; -import static org.apache.dubbo.common.utils.NetUtils.getAvailablePort; -import static org.apache.dubbo.common.utils.NetUtils.getLocalHost; -import static org.apache.dubbo.common.utils.NetUtils.isInvalidLocalHost; -import static org.apache.dubbo.common.utils.NetUtils.isInvalidPort; /** * ServiceConfig @@ -144,6 +148,8 @@ public class ServiceConfig extends AbstractServiceConfig { */ private final List> exporters = new ArrayList>(); + private final EventDispatcher eventDispatcher = EventDispatcher.getDefaultExtension(); + /** * The interface name of the exported service */ @@ -413,6 +419,9 @@ protected synchronized void doExport() { path = interfaceName; } doExportUrls(); + + // dispatch a ServiceConfigExportedEvent since 2.7.2 + dispatch(new ServiceConfigExportedEvent(this)); } private void checkRef() { @@ -445,6 +454,9 @@ public synchronized void unexport() { exporters.clear(); } unexported = true; + + // dispatch a ServiceConfigUnExportedEvent since 2.7.2 + dispatch(new ServiceConfigUnexportedEvent(this)); } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -1051,4 +1063,14 @@ public void setProviders(List providers) { public String getPrefix() { return DUBBO + ".service." + interfaceName; } + + /** + * Dispatch an {@link Event event} + * + * @param event an {@link Event event} + * @since 2.7.2 + */ + protected void dispatch(Event event) { + eventDispatcher.dispatch(event); + } } diff --git a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ReferenceConfigDestroyedEvent.java b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ReferenceConfigDestroyedEvent.java new file mode 100644 index 000000000000..58b871ea7e92 --- /dev/null +++ b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ReferenceConfigDestroyedEvent.java @@ -0,0 +1,41 @@ +/* + * 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.config.event; + +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.config.annotation.Reference; +import org.apache.dubbo.event.Event; + +/** + * The {@link ReferenceConfig Dubbo service ReferenceConfig} destroyed {@link Event event} + * + * @see Reference + * @see ReferenceConfig#destroy() + * @see Event + * @since 2.7.2 + */ +public class ReferenceConfigDestroyedEvent extends Event { + + public ReferenceConfigDestroyedEvent(ReferenceConfig referenceConfig) { + super(referenceConfig); + } + + public ReferenceConfig getReferenceConfig() { + return (ReferenceConfig) getSource(); + } + +} \ No newline at end of file diff --git a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ReferenceConfigInitializedEvent.java b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ReferenceConfigInitializedEvent.java new file mode 100644 index 000000000000..dcf02cfec774 --- /dev/null +++ b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ReferenceConfigInitializedEvent.java @@ -0,0 +1,48 @@ +/* + * 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.config.event; + +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.config.annotation.Reference; +import org.apache.dubbo.event.Event; +import org.apache.dubbo.rpc.Invoker; + +/** + * The {@link ReferenceConfig Dubbo service ReferenceConfig} initialized {@link Event event} + * + * @see Reference + * @see ReferenceConfig#get() + * @see Event + * @since 2.7.2 + */ +public class ReferenceConfigInitializedEvent extends Event { + + private final Invoker invoker; + + public ReferenceConfigInitializedEvent(ReferenceConfig referenceConfig, Invoker invoker) { + super(referenceConfig); + this.invoker = invoker; + } + + public ReferenceConfig getReferenceConfig() { + return (ReferenceConfig) getSource(); + } + + public Invoker getInvoker() { + return invoker; + } +} diff --git a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ServiceConfigExportedEvent.java b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ServiceConfigExportedEvent.java new file mode 100644 index 000000000000..e6e9c09a1085 --- /dev/null +++ b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ServiceConfigExportedEvent.java @@ -0,0 +1,36 @@ +/* + * 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.config.event; + +import org.apache.dubbo.config.ServiceConfig; +import org.apache.dubbo.event.Event; + +/** + * {@link ServiceConfig} event post-{@link ServiceConfig#export() export} + * + * @since 2.7.2 + */ +public class ServiceConfigExportedEvent extends Event { + + public ServiceConfigExportedEvent(ServiceConfig source) { + super(source); + } + + public ServiceConfig getServiceConfig() { + return (ServiceConfig) getSource(); + } +} diff --git a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ServiceConfigUnexportedEvent.java b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ServiceConfigUnexportedEvent.java new file mode 100644 index 000000000000..f4914fe80342 --- /dev/null +++ b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/event/ServiceConfigUnexportedEvent.java @@ -0,0 +1,36 @@ +/* + * 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.config.event; + +import org.apache.dubbo.config.ServiceConfig; +import org.apache.dubbo.event.Event; + +/** + * {@link ServiceConfig} event post-{@link ServiceConfig#unexport() unexport} + * + * @since 2.7.2 + */ +public class ServiceConfigUnexportedEvent extends Event { + + public ServiceConfigUnexportedEvent(ServiceConfig source) { + super(source); + } + + public ServiceConfig getServiceConfig() { + return (ServiceConfig) getSource(); + } +} diff --git a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/metadata/ConfigurableMetadataServiceExporter.java b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/metadata/ConfigurableMetadataServiceExporter.java new file mode 100644 index 000000000000..d3e4226767a5 --- /dev/null +++ b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/metadata/ConfigurableMetadataServiceExporter.java @@ -0,0 +1,118 @@ +/* + * 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.config.metadata; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.Logger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.config.AbstractConfig; +import org.apache.dubbo.config.ApplicationConfig; +import org.apache.dubbo.config.ProtocolConfig; +import org.apache.dubbo.config.RegistryConfig; +import org.apache.dubbo.config.ServiceConfig; +import org.apache.dubbo.config.context.ConfigManager; +import org.apache.dubbo.metadata.LocalMetadataService; +import org.apache.dubbo.metadata.MetadataService; +import org.apache.dubbo.metadata.MetadataServiceExporter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * {@link MetadataServiceExporter} implementation based on {@link AbstractConfig Dubbo configurations}, the clients + * should make sure the {@link ApplicationConfig}, {@link RegistryConfig} and {@link ProtocolConfig} are ready before + * {@link #export()}. + *

+ * Typically, do not worry about their ready status, because they are initialized before + * any {@link ServiceConfig} exports, or The Dubbo export will be failed. + * + * @see MetadataServiceExporter + * @see ServiceConfig + * @see ConfigManager + * @since 2.7.2 + */ +public class ConfigurableMetadataServiceExporter implements MetadataServiceExporter { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * {@link ConfigManager} stores {@link AbstractConfig the Dubbo *Config instances} + */ + private final ConfigManager configManager = ConfigManager.getInstance(); + + private volatile ServiceConfig serviceConfig; + + @Override + public List export() { + + if (!isExported()) { + + LocalMetadataService metadataService = LocalMetadataService.getDefaultExtension(); + + ServiceConfig serviceConfig = new ServiceConfig<>(); + serviceConfig.setApplication(getApplicationConfig()); + serviceConfig.setRegistries(getRegistries()); + serviceConfig.setProtocols(getProtocols()); + serviceConfig.setInterface(MetadataService.class); + serviceConfig.setRef(metadataService); + serviceConfig.setGroup(getApplicationConfig().getName()); + serviceConfig.setVersion(metadataService.version()); + + // export + serviceConfig.export(); + + if (logger.isInfoEnabled()) { + logger.info("The MetadataService exports urls : " + serviceConfig.getExportedUrls()); + } + + this.serviceConfig = serviceConfig; + } else { + if (logger.isWarnEnabled()) { + logger.warn("The MetadataService has been exported : " + serviceConfig.getExportedUrls()); + } + } + return serviceConfig.getExportedUrls(); + } + + @Override + public void unexport() { + if (isExported()) { + serviceConfig.unexport(); + } + } + + private List list(Map map) { + return new ArrayList<>(map.values()); + } + + private List getProtocols() { + return list(configManager.getProtocols()); + } + + private List getRegistries() { + return list(configManager.getRegistries()); + } + + private ApplicationConfig getApplicationConfig() { + return configManager.getApplication().get(); + } + + private boolean isExported() { + return serviceConfig != null && serviceConfig.isExported(); + } +} diff --git a/dubbo-config/dubbo-config-api/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.MetadataServiceExporter b/dubbo-config/dubbo-config-api/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.MetadataServiceExporter new file mode 100644 index 000000000000..30626fd41fc9 --- /dev/null +++ b/dubbo-config/dubbo-config-api/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.MetadataServiceExporter @@ -0,0 +1 @@ +default=org.apache.dubbo.config.metadata.ConfigurableMetadataServiceExporter \ No newline at end of file diff --git a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ReferenceConfigTest.java b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ReferenceConfigTest.java index d1e02636dd7c..f9afb3559456 100644 --- a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ReferenceConfigTest.java +++ b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ReferenceConfigTest.java @@ -21,20 +21,30 @@ import org.apache.dubbo.config.annotation.Reference; import org.apache.dubbo.config.api.DemoService; import org.apache.dubbo.config.context.ConfigManager; +import org.apache.dubbo.config.event.ReferenceConfigDestroyedEvent; +import org.apache.dubbo.config.event.ReferenceConfigInitializedEvent; import org.apache.dubbo.config.provider.impl.DemoServiceImpl; +import org.apache.dubbo.event.EventDispatcher; +import org.apache.dubbo.event.EventListener; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicReference; + import static org.apache.dubbo.rpc.Constants.LOCAL_PROTOCOL; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ReferenceConfigTest { + private final EventDispatcher eventDispatcher = EventDispatcher.getDefaultExtension(); + @BeforeEach public void setUp() { ConfigManager.getInstance().clear(); + eventDispatcher.removeAllEventListeners(); } @AfterEach @@ -67,12 +77,38 @@ public void testInjvm() throws Exception { rc.setInterface(DemoService.class.getName()); rc.setInjvm(false); + AtomicReference reference = new AtomicReference<>(); + + eventDispatcher.addEventListener(new EventListener() { + @Override + public void onEvent(ReferenceConfigInitializedEvent event) { + reference.set(event.getReferenceConfig()); + } + }); + try { System.setProperty("java.net.preferIPv4Stack", "true"); demoService.export(); rc.get(); + + assertEquals(rc, reference.get()); + + reference.compareAndSet(rc, null); + Assertions.assertTrue(!LOCAL_PROTOCOL.equalsIgnoreCase( rc.getInvoker().getUrl().getProtocol())); + + eventDispatcher.addEventListener(new EventListener() { + @Override + public void onEvent(ReferenceConfigDestroyedEvent event) { + reference.set(event.getReferenceConfig()); + } + }); + + rc.destroy(); + + assertEquals(rc, reference.get()); + } finally { System.clearProperty("java.net.preferIPv4Stack"); demoService.unexport(); @@ -134,17 +170,17 @@ public void testConstructWithReferenceAnnotation() throws NoSuchFieldException { Reference reference = getClass().getDeclaredField("innerTest").getAnnotation(Reference.class); ReferenceConfig referenceConfig = new ReferenceConfig(reference); Assertions.assertTrue(referenceConfig.getMethods().size() == 1); - Assertions.assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getName(), "sayHello"); + assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getName(), "sayHello"); Assertions.assertTrue(((MethodConfig) referenceConfig.getMethods().get(0)).getTimeout() == 1300); Assertions.assertTrue(((MethodConfig) referenceConfig.getMethods().get(0)).getRetries() == 4); - Assertions.assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getLoadbalance(), "random"); + assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getLoadbalance(), "random"); Assertions.assertTrue(((MethodConfig) referenceConfig.getMethods().get(0)).getActives() == 3); Assertions.assertTrue(((MethodConfig) referenceConfig.getMethods().get(0)).getExecutes() == 5); Assertions.assertTrue(((MethodConfig) referenceConfig.getMethods().get(0)).isAsync()); - Assertions.assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getOninvoke(), "i"); - Assertions.assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getOnreturn(), "r"); - Assertions.assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getOnthrow(), "t"); - Assertions.assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getCache(), "c"); + assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getOninvoke(), "i"); + assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getOnreturn(), "r"); + assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getOnthrow(), "t"); + assertEquals(((MethodConfig) referenceConfig.getMethods().get(0)).getCache(), "c"); } diff --git a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ServiceConfigTest.java b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ServiceConfigTest.java index 7c4b5b490244..c4f3c639073c 100644 --- a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ServiceConfigTest.java +++ b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ServiceConfigTest.java @@ -21,10 +21,14 @@ import org.apache.dubbo.config.api.DemoService; import org.apache.dubbo.config.api.Greeting; import org.apache.dubbo.config.context.ConfigManager; +import org.apache.dubbo.config.event.ServiceConfigExportedEvent; +import org.apache.dubbo.config.event.ServiceConfigUnexportedEvent; import org.apache.dubbo.config.mock.MockProtocol2; import org.apache.dubbo.config.mock.MockRegistryFactory2; import org.apache.dubbo.config.mock.TestProxyFactory; import org.apache.dubbo.config.provider.impl.DemoServiceImpl; +import org.apache.dubbo.event.EventDispatcher; +import org.apache.dubbo.event.EventListener; import org.apache.dubbo.registry.Registry; import org.apache.dubbo.rpc.Exporter; import org.apache.dubbo.rpc.Invoker; @@ -40,6 +44,7 @@ import java.util.Collections; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import static org.apache.dubbo.common.constants.CommonConstants.ANYHOST_KEY; import static org.apache.dubbo.common.constants.CommonConstants.APPLICATION_KEY; @@ -47,9 +52,9 @@ import static org.apache.dubbo.common.constants.CommonConstants.METHODS_KEY; import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER; import static org.apache.dubbo.common.constants.CommonConstants.SIDE_KEY; +import static org.apache.dubbo.common.constants.ConfigConstants.SHUTDOWN_WAIT_KEY; import static org.apache.dubbo.common.constants.RegistryConstants.EXPORT_KEY; import static org.apache.dubbo.config.Constants.SHUTDOWN_TIMEOUT_KEY; -import static org.apache.dubbo.common.constants.ConfigConstants.SHUTDOWN_WAIT_KEY; import static org.apache.dubbo.remoting.Constants.BIND_IP_KEY; import static org.apache.dubbo.remoting.Constants.BIND_PORT_KEY; import static org.apache.dubbo.rpc.Constants.GENERIC_KEY; @@ -75,6 +80,8 @@ public class ServiceConfigTest { private ServiceConfig service2 = new ServiceConfig(); private ServiceConfig delayService = new ServiceConfig(); + private final EventDispatcher eventDispatcher = EventDispatcher.getDefaultExtension(); + @BeforeEach public void setUp() throws Exception { MockProtocol2.delegate = protocolDelegate; @@ -135,8 +142,20 @@ public void tearDown() { @Test public void testExport() throws Exception { + + AtomicReference reference = new AtomicReference(); + + eventDispatcher.addEventListener(new EventListener() { + @Override + public void onEvent(ServiceConfigExportedEvent event) { + reference.set(event.getServiceConfig()); + } + }); + service.export(); + assertEquals(service, reference.get()); + assertThat(service.getExportedUrls(), hasSize(1)); URL url = service.toUrl(); assertThat(url.getProtocol(), equalTo("mockprotocol2")); @@ -178,8 +197,32 @@ public void testDelayExport() throws Exception { public void testUnexport() throws Exception { System.setProperty(SHUTDOWN_WAIT_KEY, "0"); try { + AtomicReference reference = new AtomicReference(); + + eventDispatcher.addEventListener(new EventListener() { + @Override + public void onEvent(ServiceConfigExportedEvent event) { + reference.set(event.getServiceConfig()); + } + }); + service.export(); + + assertEquals(service, reference.get()); + + assertTrue(reference.compareAndSet(service, null)); + + eventDispatcher.addEventListener(new EventListener() { + @Override + public void onEvent(ServiceConfigUnexportedEvent event) { + reference.set(event.getServiceConfig()); + } + }); + service.unexport(); + + assertEquals(service, reference.get()); + Thread.sleep(1000); Mockito.verify(exporter, Mockito.atLeastOnce()).unexport(); } finally { diff --git a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/metadata/ConfigurableMetadataServiceExporterTest.java b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/metadata/ConfigurableMetadataServiceExporterTest.java new file mode 100644 index 000000000000..48f3f38c0688 --- /dev/null +++ b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/metadata/ConfigurableMetadataServiceExporterTest.java @@ -0,0 +1,88 @@ +/* + * 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.config.metadata; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.ApplicationConfig; +import org.apache.dubbo.config.ProtocolConfig; +import org.apache.dubbo.config.RegistryConfig; +import org.apache.dubbo.config.context.ConfigManager; +import org.apache.dubbo.metadata.MetadataService; +import org.apache.dubbo.metadata.MetadataServiceExporter; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.apache.dubbo.common.constants.CommonConstants.APPLICATION_KEY; +import static org.apache.dubbo.common.constants.CommonConstants.GROUP_KEY; +import static org.apache.dubbo.common.constants.CommonConstants.VERSION_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link ConfigurableMetadataServiceExporter} Test + * + * @since 2.7.2 + */ +public class ConfigurableMetadataServiceExporterTest { + + @BeforeAll + public static void init() { + ConfigManager configManager = ConfigManager.getInstance(); + ApplicationConfig applicationConfig = new ApplicationConfig(); + applicationConfig.setName("test"); + configManager.setApplication(applicationConfig); + + // Add ProtocolConfig + configManager.addProtocol(protocolConfig()); + // Add RegistryConfig + configManager.addRegistry(registryConfig()); + } + + private static ProtocolConfig protocolConfig() { + ProtocolConfig protocolConfig = new ProtocolConfig(); + protocolConfig.setName("mockprotocol"); + protocolConfig.setPort(20880); + return protocolConfig; + } + + private static RegistryConfig registryConfig() { + RegistryConfig registryConfig = new RegistryConfig(); + registryConfig.setAddress("mockregistry://127.0.0.1"); + return registryConfig; + } + + @Test + public void testExportAndUnexport() { + MetadataServiceExporter exporter = new ConfigurableMetadataServiceExporter(); + List urls = exporter.export(); + + assertEquals(1, urls.size()); + + URL url = urls.get(0); + + assertEquals("test", url.getParameter(APPLICATION_KEY)); + assertEquals(MetadataService.class.getName(), url.getServiceInterface()); + assertEquals("test", url.getParameter(GROUP_KEY)); + assertEquals(MetadataService.VERSION, url.getParameter(VERSION_KEY)); + assertEquals("mockprotocol", url.getProtocol()); + + exporter.unexport(); + } + +} diff --git a/dubbo-dependencies-bom/pom.xml b/dubbo-dependencies-bom/pom.xml index ef42a6e4517e..e250644f5069 100644 --- a/dubbo-dependencies-bom/pom.xml +++ b/dubbo-dependencies-bom/pom.xml @@ -228,6 +228,17 @@ + + org.apache.curator + curator-x-discovery + ${curator_version} + + + org.apache.zookeeper + zookeeper + + + redis.clients jedis diff --git a/dubbo-event/pom.xml b/dubbo-event/pom.xml new file mode 100644 index 000000000000..017f0e79fde8 --- /dev/null +++ b/dubbo-event/pom.xml @@ -0,0 +1,45 @@ + + + + org.apache.dubbo + dubbo-parent + ${revision} + ../pom.xml + + 4.0.0 + + dubbo-event + jar + + dubbo-event + The event module of Dubbo project + + + + org.apache.dubbo + dubbo-common + ${project.parent.version} + true + + + + + + \ No newline at end of file diff --git a/dubbo-event/src/main/java/org/apache/dubbo/event/AbstractEventDispatcher.java b/dubbo-event/src/main/java/org/apache/dubbo/event/AbstractEventDispatcher.java new file mode 100644 index 000000000000..8989f29a0aa0 --- /dev/null +++ b/dubbo-event/src/main/java/org/apache/dubbo/event/AbstractEventDispatcher.java @@ -0,0 +1,155 @@ +/* + * 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.event; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +import static java.util.Collections.sort; +import static java.util.Collections.unmodifiableList; +import static java.util.ServiceLoader.load; +import static org.apache.dubbo.event.EventListener.findEventType; + +/** + * The abstract {@link EventDispatcher} providers the common implementation. + * + * @see EventDispatcher + * @see Listenable + * @see ServiceLoader + * @see EventListener + * @see Event + * @since 2.7.2 + */ +public abstract class AbstractEventDispatcher implements EventDispatcher { + + private final Object mutex = new Object(); + + private final ConcurrentMap, List> listenersCache = new ConcurrentHashMap<>(); + + private final Executor executor; + + /** + * Constructor with an instance of {@link Executor} + * + * @param executor {@link Executor} + * @throws NullPointerException executor is null + */ + protected AbstractEventDispatcher(Executor executor) { + if (executor == null) { + throw new NullPointerException("executor must not be null"); + } + this.executor = executor; + this.loadEventListenerInstances(); + } + + @Override + public void addEventListener(EventListener listener) throws NullPointerException, IllegalArgumentException { + Listenable.assertListener(listener); + doInListener(listener, listeners -> { + addIfAbsent(listeners, listener); + }); + } + + @Override + public void removeEventListener(EventListener listener) throws NullPointerException, IllegalArgumentException { + Listenable.assertListener(listener); + doInListener(listener, listeners -> listeners.remove(listener)); + } + + @Override + public List> getAllEventListeners() { + List> listeners = new LinkedList<>(); + + listenersCache + .entrySet() + .stream() + .map(Map.Entry::getValue) + .flatMap(Collection::stream) + .forEach(listener -> { + addIfAbsent(listeners, listener); + }); + + sort((List) listeners); + + return unmodifiableList(listeners); + } + + private void addIfAbsent(Collection collection, E element) { + if (!collection.contains(element)) { + collection.add(element); + } + } + + @Override + public void dispatch(Event event) { + + Executor executor = getExecutor(); + + // execute in sequential or parallel execution model + executor.execute(() -> { + listenersCache.entrySet() + .stream() + .filter(entry -> entry.getKey().isAssignableFrom(event.getClass())) + .map(Map.Entry::getValue) + .flatMap(Collection::stream) + .forEach(listener -> { + listener.onEvent(event); + }); + }); + } + + /** + * @return the non-null {@link Executor} + */ + @Override + public final Executor getExecutor() { + return executor; + } + + protected void doInListener(EventListener listener, Consumer> consumer) { + Class eventType = findEventType(listener); + if (eventType != null) { + synchronized (mutex) { + List listeners = listenersCache.computeIfAbsent(eventType, e -> new LinkedList<>()); + // consume + consumer.accept(listeners); + // sort + sort(listeners); + } + } + } + + /** + * Default, load the instances of {@link EventListener event listeners} by {@link ServiceLoader} + *

+ * It could be override by the sub-class + * + * @see EventListener + * @see ServiceLoader#load(Class) + */ + protected void loadEventListenerInstances() { + ServiceLoader serviceLoader = load(EventListener.class, getClass().getClassLoader()); + serviceLoader.forEach(this::addEventListener); + } +} diff --git a/dubbo-event/src/main/java/org/apache/dubbo/event/DirectEventDispatcher.java b/dubbo-event/src/main/java/org/apache/dubbo/event/DirectEventDispatcher.java new file mode 100644 index 000000000000..91282b34c6e0 --- /dev/null +++ b/dubbo-event/src/main/java/org/apache/dubbo/event/DirectEventDispatcher.java @@ -0,0 +1,30 @@ +/* + * 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.event; + +/** + * Direct {@link EventDispatcher} implementation uses current thread execution model + * + * @see EventDispatcher + * @since 2.7.2 + */ +public final class DirectEventDispatcher extends AbstractEventDispatcher { + + public DirectEventDispatcher() { + super(DIRECT_EXECUTOR); + } +} diff --git a/dubbo-event/src/main/java/org/apache/dubbo/event/Event.java b/dubbo-event/src/main/java/org/apache/dubbo/event/Event.java new file mode 100644 index 000000000000..13f2aec16e15 --- /dev/null +++ b/dubbo-event/src/main/java/org/apache/dubbo/event/Event.java @@ -0,0 +1,49 @@ +/* + * 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.event; + +import java.util.EventObject; + +/** + * An event object of Dubbo is based on the Java standard {@link EventObject event} + * + * @since 2.7.2 + */ +public abstract class Event extends EventObject { + + private static final long serialVersionUID = -1704315605423947137L; + + /** + * The timestamp of event occurs + */ + private final long timestamp; + + /** + * Constructs a prototypical Event. + * + * @param source The object on which the Event initially occurred. + * @throws IllegalArgumentException if source is null. + */ + public Event(Object source) { + super(source); + this.timestamp = System.currentTimeMillis(); + } + + public long getTimestamp() { + return timestamp; + } +} diff --git a/dubbo-event/src/main/java/org/apache/dubbo/event/EventDispatcher.java b/dubbo-event/src/main/java/org/apache/dubbo/event/EventDispatcher.java new file mode 100644 index 000000000000..ab3a2e4998b1 --- /dev/null +++ b/dubbo-event/src/main/java/org/apache/dubbo/event/EventDispatcher.java @@ -0,0 +1,66 @@ +/* + * 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.event; + +import org.apache.dubbo.common.extension.ExtensionLoader; +import org.apache.dubbo.common.extension.SPI; + +import java.util.concurrent.Executor; + +/** + * {@link Event Dubbo Event} Dispatcher + * + * @see Event + * @see EventListener + * @see DirectEventDispatcher + * @since 2.7.2 + */ +@SPI("direct") +public interface EventDispatcher extends Listenable> { + + /** + * Direct {@link Executor} uses sequential execution model + */ + Executor DIRECT_EXECUTOR = Runnable::run; + + /** + * Dispatch a Dubbo event to the registered {@link EventListener Dubbo event listeners} + * + * @param event a {@link Event Dubbo event} + */ + void dispatch(Event event); + + /** + * The {@link Executor} to dispatch a {@link Event Dubbo event} + * + * @return default implementation directly invoke {@link Runnable#run()} method, rather than multiple-threaded + * {@link Executor}. If the return value is null, the behavior is same as default. + * @see #DIRECT_EXECUTOR + */ + default Executor getExecutor() { + return DIRECT_EXECUTOR; + } + + /** + * The default extension of {@link EventDispatcher} is loaded by {@link ExtensionLoader} + * + * @return the default extension of {@link EventDispatcher} + */ + static EventDispatcher getDefaultExtension() { + return ExtensionLoader.getExtensionLoader(EventDispatcher.class).getDefaultExtension(); + } +} diff --git a/dubbo-event/src/main/java/org/apache/dubbo/event/EventListener.java b/dubbo-event/src/main/java/org/apache/dubbo/event/EventListener.java new file mode 100644 index 000000000000..92c35b9c8824 --- /dev/null +++ b/dubbo-event/src/main/java/org/apache/dubbo/event/EventListener.java @@ -0,0 +1,124 @@ +/* + * 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.event; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Objects; + +import static java.lang.Integer.compare; +import static org.apache.dubbo.common.utils.ReflectUtils.findParameterizedTypes; + +/** + * The {@link Event Dubbo Event} Listener that is based on Java standard {@link java.util.EventListener} interface supports + * the generic {@link Event}. + *

+ * The {@link #onEvent(Event) handle method} will be notified when the matched-type {@link Event Dubbo Event} is + * published, whose priority could be changed by {@link #getPriority()} method. + * + * @param the concrete class of {@link Event Dubbo Event} + * @see Event + * @see java.util.EventListener + * @since 2.7.2 + */ +@FunctionalInterface +public interface EventListener extends java.util.EventListener, Comparable> { + + /** + * Handle a {@link Event Dubbo Event} when it's be published + * + * @param event a {@link Event Dubbo Event} + */ + void onEvent(E event); + + /** + * The priority of {@link EventListener current listener}. + * + * @return the value is more greater, the priority is more lower. + * {@link Integer#MIN_VALUE} indicates the highest priority. The default value is {@link Integer#MAX_VALUE}. + * The comparison rule , refer to {@link #compareTo(EventListener)}. + * @see #compareTo(EventListener) + * @see Integer#MAX_VALUE + * @see Integer#MIN_VALUE + */ + default int getPriority() { + return Integer.MAX_VALUE; + } + + @Override + default int compareTo(EventListener another) { + return compare(this.getPriority(), another.getPriority()); + } + + /** + * Find the {@link Class type} {@link Event Dubbo event} from the specified {@link EventListener Dubbo event listener} + * + * @param listener the {@link Class class} of {@link EventListener Dubbo event listener} + * @return null if not found + */ + static Class findEventType(EventListener listener) { + return findEventType(listener.getClass()); + } + + /** + * Find the {@link Class type} {@link Event Dubbo event} from the specified {@link EventListener Dubbo event listener} + * + * @param listenerClass the {@link Class class} of {@link EventListener Dubbo event listener} + * @return null if not found + */ + static Class findEventType(Class listenerClass) { + Class eventType = null; + + if (listenerClass != null && EventListener.class.isAssignableFrom(listenerClass)) { + eventType = findParameterizedTypes(listenerClass) + .stream() + .map(EventListener::findEventType) + .filter(Objects::nonNull) + .findAny() + .orElse((Class) findEventType(listenerClass.getSuperclass())); + } + + return eventType; + } + + /** + * Find the type {@link Event Dubbo event} from the specified {@link ParameterizedType} presents + * a class of {@link EventListener Dubbo event listener} + * + * @param parameterizedType the {@link ParameterizedType} presents a class of {@link EventListener Dubbo event listener} + * @return null if not found + */ + static Class findEventType(ParameterizedType parameterizedType) { + Class eventType = null; + + Type rawType = parameterizedType.getRawType(); + if ((rawType instanceof Class) && EventListener.class.isAssignableFrom((Class) rawType)) { + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + for (Type typeArgument : typeArguments) { + if (typeArgument instanceof Class) { + Class argumentClass = (Class) typeArgument; + if (Event.class.isAssignableFrom(argumentClass)) { + eventType = argumentClass; + break; + } + } + } + } + + return eventType; + } +} \ No newline at end of file diff --git a/dubbo-event/src/main/java/org/apache/dubbo/event/GenericEvent.java b/dubbo-event/src/main/java/org/apache/dubbo/event/GenericEvent.java new file mode 100644 index 000000000000..591bcebb086a --- /dev/null +++ b/dubbo-event/src/main/java/org/apache/dubbo/event/GenericEvent.java @@ -0,0 +1,34 @@ +/* + * 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.event; + +/** + * Generic {@link Event Dubbo event} + * + * @param the type of event source + * @since 2.7.2 + */ +public class GenericEvent extends Event { + + public GenericEvent(S source) { + super(source); + } + + public S getSource() { + return (S) super.getSource(); + } +} diff --git a/dubbo-event/src/main/java/org/apache/dubbo/event/Listenable.java b/dubbo-event/src/main/java/org/apache/dubbo/event/Listenable.java new file mode 100644 index 000000000000..1857ac85f3f3 --- /dev/null +++ b/dubbo-event/src/main/java/org/apache/dubbo/event/Listenable.java @@ -0,0 +1,131 @@ +/* + * 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.event; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static java.util.stream.StreamSupport.stream; + +/** + * Dubbo Event Listenable + * + * @see EventDispatcher + * @since 2.7.2 + */ +public interface Listenable> { + + /** + * Add a {@link EventListener Dubbo event listener} + * + * @param listener a {@link EventListener Dubbo event listener} + * If current {@link EventListener} is existed, return false + * @throws NullPointerException if listener argument is null + * @throws IllegalArgumentException if listener argument is not concrete instance + */ + void addEventListener(E listener) throws NullPointerException, IllegalArgumentException; + + /** + * Add one or more {@link EventListener Dubbo event listeners} + * + * @param listener a {@link EventListener Dubbo event listener} + * @param others an optional {@link EventListener Dubbo event listeners} + * @throws NullPointerException if one of arguments is null + * @throws IllegalArgumentException if one of arguments argument is not concrete instance + */ + default void addEventListeners(E listener, E... others) throws NullPointerException, + IllegalArgumentException { + List listeners = new ArrayList<>(1 + others.length); + listeners.add(listener); + listeners.addAll(Arrays.asList(others)); + addEventListeners(listeners); + } + + /** + * Add multiple {@link EventListener Dubbo event listeners} + * + * @param listeners the {@link EventListener Dubbo event listeners} + * @throws NullPointerException if listeners argument is null + * @throws IllegalArgumentException if any element of listeners is not concrete instance + */ + default void addEventListeners(Iterable listeners) throws NullPointerException, IllegalArgumentException { + stream(listeners.spliterator(), false).forEach(this::addEventListener); + } + + /** + * Remove a {@link EventListener Dubbo event listener} + * + * @param listener a {@link EventListener Dubbo event listener} + * @return If remove successfully, return true. + * If current {@link EventListener} is existed, return false + * @throws NullPointerException if listener argument is null + */ + void removeEventListener(E listener) throws NullPointerException, IllegalArgumentException; + + /** + * Remove a {@link EventListener Dubbo event listener} + * + * @param listeners the {@link EventListener Dubbo event listeners} + * @return If remove successfully, return true. + * If current {@link EventListener} is existed, return false + * @throws NullPointerException if listener argument is null + * @throws IllegalArgumentException if any element of listeners is not concrete instance + */ + default void removeEventListeners(Iterable listeners) throws NullPointerException, IllegalArgumentException { + stream(listeners.spliterator(), false).forEach(this::removeEventListener); + } + + /** + * Remove all {@link EventListener Dubbo event listeners} + * + * @return a amount of removed listeners + */ + default void removeAllEventListeners() { + removeEventListeners(getAllEventListeners()); + } + + /** + * Get all registered {@link EventListener Dubbo event listeners} + * + * @return non-null read-only ordered {@link EventListener Dubbo event listeners} + * @see EventListener#getPriority() + */ + List getAllEventListeners(); + + + /** + * Assets the listener is valid or not + * + * @param listener the instance of {@link EventListener} + * @throws NullPointerException + */ + static void assertListener(EventListener listener) throws NullPointerException { + if (listener == null) { + throw new NullPointerException("The listener must not be null."); + } + + Class listenerClass = listener.getClass(); + + int modifiers = listenerClass.getModifiers(); + + if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers)) { + throw new IllegalArgumentException("The listener must be concrete class"); + } + } +} diff --git a/dubbo-event/src/main/java/org/apache/dubbo/event/ParallelEventDispatcher.java b/dubbo-event/src/main/java/org/apache/dubbo/event/ParallelEventDispatcher.java new file mode 100644 index 000000000000..f7b0f3297637 --- /dev/null +++ b/dubbo-event/src/main/java/org/apache/dubbo/event/ParallelEventDispatcher.java @@ -0,0 +1,32 @@ +/* + * 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.event; + +import java.util.concurrent.ForkJoinPool; + +/** + * Parallel {@link EventDispatcher} implementation uses {@link ForkJoinPool#commonPool() JDK common thread pool} + * + * @see ForkJoinPool#commonPool() + * @since 2.7.2 + */ +public class ParallelEventDispatcher extends AbstractEventDispatcher { + + public ParallelEventDispatcher() { + super(ForkJoinPool.commonPool()); + } +} diff --git a/dubbo-event/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.event.EventDispatcher b/dubbo-event/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.event.EventDispatcher new file mode 100644 index 000000000000..ecd54edb280a --- /dev/null +++ b/dubbo-event/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.event.EventDispatcher @@ -0,0 +1,2 @@ +direct=org.apache.dubbo.event.DirectEventDispatcher +parallel=org.apache.dubbo.event.ParallelEventDispatcher diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/AbstractEventListener.java b/dubbo-event/src/test/java/org/apache/dubbo/event/AbstractEventListener.java new file mode 100644 index 000000000000..692b3b6d12b9 --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/AbstractEventListener.java @@ -0,0 +1,40 @@ +/* + * 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.event; + +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class AbstractEventListener implements EventListener { + + private final AtomicInteger eventOccurs = new AtomicInteger(0); + + @Override + public final void onEvent(E event) { + eventOccurs.getAndIncrement(); + handleEvent(event); + } + + protected abstract void handleEvent(E event); + + public int getEventOccurs() { + return eventOccurs.get(); + } + + protected void println(String message) { + System.out.printf("[%s] %s\n", Thread.currentThread().getName(), message); + } +} \ No newline at end of file diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/DirectEventDispatcherTest.java b/dubbo-event/src/test/java/org/apache/dubbo/event/DirectEventDispatcherTest.java new file mode 100644 index 000000000000..c3cc7323df23 --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/DirectEventDispatcherTest.java @@ -0,0 +1,153 @@ +/* + * 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.event; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link DirectEventDispatcher} Test + * + * @since 2.7.2 + */ +public class DirectEventDispatcherTest { + + private DirectEventDispatcher dispatcher; + + private EchoEventListener echoEventListener; + + private EchoEventListener2 echoEventListener2; + + @BeforeEach + public void init() { + dispatcher = new DirectEventDispatcher(); + echoEventListener = new EchoEventListener(); + echoEventListener2 = new EchoEventListener2(); + } + + @AfterEach + public void destroy() { + dispatcher.removeAllEventListeners(); + } + + @Test + public void testGetExecutor() { + assertNotNull(dispatcher.getExecutor()); + } + + @Test + public void testGetAllListeners() { + assertTrue(dispatcher.getAllEventListeners().isEmpty()); + } + + @Test + public void testSingleListener() { + // add two listeners + dispatcher.addEventListener(echoEventListener); + dispatcher.addEventListener(echoEventListener2); + assertEquals(asList(echoEventListener2, echoEventListener), dispatcher.getAllEventListeners()); + + // add a duplicated listener + dispatcher.addEventListener(echoEventListener); + assertEquals(asList(echoEventListener2, echoEventListener), dispatcher.getAllEventListeners()); + + // remove + dispatcher.removeEventListener(echoEventListener); + assertEquals(asList(echoEventListener2), dispatcher.getAllEventListeners()); + + dispatcher.removeEventListener(echoEventListener2); + assertEquals(emptyList(), dispatcher.getAllEventListeners()); + } + + @Test + public void testMultipleListeners() { + + // add two listeners + dispatcher.addEventListeners(echoEventListener, echoEventListener2); + assertEquals(asList(echoEventListener2, echoEventListener), dispatcher.getAllEventListeners()); + + // remove all listeners + dispatcher.removeAllEventListeners(); + assertEquals(emptyList(), dispatcher.getAllEventListeners()); + + // add the duplicated listeners + dispatcher.addEventListeners(echoEventListener, echoEventListener, echoEventListener2); + assertEquals(asList(echoEventListener2, echoEventListener), dispatcher.getAllEventListeners()); + + // remove all listeners + dispatcher.removeAllEventListeners(); + assertEquals(emptyList(), dispatcher.getAllEventListeners()); + + dispatcher.addEventListeners(asList(echoEventListener, echoEventListener, echoEventListener2)); + assertEquals(asList(echoEventListener2, echoEventListener), dispatcher.getAllEventListeners()); + + dispatcher.removeEventListeners(asList(echoEventListener, echoEventListener, echoEventListener2)); + assertEquals(emptyList(), dispatcher.getAllEventListeners()); + } + + @Test + public void testDispatchEvent() { + + dispatcher.addEventListener(echoEventListener); + + // dispatch a Event + dispatcher.dispatch(new Event("Test") { + }); + + // no-op occurs + assertEquals(0, echoEventListener.getEventOccurs()); + + // dispatch a EchoEvent + dispatcher.dispatch(new EchoEvent("Hello,World")); + + // event has been handled + assertEquals(1, echoEventListener.getEventOccurs()); + + dispatcher.addEventListener(echoEventListener2); + + // reset the listeners + init(); + dispatcher.addEventListeners(echoEventListener, echoEventListener2); + + // dispatch a Event + dispatcher.dispatch(new Event("Test") { + }); + + // echoEventListener will be not triggered + 0 + // echoEventListener2 will be triggered + 1 + assertEquals(0, echoEventListener.getEventOccurs()); + assertEquals(1, echoEventListener2.getEventOccurs()); + + // dispatch a EchoEvent + // echoEventListener and echoEventListener2 are triggered both (+1) + dispatcher.dispatch(new EchoEvent("Hello,World")); + assertEquals(1, echoEventListener.getEventOccurs()); + assertEquals(2, echoEventListener2.getEventOccurs()); + + // both +1 + dispatcher.dispatch(new EchoEvent("2019")); + assertEquals(2, echoEventListener.getEventOccurs()); + assertEquals(3, echoEventListener2.getEventOccurs()); + } +} diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEvent.java b/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEvent.java new file mode 100644 index 000000000000..785d411f2564 --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEvent.java @@ -0,0 +1,29 @@ +/* + * 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.event; + +/** + * Echo {@link Event} + * + * @since 2.7.2 + */ +class EchoEvent extends Event { + + public EchoEvent(Object source) { + super(source); + } +} diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEventListener.java b/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEventListener.java new file mode 100644 index 000000000000..f88b9df33182 --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEventListener.java @@ -0,0 +1,32 @@ +/* + * 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.event; + +import java.io.Serializable; + +/** + * {@link EchoEvent} {@link EventListener} + * + * @since 2.7.2 + */ +public class EchoEventListener extends AbstractEventListener implements Serializable { + + @Override + public void handleEvent(EchoEvent event) { + println("EchoEventListener : " + event); + } +} \ No newline at end of file diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEventListener2.java b/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEventListener2.java new file mode 100644 index 000000000000..f30327c4852b --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/EchoEventListener2.java @@ -0,0 +1,61 @@ +/* + * 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.event; + +import java.io.Serializable; +import java.util.Objects; +import java.util.Vector; + +/** + * {@link EchoEvent} {@link EventListener} 2 + * + * @since 2.7.2 + */ +public class EchoEventListener2 extends Vector> implements Serializable, Comparable>, + EventListener { + + private AbstractEventListener delegate = new AbstractEventListener() { + @Override + protected void handleEvent(Event event) { + println("EchoEventListener2 : " + event); + } + }; + + @Override + public void onEvent(Event event) { + delegate.onEvent(event); + } + + @Override + public int getPriority() { + return 0; + } + + public int getEventOccurs() { + return delegate.getEventOccurs(); + } + + @Override + public boolean equals(Object o) { + return this.getClass().equals(o.getClass()); + } + + @Override + public int hashCode() { + return Objects.hash(this.getClass()); + } +} diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/EventDispatcherTest.java b/dubbo-event/src/test/java/org/apache/dubbo/event/EventDispatcherTest.java new file mode 100644 index 000000000000..b8d57f93992f --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/EventDispatcherTest.java @@ -0,0 +1,45 @@ +/* + * 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.event; + +import org.junit.jupiter.api.Test; + +import static org.apache.dubbo.event.EventDispatcher.DIRECT_EXECUTOR; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link EventDispatcher} Test + * + * @see DirectEventDispatcher + * @since 2.7.2 + */ +public class EventDispatcherTest { + + private EventDispatcher defaultInstance = EventDispatcher.getDefaultExtension(); + + @Test + public void testDefaultInstance() { + assertEquals(DirectEventDispatcher.class, defaultInstance.getClass()); + } + + @Test + public void testDefaultMethods() { + assertEquals(DIRECT_EXECUTOR, defaultInstance.getExecutor()); + assertTrue(defaultInstance.getAllEventListeners().isEmpty()); + } +} diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/EventListenerTest.java b/dubbo-event/src/test/java/org/apache/dubbo/event/EventListenerTest.java new file mode 100644 index 000000000000..eb3d27b02ea5 --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/EventListenerTest.java @@ -0,0 +1,44 @@ +/* + * 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.event; + +import org.junit.jupiter.api.Test; + +import static org.apache.dubbo.event.EventListener.findEventType; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link EventListener} Test + * + * @since 2.7.2 + */ +public class EventListenerTest { + + @Test + public void testFindEventHierarchicalTypes() { + assertEquals(EchoEvent.class, findEventType(new EchoEventListener())); + assertEquals(Event.class, findEventType(new EchoEventListener2())); + + assertEquals(EchoEvent.class, findEventType(EchoEventListener.class)); + assertEquals(Event.class, findEventType(EchoEventListener2.class)); + } + + @Test + public void testOnEvent() { + } + +} \ No newline at end of file diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/GenericEventTest.java b/dubbo-event/src/test/java/org/apache/dubbo/event/GenericEventTest.java new file mode 100644 index 000000000000..92970343f1c5 --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/GenericEventTest.java @@ -0,0 +1,41 @@ +/* + * 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.event; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link GenericEvent} Test + * + * @since 2.7.2 + */ +public class GenericEventTest { + + @Test + public void test() { + + long timestamp = System.currentTimeMillis(); + GenericEvent event = new GenericEvent("Hello,World"); + + assertEquals("Hello,World", event.getSource()); + assertTrue(event.getTimestamp() >= timestamp); + } + +} diff --git a/dubbo-event/src/test/java/org/apache/dubbo/event/ParallelEventDispatcherTest.java b/dubbo-event/src/test/java/org/apache/dubbo/event/ParallelEventDispatcherTest.java new file mode 100644 index 000000000000..755d482d2a84 --- /dev/null +++ b/dubbo-event/src/test/java/org/apache/dubbo/event/ParallelEventDispatcherTest.java @@ -0,0 +1,59 @@ +/* + * 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.event; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link ParallelEventDispatcher} Test + * + * @since 2.7.2 + */ +public class ParallelEventDispatcherTest { + + private EventDispatcher eventDispatcher; + + private AbstractEventListener listener; + + @BeforeEach + public void init() { + eventDispatcher = new ParallelEventDispatcher(); + listener = new EchoEventListener(); + eventDispatcher.addEventListener(listener); + } + + @Test + public void testDispatchEvent() throws InterruptedException { + eventDispatcher.dispatch(new EchoEvent("Hello,World")); + ForkJoinPool.commonPool().awaitTermination(1, TimeUnit.SECONDS); + // event has been handled + assertEquals(1, listener.getEventOccurs()); + } + + @AfterAll + public static void destroy() { + ForkJoinPool.commonPool().shutdown(); + } + +} diff --git a/dubbo-event/src/test/resources/META-INF/services/org.apache.dubbo.event.EventListener b/dubbo-event/src/test/resources/META-INF/services/org.apache.dubbo.event.EventListener new file mode 100644 index 000000000000..3cb4b7fdd34e --- /dev/null +++ b/dubbo-event/src/test/resources/META-INF/services/org.apache.dubbo.event.EventListener @@ -0,0 +1 @@ +#org.apache.dubbo.event.EchoEventListener2 \ No newline at end of file diff --git a/dubbo-metadata/pom.xml b/dubbo-metadata/pom.xml new file mode 100644 index 000000000000..144ca1fda494 --- /dev/null +++ b/dubbo-metadata/pom.xml @@ -0,0 +1,45 @@ + + + + org.apache.dubbo + dubbo-parent + ${revision} + ../pom.xml + + 4.0.0 + + dubbo-metadata + jar + + dubbo-metadata + The metadata module of Dubbo project + + + + + org.apache.dubbo + dubbo-rpc-api + ${project.parent.version} + true + + + + + \ No newline at end of file diff --git a/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/InMemoryLocalMetadataService.java b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/InMemoryLocalMetadataService.java new file mode 100644 index 000000000000..1f90543eb6be --- /dev/null +++ b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/InMemoryLocalMetadataService.java @@ -0,0 +1,157 @@ +/* + * 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 java.util.Collection; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import static java.util.Collections.unmodifiableList; +import static org.apache.dubbo.common.URL.buildKey; +import static org.apache.dubbo.common.constants.CommonConstants.PROTOCOL_KEY; + +/** + * The {@link LocalMetadataService} implementation stores the metadata of Dubbo services in memory locally when they + * exported. + * + * @see MetadataService + * @since 2.7.2 + */ +public class InMemoryLocalMetadataService implements LocalMetadataService { + + /** + * The class name of {@link MetadataService} + */ + static final String METADATA_SERVICE_CLASS_NAME = MetadataService.class.getName(); + + // =================================== Registration =================================== // + + /** + * All exported {@link URL urls} {@link Map} whose key is the return value of {@link URL#getServiceKey()} method + * and value is the {@link List} of the {@link URL URLs} + */ + private ConcurrentMap> exportedServiceURLs = new ConcurrentHashMap<>(); + + // ==================================================================================== // + + // =================================== Subscription =================================== // + + /** + * All subscribed service names + */ + private Set subscribedServices = new LinkedHashSet<>(); + + /** + * The subscribed {@link URL urls} {@link Map} of {@link MetadataService}, + * whose key is the return value of {@link URL#getServiceKey()} method and value is the {@link List} of + * the {@link URL URLs} + */ + private final ConcurrentMap> subscribedServiceURLs = new ConcurrentHashMap<>(); + + // ==================================================================================== // + + @Override + public List getSubscribedURLs() { + return getAllServiceURLs(subscribedServiceURLs); + } + + @Override + public List getExportedURLs(String serviceInterface, String group, String version, String protocol) { + if (ALL_SERVICE_INTERFACES.equals(serviceInterface)) { + return getAllServiceURLs(exportedServiceURLs); + } + String serviceKey = buildKey(serviceInterface, group, version); + return unmodifiableList(getServiceURLs(exportedServiceURLs, serviceKey, protocol)); + } + + protected List getServiceURLs(ConcurrentMap> exportedServiceURLs, String serviceKey, + String protocol) { + List serviceURLs = getServiceURLs(exportedServiceURLs, serviceKey); + return serviceURLs.stream().filter( + url -> protocol == null || protocol.equals(url.getParameter(PROTOCOL_KEY))) + .map(URL::toFullString) + .collect(Collectors.toList()); + } + + + private boolean isMetadataServiceURL(URL url) { + String serviceInterface = url.getServiceInterface(); + return METADATA_SERVICE_CLASS_NAME.equals(serviceInterface); + } + + @Override + public boolean exportURL(URL url) { + if (isMetadataServiceURL(url)) { // ignore MetadataService in the export phase + return true; + } + return addURL(exportedServiceURLs, url); + } + + @Override + public boolean unexportURL(URL url) { + if (isMetadataServiceURL(url)) { // ignore MetadataService in the export phase + return true; + } + return removeURL(exportedServiceURLs, url); + } + + @Override + public boolean subscribeURL(URL url) { + return addURL(subscribedServiceURLs, url); + } + + @Override + public boolean unsubscribeURL(URL url) { + return removeURL(subscribedServiceURLs, url); + } + + protected boolean addURL(Map> serviceURLs, URL url) { + String serviceKey = url.getServiceKey(); + List urls = getServiceURLs(serviceURLs, serviceKey); + if (!urls.contains(url)) { + return urls.add(url); + } + return false; + } + + protected boolean removeURL(Map> serviceURLs, URL url) { + String serviceKey = url.getServiceKey(); + List urls = getServiceURLs(serviceURLs, serviceKey); + return urls.remove(url); + } + + protected List getServiceURLs(Map> serviceURLs, String serviceKey) { + return serviceURLs.computeIfAbsent(serviceKey, s -> new LinkedList()); + } + + protected List getAllServiceURLs(Map> serviceURLs) { + return serviceURLs + .values() + .stream() + .flatMap(Collection::stream) + .map(URL::toFullString) + .collect(Collectors.toList()); + } +} diff --git a/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/LocalMetadataService.java b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/LocalMetadataService.java new file mode 100644 index 000000000000..976363389596 --- /dev/null +++ b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/LocalMetadataService.java @@ -0,0 +1,88 @@ +/* + * 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.extension.SPI; +import org.apache.dubbo.rpc.model.ApplicationModel; + +import static org.apache.dubbo.common.extension.ExtensionLoader.getExtensionLoader; + +/** + * Local {@link MetadataService} that extends {@link MetadataService} and provides the modification, which is used for + * Dubbo's consumers and providers. + * + * @since 2.7.2 + */ +@SPI("default") +public interface LocalMetadataService extends MetadataService { + + /** + * Gets the current Dubbo Service name + * + * @return non-null + */ + @Override + default String serviceName() { + return ApplicationModel.getApplication(); + } + + /** + * Exports a {@link URL} + * + * @param url a {@link URL} + * @return If success , return true + */ + boolean exportURL(URL url); + + /** + * Unexports a {@link URL} + * + * @param url a {@link URL} + * @return If success , return true + */ + boolean unexportURL(URL url); + + /** + * Subscribes a {@link URL} + * + * @param url a {@link URL} + * @return If success , return true + */ + boolean subscribeURL(URL url); + + /** + * Unsubscribes a {@link URL} + * + * @param url a {@link URL} + * @return If success , return true + */ + boolean unsubscribeURL(URL url); + + + /** + * Get {@link ExtensionLoader#getDefaultExtension() the defautl extension} of {@link LocalMetadataService} + * + * @return non-null + * @see InMemoryLocalMetadataService + */ + public static LocalMetadataService getDefaultExtension() { + return getExtensionLoader(LocalMetadataService.class).getDefaultExtension(); + } + +} diff --git a/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/MetadataService.java b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/MetadataService.java new file mode 100644 index 000000000000..4cbbae578351 --- /dev/null +++ b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/MetadataService.java @@ -0,0 +1,151 @@ +/* + * 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 java.util.List; +import java.util.stream.Collectors; + +import static java.util.stream.StreamSupport.stream; + +/** + * A framework interface of Dubbo Metadata Service defines the contract of Dubbo Services registartion and subscription + * between Dubbo service providers and its consumers. The implementationwill be exported as a normal Dubbo service that + * the clients would subscribe, whose version comes from the {@link #version()} method and group gets from + * {@link #serviceName()}, that means, The different Dubbo service(application) will export the different + * {@link MetadataService} that persists all the exported and subscribed metadata, they are present by + * {@link #getExportedURLs()} and {@link #getSubscribedURLs()} respectively. What's more, {@link MetadataService} + * also providers the fine-grain methods for the precise queries. + * + * @see InMemoryLocalMetadataService + * @since 2.7.2 + */ +public interface MetadataService { + + /** + * The value of all service names + */ + String ALL_SERVICE_NAMES = "*"; + + /** + * The value of All service instances + */ + String ALL_SERVICE_INTERFACES = "*"; + + /** + * The contract version of {@link MetadataService}, the future update must make sure compatible. + */ + String VERSION = "1.0.0"; + + /** + * Gets the current Dubbo Service name + * + * @return non-null + */ + String serviceName(); + + /** + * Gets the version of {@link MetadataService} that always equals {@link #VERSION} + * + * @return non-null + * @see #VERSION + */ + default String version() { + return VERSION; + } + + /** + * the list of String that presents all Dubbo subscribed {@link URL urls} + * + * @return non-null read-only {@link List} + */ + List getSubscribedURLs(); + + /** + * Get the list of String that presents all Dubbo exported {@link URL urls} + * + * @return non-null read-only {@link List} + */ + default List getExportedURLs() { + return getExportedURLs(ALL_SERVICE_INTERFACES); + } + + /** + * Get the list of String that presents the specified Dubbo exported {@link URL urls} by the serviceInterface + * + * @param serviceInterface The class name of Dubbo service interface + * @return non-null read-only {@link List} + * @see URL + */ + default List getExportedURLs(String serviceInterface) { + return getExportedURLs(serviceInterface, null); + } + + /** + * Get the list of String that presents the specified Dubbo exported {@link URL urls} by the + * serviceInterface and group + * + * @param serviceInterface The class name of Dubbo service interface + * @param group the Dubbo Service Group (optional) + * @return non-null read-only {@link List} + * @see URL + */ + default List getExportedURLs(String serviceInterface, String group) { + return getExportedURLs(serviceInterface, group, null); + } + + /** + * Get the list of String that presents the specified Dubbo exported {@link URL urls} by the + * serviceInterface, group and version + * + * @param serviceInterface The class name of Dubbo service interface + * @param group the Dubbo Service Group (optional) + * @param version the Dubbo Service Version (optional) + * @return non-null read-only {@link List} + * @see URL + */ + default List getExportedURLs(String serviceInterface, String group, String version) { + return getExportedURLs(serviceInterface, group, version, null); + } + + /** + * Get the list of String that presents the specified Dubbo exported {@link URL urls} by the + * serviceInterface, group, version and protocol + * + * @param serviceInterface The class name of Dubbo service interface + * @param group the Dubbo Service Group (optional) + * @param version the Dubbo Service Version (optional) + * @param protocol the Dubbo Service Protocol (optional) + * @return non-null read-only {@link List} + * @see URL + */ + List getExportedURLs(String serviceInterface, String group, String version, String protocol); + + + /** + * Convert the multiple {@link URL urls} to a {@link List list} of {@link URL urls} + * + * @param urls the strings presents the {@link URL Dubbo URLs} + * @return non-null + */ + static List toURLs(Iterable urls) { + return stream(urls.spliterator(), false) + .map(URL::valueOf) + .collect(Collectors.toList()); + } +} diff --git a/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/MetadataServiceExporter.java b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/MetadataServiceExporter.java new file mode 100644 index 000000000000..5f756a2b76c9 --- /dev/null +++ b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/MetadataServiceExporter.java @@ -0,0 +1,46 @@ +/* + * 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.SPI; + +import java.util.List; + +/** + * The exporter of {@link MetadataService} + * + * @see MetadataService + * @see #export() + * @see #unexport() + * @since 2.7.2 + */ +@SPI +public interface MetadataServiceExporter { + + /** + * Exports the {@link MetadataService} as a Dubbo service + * + * @return the exported {@link URL URLs} + */ + List export(); + + /** + * Unexports the {@link MetadataService} + */ + void unexport(); +} diff --git a/dubbo-metadata/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.LocalMetadataService b/dubbo-metadata/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.LocalMetadataService new file mode 100644 index 000000000000..2af06fde611e --- /dev/null +++ b/dubbo-metadata/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.LocalMetadataService @@ -0,0 +1 @@ +default=org.apache.dubbo.metadata.InMemoryLocalMetadataService \ No newline at end of file diff --git a/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/InMemoryLocalMetadataServiceTest.java b/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/InMemoryLocalMetadataServiceTest.java new file mode 100644 index 000000000000..835a0d56e3c2 --- /dev/null +++ b/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/InMemoryLocalMetadataServiceTest.java @@ -0,0 +1,140 @@ +/* + * 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.rpc.model.ApplicationModel; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static java.util.Arrays.asList; +import static org.apache.dubbo.common.URL.valueOf; +import static org.apache.dubbo.common.constants.CommonConstants.GROUP_KEY; +import static org.apache.dubbo.common.constants.CommonConstants.PROTOCOL_KEY; +import static org.apache.dubbo.common.constants.CommonConstants.VERSION_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link InMemoryLocalMetadataService} Test + * + * @since 2.7.2 + */ +public class InMemoryLocalMetadataServiceTest { + + private LocalMetadataService metadataService = new InMemoryLocalMetadataService(); + + private static final String TEST_SERVICE = "org.apache.dubbo.test.TestService"; + + private static final URL BASE_URL = valueOf("dubbo://127.0.0.1:20880/" + TEST_SERVICE); + private static final URL REST_BASE_URL = valueOf("rest://127.0.0.1:20880/" + TEST_SERVICE); + private static final URL BASE_URL_GROUP = BASE_URL.addParameter(GROUP_KEY, "test"); + private static final URL BASE_URL_GROUP_AND_VERSION = BASE_URL_GROUP.addParameter(VERSION_KEY, "1.0.0"); + private static final URL BASE_URL_GROUP_AND_VERSION_AND_PROTOCOL = BASE_URL_GROUP_AND_VERSION.addParameter(PROTOCOL_KEY, "rest"); + + @BeforeAll + public static void init() { + ApplicationModel.setApplication("test"); + } + + @Test + public void testServiceName() { + assertEquals("test", metadataService.serviceName()); + } + + @Test + public void testVersion() { + assertEquals("1.0.0", MetadataService.VERSION); + assertEquals("1.0.0", metadataService.version()); + } + + @Test + public void testGetExportedURLs() { + + assertTrue(metadataService.exportURL(BASE_URL)); + List exportedURLs = metadataService.getExportedURLs(TEST_SERVICE); + assertEquals(1, exportedURLs.size()); + assertEquals(asList(BASE_URL.toFullString()), exportedURLs); + assertTrue(metadataService.unexportURL(BASE_URL)); + + assertTrue(metadataService.exportURL(BASE_URL)); + assertFalse(metadataService.exportURL(BASE_URL)); + + assertTrue(metadataService.exportURL(BASE_URL_GROUP)); + assertTrue(metadataService.exportURL(BASE_URL_GROUP_AND_VERSION)); + + exportedURLs = metadataService.getExportedURLs(TEST_SERVICE); + assertEquals(asList(BASE_URL.toFullString()), exportedURLs); + assertEquals(asList( + BASE_URL.toFullString(), + BASE_URL_GROUP.toFullString(), + BASE_URL_GROUP_AND_VERSION.toFullString()), metadataService.getExportedURLs()); + + assertTrue(metadataService.exportURL(REST_BASE_URL)); + exportedURLs = metadataService.getExportedURLs(TEST_SERVICE); + assertEquals(asList(BASE_URL.toFullString(), REST_BASE_URL.toFullString()), exportedURLs); + + metadataService.exportURL(BASE_URL_GROUP_AND_VERSION_AND_PROTOCOL); + + exportedURLs = metadataService.getExportedURLs(TEST_SERVICE, "test", "1.0.0", "rest"); + + assertEquals(asList(BASE_URL_GROUP_AND_VERSION_AND_PROTOCOL.toFullString()), exportedURLs); + } + + @Test + public void testGetSubscribedURLs() { + assertTrue(metadataService.subscribeURL(BASE_URL)); + assertFalse(metadataService.subscribeURL(BASE_URL)); + + assertTrue(metadataService.subscribeURL(BASE_URL_GROUP)); + assertTrue(metadataService.subscribeURL(BASE_URL_GROUP_AND_VERSION)); + assertTrue(metadataService.subscribeURL(REST_BASE_URL)); + + List subscribedURLs = metadataService.getSubscribedURLs(); + assertEquals(4, subscribedURLs.size()); + assertEquals(asList( + BASE_URL.toFullString(), + REST_BASE_URL.toFullString(), + BASE_URL_GROUP.toFullString(), + BASE_URL_GROUP_AND_VERSION.toFullString()), subscribedURLs); + + assertTrue(metadataService.unsubscribeURL(REST_BASE_URL)); + subscribedURLs = metadataService.getSubscribedURLs(); + assertEquals(3, subscribedURLs.size()); + assertEquals(asList( + BASE_URL.toFullString(), + BASE_URL_GROUP.toFullString(), + BASE_URL_GROUP_AND_VERSION.toFullString()), subscribedURLs); + + assertTrue(metadataService.unsubscribeURL(BASE_URL_GROUP)); + subscribedURLs = metadataService.getSubscribedURLs(); + assertEquals(2, subscribedURLs.size()); + assertEquals(asList( + BASE_URL.toFullString(), + BASE_URL_GROUP_AND_VERSION.toFullString()), subscribedURLs); + + assertTrue(metadataService.unsubscribeURL(BASE_URL_GROUP_AND_VERSION)); + subscribedURLs = metadataService.getSubscribedURLs(); + assertEquals(1, subscribedURLs.size()); + assertEquals(asList( + BASE_URL.toFullString()), subscribedURLs); + } +} \ No newline at end of file diff --git a/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/LocalMetadataServiceTest.java b/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/LocalMetadataServiceTest.java new file mode 100644 index 000000000000..937ddce985f4 --- /dev/null +++ b/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/LocalMetadataServiceTest.java @@ -0,0 +1,34 @@ +/* + * 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.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link LocalMetadataService} Test + * + * @since 2.7.2 + */ +public class LocalMetadataServiceTest { + + @Test + public void testDefaultExtension() { + assertEquals(InMemoryLocalMetadataService.class, LocalMetadataService.getDefaultExtension().getClass()); + } +} diff --git a/dubbo-registry/dubbo-registry-api/pom.xml b/dubbo-registry/dubbo-registry-api/pom.xml index 5a707d0686bd..d5689b334e80 100644 --- a/dubbo-registry/dubbo-registry-api/pom.xml +++ b/dubbo-registry/dubbo-registry-api/pom.xml @@ -55,6 +55,12 @@ + + org.apache.dubbo + dubbo-event + ${project.parent.version} + + org.apache.curator curator-framework diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/CompositeServiceDiscovery.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/CompositeServiceDiscovery.java new file mode 100644 index 000000000000..d01891ddb58b --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/CompositeServiceDiscovery.java @@ -0,0 +1,118 @@ +/* + * 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.registry.client; + +import org.apache.dubbo.common.utils.DefaultPage; +import org.apache.dubbo.common.utils.Page; +import org.apache.dubbo.registry.client.event.ServiceDiscoveryChangeListener; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import static java.lang.String.format; +import static java.lang.String.valueOf; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableSet; +import static org.apache.dubbo.common.utils.CollectionUtils.isNotEmpty; +import static org.apache.dubbo.common.utils.CollectionUtils.sort; + +/** + * The serviceDiscoveries of {@link ServiceDiscovery} + * + * @since 2.7.2 + */ +public class CompositeServiceDiscovery implements ServiceDiscovery { + + private List serviceDiscoveries = new LinkedList<>(); + + public CompositeServiceDiscovery(ServiceDiscovery... serviceDiscoveries) { + this(asList(serviceDiscoveries)); + } + + public CompositeServiceDiscovery(Collection serviceDiscoveries) { + addServiceDiscoveries(serviceDiscoveries); + } + + protected void addServiceDiscoveries(Collection serviceDiscoveries) { + this.serviceDiscoveries.addAll(serviceDiscoveries); + sort(this.serviceDiscoveries); + } + + @Override + public Set getServices() { + Set allServiceNames = new TreeSet<>(); + for (ServiceDiscovery serviceDiscovery : serviceDiscoveries) { + Set serviceNames = serviceDiscovery.getServices(); + if (isNotEmpty(serviceNames)) { + allServiceNames.addAll(serviceNames); + } + } + return unmodifiableSet(allServiceNames); + } + + @Override + public List getInstances(String serviceName) throws NullPointerException { + List serviceInstances = new LinkedList<>(); + for (ServiceDiscovery serviceDiscovery : serviceDiscoveries) { + List instances = serviceDiscovery.getInstances(serviceName); + if (isNotEmpty(instances)) { + serviceInstances.addAll(instances); + } + } + return serviceInstances; + } + + @Override + public Page getInstances(String serviceName, int offset, int requestSize, boolean healthyOnly) + throws NullPointerException, IllegalArgumentException { + DefaultPage page = new DefaultPage<>(offset, requestSize); + + int totalElements = 0; + List serviceInstances = new LinkedList<>(); + + for (ServiceDiscovery serviceDiscovery : serviceDiscoveries) { + Page p = serviceDiscovery.getInstances(serviceName, offset, requestSize, healthyOnly); + totalElements += p.getTotalSize(); + if (p.hasData()) { + serviceInstances.addAll(p.getData()); + } + } + + page.setTotalSize(totalElements); + page.setData(serviceInstances); + return page; + } + + @Override + public String toString() { + return format("%s [composite : %s]", this.getClass().getSimpleName(), valueOf(serviceDiscoveries)); + } + + @Override + public int getPriority() { + return Integer.MIN_VALUE; + } + + @Override + public void addServiceDiscoveryChangeListener(String serviceName, + ServiceDiscoveryChangeListener listener) throws NullPointerException, IllegalArgumentException { + serviceDiscoveries.forEach(serviceDiscovery -> serviceDiscovery.addServiceDiscoveryChangeListener(serviceName, listener)); + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/DefaultServiceInstance.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/DefaultServiceInstance.java new file mode 100644 index 000000000000..dda1faa680c6 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/DefaultServiceInstance.java @@ -0,0 +1,133 @@ +/* + * 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.registry.client; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * The default implementation of {@link ServiceInstance}. + * + * @since 2.7.2 + */ +public class DefaultServiceInstance implements ServiceInstance { + + private final String id; + + private final String serviceName; + + private final String host; + + private final int port; + + private boolean enabled; + + private boolean healthy; + + private Map metadata = new HashMap<>(); + + public DefaultServiceInstance(String id, String serviceName, String host, int port) { + this.id = id; + this.serviceName = serviceName; + this.host = host; + this.port = port; + this.enabled = true; + this.healthy = true; + } + + public DefaultServiceInstance(String serviceName, String host, int port) { + this(null, serviceName, host, port); + } + + @Override + public String getId() { + return id; + } + + @Override + public String getServiceName() { + return serviceName; + } + + @Override + public String getHost() { + return host; + } + + @Override + public int getPort() { + return port; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean isHealthy() { + return healthy; + } + + public void setHealthy(boolean healthy) { + this.healthy = healthy; + } + + @Override + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DefaultServiceInstance)) return false; + DefaultServiceInstance that = (DefaultServiceInstance) o; + return getPort() == that.getPort() && + Objects.equals(getId(), that.getId()) && + Objects.equals(getServiceName(), that.getServiceName()) && + Objects.equals(getHost(), that.getHost()) && + Objects.equals(getMetadata(), that.getMetadata()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getServiceName(), getHost(), getPort(), getMetadata()); + } + + @Override + public String toString() { + return "DefaultServiceInstance{" + + "id='" + id + '\'' + + ", serviceName='" + serviceName + '\'' + + ", host='" + host + '\'' + + ", port=" + port + + ", enabled=" + enabled + + ", healthy=" + healthy + + ", metadata=" + metadata + + '}'; + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/EventPublishingServiceRegistry.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/EventPublishingServiceRegistry.java new file mode 100644 index 000000000000..f0459281dad8 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/EventPublishingServiceRegistry.java @@ -0,0 +1,128 @@ +/* + * 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.registry.client; + +import org.apache.dubbo.event.Event; +import org.apache.dubbo.event.EventDispatcher; +import org.apache.dubbo.registry.client.event.ServiceInstancePreRegisteredEvent; +import org.apache.dubbo.registry.client.event.ServiceInstanceRegisteredEvent; + +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +/** + * The abstract {@link ServiceRegistry} implementation publishes the {@link Event Dubbo event} on methods executing. + * + * @since 2.7.2 + */ +public abstract class EventPublishingServiceRegistry implements ServiceRegistry { + + private final EventDispatcher eventDispatcher = EventDispatcher.getDefaultExtension(); + + @Override + public final void register(ServiceInstance serviceInstance) throws RuntimeException { + executeWithEvents( + of(new ServiceInstancePreRegisteredEvent(this, serviceInstance)), + () -> doRegister(serviceInstance), + of(new ServiceInstanceRegisteredEvent(this, serviceInstance)) + ); + } + + @Override + public final void update(ServiceInstance serviceInstance) throws RuntimeException { + // TODO publish event + executeWithEvents( + empty(), + () -> doUpdate(serviceInstance), + empty() + ); + } + + @Override + public final void unregister(ServiceInstance serviceInstance) throws RuntimeException { + // TODO publish event + executeWithEvents( + empty(), + () -> doUnregister(serviceInstance), + empty() + ); + } + + @Override + public final void start() { + // TODO publish event + executeWithEvents( + empty(), + this::doStart, + empty() + ); + } + + @Override + public final void stop() { + // TODO publish event + executeWithEvents( + empty(), + this::doStop, + empty() + ); + } + + protected final void executeWithEvents(Optional beforeEvent, + Runnable action, + Optional afterEvent) { + beforeEvent.ifPresent(eventDispatcher::dispatch); + action.run(); + afterEvent.ifPresent(eventDispatcher::dispatch); + } + + /** + * Registers an instance of {@link ServiceInstance}. + * + * @param serviceInstance an instance of {@link ServiceInstance} to be registered + * @throws RuntimeException if failed + */ + protected abstract void doRegister(ServiceInstance serviceInstance) throws RuntimeException; + + /** + * Updates the registered {@link ServiceInstance}. + * + * @param serviceInstance the registered {@link ServiceInstance} + * @throws RuntimeException if failed + */ + protected abstract void doUpdate(ServiceInstance serviceInstance) throws RuntimeException; + + /** + * Unregisters an instance of {@link ServiceInstance}. + * + * @param serviceInstance an instance of {@link ServiceInstance} to be deregistered + * @throws RuntimeException if failed + */ + protected abstract void doUnregister(ServiceInstance serviceInstance) throws RuntimeException; + + /** + * Starts the ServiceRegistry. This is a lifecycle method. + */ + protected abstract void doStart(); + + /** + * Stops the ServiceRegistry. This is a lifecycle method. + */ + protected abstract void doStop(); +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceDiscovery.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceDiscovery.java new file mode 100644 index 000000000000..6a1888a4039f --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceDiscovery.java @@ -0,0 +1,195 @@ +/* + * 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.registry.client; + +import org.apache.dubbo.common.utils.DefaultPage; +import org.apache.dubbo.common.utils.Page; +import org.apache.dubbo.registry.client.event.ServiceDiscoveryChangeListener; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.lang.Integer.compare; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableMap; + +/** + * The common operations of Service Discovery + * + * @since 2.7.2 + */ +public interface ServiceDiscovery extends Comparable { + + /** + * A human-readable description of the implementation + * + * @return The description. + */ + String toString(); + + /** + * Gets all service names + * + * @return non-null read-only {@link Set} + */ + Set getServices(); + + /** + * Gets all {@link ServiceInstance service instances} by the specified service name. + * + * @param serviceName the service name + * @return non-null {@link List} + * @throws NullPointerException if serviceName is null is null + */ + List getInstances(String serviceName) throws NullPointerException; + + /** + * Gets the total size of {@link #getInstances(String)} instances} + * + * @param serviceName the service name + * @return + * @throws NullPointerException if serviceName is null is null + */ + default int getTotalSizeInstances(String serviceName) throws NullPointerException { + return getInstances(serviceName).size(); + } + + /** + * Gets the {@link Page pagination} of {@link ServiceInstance service instances} by the specified service name. + * It's equal to {@link #getInstances(String, int, int, boolean)} with healthyOnly == true + * + * @param serviceName the service name + * @param offset the offset of request , the number "0" indicates first page + * @param requestSize the number of request, the {@link Integer#MAX_VALUE max value} indicates the range is unlimited + * @return non-null {@link Page} object + * @throws NullPointerException if serviceName is null is null + * @throws IllegalArgumentException if offset or requestSize is negative number + */ + default Page getInstances(String serviceName, int offset, int requestSize) throws NullPointerException, + IllegalArgumentException { + return getInstances(serviceName, offset, requestSize, false); + } + + /** + * Get the {@link Page pagination} of {@link ServiceInstance service instances} by the specified service name. + * If healthyOnly == true, filter healthy instances only. + * + * @param serviceName the service name + * @param offset the offset of request , the number "0" indicates first page + * @param requestSize the number of request, the {@link Integer#MAX_VALUE max value} indicates the range is unlimited + * @param healthyOnly if true , filter healthy instances only + * @return non-null {@link Page} object + * @throws NullPointerException if serviceName is null is null + * @throws IllegalArgumentException if offset or requestSize is negative number + */ + default Page getInstances(String serviceName, int offset, int requestSize, boolean healthyOnly) throws + NullPointerException, IllegalArgumentException { + + List serviceInstances = getInstances(serviceName); + + DefaultPage page = new DefaultPage(offset, requestSize); + + int totalElements = getTotalSizeInstances(serviceName); + + boolean hasMore = totalElements > offset + requestSize; + + int fromIndex = offset < totalElements ? offset : -1; + + int endIndex = hasMore ? offset + requestSize : totalElements; + + List data = fromIndex < 0 ? emptyList() : + new ArrayList<>(serviceInstances.subList(fromIndex, endIndex)); + + Iterator iterator = data.iterator(); + + while (iterator.hasNext()) { + ServiceInstance serviceInstance = iterator.next(); + if (!serviceInstance.isEnabled()) { // remove disabled instance + iterator.remove(); + continue; + } + + if (healthyOnly) { + if (!serviceInstance.isHealthy()) { // remove unhealthy instance + iterator.remove(); + continue; + } + } + } + + page.setData(data); + page.setTotalSize(totalElements); + + return page; + } + + /** + * batch-get all {@link ServiceInstance service instances} by the specified service names + * + * @param serviceNames the multiple service names + * @param offset the offset of request , the number "0" indicates first page + * @param requestSize the number of request, the {@link Integer#MAX_VALUE max value} indicates the range is unlimited + * @return non-null read-only {@link Map} whose key is the service name and value is + * the {@link Page pagination} of {@link ServiceInstance service instances} + * @throws NullPointerException if serviceName is null is null + * @throws IllegalArgumentException if offset or requestSize is negative number + */ + default Map> getInstances(Iterable serviceNames, int offset, int requestSize) throws + NullPointerException, IllegalArgumentException { + Map> instances = new LinkedHashMap<>(); + for (String serviceName : serviceNames) { + instances.put(serviceName, getInstances(serviceName, offset, requestSize)); + } + return unmodifiableMap(instances); + } + + /** + * The priority of current {@link ServiceDiscovery} + * + * @return The {@link Integer#MIN_VALUE minimum integer} indicates the highest priority, in contrast, + * the lowest priority is {@link Integer#MAX_VALUE the maximum integer} + */ + default int getPriority() { + return Integer.MAX_VALUE; + } + + /** + * Compares its priority + * + * @param that {@link ServiceDiscovery} + * @return + */ + @Override + default int compareTo(ServiceDiscovery that) { + return compare(this.getPriority(), that.getPriority()); + } + + /** + * Add an instance of {@link ServiceDiscoveryChangeListener} for specified service + * + * @param serviceName the service name + * @param listener an instance of {@link ServiceDiscoveryChangeListener} + * @throws NullPointerException + * @throws IllegalArgumentException + */ + void addServiceDiscoveryChangeListener(String serviceName, ServiceDiscoveryChangeListener listener) + throws NullPointerException, IllegalArgumentException; +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceInstance.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceInstance.java new file mode 100644 index 000000000000..9006681cfe20 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceInstance.java @@ -0,0 +1,94 @@ +/* + * 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.registry.client; + +import java.util.Map; + +/** + * The model class of an instance of a service, which is used for service registration and discovery. + *

+ * + * @since 2.7.2 + */ +public interface ServiceInstance { + + /** + * The id of the registered service instance. + * + * @return nullable + */ + String getId(); + + /** + * The name of service that current instance belongs to. + * + * @return non-null + */ + String getServiceName(); + + /** + * The hostname of the registered service instance. + * + * @return non-null + */ + String getHost(); + + /** + * The port of the registered service instance. + * + * @return the positive integer + */ + int getPort(); + + /** + * The enable status of the registered service instance. + * + * @return if true, indicates current instance is enabled, or disable, the client should remove this one. + * The default value is true + */ + default boolean isEnabled() { + return true; + } + + /** + * The registered service instance is health or not. + * + * @return if true, indicates current instance is enabled, or disable, the client may ignore this one. + * The default value is true + */ + default boolean isHealthy() { + return true; + } + + /** + * The key / value pair metadata associated with the service instance. + * + * @return non-null, mutable and unsorted {@link Map} + */ + Map getMetadata(); + + /** + * @return the hash code of current instance. + */ + int hashCode(); + + /** + * @param another another {@link ServiceInstance} + * @return if equals , return true, or false + */ + boolean equals(Object another); +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceRegistry.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceRegistry.java new file mode 100644 index 000000000000..07b1bfceba68 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceRegistry.java @@ -0,0 +1,66 @@ +/* + * 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.registry.client; + +/** + * The common interface to register and unregister for a service registry + * + * @since 2.7.2 + */ +public interface ServiceRegistry { + + /** + * A human-readable description of the implementation + * + * @return The description. + */ + String toString(); + + /** + * Registers an instance of {@link ServiceInstance}. + * + * @param serviceInstance an instance of {@link ServiceInstance} to be registered + * @throws RuntimeException if failed + */ + void register(ServiceInstance serviceInstance) throws RuntimeException; + + /** + * Updates the registered {@link ServiceInstance}. + * + * @param serviceInstance the registered {@link ServiceInstance} + * @throws RuntimeException if failed + */ + void update(ServiceInstance serviceInstance) throws RuntimeException; + + /** + * Unregisters an instance of {@link ServiceInstance}. + * + * @param serviceInstance an instance of {@link ServiceInstance} to be deregistered + * @throws RuntimeException if failed + */ + void unregister(ServiceInstance serviceInstance) throws RuntimeException; + + /** + * Starts the ServiceRegistry. This is a lifecycle method. + */ + void start(); + + /** + * Stops the ServiceRegistry. This is a lifecycle method. + */ + void stop(); +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceDiscoveryChangeEvent.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceDiscoveryChangeEvent.java new file mode 100644 index 000000000000..1bbf768081c1 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceDiscoveryChangeEvent.java @@ -0,0 +1,73 @@ +/* + * 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.registry.client.event; + +import org.apache.dubbo.event.Event; +import org.apache.dubbo.registry.client.ServiceInstance; + +import java.util.Collection; +import java.util.EventObject; + +import static java.util.Collections.unmodifiableCollection; + +/** + * The Service Discovery Change {@link EventObject Event} + * + * @see ServiceDiscoveryChangeListener + * @since 2.7.2 + */ +public class ServiceDiscoveryChangeEvent extends Event { + + private final String serviceName; + + private final Collection serviceInstances; + + private final long timestamp; + + /** + * @param serviceName The name of service that was changed + * @param serviceInstances all {@link ServiceInstance service instances} + * @throws IllegalArgumentException if source is null. + */ + public ServiceDiscoveryChangeEvent(String serviceName, Collection serviceInstances) { + super(serviceName); + this.serviceName = serviceName; + this.serviceInstances = unmodifiableCollection(serviceInstances); + this.timestamp = System.currentTimeMillis(); + } + + /** + * @return The name of service that was changed + */ + public String getServiceName() { + return serviceName; + } + + /** + * @return all {@link ServiceInstance service instances} + */ + public Collection getServiceInstances() { + return serviceInstances; + } + + /** + * Return the system time in milliseconds when the event happened. + */ + public long getTimestamp() { + return this.timestamp; + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceDiscoveryChangeListener.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceDiscoveryChangeListener.java new file mode 100644 index 000000000000..654f069ff023 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceDiscoveryChangeListener.java @@ -0,0 +1,35 @@ +/* + * 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.registry.client.event; + +import org.apache.dubbo.event.EventListener; + +/** + * The Service Discovery Change {@link EventListener Event Listener} + * + * @see ServiceDiscoveryChangeEvent + * @since 2.7.2 + */ +public interface ServiceDiscoveryChangeListener extends EventListener { + + /** + * On {@link ServiceDiscoveryChangeEvent the service change event} + * + * @param event {@link ServiceDiscoveryChangeEvent} + */ + void onEvent(ServiceDiscoveryChangeEvent event); +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstanceEvent.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstanceEvent.java new file mode 100644 index 000000000000..eec9d06ac790 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstanceEvent.java @@ -0,0 +1,47 @@ +/* + * 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.registry.client.event; + +import org.apache.dubbo.event.Event; +import org.apache.dubbo.registry.client.ServiceInstance; + +/** + * The {@link Event Dubbo event} for {@link ServiceInstance an service instance} + * + * @since 2.7.2 + */ +public abstract class ServiceInstanceEvent extends Event { + + private final ServiceInstance serviceInstance; + + /** + * @param serviceInstance {@link ServiceInstance an service instance} + */ + public ServiceInstanceEvent(Object source, ServiceInstance serviceInstance) { + super(source); + this.serviceInstance = serviceInstance; + } + + /** + * Get current {@link ServiceInstance service instance} + * + * @return current {@link ServiceInstance service instance} + */ + public ServiceInstance getServiceInstance() { + return serviceInstance; + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstancePreRegisteredEvent.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstancePreRegisteredEvent.java new file mode 100644 index 000000000000..ef1151ad8fae --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstancePreRegisteredEvent.java @@ -0,0 +1,33 @@ +/* + * 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.registry.client.event; + +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.ServiceRegistry; + + +/** + * The before-{@link ServiceRegistry#register(ServiceInstance) register} event for {@link ServiceInstance} + * + * @since 2.7.2 + */ +public class ServiceInstancePreRegisteredEvent extends ServiceInstanceEvent { + + public ServiceInstancePreRegisteredEvent(Object source, ServiceInstance serviceInstance) { + super(source, serviceInstance); + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstanceRegisteredEvent.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstanceRegisteredEvent.java new file mode 100644 index 000000000000..f359d41376a4 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/event/ServiceInstanceRegisteredEvent.java @@ -0,0 +1,33 @@ +/* + * 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.registry.client.event; + +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.ServiceRegistry; + + +/** + * The after-{@link ServiceRegistry#register(ServiceInstance) register} event for {@link ServiceInstance} + * + * @since 2.7.2 + */ +public class ServiceInstanceRegisteredEvent extends ServiceInstanceEvent { + + public ServiceInstanceRegisteredEvent(Object source, ServiceInstance serviceInstance) { + super(source, serviceInstance); + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/package-info.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/package-info.java new file mode 100644 index 000000000000..3283c3c0c729 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ +/** + * * The inspiration of service registration and discovery comes from + * Spring Cloud Commons. + * + * @since 2.7.2 + */ +package org.apache.dubbo.registry.client; \ No newline at end of file diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/CompositeServiceDiscoveryTest.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/CompositeServiceDiscoveryTest.java new file mode 100644 index 000000000000..1ed3f203b776 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/CompositeServiceDiscoveryTest.java @@ -0,0 +1,221 @@ +/* + * 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.registry.client; + +import org.apache.dubbo.common.utils.Page; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import static java.lang.Integer.MIN_VALUE; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link CompositeServiceDiscovery} Test + * + * @since 2.7.2 + */ +public class CompositeServiceDiscoveryTest { + + private InMemoryServiceDiscovery instance = new InMemoryServiceDiscovery(); + + private CompositeServiceDiscovery serviceDiscovery; + + @BeforeEach + public void init() { + serviceDiscovery = new CompositeServiceDiscovery(instance); + } + + @Test + public void testToString() { + assertEquals("CompositeServiceDiscovery [composite : [InMemoryServiceDiscovery]]", serviceDiscovery.toString()); + } + + @Test + public void testGetPriority() { + assertEquals(MIN_VALUE, serviceDiscovery.getPriority()); + } + + @Test + public void testGetServices() { + instance.addServiceInstance(new DefaultServiceInstance("A", "127.0.0.1", 8080)); + instance.addServiceInstance(new DefaultServiceInstance("B", "127.0.0.1", 8080)); + instance.addServiceInstance(new DefaultServiceInstance("C", "127.0.0.1", 8080)); + assertEquals(new HashSet<>(asList("A", "B", "C")), instance.getServices()); + } + + @Test + public void testGetInstances() { + + List instances = asList( + new DefaultServiceInstance("A", "127.0.0.1", 8080), + new DefaultServiceInstance("A", "127.0.0.1", 8081), + new DefaultServiceInstance("A", "127.0.0.1", 8082) + ); + + instances.forEach(instance::addServiceInstance); + + // Duplicated + instance.addServiceInstance(new DefaultServiceInstance("A", "127.0.0.1", 8080)); + // Duplicated + instance.addServiceInstance(new DefaultServiceInstance("A", "127.0.0.1", 8081)); + + // offset starts 0 + int offset = 0; + // requestSize > total elements + int requestSize = 5; + + Page page = serviceDiscovery.getInstances("A", offset, requestSize); + assertEquals(0, page.getRequestOffset()); + assertEquals(5, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(3, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + // requestSize < total elements + requestSize = 2; + + page = serviceDiscovery.getInstances("A", offset, requestSize); + assertEquals(0, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(2, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + offset = 1; + page = serviceDiscovery.getInstances("A", offset, requestSize); + assertEquals(1, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(2, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + offset = 2; + page = serviceDiscovery.getInstances("A", offset, requestSize); + assertEquals(2, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(1, page.getData().size()); + assertTrue(page.hasData()); + + offset = 3; + page = serviceDiscovery.getInstances("A", offset, requestSize); + assertEquals(3, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + + offset = 5; + page = serviceDiscovery.getInstances("A", offset, requestSize); + assertEquals(5, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + } + + @Test + public void testGetInstancesWithHealthy() { + + List instances = new LinkedList<>(asList( + new DefaultServiceInstance("A", "127.0.0.1", 8080), + new DefaultServiceInstance("A", "127.0.0.1", 8081) + )); + + + DefaultServiceInstance serviceInstance = new DefaultServiceInstance("A", "127.0.0.1", 8082); + serviceInstance.setHealthy(false); + instances.add(serviceInstance); + + instances.forEach(instance::addServiceInstance); + + // offset starts 0 + int offset = 0; + // requestSize > total elements + int requestSize = 5; + + Page page = serviceDiscovery.getInstances("A", offset, requestSize, true); + assertEquals(0, page.getRequestOffset()); + assertEquals(5, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(2, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + // requestSize < total elements + requestSize = 2; + + offset = 1; + page = serviceDiscovery.getInstances("A", offset, requestSize, true); + assertEquals(1, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(1, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + offset = 2; + page = serviceDiscovery.getInstances("A", offset, requestSize, true); + assertEquals(2, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + + offset = 3; + page = serviceDiscovery.getInstances("A", offset, requestSize, true); + assertEquals(3, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + + offset = 5; + page = serviceDiscovery.getInstances("A", offset, requestSize, true); + assertEquals(5, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + } +} \ No newline at end of file diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/DefaultServiceInstanceTest.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/DefaultServiceInstanceTest.java new file mode 100644 index 000000000000..fb31571aed24 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/DefaultServiceInstanceTest.java @@ -0,0 +1,60 @@ +/* + * 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.registry.client; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link DefaultServiceInstance} Test + * + * @since 2.7.2 + */ +public class DefaultServiceInstanceTest { + + public static DefaultServiceInstance INSTANCE = + new DefaultServiceInstance("A", "127.0.0.1", 8080); + + @BeforeEach + public void init() { + INSTANCE = new DefaultServiceInstance("A", "127.0.0.1", 8080); + } + + @Test + public void testDefaultValues() { + assertTrue(INSTANCE.isEnabled()); + assertTrue(INSTANCE.isHealthy()); + assertTrue(INSTANCE.getMetadata().isEmpty()); + } + + @Test + public void testSetAndGetValues() { + INSTANCE.setEnabled(false); + INSTANCE.setHealthy(false); + + assertEquals("A", INSTANCE.getServiceName()); + assertEquals("127.0.0.1", INSTANCE.getHost()); + assertEquals(8080, INSTANCE.getPort()); + assertFalse(INSTANCE.isEnabled()); + assertFalse(INSTANCE.isHealthy()); + assertTrue(INSTANCE.getMetadata().isEmpty()); + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/EventPublishingServiceRegistryTest.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/EventPublishingServiceRegistryTest.java new file mode 100644 index 000000000000..4a3f61f0b53e --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/EventPublishingServiceRegistryTest.java @@ -0,0 +1,77 @@ +/* + * 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.registry.client; + +import org.junit.jupiter.api.Test; + +import static org.apache.dubbo.registry.client.DefaultServiceInstanceTest.INSTANCE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link EventPublishingServiceRegistry} Test + * + * @since 2.7.2 + */ +public class EventPublishingServiceRegistryTest { + + private static ServiceRegistry serviceRegistry = new DefaultServiceRegistry(); + + private ServiceInstance serviceInstance = INSTANCE; + + @Test + public void testRegister() { + serviceRegistry.register(serviceInstance); + } + + @Test + public void testUpdate() { + serviceRegistry.update(serviceInstance); + } + + @Test + public void testUnregister() { + serviceRegistry.unregister(serviceInstance); + } +} + +class DefaultServiceRegistry extends EventPublishingServiceRegistry { + + @Override + protected void doRegister(ServiceInstance serviceInstance) throws RuntimeException { + assertEquals(INSTANCE, serviceInstance); + } + + @Override + protected void doUpdate(ServiceInstance serviceInstance) throws RuntimeException { + assertEquals(INSTANCE, serviceInstance); + } + + @Override + protected void doUnregister(ServiceInstance serviceInstance) throws RuntimeException { + assertEquals(INSTANCE, serviceInstance); + } + + @Override + protected void doStart() { + + } + + @Override + protected void doStop() { + + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/InMemoryServiceDiscovery.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/InMemoryServiceDiscovery.java new file mode 100644 index 000000000000..4311d8259fdc --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/InMemoryServiceDiscovery.java @@ -0,0 +1,69 @@ +/* + * 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.registry.client; + +import org.apache.dubbo.event.EventDispatcher; +import org.apache.dubbo.registry.client.event.ServiceDiscoveryChangeListener; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.apache.dubbo.event.EventDispatcher.getDefaultExtension; + +/** + * In-Memory {@link ServiceDiscovery} implementation + * + * @since 2.7.2 + */ +public class InMemoryServiceDiscovery implements ServiceDiscovery { + + private final EventDispatcher dispatcher = getDefaultExtension(); + + private Map> repository = new HashMap<>(); + + @Override + public Set getServices() { + return repository.keySet(); + } + + @Override + public List getInstances(String serviceName) throws NullPointerException { + return repository.computeIfAbsent(serviceName, s -> new LinkedList<>()); + } + + public InMemoryServiceDiscovery addServiceInstance(ServiceInstance serviceInstance) { + String serviceName = serviceInstance.getServiceName(); + List serviceInstances = repository.computeIfAbsent(serviceName, s -> new LinkedList<>()); + if (!serviceInstances.contains(serviceInstance)) { + serviceInstances.add(serviceInstance); + } + return this; + } + + public String toString() { + return "InMemoryServiceDiscovery"; + } + + @Override + public void addServiceDiscoveryChangeListener(String serviceName, ServiceDiscoveryChangeListener listener) throws NullPointerException, IllegalArgumentException { + dispatcher.addEventListener(listener); + } + +} diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/ServiceDiscoveryTest.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/ServiceDiscoveryTest.java new file mode 100644 index 000000000000..30bdd9c9bb39 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/ServiceDiscoveryTest.java @@ -0,0 +1,253 @@ +/* + * 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.registry.client; + +import org.apache.dubbo.common.utils.Page; +import org.apache.dubbo.event.EventDispatcher; +import org.apache.dubbo.event.EventListener; +import org.apache.dubbo.registry.client.event.ServiceInstanceEvent; +import org.apache.dubbo.registry.client.event.ServiceInstancePreRegisteredEvent; +import org.apache.dubbo.registry.client.event.ServiceInstanceRegisteredEvent; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import static java.lang.Integer.MAX_VALUE; +import static java.util.Arrays.asList; +import static org.apache.dubbo.registry.client.DefaultServiceInstanceTest.INSTANCE; +import static org.apache.dubbo.registry.client.ServiceDiscoveryTest.handleEvent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link ServiceDiscovery} Test case + * + * @since 2.7.2 + */ +public class ServiceDiscoveryTest { + + private static InMemoryServiceDiscovery instance; + + private EventDispatcher dispatcher = EventDispatcher.getDefaultExtension(); + + @BeforeEach + public void init() { + instance = new InMemoryServiceDiscovery(); + dispatcher.addEventListener(new BeforeEventListener()); + dispatcher.addEventListener(new AfterEventListener()); + dispatcher.removeAllEventListeners(); + } + + @Test + public void testToString() { + assertEquals("InMemoryServiceDiscovery", instance.toString()); + } + + @Test + public void testGetPriority() { + assertEquals(MAX_VALUE, instance.getPriority()); + } + + @Test + public void testGetServices() { + instance.addServiceInstance(new DefaultServiceInstance("A", "127.0.0.1", 8080)); + instance.addServiceInstance(new DefaultServiceInstance("B", "127.0.0.1", 8080)); + instance.addServiceInstance(new DefaultServiceInstance("C", "127.0.0.1", 8080)); + assertEquals(new HashSet<>(asList("A", "B", "C")), instance.getServices()); + } + + @Test + public void testGetInstances() { + + List instances = asList( + new DefaultServiceInstance("A", "127.0.0.1", 8080), + new DefaultServiceInstance("A", "127.0.0.1", 8081), + new DefaultServiceInstance("A", "127.0.0.1", 8082) + ); + + instances.forEach(instance::addServiceInstance); + + // Duplicated + instance.addServiceInstance(new DefaultServiceInstance("A", "127.0.0.1", 8080)); + // Duplicated + instance.addServiceInstance(new DefaultServiceInstance("A", "127.0.0.1", 8081)); + + // offset starts 0 + int offset = 0; + // requestSize > total elements + int requestSize = 5; + + Page page = instance.getInstances("A", offset, requestSize); + assertEquals(0, page.getRequestOffset()); + assertEquals(5, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(3, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + // requestSize < total elements + requestSize = 2; + + page = instance.getInstances("A", offset, requestSize); + assertEquals(0, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(2, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + offset = 1; + page = instance.getInstances("A", offset, requestSize); + assertEquals(1, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(2, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + offset = 2; + page = instance.getInstances("A", offset, requestSize); + assertEquals(2, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(1, page.getData().size()); + assertTrue(page.hasData()); + + offset = 3; + page = instance.getInstances("A", offset, requestSize); + assertEquals(3, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + + offset = 5; + page = instance.getInstances("A", offset, requestSize); + assertEquals(5, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + } + + @Test + public void testGetInstancesWithHealthy() { + + List instances = new LinkedList<>(asList( + new DefaultServiceInstance("A", "127.0.0.1", 8080), + new DefaultServiceInstance("A", "127.0.0.1", 8081) + )); + + + DefaultServiceInstance serviceInstance = new DefaultServiceInstance("A", "127.0.0.1", 8082); + serviceInstance.setHealthy(false); + instances.add(serviceInstance); + + instances.forEach(instance::addServiceInstance); + + // offset starts 0 + int offset = 0; + // requestSize > total elements + int requestSize = 5; + + Page page = instance.getInstances("A", offset, requestSize, true); + assertEquals(0, page.getRequestOffset()); + assertEquals(5, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(2, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + // requestSize < total elements + requestSize = 2; + + offset = 1; + page = instance.getInstances("A", offset, requestSize, true); + assertEquals(1, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(1, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + offset = 2; + page = instance.getInstances("A", offset, requestSize, true); + assertEquals(2, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + + offset = 3; + page = instance.getInstances("A", offset, requestSize, true); + assertEquals(3, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + + offset = 5; + page = instance.getInstances("A", offset, requestSize, true); + assertEquals(5, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + } + + + static void handleEvent(ServiceInstanceEvent event) { + assertEquals(INSTANCE, event.getServiceInstance()); + assertEquals(instance, event.getSource()); + } +} + +class BeforeEventListener implements EventListener { + + @Override + public void onEvent(ServiceInstancePreRegisteredEvent event) { + handleEvent(event); + } +} + +class AfterEventListener implements EventListener { + + @Override + public void onEvent(ServiceInstanceRegisteredEvent event) { + handleEvent(event); + } +} diff --git a/dubbo-registry/dubbo-registry-zookeeper/pom.xml b/dubbo-registry/dubbo-registry-zookeeper/pom.xml index 3aa13d3fcfda..cf3a1a90e1da 100644 --- a/dubbo-registry/dubbo-registry-zookeeper/pom.xml +++ b/dubbo-registry/dubbo-registry-zookeeper/pom.xml @@ -39,6 +39,10 @@ dubbo-remoting-zookeeper ${project.parent.version} + + org.apache.curator + curator-x-discovery + org.apache.curator curator-test diff --git a/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperInstance.java b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperInstance.java new file mode 100644 index 000000000000..71cca53a3940 --- /dev/null +++ b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperInstance.java @@ -0,0 +1,77 @@ +/* + * 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.registry.zookeeper; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents the default payload of a registered service in Zookeeper. + *

+ * It's compatible with Spring Cloud + * + * @since 2.7.2 + */ +public class ZookeeperInstance { + + private String id; + + private String name; + + private Map metadata = new HashMap<>(); + + @SuppressWarnings("unused") + private ZookeeperInstance() { + } + + public ZookeeperInstance(String id, String name, Map metadata) { + this.id = id; + this.name = name; + this.metadata = metadata; + } + + public String getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public Map getMetadata() { + return this.metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + @Override + public String toString() { + return "ZookeeperInstance{" + "id='" + this.id + '\'' + ", name='" + this.name + + '\'' + ", metadata=" + this.metadata + '}'; + } + +} diff --git a/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscovery.java b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscovery.java new file mode 100644 index 000000000000..d9d6df0c35f9 --- /dev/null +++ b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscovery.java @@ -0,0 +1,160 @@ +/* + * 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.registry.zookeeper; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.function.ThrowableConsumer; +import org.apache.dubbo.common.function.ThrowableFunction; +import org.apache.dubbo.common.logger.Logger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.event.EventDispatcher; +import org.apache.dubbo.registry.client.EventPublishingServiceRegistry; +import org.apache.dubbo.registry.client.ServiceDiscovery; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.event.ServiceDiscoveryChangeListener; + +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.api.CuratorWatcher; +import org.apache.zookeeper.KeeperException; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.apache.dubbo.common.function.ThrowableFunction.execute; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkParams.ROOT_PATH; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkUtils.build; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkUtils.buildCuratorFramework; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkUtils.buildServiceDiscovery; + +/** + * Zookeeper {@link ServiceDiscovery} implementation based on + * Apache Curator X Discovery + */ +public class ZookeeperServiceDiscovery extends EventPublishingServiceRegistry implements ServiceDiscovery { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final CuratorFramework curatorFramework; + + private final String rootPath; + + private final org.apache.curator.x.discovery.ServiceDiscovery serviceDiscovery; + + private final EventDispatcher dispatcher; + + /** + * The Key is watched Zookeeper path, the value is an instance of {@link CuratorWatcher} + */ + private final Map watcherCaches = new ConcurrentHashMap<>(); + + public ZookeeperServiceDiscovery(URL registerURL) throws Exception { + this.curatorFramework = buildCuratorFramework(registerURL); + this.rootPath = ROOT_PATH.getParameterValue(registerURL); + this.serviceDiscovery = buildServiceDiscovery(curatorFramework, rootPath); + this.dispatcher = EventDispatcher.getDefaultExtension(); + } + + @Override + protected void doRegister(ServiceInstance serviceInstance) throws RuntimeException { + doInServiceRegistry(serviceDiscovery -> { + serviceDiscovery.registerService(build(serviceInstance)); + }); + } + + @Override + protected void doUpdate(ServiceInstance serviceInstance) throws RuntimeException { + doInServiceRegistry(serviceDiscovery -> { + serviceDiscovery.updateService(build(serviceInstance)); + }); + } + + @Override + protected void doUnregister(ServiceInstance serviceInstance) throws RuntimeException { + doInServiceRegistry(serviceDiscovery -> { + serviceDiscovery.unregisterService(build(serviceInstance)); + }); + } + + @Override + protected void doStart() { + doInServiceRegistry(serviceDiscovery -> { + serviceDiscovery.start(); + }); + } + + @Override + protected void doStop() { + doInServiceRegistry(serviceDiscovery -> { + serviceDiscovery.close(); + }); + } + + @Override + public Set getServices() { + return doInServiceDiscovery(s -> new LinkedHashSet<>(s.queryForNames())); + } + + @Override + public List getInstances(String serviceName) throws NullPointerException { + return doInServiceDiscovery(s -> build(s.queryForInstances(serviceName))); + } + + @Override + public void addServiceDiscoveryChangeListener(String serviceName, ServiceDiscoveryChangeListener listener) + throws NullPointerException, IllegalArgumentException { + addServiceWatcherIfAbsent(serviceName); + dispatcher.addEventListener(listener); + } + + private void doInServiceRegistry(ThrowableConsumer consumer) { + ThrowableConsumer.execute(serviceDiscovery, s -> { + consumer.accept(s); + }); + } + + private R doInServiceDiscovery(ThrowableFunction function) { + return execute(serviceDiscovery, function); + } + + private void addWatcherIfAbsent(String path, CuratorWatcher watcher) { + if (!watcherCaches.containsKey(path)) { + try { + curatorFramework.getChildren().usingWatcher(watcher).forPath(path); + watcherCaches.put(path, watcher); + } catch (KeeperException.NoNodeException e) { + // ignored + if (logger.isErrorEnabled()) { + logger.error(e.getMessage()); + } + } catch (Exception e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + } + + private void addServiceWatcherIfAbsent(String serviceName) { + addWatcherIfAbsent(buildServicePath(serviceName), + new ZookeeperServiceDiscoveryChangeWatcher(this, serviceName, dispatcher)); + } + + private String buildServicePath(String serviceName) { + return rootPath + "/" + serviceName; + } +} \ No newline at end of file diff --git a/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscoveryChangeWatcher.java b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscoveryChangeWatcher.java new file mode 100644 index 000000000000..8e055b65b6d9 --- /dev/null +++ b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscoveryChangeWatcher.java @@ -0,0 +1,68 @@ +/* + * 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.registry.zookeeper; + +import org.apache.dubbo.event.EventDispatcher; +import org.apache.dubbo.registry.client.ServiceDiscovery; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.event.ServiceDiscoveryChangeEvent; + +import org.apache.curator.framework.api.CuratorWatcher; +import org.apache.zookeeper.WatchedEvent; +import org.apache.zookeeper.Watcher; + +import java.util.Collection; + +import static org.apache.zookeeper.Watcher.Event.EventType.NodeChildrenChanged; +import static org.apache.zookeeper.Watcher.Event.EventType.NodeDataChanged; + +/** + * Zookeeper {@link ServiceDiscovery} Change {@link CuratorWatcher watcher} only interests in + * {@link Watcher.Event.EventType#NodeChildrenChanged} and {@link Watcher.Event.EventType#NodeDataChanged} event types, + * which will multicast a {@link ServiceDiscoveryChangeEvent} when the service node has been changed. + * + * @since 2.7.2 + */ +public class ZookeeperServiceDiscoveryChangeWatcher implements CuratorWatcher { + + private final ZookeeperServiceDiscovery zookeeperServiceDiscovery; + + private final String serviceName; + + private final EventDispatcher dispatcher; + + public ZookeeperServiceDiscoveryChangeWatcher(ZookeeperServiceDiscovery zookeeperServiceDiscovery, + String serviceName, EventDispatcher dispatcher) { + this.zookeeperServiceDiscovery = zookeeperServiceDiscovery; + this.serviceName = serviceName; + this.dispatcher = dispatcher; + } + + @Override + public void process(WatchedEvent event) throws Exception { + + Watcher.Event.EventType eventType = event.getType(); + + if (NodeChildrenChanged.equals(eventType) || NodeDataChanged.equals(eventType)) { + dispatcher.dispatch(new ServiceDiscoveryChangeEvent(serviceName, getServiceInstances(serviceName))); + } + } + + private Collection getServiceInstances(String serviceName) { + return zookeeperServiceDiscovery.getInstances(serviceName); + } +} diff --git a/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/util/CuratorFrameworkParams.java b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/util/CuratorFrameworkParams.java new file mode 100644 index 000000000000..7e9db4c54e61 --- /dev/null +++ b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/util/CuratorFrameworkParams.java @@ -0,0 +1,102 @@ +/* + * 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.registry.zookeeper.util; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.client.ServiceInstance; + +import org.apache.curator.framework.CuratorFramework; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * The enumeration for the parameters of {@link CuratorFramework} + * + * @see CuratorFramework + * @since 2.7.2 + */ +public enum CuratorFrameworkParams { + + /** + * The root path of Dubbo Service + */ + ROOT_PATH("rootPath", "/services", value -> value), + + /** + * The host of current {@link ServiceInstance service instance} that will be registered + */ + INSTANCE_HOST("instanceHost", null, value -> value), + + /** + * The port of current {@link ServiceInstance service instance} that will be registered + */ + INSTANCE_PORT("instancePort", null, value -> value), + + /** + * Initial amount of time to wait between retries + */ + BASE_SLEEP_TIME("baseSleepTimeMs", 50, Integer::valueOf), + + /** + * Max number of times to retry. + */ + MAX_RETRIES("maxRetries", 10, Integer::valueOf), + + /** + * Max time in ms to sleep on each retry. + */ + MAX_SLEEP("maxSleepMs", 500, Integer::valueOf), + + /** + * Wait time to block on connection to Zookeeper. + */ + BLOCK_UNTIL_CONNECTED_WAIT("blockUntilConnectedWait", 10, Integer::valueOf), + + /** + * The unit of time related to blocking on connection to Zookeeper. + */ + BLOCK_UNTIL_CONNECTED_UNIT("blockUntilConnectedUnit", TimeUnit.SECONDS, TimeUnit::valueOf), + + ; + + private final String name; + + private final Object defaultValue; + + private final Function converter; + + CuratorFrameworkParams(String name, T defaultValue, Function converter) { + this.name = name; + this.defaultValue = defaultValue; + this.converter = (Function) converter; + } + + /** + * Get the parameter value from the specified {@link URL} + * + * @param url the Dubbo registry {@link URL} + * @param the type of value + * @return the parameter value if present, or return null + */ + public T getParameterValue(URL url) { + String param = url.getParameter(name); + Object value = param != null ? converter.apply(param) : defaultValue; + return (T) value; + } +} + diff --git a/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/util/CuratorFrameworkUtils.java b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/util/CuratorFrameworkUtils.java new file mode 100644 index 000000000000..bc466b8dcdd9 --- /dev/null +++ b/dubbo-registry/dubbo-registry-zookeeper/src/main/java/org/apache/dubbo/registry/zookeeper/util/CuratorFrameworkUtils.java @@ -0,0 +1,122 @@ +/* + * 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.registry.zookeeper.util; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.client.DefaultServiceInstance; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.zookeeper.ZookeeperInstance; +import org.apache.dubbo.registry.zookeeper.ZookeeperServiceDiscovery; + +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.curator.x.discovery.ServiceDiscovery; +import org.apache.curator.x.discovery.ServiceDiscoveryBuilder; +import org.apache.curator.x.discovery.ServiceInstanceBuilder; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.apache.curator.x.discovery.ServiceInstance.builder; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkParams.BASE_SLEEP_TIME; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkParams.BLOCK_UNTIL_CONNECTED_UNIT; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkParams.BLOCK_UNTIL_CONNECTED_WAIT; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkParams.MAX_RETRIES; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkParams.MAX_SLEEP; + +/** + * Curator Framework Utilities Class + * + * @since 2.7.2 + */ +public abstract class CuratorFrameworkUtils { + + public static ZookeeperServiceDiscovery buildZookeeperServiceDiscovery(URL registerURL) throws Exception { + return new ZookeeperServiceDiscovery(registerURL); + } + + public static ServiceDiscovery buildServiceDiscovery(CuratorFramework curatorFramework, + String basePath) { + return ServiceDiscoveryBuilder.builder(ZookeeperInstance.class) + .client(curatorFramework) + .basePath(basePath) + .build(); + } + + public static CuratorFramework buildCuratorFramework(URL registerURL) throws Exception { + CuratorFramework curatorFramework = CuratorFrameworkFactory.builder() + .connectString(registerURL.getIp() + ":" + registerURL.getPort()) + .retryPolicy(buildRetryPolicy(registerURL)) + .build(); + curatorFramework.start(); + curatorFramework.blockUntilConnected(BLOCK_UNTIL_CONNECTED_WAIT.getParameterValue(registerURL), + BLOCK_UNTIL_CONNECTED_UNIT.getParameterValue(registerURL)); + return curatorFramework; + } + + public static RetryPolicy buildRetryPolicy(URL registerURL) { + int baseSleepTimeMs = BASE_SLEEP_TIME.getParameterValue(registerURL); + int maxRetries = MAX_RETRIES.getParameterValue(registerURL); + int getMaxSleepMs = MAX_SLEEP.getParameterValue(registerURL); + return new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries, getMaxSleepMs); + } + + + public static List build(Collection> + instances) { + return instances.stream().map(CuratorFrameworkUtils::build).collect(Collectors.toList()); + } + + public static ServiceInstance build(org.apache.curator.x.discovery.ServiceInstance instance) { + String name = instance.getName(); + String host = instance.getAddress(); + int port = instance.getPort(); + ZookeeperInstance zookeeperInstance = instance.getPayload(); + DefaultServiceInstance serviceInstance = new DefaultServiceInstance(instance.getId(), name, host, port); + serviceInstance.setMetadata(zookeeperInstance.getMetadata()); + return serviceInstance; + } + + public static org.apache.curator.x.discovery.ServiceInstance build(ServiceInstance serviceInstance) { + ServiceInstanceBuilder builder = null; + String serviceName = serviceInstance.getServiceName(); + String host = serviceInstance.getHost(); + int port = serviceInstance.getPort(); + Map metadata = serviceInstance.getMetadata(); + String id = generateId(host, port); + ZookeeperInstance zookeeperInstance = new ZookeeperInstance(null, serviceName, metadata); + try { + builder = builder() + .id(id) + .name(serviceName) + .address(host) + .port(port) + .payload(zookeeperInstance); + } catch (Exception e) { + throw new RuntimeException(e); + } + return builder.build(); + } + + public static final String generateId(String host, int port) { + return host + ":" + port; + } +} diff --git a/dubbo-registry/dubbo-registry-zookeeper/src/test/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscoveryTest.java b/dubbo-registry/dubbo-registry-zookeeper/src/test/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscoveryTest.java new file mode 100644 index 000000000000..e74425f9f2a7 --- /dev/null +++ b/dubbo-registry/dubbo-registry-zookeeper/src/test/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscoveryTest.java @@ -0,0 +1,206 @@ +/* + * 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.registry.zookeeper; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.utils.Page; +import org.apache.dubbo.registry.client.DefaultServiceInstance; +import org.apache.dubbo.registry.client.ServiceInstance; + +import org.apache.curator.test.TestingServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import static java.util.Arrays.asList; +import static org.apache.dubbo.common.utils.NetUtils.getAvailablePort; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkUtils.buildZookeeperServiceDiscovery; +import static org.apache.dubbo.registry.zookeeper.util.CuratorFrameworkUtils.generateId; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link ZookeeperServiceDiscovery} Test + * + * @since 2.7.2 + */ +public class ZookeeperServiceDiscoveryTest { + + private static final String SERVICE_NAME = "A"; + + private static final String LOCALHOST = "127.0.0.1"; + + private TestingServer zkServer; + private int zkServerPort; + private URL registryUrl; + + private ZookeeperServiceDiscovery discovery; + + @BeforeEach + public void init() throws Exception { + zkServerPort = getAvailablePort(); + zkServer = new TestingServer(zkServerPort, true); + zkServer.start(); + + this.registryUrl = URL.valueOf("zookeeper://127.0.0.1:" + zkServerPort); + this.discovery = buildZookeeperServiceDiscovery(registryUrl); + this.discovery.start(); + } + + @AfterEach + public void close() throws IOException { + discovery.stop(); + zkServer.stop(); + } + + @Test + public void testRegistration() { + + DefaultServiceInstance serviceInstance = createServiceInstance(SERVICE_NAME, LOCALHOST, 8080); + + discovery.register(serviceInstance); + + List serviceInstances = discovery.getInstances(SERVICE_NAME); + + assertTrue(serviceInstances.contains(serviceInstance)); + assertEquals(asList(serviceInstance), serviceInstances); + + Map metadata = new HashMap<>(); + metadata.put("message", "Hello,World"); + serviceInstance.setMetadata(metadata); + + discovery.update(serviceInstance); + + serviceInstances = discovery.getInstances(SERVICE_NAME); + + assertEquals(serviceInstance, serviceInstances.get(0)); + + discovery.unregister(serviceInstance); + + serviceInstances = discovery.getInstances(SERVICE_NAME); + + assertTrue(serviceInstances.isEmpty()); + } + + private DefaultServiceInstance createServiceInstance(String serviceName, String host, int port) { + return new DefaultServiceInstance(generateId(host, port), serviceName, host, port); + } + + @Test + public void testGetInstances() throws InterruptedException { + + List instances = asList( + createServiceInstance(SERVICE_NAME, LOCALHOST, 8080), + createServiceInstance(SERVICE_NAME, LOCALHOST, 8081), + createServiceInstance(SERVICE_NAME, LOCALHOST, 8082) + ); + + instances.forEach(discovery::register); + + List serviceInstances = new LinkedList<>(); + + CountDownLatch latch = new CountDownLatch(1); + + // Add Listener + discovery.addServiceDiscoveryChangeListener(SERVICE_NAME, event -> { + serviceInstances.addAll(event.getServiceInstances()); + latch.countDown(); + }); + + discovery.register(createServiceInstance(SERVICE_NAME, LOCALHOST, 8082)); + discovery.update(createServiceInstance(SERVICE_NAME, LOCALHOST, 8082)); + + latch.await(); + + assertFalse(serviceInstances.isEmpty()); + + // offset starts 0 + int offset = 0; + // requestSize > total elements + int requestSize = 5; + + Page page = discovery.getInstances(SERVICE_NAME, offset, requestSize); + assertEquals(0, page.getRequestOffset()); + assertEquals(5, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(3, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + // requestSize < total elements + requestSize = 2; + + page = discovery.getInstances(SERVICE_NAME, offset, requestSize); + assertEquals(0, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(2, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + offset = 1; + page = discovery.getInstances(SERVICE_NAME, offset, requestSize); + assertEquals(1, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(2, page.getData().size()); + assertTrue(page.hasData()); + + for (ServiceInstance instance : page.getData()) { + assertTrue(instances.contains(instance)); + } + + offset = 2; + page = discovery.getInstances(SERVICE_NAME, offset, requestSize); + assertEquals(2, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(1, page.getData().size()); + assertTrue(page.hasData()); + + offset = 3; + page = discovery.getInstances(SERVICE_NAME, offset, requestSize); + assertEquals(3, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + + offset = 5; + page = discovery.getInstances(SERVICE_NAME, offset, requestSize); + assertEquals(5, page.getRequestOffset()); + assertEquals(2, page.getRequestSize()); + assertEquals(3, page.getTotalSize()); + assertEquals(0, page.getData().size()); + assertFalse(page.hasData()); + + } +} diff --git a/pom.xml b/pom.xml index a1f7b56820d9..0c9cb3d0c6ac 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,8 @@ dubbo-metadata-report dubbo-configcenter dubbo-dependencies + dubbo-event + dubbo-metadata