diff --git a/dubbo-all/pom.xml b/dubbo-all/pom.xml index be5a2e3fd77..6ae3fbf0909 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 00000000000..317d0456998 --- /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 00000000000..1acd3fa137f --- /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 00000000000..71dd2afe895 --- /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 00000000000..58a4249195d --- /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 0c1a13dfe25..bac04c907a6 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 @@ -1,1122 +1,1187 @@ -/* - * 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 javassist.CtClass; -import javassist.CtConstructor; -import javassist.CtMethod; -import javassist.NotFoundException; - -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.GenericArrayType; -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.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Future; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * ReflectUtils - */ -public final class ReflectUtils { - - /** - * void(V). - */ - public static final char JVM_VOID = 'V'; - - /** - * boolean(Z). - */ - public static final char JVM_BOOLEAN = 'Z'; - - /** - * byte(B). - */ - public static final char JVM_BYTE = 'B'; - - /** - * char(C). - */ - public static final char JVM_CHAR = 'C'; - - /** - * double(D). - */ - public static final char JVM_DOUBLE = 'D'; - - /** - * float(F). - */ - public static final char JVM_FLOAT = 'F'; - - /** - * int(I). - */ - public static final char JVM_INT = 'I'; - - /** - * long(J). - */ - public static final char JVM_LONG = 'J'; - - /** - * short(S). - */ - public static final char JVM_SHORT = 'S'; - - public static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; - - public static final String JAVA_IDENT_REGEX = "(?:[_$a-zA-Z][_$a-zA-Z0-9]*)"; - - public static final String JAVA_NAME_REGEX = "(?:" + JAVA_IDENT_REGEX + "(?:\\." + JAVA_IDENT_REGEX + ")*)"; - - public static final String CLASS_DESC = "(?:L" + JAVA_IDENT_REGEX + "(?:\\/" + JAVA_IDENT_REGEX + ")*;)"; - - public static final String ARRAY_DESC = "(?:\\[+(?:(?:[VZBCDFIJS])|" + CLASS_DESC + "))"; - - public static final String DESC_REGEX = "(?:(?:[VZBCDFIJS])|" + CLASS_DESC + "|" + ARRAY_DESC + ")"; - - public static final Pattern DESC_PATTERN = Pattern.compile(DESC_REGEX); - - public static final String METHOD_DESC_REGEX = "(?:(" + JAVA_IDENT_REGEX + ")?\\((" + DESC_REGEX + "*)\\)(" + DESC_REGEX + ")?)"; - - public static final Pattern METHOD_DESC_PATTERN = Pattern.compile(METHOD_DESC_REGEX); - - public static final Pattern GETTER_METHOD_DESC_PATTERN = Pattern.compile("get([A-Z][_a-zA-Z0-9]*)\\(\\)(" + DESC_REGEX + ")"); - - public static final Pattern SETTER_METHOD_DESC_PATTERN = Pattern.compile("set([A-Z][_a-zA-Z0-9]*)\\((" + DESC_REGEX + ")\\)V"); - - public static final Pattern IS_HAS_CAN_METHOD_DESC_PATTERN = Pattern.compile("(?:is|has|can)([A-Z][_a-zA-Z0-9]*)\\(\\)Z"); - - private static final ConcurrentMap> DESC_CLASS_CACHE = new ConcurrentHashMap>(); - - private static final ConcurrentMap> NAME_CLASS_CACHE = new ConcurrentHashMap>(); - - private static final ConcurrentMap Signature_METHODS_CACHE = new ConcurrentHashMap(); - - private ReflectUtils() { - } - - public static boolean isPrimitives(Class cls) { - if (cls.isArray()) { - return isPrimitive(cls.getComponentType()); - } - return isPrimitive(cls); - } - - public static boolean isPrimitive(Class cls) { - return cls.isPrimitive() || cls == String.class || cls == Boolean.class || cls == Character.class - || Number.class.isAssignableFrom(cls) || Date.class.isAssignableFrom(cls); - } - - public static Class getBoxedClass(Class c) { - if (c == int.class) { - c = Integer.class; - } else if (c == boolean.class) { - c = Boolean.class; - } else if (c == long.class) { - c = Long.class; - } else if (c == float.class) { - c = Float.class; - } else if (c == double.class) { - c = Double.class; - } else if (c == char.class) { - c = Character.class; - } else if (c == byte.class) { - c = Byte.class; - } else if (c == short.class) { - c = Short.class; - } - return c; - } - - /** - * is compatible. - * - * @param c class. - * @param o instance. - * @return compatible or not. - */ - public static boolean isCompatible(Class c, Object o) { - boolean pt = c.isPrimitive(); - if (o == null) { - return !pt; - } - - if (pt) { - c = getBoxedClass(c); - } - - return c == o.getClass() || c.isInstance(o); - } - - /** - * is compatible. - * - * @param cs class array. - * @param os object array. - * @return compatible or not. - */ - public static boolean isCompatible(Class[] cs, Object[] os) { - int len = cs.length; - if (len != os.length) { - return false; - } - if (len == 0) { - return true; - } - for (int i = 0; i < len; i++) { - if (!isCompatible(cs[i], os[i])) { - return false; - } - } - return true; - } - - public static String getCodeBase(Class cls) { - if (cls == null) { - return null; - } - ProtectionDomain domain = cls.getProtectionDomain(); - if (domain == null) { - return null; - } - CodeSource source = domain.getCodeSource(); - if (source == null) { - return null; - } - URL location = source.getLocation(); - if (location == null) { - return null; - } - return location.getFile(); - } - - /** - * get name. - * java.lang.Object[][].class => "java.lang.Object[][]" - * - * @param c class. - * @return name. - */ - public static String getName(Class c) { - if (c.isArray()) { - StringBuilder sb = new StringBuilder(); - do { - sb.append("[]"); - c = c.getComponentType(); - } - while (c.isArray()); - - return c.getName() + sb.toString(); - } - return c.getName(); - } - - public static Class getGenericClass(Class cls) { - return getGenericClass(cls, 0); - } - - public static Class getGenericClass(Class cls, int i) { - try { - ParameterizedType parameterizedType = ((ParameterizedType) cls.getGenericInterfaces()[0]); - Object genericClass = parameterizedType.getActualTypeArguments()[i]; - if (genericClass instanceof ParameterizedType) { // handle nested generic type - return (Class) ((ParameterizedType) genericClass).getRawType(); - } else if (genericClass instanceof GenericArrayType) { // handle array generic type - return (Class) ((GenericArrayType) genericClass).getGenericComponentType(); - } else if (((Class) genericClass).isArray()) { - // Requires JDK 7 or higher, Foo is no longer GenericArrayType - return ((Class) genericClass).getComponentType(); - } else { - return (Class) genericClass; - } - } catch (Throwable e) { - throw new IllegalArgumentException(cls.getName() - + " generic type undefined!", e); - } - } - - /** - * get method name. - * "void do(int)", "void do()", "int do(java.lang.String,boolean)" - * - * @param m method. - * @return name. - */ - public static String getName(final Method m) { - StringBuilder ret = new StringBuilder(); - ret.append(getName(m.getReturnType())).append(' '); - ret.append(m.getName()).append('('); - Class[] parameterTypes = m.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - if (i > 0) { - ret.append(','); - } - ret.append(getName(parameterTypes[i])); - } - ret.append(')'); - return ret.toString(); - } - - public static String getSignature(String methodName, Class[] parameterTypes) { - StringBuilder sb = new StringBuilder(methodName); - sb.append("("); - if (parameterTypes != null && parameterTypes.length > 0) { - boolean first = true; - for (Class type : parameterTypes) { - if (first) { - first = false; - } else { - sb.append(","); - } - sb.append(type.getName()); - } - } - sb.append(")"); - return sb.toString(); - } - - /** - * get constructor name. - * "()", "(java.lang.String,int)" - * - * @param c constructor. - * @return name. - */ - public static String getName(final Constructor c) { - StringBuilder ret = new StringBuilder("("); - Class[] parameterTypes = c.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - if (i > 0) { - ret.append(','); - } - ret.append(getName(parameterTypes[i])); - } - ret.append(')'); - return ret.toString(); - } - - /** - * get class desc. - * boolean[].class => "[Z" - * Object.class => "Ljava/lang/Object;" - * - * @param c class. - * @return desc. - * @throws NotFoundException - */ - public static String getDesc(Class c) { - StringBuilder ret = new StringBuilder(); - - while (c.isArray()) { - ret.append('['); - c = c.getComponentType(); - } - - if (c.isPrimitive()) { - String t = c.getName(); - if ("void".equals(t)) { - ret.append(JVM_VOID); - } else if ("boolean".equals(t)) { - ret.append(JVM_BOOLEAN); - } else if ("byte".equals(t)) { - ret.append(JVM_BYTE); - } else if ("char".equals(t)) { - ret.append(JVM_CHAR); - } else if ("double".equals(t)) { - ret.append(JVM_DOUBLE); - } else if ("float".equals(t)) { - ret.append(JVM_FLOAT); - } else if ("int".equals(t)) { - ret.append(JVM_INT); - } else if ("long".equals(t)) { - ret.append(JVM_LONG); - } else if ("short".equals(t)) { - ret.append(JVM_SHORT); - } - } else { - ret.append('L'); - ret.append(c.getName().replace('.', '/')); - ret.append(';'); - } - return ret.toString(); - } - - /** - * get class array desc. - * [int.class, boolean[].class, Object.class] => "I[ZLjava/lang/Object;" - * - * @param cs class array. - * @return desc. - * @throws NotFoundException - */ - public static String getDesc(final Class[] cs) { - if (cs.length == 0) { - return ""; - } - - StringBuilder sb = new StringBuilder(64); - for (Class c : cs) { - sb.append(getDesc(c)); - } - return sb.toString(); - } - - /** - * get method desc. - * int do(int arg1) => "do(I)I" - * void do(String arg1,boolean arg2) => "do(Ljava/lang/String;Z)V" - * - * @param m method. - * @return desc. - */ - public static String getDesc(final Method m) { - StringBuilder ret = new StringBuilder(m.getName()).append('('); - Class[] parameterTypes = m.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - ret.append(getDesc(parameterTypes[i])); - } - ret.append(')').append(getDesc(m.getReturnType())); - return ret.toString(); - } - - /** - * get constructor desc. - * "()V", "(Ljava/lang/String;I)V" - * - * @param c constructor. - * @return desc - */ - public static String getDesc(final Constructor c) { - StringBuilder ret = new StringBuilder("("); - Class[] parameterTypes = c.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - ret.append(getDesc(parameterTypes[i])); - } - ret.append(')').append('V'); - return ret.toString(); - } - - /** - * get method desc. - * "(I)I", "()V", "(Ljava/lang/String;Z)V" - * - * @param m method. - * @return desc. - */ - public static String getDescWithoutMethodName(Method m) { - StringBuilder ret = new StringBuilder(); - ret.append('('); - Class[] parameterTypes = m.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - ret.append(getDesc(parameterTypes[i])); - } - ret.append(')').append(getDesc(m.getReturnType())); - return ret.toString(); - } - - /** - * get class desc. - * Object.class => "Ljava/lang/Object;" - * boolean[].class => "[Z" - * - * @param c class. - * @return desc. - * @throws NotFoundException - */ - public static String getDesc(final CtClass c) throws NotFoundException { - StringBuilder ret = new StringBuilder(); - if (c.isArray()) { - ret.append('['); - ret.append(getDesc(c.getComponentType())); - } else if (c.isPrimitive()) { - String t = c.getName(); - if ("void".equals(t)) { - ret.append(JVM_VOID); - } else if ("boolean".equals(t)) { - ret.append(JVM_BOOLEAN); - } else if ("byte".equals(t)) { - ret.append(JVM_BYTE); - } else if ("char".equals(t)) { - ret.append(JVM_CHAR); - } else if ("double".equals(t)) { - ret.append(JVM_DOUBLE); - } else if ("float".equals(t)) { - ret.append(JVM_FLOAT); - } else if ("int".equals(t)) { - ret.append(JVM_INT); - } else if ("long".equals(t)) { - ret.append(JVM_LONG); - } else if ("short".equals(t)) { - ret.append(JVM_SHORT); - } - } else { - ret.append('L'); - ret.append(c.getName().replace('.', '/')); - ret.append(';'); - } - return ret.toString(); - } - - /** - * get method desc. - * "do(I)I", "do()V", "do(Ljava/lang/String;Z)V" - * - * @param m method. - * @return desc. - */ - public static String getDesc(final CtMethod m) throws NotFoundException { - StringBuilder ret = new StringBuilder(m.getName()).append('('); - CtClass[] parameterTypes = m.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - ret.append(getDesc(parameterTypes[i])); - } - ret.append(')').append(getDesc(m.getReturnType())); - return ret.toString(); - } - - /** - * get constructor desc. - * "()V", "(Ljava/lang/String;I)V" - * - * @param c constructor. - * @return desc - */ - public static String getDesc(final CtConstructor c) throws NotFoundException { - StringBuilder ret = new StringBuilder("("); - CtClass[] parameterTypes = c.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - ret.append(getDesc(parameterTypes[i])); - } - ret.append(')').append('V'); - return ret.toString(); - } - - /** - * get method desc. - * "(I)I", "()V", "(Ljava/lang/String;Z)V". - * - * @param m method. - * @return desc. - */ - public static String getDescWithoutMethodName(final CtMethod m) throws NotFoundException { - StringBuilder ret = new StringBuilder(); - ret.append('('); - CtClass[] parameterTypes = m.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - ret.append(getDesc(parameterTypes[i])); - } - ret.append(')').append(getDesc(m.getReturnType())); - return ret.toString(); - } - - /** - * name to desc. - * java.util.Map[][] => "[[Ljava/util/Map;" - * - * @param name name. - * @return desc. - */ - public static String name2desc(String name) { - StringBuilder sb = new StringBuilder(); - int c = 0, index = name.indexOf('['); - if (index > 0) { - c = (name.length() - index) / 2; - name = name.substring(0, index); - } - while (c-- > 0) { - sb.append("["); - } - if ("void".equals(name)) { - sb.append(JVM_VOID); - } else if ("boolean".equals(name)) { - sb.append(JVM_BOOLEAN); - } else if ("byte".equals(name)) { - sb.append(JVM_BYTE); - } else if ("char".equals(name)) { - sb.append(JVM_CHAR); - } else if ("double".equals(name)) { - sb.append(JVM_DOUBLE); - } else if ("float".equals(name)) { - sb.append(JVM_FLOAT); - } else if ("int".equals(name)) { - sb.append(JVM_INT); - } else if ("long".equals(name)) { - sb.append(JVM_LONG); - } else if ("short".equals(name)) { - sb.append(JVM_SHORT); - } else { - sb.append('L').append(name.replace('.', '/')).append(';'); - } - return sb.toString(); - } - - /** - * desc to name. - * "[[I" => "int[][]" - * - * @param desc desc. - * @return name. - */ - public static String desc2name(String desc) { - StringBuilder sb = new StringBuilder(); - int c = desc.lastIndexOf('[') + 1; - if (desc.length() == c + 1) { - switch (desc.charAt(c)) { - case JVM_VOID: { - sb.append("void"); - break; - } - case JVM_BOOLEAN: { - sb.append("boolean"); - break; - } - case JVM_BYTE: { - sb.append("byte"); - break; - } - case JVM_CHAR: { - sb.append("char"); - break; - } - case JVM_DOUBLE: { - sb.append("double"); - break; - } - case JVM_FLOAT: { - sb.append("float"); - break; - } - case JVM_INT: { - sb.append("int"); - break; - } - case JVM_LONG: { - sb.append("long"); - break; - } - case JVM_SHORT: { - sb.append("short"); - break; - } - default: - throw new RuntimeException(); - } - } else { - sb.append(desc.substring(c + 1, desc.length() - 1).replace('/', '.')); - } - while (c-- > 0) { - sb.append("[]"); - } - return sb.toString(); - } - - public static Class forName(String name) { - try { - return name2class(name); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Not found class " + name + ", cause: " + e.getMessage(), e); - } - } - - public static Class forName(ClassLoader cl, String name) { - try { - return name2class(cl, name); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Not found class " + name + ", cause: " + e.getMessage(), e); - } - } - - /** - * name to class. - * "boolean" => boolean.class - * "java.util.Map[][]" => java.util.Map[][].class - * - * @param name name. - * @return Class instance. - */ - public static Class name2class(String name) throws ClassNotFoundException { - return name2class(ClassUtils.getClassLoader(), name); - } - - /** - * name to class. - * "boolean" => boolean.class - * "java.util.Map[][]" => java.util.Map[][].class - * - * @param cl ClassLoader instance. - * @param name name. - * @return Class instance. - */ - private static Class name2class(ClassLoader cl, String name) throws ClassNotFoundException { - int c = 0, index = name.indexOf('['); - if (index > 0) { - c = (name.length() - index) / 2; - name = name.substring(0, index); - } - if (c > 0) { - StringBuilder sb = new StringBuilder(); - while (c-- > 0) { - sb.append("["); - } - - if ("void".equals(name)) { - sb.append(JVM_VOID); - } else if ("boolean".equals(name)) { - sb.append(JVM_BOOLEAN); - } else if ("byte".equals(name)) { - sb.append(JVM_BYTE); - } else if ("char".equals(name)) { - sb.append(JVM_CHAR); - } else if ("double".equals(name)) { - sb.append(JVM_DOUBLE); - } else if ("float".equals(name)) { - sb.append(JVM_FLOAT); - } else if ("int".equals(name)) { - sb.append(JVM_INT); - } else if ("long".equals(name)) { - sb.append(JVM_LONG); - } else if ("short".equals(name)) { - sb.append(JVM_SHORT); - } else { - sb.append('L').append(name).append(';'); // "java.lang.Object" ==> "Ljava.lang.Object;" - } - name = sb.toString(); - } else { - if ("void".equals(name)) { - return void.class; - } else if ("boolean".equals(name)) { - return boolean.class; - } else if ("byte".equals(name)) { - return byte.class; - } else if ("char".equals(name)) { - return char.class; - } else if ("double".equals(name)) { - return double.class; - } else if ("float".equals(name)) { - return float.class; - } else if ("int".equals(name)) { - return int.class; - } else if ("long".equals(name)) { - return long.class; - } else if ("short".equals(name)) { - return short.class; - } - } - - if (cl == null) { - cl = ClassUtils.getClassLoader(); - } - Class clazz = NAME_CLASS_CACHE.get(name); - if (clazz == null) { - clazz = Class.forName(name, true, cl); - NAME_CLASS_CACHE.put(name, clazz); - } - return clazz; - } - - /** - * desc to class. - * "[Z" => boolean[].class - * "[[Ljava/util/Map;" => java.util.Map[][].class - * - * @param desc desc. - * @return Class instance. - * @throws ClassNotFoundException - */ - public static Class desc2class(String desc) throws ClassNotFoundException { - return desc2class(ClassUtils.getClassLoader(), desc); - } - - /** - * desc to class. - * "[Z" => boolean[].class - * "[[Ljava/util/Map;" => java.util.Map[][].class - * - * @param cl ClassLoader instance. - * @param desc desc. - * @return Class instance. - * @throws ClassNotFoundException - */ - private static Class desc2class(ClassLoader cl, String desc) throws ClassNotFoundException { - switch (desc.charAt(0)) { - case JVM_VOID: - return void.class; - case JVM_BOOLEAN: - return boolean.class; - case JVM_BYTE: - return byte.class; - case JVM_CHAR: - return char.class; - case JVM_DOUBLE: - return double.class; - case JVM_FLOAT: - return float.class; - case JVM_INT: - return int.class; - case JVM_LONG: - return long.class; - case JVM_SHORT: - return short.class; - case 'L': - desc = desc.substring(1, desc.length() - 1).replace('/', '.'); // "Ljava/lang/Object;" ==> "java.lang.Object" - break; - case '[': - desc = desc.replace('/', '.'); // "[[Ljava/lang/Object;" ==> "[[Ljava.lang.Object;" - break; - default: - throw new ClassNotFoundException("Class not found: " + desc); - } - - if (cl == null) { - cl = ClassUtils.getClassLoader(); - } - Class clazz = DESC_CLASS_CACHE.get(desc); - if (clazz == null) { - clazz = Class.forName(desc, true, cl); - DESC_CLASS_CACHE.put(desc, clazz); - } - return clazz; - } - - /** - * get class array instance. - * - * @param desc desc. - * @return Class class array. - * @throws ClassNotFoundException - */ - public static Class[] desc2classArray(String desc) throws ClassNotFoundException { - Class[] ret = desc2classArray(ClassUtils.getClassLoader(), desc); - return ret; - } - - /** - * get class array instance. - * - * @param cl ClassLoader instance. - * @param desc desc. - * @return Class[] class array. - * @throws ClassNotFoundException - */ - private static Class[] desc2classArray(ClassLoader cl, String desc) throws ClassNotFoundException { - if (desc.length() == 0) { - return EMPTY_CLASS_ARRAY; - } - - List> cs = new ArrayList>(); - Matcher m = DESC_PATTERN.matcher(desc); - while (m.find()) { - cs.add(desc2class(cl, m.group())); - } - return cs.toArray(EMPTY_CLASS_ARRAY); - } - - /** - * Find method from method signature - * - * @param clazz Target class to find method - * @param methodName Method signature, e.g.: method1(int, String). It is allowed to provide method name only, e.g.: method2 - * @return target method - * @throws NoSuchMethodException - * @throws ClassNotFoundException - * @throws IllegalStateException when multiple methods are found (overridden method when parameter info is not provided) - */ - public static Method findMethodByMethodSignature(Class clazz, String methodName, String[] parameterTypes) - throws NoSuchMethodException, ClassNotFoundException { - String signature = clazz.getName() + "." + methodName; - if (parameterTypes != null && parameterTypes.length > 0) { - signature += StringUtils.join(parameterTypes); - } - Method method = Signature_METHODS_CACHE.get(signature); - if (method != null) { - return method; - } - if (parameterTypes == null) { - List finded = new ArrayList(); - for (Method m : clazz.getMethods()) { - if (m.getName().equals(methodName)) { - finded.add(m); - } - } - if (finded.isEmpty()) { - throw new NoSuchMethodException("No such method " + methodName + " in class " + clazz); - } - if (finded.size() > 1) { - String msg = String.format("Not unique method for method name(%s) in class(%s), find %d methods.", - methodName, clazz.getName(), finded.size()); - throw new IllegalStateException(msg); - } - method = finded.get(0); - } else { - Class[] types = new Class[parameterTypes.length]; - for (int i = 0; i < parameterTypes.length; i++) { - types[i] = ReflectUtils.name2class(parameterTypes[i]); - } - method = clazz.getMethod(methodName, types); - - } - Signature_METHODS_CACHE.put(signature, method); - return method; - } - - public static Method findMethodByMethodName(Class clazz, String methodName) - throws NoSuchMethodException, ClassNotFoundException { - return findMethodByMethodSignature(clazz, methodName, null); - } - - public static Constructor findConstructor(Class clazz, Class paramType) throws NoSuchMethodException { - Constructor targetConstructor; - try { - targetConstructor = clazz.getConstructor(new Class[]{paramType}); - } catch (NoSuchMethodException e) { - targetConstructor = null; - Constructor[] constructors = clazz.getConstructors(); - for (Constructor constructor : constructors) { - if (Modifier.isPublic(constructor.getModifiers()) - && constructor.getParameterTypes().length == 1 - && constructor.getParameterTypes()[0].isAssignableFrom(paramType)) { - targetConstructor = constructor; - break; - } - } - if (targetConstructor == null) { - throw e; - } - } - return targetConstructor; - } - - /** - * Check if one object is the implementation for a given interface. - *

- * This method will not trigger classloading for the given interface, therefore it will not lead to error when - * the given interface is not visible by the classloader - * - * @param obj Object to examine - * @param interfaceClazzName The given interface - * @return true if the object implements the given interface, otherwise return false - */ - public static boolean isInstance(Object obj, String interfaceClazzName) { - for (Class clazz = obj.getClass(); - clazz != null && !clazz.equals(Object.class); - clazz = clazz.getSuperclass()) { - Class[] interfaces = clazz.getInterfaces(); - for (Class itf : interfaces) { - if (itf.getName().equals(interfaceClazzName)) { - return true; - } - } - } - return false; - } - - public static Object getEmptyObject(Class returnType) { - return getEmptyObject(returnType, new HashMap, Object>(), 0); - } - - private static Object getEmptyObject(Class returnType, Map, Object> emptyInstances, int level) { - if (level > 2) { - return null; - } - if (returnType == null) { - return null; - } else if (returnType == boolean.class || returnType == Boolean.class) { - return false; - } else if (returnType == char.class || returnType == Character.class) { - return '\0'; - } else if (returnType == byte.class || returnType == Byte.class) { - return (byte) 0; - } else if (returnType == short.class || returnType == Short.class) { - return (short) 0; - } else if (returnType == int.class || returnType == Integer.class) { - return 0; - } else if (returnType == long.class || returnType == Long.class) { - return 0L; - } else if (returnType == float.class || returnType == Float.class) { - return 0F; - } else if (returnType == double.class || returnType == Double.class) { - return 0D; - } else if (returnType.isArray()) { - return Array.newInstance(returnType.getComponentType(), 0); - } else if (returnType.isAssignableFrom(ArrayList.class)) { - return new ArrayList(0); - } else if (returnType.isAssignableFrom(HashSet.class)) { - return new HashSet(0); - } else if (returnType.isAssignableFrom(HashMap.class)) { - return new HashMap(0); - } else if (String.class.equals(returnType)) { - return ""; - } else if (!returnType.isInterface()) { - try { - Object value = emptyInstances.get(returnType); - if (value == null) { - value = returnType.newInstance(); - emptyInstances.put(returnType, value); - } - Class cls = value.getClass(); - while (cls != null && cls != Object.class) { - Field[] fields = cls.getDeclaredFields(); - for (Field field : fields) { - if (field.isSynthetic()) { - continue; - } - Object property = getEmptyObject(field.getType(), emptyInstances, level + 1); - if (property != null) { - try { - if (!field.isAccessible()) { - field.setAccessible(true); - } - field.set(value, property); - } catch (Throwable e) { - } - } - } - cls = cls.getSuperclass(); - } - return value; - } catch (Throwable e) { - return null; - } - } else { - return null; - } - } - - public static boolean isBeanPropertyReadMethod(Method method) { - return method != null - && Modifier.isPublic(method.getModifiers()) - && !Modifier.isStatic(method.getModifiers()) - && method.getReturnType() != void.class - && method.getDeclaringClass() != Object.class - && method.getParameterTypes().length == 0 - && ((method.getName().startsWith("get") && method.getName().length() > 3) - || (method.getName().startsWith("is") && method.getName().length() > 2)); - } - - public static String getPropertyNameFromBeanReadMethod(Method method) { - if (isBeanPropertyReadMethod(method)) { - if (method.getName().startsWith("get")) { - return method.getName().substring(3, 4).toLowerCase() - + method.getName().substring(4); - } - if (method.getName().startsWith("is")) { - return method.getName().substring(2, 3).toLowerCase() - + method.getName().substring(3); - } - } - return null; - } - - public static boolean isBeanPropertyWriteMethod(Method method) { - return method != null - && Modifier.isPublic(method.getModifiers()) - && !Modifier.isStatic(method.getModifiers()) - && method.getDeclaringClass() != Object.class - && method.getParameterTypes().length == 1 - && method.getName().startsWith("set") - && method.getName().length() > 3; - } - - public static String getPropertyNameFromBeanWriteMethod(Method method) { - if (isBeanPropertyWriteMethod(method)) { - return method.getName().substring(3, 4).toLowerCase() - + method.getName().substring(4); - } - return null; - } - - public static boolean isPublicInstanceField(Field field) { - return Modifier.isPublic(field.getModifiers()) - && !Modifier.isStatic(field.getModifiers()) - && !Modifier.isFinal(field.getModifiers()) - && !field.isSynthetic(); - } - - public static Map getBeanPropertyFields(Class cl) { - Map properties = new HashMap(); - for (; cl != null; cl = cl.getSuperclass()) { - Field[] fields = cl.getDeclaredFields(); - for (Field field : fields) { - if (Modifier.isTransient(field.getModifiers()) - || Modifier.isStatic(field.getModifiers())) { - continue; - } - - field.setAccessible(true); - - properties.put(field.getName(), field); - } - } - - return properties; - } - - public static Map getBeanPropertyReadMethods(Class cl) { - Map properties = new HashMap(); - for (; cl != null; cl = cl.getSuperclass()) { - Method[] methods = cl.getDeclaredMethods(); - for (Method method : methods) { - if (isBeanPropertyReadMethod(method)) { - method.setAccessible(true); - String property = getPropertyNameFromBeanReadMethod(method); - properties.put(property, method); - } - } - } - - return properties; - } - - public static Type[] getReturnTypes(Method method) { - Class returnType = method.getReturnType(); - Type genericReturnType = method.getGenericReturnType(); - if (Future.class.isAssignableFrom(returnType)) { - if (genericReturnType instanceof ParameterizedType) { - Type actualArgType = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0]; - if (actualArgType instanceof ParameterizedType) { - returnType = (Class) ((ParameterizedType) actualArgType).getRawType(); - genericReturnType = actualArgType; - } else { - returnType = (Class) actualArgType; - genericReturnType = returnType; - } - } else { - returnType = null; - genericReturnType = null; - } - } - return new Type[]{returnType, genericReturnType}; - } +/* + * 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 javassist.CtClass; +import javassist.CtConstructor; +import javassist.CtMethod; +import javassist.NotFoundException; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +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.concurrent.Future; +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 + */ +public final class ReflectUtils { + + /** + * void(V). + */ + public static final char JVM_VOID = 'V'; + + /** + * boolean(Z). + */ + public static final char JVM_BOOLEAN = 'Z'; + + /** + * byte(B). + */ + public static final char JVM_BYTE = 'B'; + + /** + * char(C). + */ + public static final char JVM_CHAR = 'C'; + + /** + * double(D). + */ + public static final char JVM_DOUBLE = 'D'; + + /** + * float(F). + */ + public static final char JVM_FLOAT = 'F'; + + /** + * int(I). + */ + public static final char JVM_INT = 'I'; + + /** + * long(J). + */ + public static final char JVM_LONG = 'J'; + + /** + * short(S). + */ + public static final char JVM_SHORT = 'S'; + + public static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + public static final String JAVA_IDENT_REGEX = "(?:[_$a-zA-Z][_$a-zA-Z0-9]*)"; + + public static final String JAVA_NAME_REGEX = "(?:" + JAVA_IDENT_REGEX + "(?:\\." + JAVA_IDENT_REGEX + ")*)"; + + public static final String CLASS_DESC = "(?:L" + JAVA_IDENT_REGEX + "(?:\\/" + JAVA_IDENT_REGEX + ")*;)"; + + public static final String ARRAY_DESC = "(?:\\[+(?:(?:[VZBCDFIJS])|" + CLASS_DESC + "))"; + + public static final String DESC_REGEX = "(?:(?:[VZBCDFIJS])|" + CLASS_DESC + "|" + ARRAY_DESC + ")"; + + public static final Pattern DESC_PATTERN = Pattern.compile(DESC_REGEX); + + public static final String METHOD_DESC_REGEX = "(?:(" + JAVA_IDENT_REGEX + ")?\\((" + DESC_REGEX + "*)\\)(" + DESC_REGEX + ")?)"; + + public static final Pattern METHOD_DESC_PATTERN = Pattern.compile(METHOD_DESC_REGEX); + + public static final Pattern GETTER_METHOD_DESC_PATTERN = Pattern.compile("get([A-Z][_a-zA-Z0-9]*)\\(\\)(" + DESC_REGEX + ")"); + + public static final Pattern SETTER_METHOD_DESC_PATTERN = Pattern.compile("set([A-Z][_a-zA-Z0-9]*)\\((" + DESC_REGEX + ")\\)V"); + + public static final Pattern IS_HAS_CAN_METHOD_DESC_PATTERN = Pattern.compile("(?:is|has|can)([A-Z][_a-zA-Z0-9]*)\\(\\)Z"); + + private static final ConcurrentMap> DESC_CLASS_CACHE = new ConcurrentHashMap>(); + + private static final ConcurrentMap> NAME_CLASS_CACHE = new ConcurrentHashMap>(); + + private static final ConcurrentMap Signature_METHODS_CACHE = new ConcurrentHashMap(); + + private ReflectUtils() { + } + + public static boolean isPrimitives(Class cls) { + if (cls.isArray()) { + return isPrimitive(cls.getComponentType()); + } + return isPrimitive(cls); + } + + public static boolean isPrimitive(Class cls) { + return cls.isPrimitive() || cls == String.class || cls == Boolean.class || cls == Character.class + || Number.class.isAssignableFrom(cls) || Date.class.isAssignableFrom(cls); + } + + public static Class getBoxedClass(Class c) { + if (c == int.class) { + c = Integer.class; + } else if (c == boolean.class) { + c = Boolean.class; + } else if (c == long.class) { + c = Long.class; + } else if (c == float.class) { + c = Float.class; + } else if (c == double.class) { + c = Double.class; + } else if (c == char.class) { + c = Character.class; + } else if (c == byte.class) { + c = Byte.class; + } else if (c == short.class) { + c = Short.class; + } + return c; + } + + /** + * is compatible. + * + * @param c class. + * @param o instance. + * @return compatible or not. + */ + public static boolean isCompatible(Class c, Object o) { + boolean pt = c.isPrimitive(); + if (o == null) { + return !pt; + } + + if (pt) { + c = getBoxedClass(c); + } + + return c == o.getClass() || c.isInstance(o); + } + + /** + * is compatible. + * + * @param cs class array. + * @param os object array. + * @return compatible or not. + */ + public static boolean isCompatible(Class[] cs, Object[] os) { + int len = cs.length; + if (len != os.length) { + return false; + } + if (len == 0) { + return true; + } + for (int i = 0; i < len; i++) { + if (!isCompatible(cs[i], os[i])) { + return false; + } + } + return true; + } + + public static String getCodeBase(Class cls) { + if (cls == null) { + return null; + } + ProtectionDomain domain = cls.getProtectionDomain(); + if (domain == null) { + return null; + } + CodeSource source = domain.getCodeSource(); + if (source == null) { + return null; + } + URL location = source.getLocation(); + if (location == null) { + return null; + } + return location.getFile(); + } + + /** + * get name. + * java.lang.Object[][].class => "java.lang.Object[][]" + * + * @param c class. + * @return name. + */ + public static String getName(Class c) { + if (c.isArray()) { + StringBuilder sb = new StringBuilder(); + do { + sb.append("[]"); + c = c.getComponentType(); + } + while (c.isArray()); + + return c.getName() + sb.toString(); + } + return c.getName(); + } + + public static Class getGenericClass(Class cls) { + return getGenericClass(cls, 0); + } + + public static Class getGenericClass(Class cls, int i) { + try { + ParameterizedType parameterizedType = ((ParameterizedType) cls.getGenericInterfaces()[0]); + Object genericClass = parameterizedType.getActualTypeArguments()[i]; + if (genericClass instanceof ParameterizedType) { // handle nested generic type + return (Class) ((ParameterizedType) genericClass).getRawType(); + } else if (genericClass instanceof GenericArrayType) { // handle array generic type + return (Class) ((GenericArrayType) genericClass).getGenericComponentType(); + } else if (((Class) genericClass).isArray()) { + // Requires JDK 7 or higher, Foo is no longer GenericArrayType + return ((Class) genericClass).getComponentType(); + } else { + return (Class) genericClass; + } + } catch (Throwable e) { + throw new IllegalArgumentException(cls.getName() + + " generic type undefined!", e); + } + } + + /** + * get method name. + * "void do(int)", "void do()", "int do(java.lang.String,boolean)" + * + * @param m method. + * @return name. + */ + public static String getName(final Method m) { + StringBuilder ret = new StringBuilder(); + ret.append(getName(m.getReturnType())).append(' '); + ret.append(m.getName()).append('('); + Class[] parameterTypes = m.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + if (i > 0) { + ret.append(','); + } + ret.append(getName(parameterTypes[i])); + } + ret.append(')'); + return ret.toString(); + } + + public static String getSignature(String methodName, Class[] parameterTypes) { + StringBuilder sb = new StringBuilder(methodName); + sb.append("("); + if (parameterTypes != null && parameterTypes.length > 0) { + boolean first = true; + for (Class type : parameterTypes) { + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append(type.getName()); + } + } + sb.append(")"); + return sb.toString(); + } + + /** + * get constructor name. + * "()", "(java.lang.String,int)" + * + * @param c constructor. + * @return name. + */ + public static String getName(final Constructor c) { + StringBuilder ret = new StringBuilder("("); + Class[] parameterTypes = c.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + if (i > 0) { + ret.append(','); + } + ret.append(getName(parameterTypes[i])); + } + ret.append(')'); + return ret.toString(); + } + + /** + * get class desc. + * boolean[].class => "[Z" + * Object.class => "Ljava/lang/Object;" + * + * @param c class. + * @return desc. + * @throws NotFoundException + */ + public static String getDesc(Class c) { + StringBuilder ret = new StringBuilder(); + + while (c.isArray()) { + ret.append('['); + c = c.getComponentType(); + } + + if (c.isPrimitive()) { + String t = c.getName(); + if ("void".equals(t)) { + ret.append(JVM_VOID); + } else if ("boolean".equals(t)) { + ret.append(JVM_BOOLEAN); + } else if ("byte".equals(t)) { + ret.append(JVM_BYTE); + } else if ("char".equals(t)) { + ret.append(JVM_CHAR); + } else if ("double".equals(t)) { + ret.append(JVM_DOUBLE); + } else if ("float".equals(t)) { + ret.append(JVM_FLOAT); + } else if ("int".equals(t)) { + ret.append(JVM_INT); + } else if ("long".equals(t)) { + ret.append(JVM_LONG); + } else if ("short".equals(t)) { + ret.append(JVM_SHORT); + } + } else { + ret.append('L'); + ret.append(c.getName().replace('.', '/')); + ret.append(';'); + } + return ret.toString(); + } + + /** + * get class array desc. + * [int.class, boolean[].class, Object.class] => "I[ZLjava/lang/Object;" + * + * @param cs class array. + * @return desc. + * @throws NotFoundException + */ + public static String getDesc(final Class[] cs) { + if (cs.length == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder(64); + for (Class c : cs) { + sb.append(getDesc(c)); + } + return sb.toString(); + } + + /** + * get method desc. + * int do(int arg1) => "do(I)I" + * void do(String arg1,boolean arg2) => "do(Ljava/lang/String;Z)V" + * + * @param m method. + * @return desc. + */ + public static String getDesc(final Method m) { + StringBuilder ret = new StringBuilder(m.getName()).append('('); + Class[] parameterTypes = m.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + ret.append(getDesc(parameterTypes[i])); + } + ret.append(')').append(getDesc(m.getReturnType())); + return ret.toString(); + } + + /** + * get constructor desc. + * "()V", "(Ljava/lang/String;I)V" + * + * @param c constructor. + * @return desc + */ + public static String getDesc(final Constructor c) { + StringBuilder ret = new StringBuilder("("); + Class[] parameterTypes = c.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + ret.append(getDesc(parameterTypes[i])); + } + ret.append(')').append('V'); + return ret.toString(); + } + + /** + * get method desc. + * "(I)I", "()V", "(Ljava/lang/String;Z)V" + * + * @param m method. + * @return desc. + */ + public static String getDescWithoutMethodName(Method m) { + StringBuilder ret = new StringBuilder(); + ret.append('('); + Class[] parameterTypes = m.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + ret.append(getDesc(parameterTypes[i])); + } + ret.append(')').append(getDesc(m.getReturnType())); + return ret.toString(); + } + + /** + * get class desc. + * Object.class => "Ljava/lang/Object;" + * boolean[].class => "[Z" + * + * @param c class. + * @return desc. + * @throws NotFoundException + */ + public static String getDesc(final CtClass c) throws NotFoundException { + StringBuilder ret = new StringBuilder(); + if (c.isArray()) { + ret.append('['); + ret.append(getDesc(c.getComponentType())); + } else if (c.isPrimitive()) { + String t = c.getName(); + if ("void".equals(t)) { + ret.append(JVM_VOID); + } else if ("boolean".equals(t)) { + ret.append(JVM_BOOLEAN); + } else if ("byte".equals(t)) { + ret.append(JVM_BYTE); + } else if ("char".equals(t)) { + ret.append(JVM_CHAR); + } else if ("double".equals(t)) { + ret.append(JVM_DOUBLE); + } else if ("float".equals(t)) { + ret.append(JVM_FLOAT); + } else if ("int".equals(t)) { + ret.append(JVM_INT); + } else if ("long".equals(t)) { + ret.append(JVM_LONG); + } else if ("short".equals(t)) { + ret.append(JVM_SHORT); + } + } else { + ret.append('L'); + ret.append(c.getName().replace('.', '/')); + ret.append(';'); + } + return ret.toString(); + } + + /** + * get method desc. + * "do(I)I", "do()V", "do(Ljava/lang/String;Z)V" + * + * @param m method. + * @return desc. + */ + public static String getDesc(final CtMethod m) throws NotFoundException { + StringBuilder ret = new StringBuilder(m.getName()).append('('); + CtClass[] parameterTypes = m.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + ret.append(getDesc(parameterTypes[i])); + } + ret.append(')').append(getDesc(m.getReturnType())); + return ret.toString(); + } + + /** + * get constructor desc. + * "()V", "(Ljava/lang/String;I)V" + * + * @param c constructor. + * @return desc + */ + public static String getDesc(final CtConstructor c) throws NotFoundException { + StringBuilder ret = new StringBuilder("("); + CtClass[] parameterTypes = c.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + ret.append(getDesc(parameterTypes[i])); + } + ret.append(')').append('V'); + return ret.toString(); + } + + /** + * get method desc. + * "(I)I", "()V", "(Ljava/lang/String;Z)V". + * + * @param m method. + * @return desc. + */ + public static String getDescWithoutMethodName(final CtMethod m) throws NotFoundException { + StringBuilder ret = new StringBuilder(); + ret.append('('); + CtClass[] parameterTypes = m.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + ret.append(getDesc(parameterTypes[i])); + } + ret.append(')').append(getDesc(m.getReturnType())); + return ret.toString(); + } + + /** + * name to desc. + * java.util.Map[][] => "[[Ljava/util/Map;" + * + * @param name name. + * @return desc. + */ + public static String name2desc(String name) { + StringBuilder sb = new StringBuilder(); + int c = 0, index = name.indexOf('['); + if (index > 0) { + c = (name.length() - index) / 2; + name = name.substring(0, index); + } + while (c-- > 0) { + sb.append("["); + } + if ("void".equals(name)) { + sb.append(JVM_VOID); + } else if ("boolean".equals(name)) { + sb.append(JVM_BOOLEAN); + } else if ("byte".equals(name)) { + sb.append(JVM_BYTE); + } else if ("char".equals(name)) { + sb.append(JVM_CHAR); + } else if ("double".equals(name)) { + sb.append(JVM_DOUBLE); + } else if ("float".equals(name)) { + sb.append(JVM_FLOAT); + } else if ("int".equals(name)) { + sb.append(JVM_INT); + } else if ("long".equals(name)) { + sb.append(JVM_LONG); + } else if ("short".equals(name)) { + sb.append(JVM_SHORT); + } else { + sb.append('L').append(name.replace('.', '/')).append(';'); + } + return sb.toString(); + } + + /** + * desc to name. + * "[[I" => "int[][]" + * + * @param desc desc. + * @return name. + */ + public static String desc2name(String desc) { + StringBuilder sb = new StringBuilder(); + int c = desc.lastIndexOf('[') + 1; + if (desc.length() == c + 1) { + switch (desc.charAt(c)) { + case JVM_VOID: { + sb.append("void"); + break; + } + case JVM_BOOLEAN: { + sb.append("boolean"); + break; + } + case JVM_BYTE: { + sb.append("byte"); + break; + } + case JVM_CHAR: { + sb.append("char"); + break; + } + case JVM_DOUBLE: { + sb.append("double"); + break; + } + case JVM_FLOAT: { + sb.append("float"); + break; + } + case JVM_INT: { + sb.append("int"); + break; + } + case JVM_LONG: { + sb.append("long"); + break; + } + case JVM_SHORT: { + sb.append("short"); + break; + } + default: + throw new RuntimeException(); + } + } else { + sb.append(desc.substring(c + 1, desc.length() - 1).replace('/', '.')); + } + while (c-- > 0) { + sb.append("[]"); + } + return sb.toString(); + } + + public static Class forName(String name) { + try { + return name2class(name); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Not found class " + name + ", cause: " + e.getMessage(), e); + } + } + + public static Class forName(ClassLoader cl, String name) { + try { + return name2class(cl, name); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Not found class " + name + ", cause: " + e.getMessage(), e); + } + } + + /** + * name to class. + * "boolean" => boolean.class + * "java.util.Map[][]" => java.util.Map[][].class + * + * @param name name. + * @return Class instance. + */ + public static Class name2class(String name) throws ClassNotFoundException { + return name2class(ClassUtils.getClassLoader(), name); + } + + /** + * name to class. + * "boolean" => boolean.class + * "java.util.Map[][]" => java.util.Map[][].class + * + * @param cl ClassLoader instance. + * @param name name. + * @return Class instance. + */ + private static Class name2class(ClassLoader cl, String name) throws ClassNotFoundException { + int c = 0, index = name.indexOf('['); + if (index > 0) { + c = (name.length() - index) / 2; + name = name.substring(0, index); + } + if (c > 0) { + StringBuilder sb = new StringBuilder(); + while (c-- > 0) { + sb.append("["); + } + + if ("void".equals(name)) { + sb.append(JVM_VOID); + } else if ("boolean".equals(name)) { + sb.append(JVM_BOOLEAN); + } else if ("byte".equals(name)) { + sb.append(JVM_BYTE); + } else if ("char".equals(name)) { + sb.append(JVM_CHAR); + } else if ("double".equals(name)) { + sb.append(JVM_DOUBLE); + } else if ("float".equals(name)) { + sb.append(JVM_FLOAT); + } else if ("int".equals(name)) { + sb.append(JVM_INT); + } else if ("long".equals(name)) { + sb.append(JVM_LONG); + } else if ("short".equals(name)) { + sb.append(JVM_SHORT); + } else { + sb.append('L').append(name).append(';'); // "java.lang.Object" ==> "Ljava.lang.Object;" + } + name = sb.toString(); + } else { + if ("void".equals(name)) { + return void.class; + } else if ("boolean".equals(name)) { + return boolean.class; + } else if ("byte".equals(name)) { + return byte.class; + } else if ("char".equals(name)) { + return char.class; + } else if ("double".equals(name)) { + return double.class; + } else if ("float".equals(name)) { + return float.class; + } else if ("int".equals(name)) { + return int.class; + } else if ("long".equals(name)) { + return long.class; + } else if ("short".equals(name)) { + return short.class; + } + } + + if (cl == null) { + cl = ClassUtils.getClassLoader(); + } + Class clazz = NAME_CLASS_CACHE.get(name); + if (clazz == null) { + clazz = Class.forName(name, true, cl); + NAME_CLASS_CACHE.put(name, clazz); + } + return clazz; + } + + /** + * desc to class. + * "[Z" => boolean[].class + * "[[Ljava/util/Map;" => java.util.Map[][].class + * + * @param desc desc. + * @return Class instance. + * @throws ClassNotFoundException + */ + public static Class desc2class(String desc) throws ClassNotFoundException { + return desc2class(ClassUtils.getClassLoader(), desc); + } + + /** + * desc to class. + * "[Z" => boolean[].class + * "[[Ljava/util/Map;" => java.util.Map[][].class + * + * @param cl ClassLoader instance. + * @param desc desc. + * @return Class instance. + * @throws ClassNotFoundException + */ + private static Class desc2class(ClassLoader cl, String desc) throws ClassNotFoundException { + switch (desc.charAt(0)) { + case JVM_VOID: + return void.class; + case JVM_BOOLEAN: + return boolean.class; + case JVM_BYTE: + return byte.class; + case JVM_CHAR: + return char.class; + case JVM_DOUBLE: + return double.class; + case JVM_FLOAT: + return float.class; + case JVM_INT: + return int.class; + case JVM_LONG: + return long.class; + case JVM_SHORT: + return short.class; + case 'L': + desc = desc.substring(1, desc.length() - 1).replace('/', '.'); // "Ljava/lang/Object;" ==> "java.lang.Object" + break; + case '[': + desc = desc.replace('/', '.'); // "[[Ljava/lang/Object;" ==> "[[Ljava.lang.Object;" + break; + default: + throw new ClassNotFoundException("Class not found: " + desc); + } + + if (cl == null) { + cl = ClassUtils.getClassLoader(); + } + Class clazz = DESC_CLASS_CACHE.get(desc); + if (clazz == null) { + clazz = Class.forName(desc, true, cl); + DESC_CLASS_CACHE.put(desc, clazz); + } + return clazz; + } + + /** + * get class array instance. + * + * @param desc desc. + * @return Class class array. + * @throws ClassNotFoundException + */ + public static Class[] desc2classArray(String desc) throws ClassNotFoundException { + Class[] ret = desc2classArray(ClassUtils.getClassLoader(), desc); + return ret; + } + + /** + * get class array instance. + * + * @param cl ClassLoader instance. + * @param desc desc. + * @return Class[] class array. + * @throws ClassNotFoundException + */ + private static Class[] desc2classArray(ClassLoader cl, String desc) throws ClassNotFoundException { + if (desc.length() == 0) { + return EMPTY_CLASS_ARRAY; + } + + List> cs = new ArrayList>(); + Matcher m = DESC_PATTERN.matcher(desc); + while (m.find()) { + cs.add(desc2class(cl, m.group())); + } + return cs.toArray(EMPTY_CLASS_ARRAY); + } + + /** + * Find method from method signature + * + * @param clazz Target class to find method + * @param methodName Method signature, e.g.: method1(int, String). It is allowed to provide method name only, e.g.: method2 + * @return target method + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws IllegalStateException when multiple methods are found (overridden method when parameter info is not provided) + */ + public static Method findMethodByMethodSignature(Class clazz, String methodName, String[] parameterTypes) + throws NoSuchMethodException, ClassNotFoundException { + String signature = clazz.getName() + "." + methodName; + if (parameterTypes != null && parameterTypes.length > 0) { + signature += StringUtils.join(parameterTypes); + } + Method method = Signature_METHODS_CACHE.get(signature); + if (method != null) { + return method; + } + if (parameterTypes == null) { + List finded = new ArrayList(); + for (Method m : clazz.getMethods()) { + if (m.getName().equals(methodName)) { + finded.add(m); + } + } + if (finded.isEmpty()) { + throw new NoSuchMethodException("No such method " + methodName + " in class " + clazz); + } + if (finded.size() > 1) { + String msg = String.format("Not unique method for method name(%s) in class(%s), find %d methods.", + methodName, clazz.getName(), finded.size()); + throw new IllegalStateException(msg); + } + method = finded.get(0); + } else { + Class[] types = new Class[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + types[i] = ReflectUtils.name2class(parameterTypes[i]); + } + method = clazz.getMethod(methodName, types); + + } + Signature_METHODS_CACHE.put(signature, method); + return method; + } + + public static Method findMethodByMethodName(Class clazz, String methodName) + throws NoSuchMethodException, ClassNotFoundException { + return findMethodByMethodSignature(clazz, methodName, null); + } + + public static Constructor findConstructor(Class clazz, Class paramType) throws NoSuchMethodException { + Constructor targetConstructor; + try { + targetConstructor = clazz.getConstructor(new Class[]{paramType}); + } catch (NoSuchMethodException e) { + targetConstructor = null; + Constructor[] constructors = clazz.getConstructors(); + for (Constructor constructor : constructors) { + if (Modifier.isPublic(constructor.getModifiers()) + && constructor.getParameterTypes().length == 1 + && constructor.getParameterTypes()[0].isAssignableFrom(paramType)) { + targetConstructor = constructor; + break; + } + } + if (targetConstructor == null) { + throw e; + } + } + return targetConstructor; + } + + /** + * Check if one object is the implementation for a given interface. + *

+ * This method will not trigger classloading for the given interface, therefore it will not lead to error when + * the given interface is not visible by the classloader + * + * @param obj Object to examine + * @param interfaceClazzName The given interface + * @return true if the object implements the given interface, otherwise return false + */ + public static boolean isInstance(Object obj, String interfaceClazzName) { + for (Class clazz = obj.getClass(); + clazz != null && !clazz.equals(Object.class); + clazz = clazz.getSuperclass()) { + Class[] interfaces = clazz.getInterfaces(); + for (Class itf : interfaces) { + if (itf.getName().equals(interfaceClazzName)) { + return true; + } + } + } + return false; + } + + public static Object getEmptyObject(Class returnType) { + return getEmptyObject(returnType, new HashMap, Object>(), 0); + } + + private static Object getEmptyObject(Class returnType, Map, Object> emptyInstances, int level) { + if (level > 2) { + return null; + } + if (returnType == null) { + return null; + } else if (returnType == boolean.class || returnType == Boolean.class) { + return false; + } else if (returnType == char.class || returnType == Character.class) { + return '\0'; + } else if (returnType == byte.class || returnType == Byte.class) { + return (byte) 0; + } else if (returnType == short.class || returnType == Short.class) { + return (short) 0; + } else if (returnType == int.class || returnType == Integer.class) { + return 0; + } else if (returnType == long.class || returnType == Long.class) { + return 0L; + } else if (returnType == float.class || returnType == Float.class) { + return 0F; + } else if (returnType == double.class || returnType == Double.class) { + return 0D; + } else if (returnType.isArray()) { + return Array.newInstance(returnType.getComponentType(), 0); + } else if (returnType.isAssignableFrom(ArrayList.class)) { + return new ArrayList(0); + } else if (returnType.isAssignableFrom(HashSet.class)) { + return new HashSet(0); + } else if (returnType.isAssignableFrom(HashMap.class)) { + return new HashMap(0); + } else if (String.class.equals(returnType)) { + return ""; + } else if (!returnType.isInterface()) { + try { + Object value = emptyInstances.get(returnType); + if (value == null) { + value = returnType.newInstance(); + emptyInstances.put(returnType, value); + } + Class cls = value.getClass(); + while (cls != null && cls != Object.class) { + Field[] fields = cls.getDeclaredFields(); + for (Field field : fields) { + if (field.isSynthetic()) { + continue; + } + Object property = getEmptyObject(field.getType(), emptyInstances, level + 1); + if (property != null) { + try { + if (!field.isAccessible()) { + field.setAccessible(true); + } + field.set(value, property); + } catch (Throwable e) { + } + } + } + cls = cls.getSuperclass(); + } + return value; + } catch (Throwable e) { + return null; + } + } else { + return null; + } + } + + public static boolean isBeanPropertyReadMethod(Method method) { + return method != null + && Modifier.isPublic(method.getModifiers()) + && !Modifier.isStatic(method.getModifiers()) + && method.getReturnType() != void.class + && method.getDeclaringClass() != Object.class + && method.getParameterTypes().length == 0 + && ((method.getName().startsWith("get") && method.getName().length() > 3) + || (method.getName().startsWith("is") && method.getName().length() > 2)); + } + + public static String getPropertyNameFromBeanReadMethod(Method method) { + if (isBeanPropertyReadMethod(method)) { + if (method.getName().startsWith("get")) { + return method.getName().substring(3, 4).toLowerCase() + + method.getName().substring(4); + } + if (method.getName().startsWith("is")) { + return method.getName().substring(2, 3).toLowerCase() + + method.getName().substring(3); + } + } + return null; + } + + public static boolean isBeanPropertyWriteMethod(Method method) { + return method != null + && Modifier.isPublic(method.getModifiers()) + && !Modifier.isStatic(method.getModifiers()) + && method.getDeclaringClass() != Object.class + && method.getParameterTypes().length == 1 + && method.getName().startsWith("set") + && method.getName().length() > 3; + } + + public static String getPropertyNameFromBeanWriteMethod(Method method) { + if (isBeanPropertyWriteMethod(method)) { + return method.getName().substring(3, 4).toLowerCase() + + method.getName().substring(4); + } + return null; + } + + public static boolean isPublicInstanceField(Field field) { + return Modifier.isPublic(field.getModifiers()) + && !Modifier.isStatic(field.getModifiers()) + && !Modifier.isFinal(field.getModifiers()) + && !field.isSynthetic(); + } + + public static Map getBeanPropertyFields(Class cl) { + Map properties = new HashMap(); + for (; cl != null; cl = cl.getSuperclass()) { + Field[] fields = cl.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isTransient(field.getModifiers()) + || Modifier.isStatic(field.getModifiers())) { + continue; + } + + field.setAccessible(true); + + properties.put(field.getName(), field); + } + } + + return properties; + } + + public static Map getBeanPropertyReadMethods(Class cl) { + Map properties = new HashMap(); + for (; cl != null; cl = cl.getSuperclass()) { + Method[] methods = cl.getDeclaredMethods(); + for (Method method : methods) { + if (isBeanPropertyReadMethod(method)) { + method.setAccessible(true); + String property = getPropertyNameFromBeanReadMethod(method); + properties.put(property, method); + } + } + } + + return properties; + } + + public static Type[] getReturnTypes(Method method) { + Class returnType = method.getReturnType(); + Type genericReturnType = method.getGenericReturnType(); + if (Future.class.isAssignableFrom(returnType)) { + if (genericReturnType instanceof ParameterizedType) { + Type actualArgType = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0]; + if (actualArgType instanceof ParameterizedType) { + returnType = (Class) ((ParameterizedType) actualArgType).getRawType(); + genericReturnType = actualArgType; + } else { + returnType = (Class) actualArgType; + genericReturnType = returnType; + } + } else { + returnType = null; + genericReturnType = null; + } + } + return new Type[]{returnType, genericReturnType}; + } + + /** + * 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 3a4584d90c3..0b145d8cb22 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 3b8eb93add7..0b99f114044 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 @@ -1,663 +1,693 @@ -/* - * 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; - -import org.apache.dubbo.common.URL; -import org.apache.dubbo.common.Version; -import org.apache.dubbo.common.bytecode.Wrapper; -import org.apache.dubbo.common.extension.ExtensionLoader; -import org.apache.dubbo.common.utils.ClassUtils; -import org.apache.dubbo.common.utils.CollectionUtils; -import org.apache.dubbo.common.utils.ConfigUtils; -import org.apache.dubbo.common.utils.NetUtils; -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.support.Parameter; -import org.apache.dubbo.metadata.integration.MetadataReportService; -import org.apache.dubbo.remoting.Constants; -import org.apache.dubbo.rpc.Invoker; -import org.apache.dubbo.rpc.Protocol; -import org.apache.dubbo.rpc.ProxyFactory; -import org.apache.dubbo.rpc.cluster.Cluster; -import org.apache.dubbo.rpc.cluster.directory.StaticDirectory; -import org.apache.dubbo.rpc.cluster.support.ClusterUtils; -import org.apache.dubbo.rpc.cluster.support.RegistryAwareCluster; -import org.apache.dubbo.rpc.model.ApplicationModel; -import org.apache.dubbo.rpc.model.ConsumerModel; -import org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol; -import org.apache.dubbo.rpc.service.GenericService; -import org.apache.dubbo.rpc.support.ProtocolUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import static org.apache.dubbo.common.constants.CommonConstants.ANY_VALUE; -import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SEPARATOR; -import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER_SIDE; -import static org.apache.dubbo.common.constants.CommonConstants.DUBBO; -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.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.rpc.cluster.Constants.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.registry.Constants.CONSUMER_PROTOCOL; -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; - -/** - * ReferenceConfig - * - * @export - */ -public class ReferenceConfig extends AbstractReferenceConfig { - - private static final long serialVersionUID = -5864351140409987595L; - - /** - * The {@link Protocol} implementation with adaptive functionality,it will be different in different scenarios. - * A particular {@link Protocol} implementation is determined by the protocol attribute in the {@link URL}. - * For example: - * - *
  • when the url is registry://224.5.6.7:1234/org.apache.dubbo.registry.RegistryService?application=dubbo-sample, - * then the protocol is RegistryProtocol
  • - * - *
  • when the url is dubbo://224.5.6.7:1234/org.apache.dubbo.config.api.DemoService?application=dubbo-sample, then - * the protocol is DubboProtocol
  • - *

    - * Actually,when the {@link ExtensionLoader} init the {@link Protocol} instants,it will automatically wraps two - * layers, and eventually will get a ProtocolFilterWrapper or ProtocolListenerWrapper - */ - private static final Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); - - /** - * The {@link Cluster}'s implementation with adaptive functionality, and actually it will get a {@link Cluster}'s - * specific implementation who is wrapped with MockClusterInvoker - */ - private static final Cluster CLUSTER = ExtensionLoader.getExtensionLoader(Cluster.class).getAdaptiveExtension(); - - /** - * A {@link ProxyFactory} implementation that will generate a reference service's proxy,the JavassistProxyFactory is - * its default implementation - */ - private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); - - /** - * The url of the reference service - */ - private final List urls = new ArrayList(); - - /** - * The interface name of the reference service - */ - private String interfaceName; - - /** - * The interface class of the reference service - */ - private Class interfaceClass; - - /** - * client type - */ - private String client; - - /** - * The url for peer-to-peer invocation - */ - private String url; - - /** - * The method configs - */ - private List methods; - - /** - * The consumer config (default) - */ - private ConsumerConfig consumer; - - /** - * Only the service provider of the specified protocol is invoked, and other protocols are ignored. - */ - private String protocol; - - /** - * The interface proxy reference - */ - private transient volatile T ref; - - /** - * The invoker of the reference service - */ - private transient volatile Invoker invoker; - - /** - * The flag whether the ReferenceConfig has been initialized - */ - private transient volatile boolean initialized; - - /** - * whether this ReferenceConfig has been destroyed - */ - private transient volatile boolean destroyed; - - @SuppressWarnings("unused") - private final Object finalizerGuardian = new Object() { - @Override - protected void finalize() throws Throwable { - super.finalize(); - - if (!ReferenceConfig.this.destroyed) { - logger.warn("ReferenceConfig(" + url + ") is not DESTROYED when FINALIZE"); - - /* don't destroy for now - try { - ReferenceConfig.this.destroy(); - } catch (Throwable t) { - logger.warn("Unexpected err when destroy invoker of ReferenceConfig(" + url + ") in finalize method!", t); - } - */ - } - } - }; - - public ReferenceConfig() { - } - - public ReferenceConfig(Reference reference) { - appendAnnotation(Reference.class, reference); - setMethods(MethodConfig.constructMethodConfig(reference.methods())); - } - - public URL toUrl() { - return urls.isEmpty() ? null : urls.iterator().next(); - } - - public List toUrls() { - return urls; - } - - /** - * This method should be called right after the creation of this class's instance, before any property in other config modules is used. - * Check each config modules are created properly and override their properties if necessary. - */ - public void checkAndUpdateSubConfigs() { - if (StringUtils.isEmpty(interfaceName)) { - throw new IllegalStateException(" interface not allow null!"); - } - completeCompoundConfigs(); - startConfigCenter(); - // get consumer's global configuration - checkDefault(); - this.refresh(); - if (getGeneric() == null && getConsumer() != null) { - setGeneric(getConsumer().getGeneric()); - } - if (ProtocolUtils.isGeneric(getGeneric())) { - interfaceClass = GenericService.class; - } else { - try { - interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() - .getContextClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(e.getMessage(), e); - } - checkInterfaceAndMethods(interfaceClass, methods); - } - resolveFile(); - checkApplication(); - checkMetadataReport(); - } - - public synchronized T get() { - checkAndUpdateSubConfigs(); - - if (destroyed) { - throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!"); - } - if (ref == null) { - init(); - } - return ref; - } - - public synchronized void destroy() { - if (ref == null) { - return; - } - if (destroyed) { - return; - } - destroyed = true; - try { - invoker.destroy(); - } catch (Throwable t) { - logger.warn("Unexpected error occured when destroy invoker of ReferenceConfig(" + url + ").", t); - } - invoker = null; - ref = null; - } - - private void init() { - if (initialized) { - return; - } - checkStubAndLocal(interfaceClass); - checkMock(interfaceClass); - Map map = new HashMap(); - - map.put(SIDE_KEY, CONSUMER_SIDE); - - appendRuntimeParameters(map); - if (!isGeneric()) { - String revision = Version.getVersion(interfaceClass, version); - if (revision != null && revision.length() > 0) { - map.put(REVISION_KEY, revision); - } - - String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); - if (methods.length == 0) { - logger.warn("No method found in service interface " + interfaceClass.getName()); - map.put(METHODS_KEY, ANY_VALUE); - } else { - map.put(METHODS_KEY, StringUtils.join(new HashSet(Arrays.asList(methods)), COMMA_SEPARATOR)); - } - } - map.put(INTERFACE_KEY, interfaceName); - appendParameters(map, metrics); - appendParameters(map, application); - appendParameters(map, module); - // remove 'default.' prefix for configs from ConsumerConfig - // appendParameters(map, consumer, Constants.DEFAULT_KEY); - appendParameters(map, consumer); - appendParameters(map, this); - Map attributes = null; - if (CollectionUtils.isNotEmpty(methods)) { - attributes = new HashMap(); - for (MethodConfig methodConfig : methods) { - appendParameters(map, methodConfig, methodConfig.getName()); - String retryKey = methodConfig.getName() + ".retry"; - if (map.containsKey(retryKey)) { - String retryValue = map.remove(retryKey); - if ("false".equals(retryValue)) { - map.put(methodConfig.getName() + ".retries", "0"); - } - } - attributes.put(methodConfig.getName(), convertMethodConfig2AyncInfo(methodConfig)); - } - } - - String hostToRegistry = ConfigUtils.getSystemProperty(DUBBO_IP_TO_REGISTRY); - if (StringUtils.isEmpty(hostToRegistry)) { - hostToRegistry = NetUtils.getLocalHost(); - } else if (isInvalidLocalHost(hostToRegistry)) { - throw new IllegalArgumentException("Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry); - } - map.put(REGISTER_IP_KEY, hostToRegistry); - - ref = createProxy(map); - - String serviceKey = URL.buildKey(interfaceName, group, version); - ApplicationModel.initConsumerModel(serviceKey, buildConsumerModel(serviceKey, attributes)); - initialized = true; - } - - private ConsumerModel buildConsumerModel(String serviceKey, Map attributes) { - Method[] methods = interfaceClass.getMethods(); - Class serviceInterface = interfaceClass; - if (interfaceClass == GenericService.class) { - try { - serviceInterface = Class.forName(interfaceName); - methods = serviceInterface.getMethods(); - } catch (ClassNotFoundException e) { - methods = interfaceClass.getMethods(); - } - } - return new ConsumerModel(serviceKey, serviceInterface, ref, methods, attributes); - } - - @SuppressWarnings({"unchecked", "rawtypes", "deprecation"}) - private T createProxy(Map map) { - if (shouldJvmRefer(map)) { - URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map); - invoker = REF_PROTOCOL.refer(interfaceClass, url); - if (logger.isInfoEnabled()) { - logger.info("Using injvm service " + interfaceClass.getName()); - } - } else { - if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address. - String[] us = SEMICOLON_SPLIT_PATTERN.split(url); - if (us != null && us.length > 0) { - for (String u : us) { - URL url = URL.valueOf(u); - if (StringUtils.isEmpty(url.getPath())) { - url = url.setPath(interfaceName); - } - if (REGISTRY_PROTOCOL.equals(url.getProtocol())) { - urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map))); - } else { - urls.add(ClusterUtils.mergeUrl(url, map)); - } - } - } - } else { // assemble URL from register center's configuration - // if protocols not injvm checkRegistry - if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())){ - checkRegistry(); - List us = loadRegistries(false); - if (CollectionUtils.isNotEmpty(us)) { - for (URL u : us) { - URL monitorUrl = loadMonitor(u); - if (monitorUrl != null) { - map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString())); - } - urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map))); - } - } - if (urls.isEmpty()) { - throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config to your spring config."); - } - } - } - - if (urls.size() == 1) { - invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0)); - } else { - List> invokers = new ArrayList>(); - URL registryURL = null; - for (URL url : urls) { - invokers.add(REF_PROTOCOL.refer(interfaceClass, url)); - if (REGISTRY_PROTOCOL.equals(url.getProtocol())) { - registryURL = url; // use last registry url - } - } - if (registryURL != null) { // registry url is available - // use RegistryAwareCluster only when register's CLUSTER is available - URL u = registryURL.addParameter(CLUSTER_KEY, RegistryAwareCluster.NAME); - // The invoker wrap relation would be: RegistryAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, will execute route) -> Invoker - invoker = CLUSTER.join(new StaticDirectory(u, invokers)); - } else { // not a registry url, must be direct invoke. - invoker = CLUSTER.join(new StaticDirectory(invokers)); - } - } - } - - if (shouldCheck() && !invoker.isAvailable()) { - throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion()); - } - if (logger.isInfoEnabled()) { - logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl()); - } - /** - * @since 2.7.0 - * ServiceData Store - */ - MetadataReportService metadataReportService = null; - if ((metadataReportService = getMetadataReportService()) != null) { - URL consumerURL = new URL(CONSUMER_PROTOCOL, map.remove(REGISTER_IP_KEY), 0, map.get(INTERFACE_KEY), map); - metadataReportService.publishConsumer(consumerURL); - } - // create service proxy - return (T) PROXY_FACTORY.getProxy(invoker); - } - - /** - * Figure out should refer the service in the same JVM from configurations. The default behavior is true - * 1. if injvm is specified, then use it - * 2. then if a url is specified, then assume it's a remote call - * 3. otherwise, check scope parameter - * 4. if scope is not specified but the target service is provided in the same JVM, then prefer to make the local - * call, which is the default behavior - */ - protected boolean shouldJvmRefer(Map map) { - URL tmpUrl = new URL("temp", "localhost", 0, map); - boolean isJvmRefer; - if (isInjvm() == null) { - // if a url is specified, don't do local reference - if (url != null && url.length() > 0) { - isJvmRefer = false; - } else { - // by default, reference local service if there is - isJvmRefer = InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl); - } - } else { - isJvmRefer = isInjvm(); - } - return isJvmRefer; - } - - protected boolean shouldCheck() { - Boolean shouldCheck = isCheck(); - if (shouldCheck == null && getConsumer() != null) { - shouldCheck = getConsumer().isCheck(); - } - if (shouldCheck == null) { - // default true - shouldCheck = true; - } - return shouldCheck; - } - - protected boolean shouldInit() { - Boolean shouldInit = isInit(); - if (shouldInit == null && getConsumer() != null) { - shouldInit = getConsumer().isInit(); - } - if (shouldInit == null) { - // default is false - return false; - } - return shouldInit; - } - - private void checkDefault() { - if (consumer != null) { - return; - } - setConsumer(ConfigManager.getInstance().getDefaultConsumer().orElseGet(() -> { - ConsumerConfig consumerConfig = new ConsumerConfig(); - consumerConfig.refresh(); - return consumerConfig; - })); - } - - private void completeCompoundConfigs() { - if (consumer != null) { - if (application == null) { - setApplication(consumer.getApplication()); - } - if (module == null) { - setModule(consumer.getModule()); - } - if (registries == null) { - setRegistries(consumer.getRegistries()); - } - if (monitor == null) { - setMonitor(consumer.getMonitor()); - } - } - if (module != null) { - if (registries == null) { - setRegistries(module.getRegistries()); - } - if (monitor == null) { - setMonitor(module.getMonitor()); - } - } - if (application != null) { - if (registries == null) { - setRegistries(application.getRegistries()); - } - if (monitor == null) { - setMonitor(application.getMonitor()); - } - } - } - - public Class getInterfaceClass() { - if (interfaceClass != null) { - return interfaceClass; - } - if (isGeneric() - || (getConsumer() != null && getConsumer().isGeneric())) { - return GenericService.class; - } - try { - if (interfaceName != null && interfaceName.length() > 0) { - this.interfaceClass = Class.forName(interfaceName, true, ClassUtils.getClassLoader()); - } - } catch (ClassNotFoundException t) { - throw new IllegalStateException(t.getMessage(), t); - } - return interfaceClass; - } - - /** - * @param interfaceClass - * @see #setInterface(Class) - * @deprecated - */ - @Deprecated - public void setInterfaceClass(Class interfaceClass) { - setInterface(interfaceClass); - } - - public String getInterface() { - return interfaceName; - } - - public void setInterface(String interfaceName) { - this.interfaceName = interfaceName; - if (StringUtils.isEmpty(id)) { - id = interfaceName; - } - } - - public void setInterface(Class interfaceClass) { - if (interfaceClass != null && !interfaceClass.isInterface()) { - throw new IllegalStateException("The interface class " + interfaceClass + " is not a interface!"); - } - this.interfaceClass = interfaceClass; - setInterface(interfaceClass == null ? null : interfaceClass.getName()); - } - - public String getClient() { - return client; - } - - public void setClient(String client) { - checkName(Constants.CLIENT_KEY, client); - this.client = client; - } - - @Parameter(excluded = true) - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public List getMethods() { - return methods; - } - - @SuppressWarnings("unchecked") - public void setMethods(List methods) { - this.methods = (List) methods; - } - - public ConsumerConfig getConsumer() { - return consumer; - } - - public void setConsumer(ConsumerConfig consumer) { - ConfigManager.getInstance().addConsumer(consumer); - this.consumer = consumer; - } - - public String getProtocol() { - return protocol; - } - - public void setProtocol(String protocol) { - this.protocol = protocol; - } - - // just for test - Invoker getInvoker() { - return invoker; - } - - @Override - @Parameter(excluded = true) - public String getPrefix() { - return DUBBO + ".reference." + interfaceName; - } - - private void resolveFile() { - String resolve = System.getProperty(interfaceName); - String resolveFile = null; - if (StringUtils.isEmpty(resolve)) { - resolveFile = System.getProperty("dubbo.resolve.file"); - if (StringUtils.isEmpty(resolveFile)) { - File userResolveFile = new File(new File(System.getProperty("user.home")), "dubbo-resolve.properties"); - if (userResolveFile.exists()) { - resolveFile = userResolveFile.getAbsolutePath(); - } - } - if (resolveFile != null && resolveFile.length() > 0) { - Properties properties = new Properties(); - try (FileInputStream fis = new FileInputStream(new File(resolveFile))) { - properties.load(fis); - } catch (IOException e) { - throw new IllegalStateException("Failed to load " + resolveFile + ", cause: " + e.getMessage(), e); - } - - resolve = properties.getProperty(interfaceName); - } - } - if (resolve != null && resolve.length() > 0) { - url = resolve; - if (logger.isWarnEnabled()) { - if (resolveFile != null) { - logger.warn("Using default dubbo resolve file " + resolveFile + " replace " + interfaceName + "" + resolve + " to p2p invoke remote service."); - } else { - logger.warn("Using -D" + interfaceName + "=" + resolve + " to p2p invoke remote service."); - } - } - } - } -} +/* + * 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; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.Version; +import org.apache.dubbo.common.bytecode.Wrapper; +import org.apache.dubbo.common.extension.ExtensionLoader; +import org.apache.dubbo.common.utils.ClassUtils; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.ConfigUtils; +import org.apache.dubbo.common.utils.NetUtils; +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; +import org.apache.dubbo.rpc.Protocol; +import org.apache.dubbo.rpc.ProxyFactory; +import org.apache.dubbo.rpc.cluster.Cluster; +import org.apache.dubbo.rpc.cluster.directory.StaticDirectory; +import org.apache.dubbo.rpc.cluster.support.ClusterUtils; +import org.apache.dubbo.rpc.cluster.support.RegistryAwareCluster; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ConsumerModel; +import org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol; +import org.apache.dubbo.rpc.service.GenericService; +import org.apache.dubbo.rpc.support.ProtocolUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.apache.dubbo.common.constants.CommonConstants.ANY_VALUE; +import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SEPARATOR; +import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER_SIDE; +import static org.apache.dubbo.common.constants.CommonConstants.DUBBO; +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.rpc.cluster.Constants.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.registry.Constants.CONSUMER_PROTOCOL; +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.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 + * + * @export + */ +public class ReferenceConfig extends AbstractReferenceConfig { + + private static final long serialVersionUID = -5864351140409987595L; + + /** + * The {@link Protocol} implementation with adaptive functionality,it will be different in different scenarios. + * A particular {@link Protocol} implementation is determined by the protocol attribute in the {@link URL}. + * For example: + * + *

  • when the url is registry://224.5.6.7:1234/org.apache.dubbo.registry.RegistryService?application=dubbo-sample, + * then the protocol is RegistryProtocol
  • + * + *
  • when the url is dubbo://224.5.6.7:1234/org.apache.dubbo.config.api.DemoService?application=dubbo-sample, then + * the protocol is DubboProtocol
  • + *

    + * Actually,when the {@link ExtensionLoader} init the {@link Protocol} instants,it will automatically wraps two + * layers, and eventually will get a ProtocolFilterWrapper or ProtocolListenerWrapper + */ + private static final Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + + /** + * The {@link Cluster}'s implementation with adaptive functionality, and actually it will get a {@link Cluster}'s + * specific implementation who is wrapped with MockClusterInvoker + */ + private static final Cluster CLUSTER = ExtensionLoader.getExtensionLoader(Cluster.class).getAdaptiveExtension(); + + /** + * A {@link ProxyFactory} implementation that will generate a reference service's proxy,the JavassistProxyFactory is + * its default implementation + */ + private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + + /** + * The url of the reference service + */ + private final List urls = new ArrayList(); + + /** + * The {@link EventDispatcher} + */ + private final EventDispatcher eventDispatcher = EventDispatcher.getDefaultExtension(); + + /** + * The interface name of the reference service + */ + private String interfaceName; + + /** + * The interface class of the reference service + */ + private Class interfaceClass; + + /** + * client type + */ + private String client; + + /** + * The url for peer-to-peer invocation + */ + private String url; + + /** + * The method configs + */ + private List methods; + + /** + * The consumer config (default) + */ + private ConsumerConfig consumer; + + /** + * Only the service provider of the specified protocol is invoked, and other protocols are ignored. + */ + private String protocol; + + /** + * The interface proxy reference + */ + private transient volatile T ref; + + /** + * The invoker of the reference service + */ + private transient volatile Invoker invoker; + + /** + * The flag whether the ReferenceConfig has been initialized + */ + private transient volatile boolean initialized; + + /** + * whether this ReferenceConfig has been destroyed + */ + private transient volatile boolean destroyed; + + @SuppressWarnings("unused") + private final Object finalizerGuardian = new Object() { + @Override + protected void finalize() throws Throwable { + super.finalize(); + + if (!ReferenceConfig.this.destroyed) { + logger.warn("ReferenceConfig(" + url + ") is not DESTROYED when FINALIZE"); + + /* don't destroy for now + try { + ReferenceConfig.this.destroy(); + } catch (Throwable t) { + logger.warn("Unexpected err when destroy invoker of ReferenceConfig(" + url + ") in finalize method!", t); + } + */ + } + } + }; + + public ReferenceConfig() { + } + + public ReferenceConfig(Reference reference) { + appendAnnotation(Reference.class, reference); + setMethods(MethodConfig.constructMethodConfig(reference.methods())); + } + + public URL toUrl() { + return urls.isEmpty() ? null : urls.iterator().next(); + } + + public List toUrls() { + return urls; + } + + /** + * This method should be called right after the creation of this class's instance, before any property in other config modules is used. + * Check each config modules are created properly and override their properties if necessary. + */ + public void checkAndUpdateSubConfigs() { + if (StringUtils.isEmpty(interfaceName)) { + throw new IllegalStateException(" interface not allow null!"); + } + completeCompoundConfigs(); + startConfigCenter(); + // get consumer's global configuration + checkDefault(); + this.refresh(); + if (getGeneric() == null && getConsumer() != null) { + setGeneric(getConsumer().getGeneric()); + } + if (ProtocolUtils.isGeneric(getGeneric())) { + interfaceClass = GenericService.class; + } else { + try { + interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() + .getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e.getMessage(), e); + } + checkInterfaceAndMethods(interfaceClass, methods); + } + resolveFile(); + checkApplication(); + checkMetadataReport(); + } + + public synchronized T get() { + checkAndUpdateSubConfigs(); + + if (destroyed) { + throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!"); + } + if (ref == null) { + init(); + } + return ref; + } + + public synchronized void destroy() { + if (ref == null) { + return; + } + if (destroyed) { + return; + } + destroyed = true; + try { + invoker.destroy(); + } catch (Throwable t) { + logger.warn("Unexpected error occured when destroy invoker of ReferenceConfig(" + url + ").", t); + } + invoker = null; + ref = null; + + // dispatch a ReferenceConfigDestroyedEvent since 2.7.2 + dispatch(new ReferenceConfigDestroyedEvent(this)); + } + + private void init() { + if (initialized) { + return; + } + checkStubAndLocal(interfaceClass); + checkMock(interfaceClass); + Map map = new HashMap(); + + map.put(SIDE_KEY, CONSUMER_SIDE); + + appendRuntimeParameters(map); + if (!isGeneric()) { + String revision = Version.getVersion(interfaceClass, version); + if (revision != null && revision.length() > 0) { + map.put(REVISION_KEY, revision); + } + + String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); + if (methods.length == 0) { + logger.warn("No method found in service interface " + interfaceClass.getName()); + map.put(METHODS_KEY, ANY_VALUE); + } else { + map.put(METHODS_KEY, StringUtils.join(new HashSet(Arrays.asList(methods)), COMMA_SEPARATOR)); + } + } + map.put(INTERFACE_KEY, interfaceName); + appendParameters(map, metrics); + appendParameters(map, application); + appendParameters(map, module); + // remove 'default.' prefix for configs from ConsumerConfig + // appendParameters(map, consumer, Constants.DEFAULT_KEY); + appendParameters(map, consumer); + appendParameters(map, this); + Map attributes = null; + if (CollectionUtils.isNotEmpty(methods)) { + attributes = new HashMap(); + for (MethodConfig methodConfig : methods) { + appendParameters(map, methodConfig, methodConfig.getName()); + String retryKey = methodConfig.getName() + ".retry"; + if (map.containsKey(retryKey)) { + String retryValue = map.remove(retryKey); + if ("false".equals(retryValue)) { + map.put(methodConfig.getName() + ".retries", "0"); + } + } + attributes.put(methodConfig.getName(), convertMethodConfig2AyncInfo(methodConfig)); + } + } + + String hostToRegistry = ConfigUtils.getSystemProperty(DUBBO_IP_TO_REGISTRY); + if (StringUtils.isEmpty(hostToRegistry)) { + hostToRegistry = NetUtils.getLocalHost(); + } else if (isInvalidLocalHost(hostToRegistry)) { + throw new IllegalArgumentException("Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry); + } + map.put(REGISTER_IP_KEY, hostToRegistry); + + ref = createProxy(map); + + 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) { + Method[] methods = interfaceClass.getMethods(); + Class serviceInterface = interfaceClass; + if (interfaceClass == GenericService.class) { + try { + serviceInterface = Class.forName(interfaceName); + methods = serviceInterface.getMethods(); + } catch (ClassNotFoundException e) { + methods = interfaceClass.getMethods(); + } + } + return new ConsumerModel(serviceKey, serviceInterface, ref, methods, attributes); + } + + @SuppressWarnings({"unchecked", "rawtypes", "deprecation"}) + private T createProxy(Map map) { + if (shouldJvmRefer(map)) { + URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map); + invoker = REF_PROTOCOL.refer(interfaceClass, url); + if (logger.isInfoEnabled()) { + logger.info("Using injvm service " + interfaceClass.getName()); + } + } else { + if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address. + String[] us = SEMICOLON_SPLIT_PATTERN.split(url); + if (us != null && us.length > 0) { + for (String u : us) { + URL url = URL.valueOf(u); + if (StringUtils.isEmpty(url.getPath())) { + url = url.setPath(interfaceName); + } + if (REGISTRY_PROTOCOL.equals(url.getProtocol())) { + urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map))); + } else { + urls.add(ClusterUtils.mergeUrl(url, map)); + } + } + } + } else { // assemble URL from register center's configuration + // if protocols not injvm checkRegistry + if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) { + checkRegistry(); + List us = loadRegistries(false); + if (CollectionUtils.isNotEmpty(us)) { + for (URL u : us) { + URL monitorUrl = loadMonitor(u); + if (monitorUrl != null) { + map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString())); + } + urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map))); + } + } + if (urls.isEmpty()) { + throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config to your spring config."); + } + } + } + + if (urls.size() == 1) { + invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0)); + } else { + List> invokers = new ArrayList>(); + URL registryURL = null; + for (URL url : urls) { + invokers.add(REF_PROTOCOL.refer(interfaceClass, url)); + if (REGISTRY_PROTOCOL.equals(url.getProtocol())) { + registryURL = url; // use last registry url + } + } + if (registryURL != null) { // registry url is available + // use RegistryAwareCluster only when register's CLUSTER is available + URL u = registryURL.addParameter(CLUSTER_KEY, RegistryAwareCluster.NAME); + // The invoker wrap relation would be: RegistryAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, will execute route) -> Invoker + invoker = CLUSTER.join(new StaticDirectory(u, invokers)); + } else { // not a registry url, must be direct invoke. + invoker = CLUSTER.join(new StaticDirectory(invokers)); + } + } + } + + if (shouldCheck() && !invoker.isAvailable()) { + throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion()); + } + if (logger.isInfoEnabled()) { + logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl()); + } + /** + * @since 2.7.0 + * ServiceData Store + */ + MetadataReportService metadataReportService = null; + if ((metadataReportService = getMetadataReportService()) != null) { + URL consumerURL = new URL(CONSUMER_PROTOCOL, map.remove(REGISTER_IP_KEY), 0, map.get(INTERFACE_KEY), map); + metadataReportService.publishConsumer(consumerURL); + } + // create service proxy + return (T) PROXY_FACTORY.getProxy(invoker); + } + + /** + * Figure out should refer the service in the same JVM from configurations. The default behavior is true + * 1. if injvm is specified, then use it + * 2. then if a url is specified, then assume it's a remote call + * 3. otherwise, check scope parameter + * 4. if scope is not specified but the target service is provided in the same JVM, then prefer to make the local + * call, which is the default behavior + */ + protected boolean shouldJvmRefer(Map map) { + URL tmpUrl = new URL("temp", "localhost", 0, map); + boolean isJvmRefer; + if (isInjvm() == null) { + // if a url is specified, don't do local reference + if (url != null && url.length() > 0) { + isJvmRefer = false; + } else { + // by default, reference local service if there is + isJvmRefer = InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl); + } + } else { + isJvmRefer = isInjvm(); + } + return isJvmRefer; + } + + protected boolean shouldCheck() { + Boolean shouldCheck = isCheck(); + if (shouldCheck == null && getConsumer() != null) { + shouldCheck = getConsumer().isCheck(); + } + if (shouldCheck == null) { + // default true + shouldCheck = true; + } + return shouldCheck; + } + + protected boolean shouldInit() { + Boolean shouldInit = isInit(); + if (shouldInit == null && getConsumer() != null) { + shouldInit = getConsumer().isInit(); + } + if (shouldInit == null) { + // default is false + return false; + } + return shouldInit; + } + + private void checkDefault() { + if (consumer != null) { + return; + } + setConsumer(ConfigManager.getInstance().getDefaultConsumer().orElseGet(() -> { + ConsumerConfig consumerConfig = new ConsumerConfig(); + consumerConfig.refresh(); + return consumerConfig; + })); + } + + private void completeCompoundConfigs() { + if (consumer != null) { + if (application == null) { + setApplication(consumer.getApplication()); + } + if (module == null) { + setModule(consumer.getModule()); + } + if (registries == null) { + setRegistries(consumer.getRegistries()); + } + if (monitor == null) { + setMonitor(consumer.getMonitor()); + } + } + if (module != null) { + if (registries == null) { + setRegistries(module.getRegistries()); + } + if (monitor == null) { + setMonitor(module.getMonitor()); + } + } + if (application != null) { + if (registries == null) { + setRegistries(application.getRegistries()); + } + if (monitor == null) { + setMonitor(application.getMonitor()); + } + } + } + + public Class getInterfaceClass() { + if (interfaceClass != null) { + return interfaceClass; + } + if (isGeneric() + || (getConsumer() != null && getConsumer().isGeneric())) { + return GenericService.class; + } + try { + if (interfaceName != null && interfaceName.length() > 0) { + this.interfaceClass = Class.forName(interfaceName, true, ClassUtils.getClassLoader()); + } + } catch (ClassNotFoundException t) { + throw new IllegalStateException(t.getMessage(), t); + } + return interfaceClass; + } + + /** + * @param interfaceClass + * @see #setInterface(Class) + * @deprecated + */ + @Deprecated + public void setInterfaceClass(Class interfaceClass) { + setInterface(interfaceClass); + } + + public String getInterface() { + return interfaceName; + } + + public void setInterface(String interfaceName) { + this.interfaceName = interfaceName; + if (StringUtils.isEmpty(id)) { + id = interfaceName; + } + } + + public void setInterface(Class interfaceClass) { + if (interfaceClass != null && !interfaceClass.isInterface()) { + throw new IllegalStateException("The interface class " + interfaceClass + " is not a interface!"); + } + this.interfaceClass = interfaceClass; + setInterface(interfaceClass == null ? null : interfaceClass.getName()); + } + + public String getClient() { + return client; + } + + public void setClient(String client) { + checkName(Constants.CLIENT_KEY, client); + this.client = client; + } + + @Parameter(excluded = true) + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public List getMethods() { + return methods; + } + + @SuppressWarnings("unchecked") + public void setMethods(List methods) { + this.methods = (List) methods; + } + + public ConsumerConfig getConsumer() { + return consumer; + } + + public void setConsumer(ConsumerConfig consumer) { + ConfigManager.getInstance().addConsumer(consumer); + this.consumer = consumer; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + // just for test + Invoker getInvoker() { + return invoker; + } + + @Override + @Parameter(excluded = true) + public String getPrefix() { + return DUBBO + ".reference." + interfaceName; + } + + private void resolveFile() { + String resolve = System.getProperty(interfaceName); + String resolveFile = null; + if (StringUtils.isEmpty(resolve)) { + resolveFile = System.getProperty("dubbo.resolve.file"); + if (StringUtils.isEmpty(resolveFile)) { + File userResolveFile = new File(new File(System.getProperty("user.home")), "dubbo-resolve.properties"); + if (userResolveFile.exists()) { + resolveFile = userResolveFile.getAbsolutePath(); + } + } + if (resolveFile != null && resolveFile.length() > 0) { + Properties properties = new Properties(); + try (FileInputStream fis = new FileInputStream(new File(resolveFile))) { + properties.load(fis); + } catch (IOException e) { + throw new IllegalStateException("Failed to load " + resolveFile + ", cause: " + e.getMessage(), e); + } + + resolve = properties.getProperty(interfaceName); + } + } + if (resolve != null && resolve.length() > 0) { + url = resolve; + if (logger.isWarnEnabled()) { + if (resolveFile != null) { + logger.warn("Using default dubbo resolve file " + resolveFile + " replace " + interfaceName + "" + resolve + " to p2p invoke remote service."); + } else { + logger.warn("Using -D" + interfaceName + "=" + resolve + " to p2p invoke remote service."); + } + } + } + } + + /** + * 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 f95e15e0e55..5be6e76f959 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 @@ -1,1054 +1,1077 @@ -/* - * 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; - -import org.apache.dubbo.common.URL; -import org.apache.dubbo.common.URLBuilder; -import org.apache.dubbo.common.Version; -import org.apache.dubbo.common.bytecode.Wrapper; -import org.apache.dubbo.common.config.Environment; -import org.apache.dubbo.common.extension.ExtensionLoader; -import org.apache.dubbo.common.utils.ClassUtils; -import org.apache.dubbo.common.utils.CollectionUtils; -import org.apache.dubbo.common.utils.ConfigUtils; -import org.apache.dubbo.common.utils.NamedThreadFactory; -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.invoker.DelegateProviderMetaDataInvoker; -import org.apache.dubbo.config.support.Parameter; -import org.apache.dubbo.metadata.integration.MetadataReportService; -import org.apache.dubbo.remoting.Constants; -import org.apache.dubbo.rpc.Exporter; -import org.apache.dubbo.rpc.Invoker; -import org.apache.dubbo.rpc.Protocol; -import org.apache.dubbo.rpc.ProxyFactory; -import org.apache.dubbo.rpc.cluster.ConfiguratorFactory; -import org.apache.dubbo.rpc.model.ApplicationModel; -import org.apache.dubbo.rpc.model.ProviderModel; -import org.apache.dubbo.rpc.service.GenericService; -import org.apache.dubbo.rpc.support.ProtocolUtils; - -import java.lang.reflect.Method; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import static org.apache.dubbo.common.constants.CommonConstants.ANYHOST_KEY; -import static org.apache.dubbo.common.constants.CommonConstants.ANY_VALUE; -import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SPLIT_PATTERN; -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.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.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.rpc.cluster.Constants.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.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 - * - * @export - */ -public class ServiceConfig extends AbstractServiceConfig { - - private static final long serialVersionUID = 3033787999037024738L; - - /** - * The {@link Protocol} implementation with adaptive functionality,it will be different in different scenarios. - * A particular {@link Protocol} implementation is determined by the protocol attribute in the {@link URL}. - * For example: - * - *

  • when the url is registry://224.5.6.7:1234/org.apache.dubbo.registry.RegistryService?application=dubbo-sample, - * then the protocol is RegistryProtocol
  • - * - *
  • when the url is dubbo://224.5.6.7:1234/org.apache.dubbo.config.api.DemoService?application=dubbo-sample, then - * the protocol is DubboProtocol
  • - *

    - * Actually,when the {@link ExtensionLoader} init the {@link Protocol} instants,it will automatically wraps two - * layers, and eventually will get a ProtocolFilterWrapper or ProtocolListenerWrapper - */ - private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); - - /** - * A {@link ProxyFactory} implementation that will generate a exported service proxy,the JavassistProxyFactory is its - * default implementation - */ - private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); - - /** - * A random port cache, the different protocols who has no port specified have different random port - */ - private static final Map RANDOM_PORT_MAP = new HashMap(); - - /** - * A delayed exposure service timer - */ - private static final ScheduledExecutorService delayExportExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("DubboServiceDelayExporter", true)); - - /** - * The urls of the services exported - */ - private final List urls = new ArrayList(); - - /** - * The exported services - */ - private final List> exporters = new ArrayList>(); - - /** - * The interface name of the exported service - */ - private String interfaceName; - - /** - * The interface class of the exported service - */ - private Class interfaceClass; - - /** - * The reference of the interface implementation - */ - private T ref; - - /** - * The service name - */ - private String path; - - /** - * The method configuration - */ - private List methods; - - /** - * The provider configuration - */ - private ProviderConfig provider; - - /** - * The providerIds - */ - private String providerIds; - - /** - * Whether the provider has been exported - */ - private transient volatile boolean exported; - - /** - * The flag whether a service has unexported ,if the method unexported is invoked, the value is true - */ - private transient volatile boolean unexported; - - /** - * whether it is a GenericService - */ - private volatile String generic; - - public ServiceConfig() { - } - - public ServiceConfig(Service service) { - appendAnnotation(Service.class, service); - setMethods(MethodConfig.constructMethodConfig(service.methods())); - } - - @Deprecated - private static List convertProviderToProtocol(List providers) { - if (CollectionUtils.isEmpty(providers)) { - return null; - } - List protocols = new ArrayList(providers.size()); - for (ProviderConfig provider : providers) { - protocols.add(convertProviderToProtocol(provider)); - } - return protocols; - } - - @Deprecated - private static List convertProtocolToProvider(List protocols) { - if (CollectionUtils.isEmpty(protocols)) { - return null; - } - List providers = new ArrayList(protocols.size()); - for (ProtocolConfig provider : protocols) { - providers.add(convertProtocolToProvider(provider)); - } - return providers; - } - - @Deprecated - private static ProtocolConfig convertProviderToProtocol(ProviderConfig provider) { - ProtocolConfig protocol = new ProtocolConfig(); - protocol.setName(provider.getProtocol().getName()); - protocol.setServer(provider.getServer()); - protocol.setClient(provider.getClient()); - protocol.setCodec(provider.getCodec()); - protocol.setHost(provider.getHost()); - protocol.setPort(provider.getPort()); - protocol.setPath(provider.getPath()); - protocol.setPayload(provider.getPayload()); - protocol.setThreads(provider.getThreads()); - protocol.setParameters(provider.getParameters()); - return protocol; - } - - @Deprecated - private static ProviderConfig convertProtocolToProvider(ProtocolConfig protocol) { - ProviderConfig provider = new ProviderConfig(); - provider.setProtocol(protocol); - provider.setServer(protocol.getServer()); - provider.setClient(protocol.getClient()); - provider.setCodec(protocol.getCodec()); - provider.setHost(protocol.getHost()); - provider.setPort(protocol.getPort()); - provider.setPath(protocol.getPath()); - provider.setPayload(protocol.getPayload()); - provider.setThreads(protocol.getThreads()); - provider.setParameters(protocol.getParameters()); - return provider; - } - - private static Integer getRandomPort(String protocol) { - protocol = protocol.toLowerCase(); - return RANDOM_PORT_MAP.getOrDefault(protocol, Integer.MIN_VALUE); - } - - private static void putRandomPort(String protocol, Integer port) { - protocol = protocol.toLowerCase(); - if (!RANDOM_PORT_MAP.containsKey(protocol)) { - RANDOM_PORT_MAP.put(protocol, port); - logger.warn("Use random available port(" + port + ") for protocol " + protocol); - } - } - - public URL toUrl() { - return urls.isEmpty() ? null : urls.iterator().next(); - } - - public List toUrls() { - return urls; - } - - @Parameter(excluded = true) - public boolean isExported() { - return exported; - } - - @Parameter(excluded = true) - public boolean isUnexported() { - return unexported; - } - - public void checkAndUpdateSubConfigs() { - // Use default configs defined explicitly on global configs - completeCompoundConfigs(); - // Config Center should always being started first. - startConfigCenter(); - checkDefault(); - checkProtocol(); - checkApplication(); - // if protocol is not injvm checkRegistry - if (!isOnlyInJvm()) { - checkRegistry(); - } - this.refresh(); - checkMetadataReport(); - - if (StringUtils.isEmpty(interfaceName)) { - throw new IllegalStateException(" interface not allow null!"); - } - - if (ref instanceof GenericService) { - interfaceClass = GenericService.class; - if (StringUtils.isEmpty(generic)) { - generic = Boolean.TRUE.toString(); - } - } else { - try { - interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() - .getContextClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(e.getMessage(), e); - } - checkInterfaceAndMethods(interfaceClass, methods); - checkRef(); - generic = Boolean.FALSE.toString(); - } - if (local != null) { - if ("true".equals(local)) { - local = interfaceName + "Local"; - } - Class localClass; - try { - localClass = ClassUtils.forNameWithThreadContextClassLoader(local); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(e.getMessage(), e); - } - if (!interfaceClass.isAssignableFrom(localClass)) { - throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName); - } - } - if (stub != null) { - if ("true".equals(stub)) { - stub = interfaceName + "Stub"; - } - Class stubClass; - try { - stubClass = ClassUtils.forNameWithThreadContextClassLoader(stub); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(e.getMessage(), e); - } - if (!interfaceClass.isAssignableFrom(stubClass)) { - throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName); - } - } - checkStubAndLocal(interfaceClass); - checkMock(interfaceClass); - } - - /** - * Determine if it is injvm - * - * @return - */ - private boolean isOnlyInJvm() { - return getProtocols().size() == 1 && LOCAL_PROTOCOL.equalsIgnoreCase(getProtocols().get(0).getName()); - } - - public synchronized void export() { - checkAndUpdateSubConfigs(); - - if (!shouldExport()) { - return; - } - - if (shouldDelay()) { - delayExportExecutor.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS); - } else { - doExport(); - } - } - - private boolean shouldExport() { - Boolean export = getExport(); - // default value is true - return export == null ? true : export; - } - - @Override - public Boolean getExport() { - return (export == null && provider != null) ? provider.getExport() : export; - } - - private boolean shouldDelay() { - Integer delay = getDelay(); - return delay != null && delay > 0; - } - - @Override - public Integer getDelay() { - return (delay == null && provider != null) ? provider.getDelay() : delay; - } - - protected synchronized void doExport() { - if (unexported) { - throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!"); - } - if (exported) { - return; - } - exported = true; - - if (StringUtils.isEmpty(path)) { - path = interfaceName; - } - doExportUrls(); - } - - private void checkRef() { - // reference should not be null, and is the implementation of the given interface - if (ref == null) { - throw new IllegalStateException("ref not allow null!"); - } - if (!interfaceClass.isInstance(ref)) { - throw new IllegalStateException("The class " - + ref.getClass().getName() + " unimplemented interface " - + interfaceClass + "!"); - } - } - - public synchronized void unexport() { - if (!exported) { - return; - } - if (unexported) { - return; - } - if (!exporters.isEmpty()) { - for (Exporter exporter : exporters) { - try { - exporter.unexport(); - } catch (Throwable t) { - logger.warn("Unexpected error occured when unexport " + exporter, t); - } - } - exporters.clear(); - } - unexported = true; - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - private void doExportUrls() { - List registryURLs = loadRegistries(true); - for (ProtocolConfig protocolConfig : protocols) { - String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version); - ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass); - ApplicationModel.initProviderModel(pathKey, providerModel); - doExportUrlsFor1Protocol(protocolConfig, registryURLs); - } - } - - private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) { - String name = protocolConfig.getName(); - if (StringUtils.isEmpty(name)) { - name = DUBBO; - } - - Map map = new HashMap(); - map.put(SIDE_KEY, PROVIDER_SIDE); - - appendRuntimeParameters(map); - appendParameters(map, metrics); - appendParameters(map, application); - appendParameters(map, module); - // remove 'default.' prefix for configs from ProviderConfig - // appendParameters(map, provider, Constants.DEFAULT_KEY); - appendParameters(map, provider); - appendParameters(map, protocolConfig); - appendParameters(map, this); - if (CollectionUtils.isNotEmpty(methods)) { - for (MethodConfig method : methods) { - appendParameters(map, method, method.getName()); - String retryKey = method.getName() + ".retry"; - if (map.containsKey(retryKey)) { - String retryValue = map.remove(retryKey); - if ("false".equals(retryValue)) { - map.put(method.getName() + ".retries", "0"); - } - } - List arguments = method.getArguments(); - if (CollectionUtils.isNotEmpty(arguments)) { - for (ArgumentConfig argument : arguments) { - // convert argument type - if (argument.getType() != null && argument.getType().length() > 0) { - Method[] methods = interfaceClass.getMethods(); - // visit all methods - if (methods != null && methods.length > 0) { - for (int i = 0; i < methods.length; i++) { - String methodName = methods[i].getName(); - // target the method, and get its signature - if (methodName.equals(method.getName())) { - Class[] argtypes = methods[i].getParameterTypes(); - // one callback in the method - if (argument.getIndex() != -1) { - if (argtypes[argument.getIndex()].getName().equals(argument.getType())) { - appendParameters(map, argument, method.getName() + "." + argument.getIndex()); - } else { - throw new IllegalArgumentException("Argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType()); - } - } else { - // multiple callbacks in the method - for (int j = 0; j < argtypes.length; j++) { - Class argclazz = argtypes[j]; - if (argclazz.getName().equals(argument.getType())) { - appendParameters(map, argument, method.getName() + "." + j); - if (argument.getIndex() != -1 && argument.getIndex() != j) { - throw new IllegalArgumentException("Argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType()); - } - } - } - } - } - } - } - } else if (argument.getIndex() != -1) { - appendParameters(map, argument, method.getName() + "." + argument.getIndex()); - } else { - throw new IllegalArgumentException("Argument config must set index or type attribute.eg: or "); - } - - } - } - } // end of methods for - } - - if (ProtocolUtils.isGeneric(generic)) { - map.put(GENERIC_KEY, generic); - map.put(METHODS_KEY, ANY_VALUE); - } else { - String revision = Version.getVersion(interfaceClass, version); - if (revision != null && revision.length() > 0) { - map.put(REVISION_KEY, revision); - } - - String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); - if (methods.length == 0) { - logger.warn("No method found in service interface " + interfaceClass.getName()); - map.put(METHODS_KEY, ANY_VALUE); - } else { - map.put(METHODS_KEY, StringUtils.join(new HashSet(Arrays.asList(methods)), ",")); - } - } - if (!ConfigUtils.isEmpty(token)) { - if (ConfigUtils.isDefault(token)) { - map.put(TOKEN_KEY, UUID.randomUUID().toString()); - } else { - map.put(TOKEN_KEY, token); - } - } - // export service - String host = this.findConfigedHosts(protocolConfig, registryURLs, map); - Integer port = this.findConfigedPorts(protocolConfig, name, map); - URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map); - - if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) - .hasExtension(url.getProtocol())) { - url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) - .getExtension(url.getProtocol()).getConfigurator(url).configure(url); - } - - String scope = url.getParameter(SCOPE_KEY); - // don't export when none is configured - if (!SCOPE_NONE.equalsIgnoreCase(scope)) { - - // export to local if the config is not remote (export to remote only when config is remote) - if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) { - exportLocal(url); - } - // export to remote if the config is not local (export to local only when config is local) - if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) { - if (!isOnlyInJvm() && logger.isInfoEnabled()) { - logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url); - } - if (CollectionUtils.isNotEmpty(registryURLs)) { - for (URL registryURL : registryURLs) { - //if protocol is only injvm ,not register - if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) { - continue; - } - url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY)); - URL monitorUrl = loadMonitor(registryURL); - if (monitorUrl != null) { - url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString()); - } - if (logger.isInfoEnabled()) { - logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL); - } - - // For providers, this is used to enable custom proxy to generate invoker - String proxy = url.getParameter(PROXY_KEY); - if (StringUtils.isNotEmpty(proxy)) { - registryURL = registryURL.addParameter(PROXY_KEY, proxy); - } - - Invoker invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString())); - DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); - - Exporter exporter = protocol.export(wrapperInvoker); - exporters.add(exporter); - } - } else { - Invoker invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); - DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); - - Exporter exporter = protocol.export(wrapperInvoker); - exporters.add(exporter); - } - /** - * @since 2.7.0 - * ServiceData Store - */ - MetadataReportService metadataReportService = null; - if ((metadataReportService = getMetadataReportService()) != null) { - metadataReportService.publishProvider(url); - } - } - } - this.urls.add(url); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - /** - * always export injvm - */ - private void exportLocal(URL url) { - URL local = URLBuilder.from(url) - .setProtocol(LOCAL_PROTOCOL) - .setHost(LOCALHOST_VALUE) - .setPort(0) - .build(); - Exporter exporter = protocol.export( - proxyFactory.getInvoker(ref, (Class) interfaceClass, local)); - exporters.add(exporter); - logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local); - } - - private Optional getContextPath(ProtocolConfig protocolConfig) { - String contextPath = protocolConfig.getContextpath(); - if (StringUtils.isEmpty(contextPath) && provider != null) { - contextPath = provider.getContextpath(); - } - return Optional.ofNullable(contextPath); - } - - protected Class getServiceClass(T ref) { - return ref.getClass(); - } - - /** - * Register & bind IP address for service provider, can be configured separately. - * Configuration priority: environment variables -> java system properties -> host property in config file -> - * /etc/hosts -> default network address -> first available network address - * - * @param protocolConfig - * @param registryURLs - * @param map - * @return - */ - private String findConfigedHosts(ProtocolConfig protocolConfig, List registryURLs, Map map) { - boolean anyhost = false; - - String hostToBind = getValueFromConfig(protocolConfig, DUBBO_IP_TO_BIND); - if (hostToBind != null && hostToBind.length() > 0 && isInvalidLocalHost(hostToBind)) { - throw new IllegalArgumentException("Specified invalid bind ip from property:" + DUBBO_IP_TO_BIND + ", value:" + hostToBind); - } - - // if bind ip is not found in environment, keep looking up - if (StringUtils.isEmpty(hostToBind)) { - hostToBind = protocolConfig.getHost(); - if (provider != null && StringUtils.isEmpty(hostToBind)) { - hostToBind = provider.getHost(); - } - if (isInvalidLocalHost(hostToBind)) { - anyhost = true; - try { - hostToBind = InetAddress.getLocalHost().getHostAddress(); - } catch (UnknownHostException e) { - logger.warn(e.getMessage(), e); - } - if (isInvalidLocalHost(hostToBind)) { - if (CollectionUtils.isNotEmpty(registryURLs)) { - for (URL registryURL : registryURLs) { - if (MULTICAST.equalsIgnoreCase(registryURL.getParameter("registry"))) { - // skip multicast registry since we cannot connect to it via Socket - continue; - } - try (Socket socket = new Socket()) { - SocketAddress addr = new InetSocketAddress(registryURL.getHost(), registryURL.getPort()); - socket.connect(addr, 1000); - hostToBind = socket.getLocalAddress().getHostAddress(); - break; - } catch (Exception e) { - logger.warn(e.getMessage(), e); - } - } - } - if (isInvalidLocalHost(hostToBind)) { - hostToBind = getLocalHost(); - } - } - } - } - - map.put(Constants.BIND_IP_KEY, hostToBind); - - // registry ip is not used for bind ip by default - String hostToRegistry = getValueFromConfig(protocolConfig, DUBBO_IP_TO_REGISTRY); - if (hostToRegistry != null && hostToRegistry.length() > 0 && isInvalidLocalHost(hostToRegistry)) { - throw new IllegalArgumentException("Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry); - } else if (StringUtils.isEmpty(hostToRegistry)) { - // bind ip is used as registry ip by default - hostToRegistry = hostToBind; - } - - map.put(ANYHOST_KEY, String.valueOf(anyhost)); - - return hostToRegistry; - } - - /** - * Register port and bind port for the provider, can be configured separately - * Configuration priority: environment variable -> java system properties -> port property in protocol config file - * -> protocol default port - * - * @param protocolConfig - * @param name - * @return - */ - private Integer findConfigedPorts(ProtocolConfig protocolConfig, String name, Map map) { - Integer portToBind = null; - - // parse bind port from environment - String port = getValueFromConfig(protocolConfig, DUBBO_PORT_TO_BIND); - portToBind = parsePort(port); - - // if there's no bind port found from environment, keep looking up. - if (portToBind == null) { - portToBind = protocolConfig.getPort(); - if (provider != null && (portToBind == null || portToBind == 0)) { - portToBind = provider.getPort(); - } - final int defaultPort = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(name).getDefaultPort(); - if (portToBind == null || portToBind == 0) { - portToBind = defaultPort; - } - if (portToBind == null || portToBind <= 0) { - portToBind = getRandomPort(name); - if (portToBind == null || portToBind < 0) { - portToBind = getAvailablePort(defaultPort); - putRandomPort(name, portToBind); - } - } - } - - // save bind port, used as url's key later - map.put(Constants.BIND_PORT_KEY, String.valueOf(portToBind)); - - // registry port, not used as bind port by default - String portToRegistryStr = getValueFromConfig(protocolConfig, DUBBO_PORT_TO_REGISTRY); - Integer portToRegistry = parsePort(portToRegistryStr); - if (portToRegistry == null) { - portToRegistry = portToBind; - } - - return portToRegistry; - } - - private Integer parsePort(String configPort) { - Integer port = null; - if (configPort != null && configPort.length() > 0) { - try { - Integer intPort = Integer.parseInt(configPort); - if (isInvalidPort(intPort)) { - throw new IllegalArgumentException("Specified invalid port from env value:" + configPort); - } - port = intPort; - } catch (Exception e) { - throw new IllegalArgumentException("Specified invalid port from env value:" + configPort); - } - } - return port; - } - - private String getValueFromConfig(ProtocolConfig protocolConfig, String key) { - String protocolPrefix = protocolConfig.getName().toUpperCase() + "_"; - String port = ConfigUtils.getSystemProperty(protocolPrefix + key); - if (StringUtils.isEmpty(port)) { - port = ConfigUtils.getSystemProperty(key); - } - return port; - } - - private void completeCompoundConfigs() { - if (provider != null) { - if (application == null) { - setApplication(provider.getApplication()); - } - if (module == null) { - setModule(provider.getModule()); - } - if (registries == null) { - setRegistries(provider.getRegistries()); - } - if (monitor == null) { - setMonitor(provider.getMonitor()); - } - if (protocols == null) { - setProtocols(provider.getProtocols()); - } - if (configCenter == null) { - setConfigCenter(provider.getConfigCenter()); - } - } - if (module != null) { - if (registries == null) { - setRegistries(module.getRegistries()); - } - if (monitor == null) { - setMonitor(module.getMonitor()); - } - } - if (application != null) { - if (registries == null) { - setRegistries(application.getRegistries()); - } - if (monitor == null) { - setMonitor(application.getMonitor()); - } - } - } - - private void checkDefault() { - createProviderIfAbsent(); - } - - private void createProviderIfAbsent() { - if (provider != null) { - return; - } - setProvider( - ConfigManager.getInstance() - .getDefaultProvider() - .orElseGet(() -> { - ProviderConfig providerConfig = new ProviderConfig(); - providerConfig.refresh(); - return providerConfig; - }) - ); - } - - private void checkProtocol() { - if (CollectionUtils.isEmpty(protocols) && provider != null) { - setProtocols(provider.getProtocols()); - } - convertProtocolIdsToProtocols(); - } - - private void convertProtocolIdsToProtocols() { - if (StringUtils.isEmpty(protocolIds) && CollectionUtils.isEmpty(protocols)) { - List configedProtocols = new ArrayList<>(); - configedProtocols.addAll(getSubProperties(Environment.getInstance() - .getExternalConfigurationMap(), PROTOCOLS_SUFFIX)); - configedProtocols.addAll(getSubProperties(Environment.getInstance() - .getAppExternalConfigurationMap(), PROTOCOLS_SUFFIX)); - - protocolIds = String.join(",", configedProtocols); - } - - if (StringUtils.isEmpty(protocolIds)) { - if (CollectionUtils.isEmpty(protocols)) { - setProtocols( - ConfigManager.getInstance().getDefaultProtocols() - .filter(CollectionUtils::isNotEmpty) - .orElseGet(() -> { - ProtocolConfig protocolConfig = new ProtocolConfig(); - protocolConfig.refresh(); - return new ArrayList<>(Arrays.asList(protocolConfig)); - }) - ); - } - } else { - String[] arr = COMMA_SPLIT_PATTERN.split(protocolIds); - List tmpProtocols = CollectionUtils.isNotEmpty(protocols) ? protocols : new ArrayList<>(); - Arrays.stream(arr).forEach(id -> { - if (tmpProtocols.stream().noneMatch(prot -> prot.getId().equals(id))) { - tmpProtocols.add(ConfigManager.getInstance().getProtocol(id).orElseGet(() -> { - ProtocolConfig protocolConfig = new ProtocolConfig(); - protocolConfig.setId(id); - protocolConfig.refresh(); - return protocolConfig; - })); - } - }); - if (tmpProtocols.size() > arr.length) { - throw new IllegalStateException("Too much protocols found, the protocols comply to this service are :" + protocolIds + " but got " + protocols - .size() + " registries!"); - } - setProtocols(tmpProtocols); - } - } - - public Class getInterfaceClass() { - if (interfaceClass != null) { - return interfaceClass; - } - if (ref instanceof GenericService) { - return GenericService.class; - } - try { - if (interfaceName != null && interfaceName.length() > 0) { - this.interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() - .getContextClassLoader()); - } - } catch (ClassNotFoundException t) { - throw new IllegalStateException(t.getMessage(), t); - } - return interfaceClass; - } - - /** - * @param interfaceClass - * @see #setInterface(Class) - * @deprecated - */ - public void setInterfaceClass(Class interfaceClass) { - setInterface(interfaceClass); - } - - public String getInterface() { - return interfaceName; - } - - public void setInterface(Class interfaceClass) { - if (interfaceClass != null && !interfaceClass.isInterface()) { - throw new IllegalStateException("The interface class " + interfaceClass + " is not a interface!"); - } - this.interfaceClass = interfaceClass; - setInterface(interfaceClass == null ? null : interfaceClass.getName()); - } - - public void setInterface(String interfaceName) { - this.interfaceName = interfaceName; - if (StringUtils.isEmpty(id)) { - id = interfaceName; - } - } - - public T getRef() { - return ref; - } - - public void setRef(T ref) { - this.ref = ref; - } - - @Parameter(excluded = true) - public String getPath() { - return path; - } - - public void setPath(String path) { - checkPathName(PATH_KEY, path); - this.path = path; - } - - public List getMethods() { - return methods; - } - - // ======== Deprecated ======== - - @SuppressWarnings("unchecked") - public void setMethods(List methods) { - this.methods = (List) methods; - } - - public ProviderConfig getProvider() { - return provider; - } - - public void setProvider(ProviderConfig provider) { - ConfigManager.getInstance().addProvider(provider); - this.provider = provider; - } - - @Parameter(excluded = true) - public String getProviderIds() { - return providerIds; - } - - public void setProviderIds(String providerIds) { - this.providerIds = providerIds; - } - - public String getGeneric() { - return generic; - } - - public void setGeneric(String generic) { - if (StringUtils.isEmpty(generic)) { - return; - } - if (ProtocolUtils.isGeneric(generic)) { - this.generic = generic; - } else { - throw new IllegalArgumentException("Unsupported generic type " + generic); - } - } - - @Override - public void setMock(Boolean mock) { - throw new IllegalArgumentException("mock doesn't support on provider side"); - } - - @Override - public void setMock(String mock) { - throw new IllegalArgumentException("mock doesn't support on provider side"); - } - - public List getExportedUrls() { - return urls; - } - - /** - * @deprecated Replace to getProtocols() - */ - @Deprecated - public List getProviders() { - return convertProtocolToProvider(protocols); - } - - /** - * @deprecated Replace to setProtocols() - */ - @Deprecated - public void setProviders(List providers) { - this.protocols = convertProviderToProtocol(providers); - } - - @Override - @Parameter(excluded = true) - public String getPrefix() { - return DUBBO + ".service." + interfaceName; - } -} +/* + * 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; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.URLBuilder; +import org.apache.dubbo.common.Version; +import org.apache.dubbo.common.bytecode.Wrapper; +import org.apache.dubbo.common.config.Environment; +import org.apache.dubbo.common.extension.ExtensionLoader; +import org.apache.dubbo.common.utils.ClassUtils; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.ConfigUtils; +import org.apache.dubbo.common.utils.NamedThreadFactory; +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; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Protocol; +import org.apache.dubbo.rpc.ProxyFactory; +import org.apache.dubbo.rpc.cluster.ConfiguratorFactory; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ProviderModel; +import org.apache.dubbo.rpc.service.GenericService; +import org.apache.dubbo.rpc.support.ProtocolUtils; + +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.apache.dubbo.common.constants.CommonConstants.ANYHOST_KEY; +import static org.apache.dubbo.common.constants.CommonConstants.ANY_VALUE; +import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SPLIT_PATTERN; +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.rpc.cluster.Constants.EXPORT_KEY; +import static org.apache.dubbo.config.Constants.MULTICAST; +import static org.apache.dubbo.config.Constants.PROTOCOLS_SUFFIX; +import static org.apache.dubbo.config.Constants.SCOPE_NONE; +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; + +/** + * ServiceConfig + * + * @export + */ +public class ServiceConfig extends AbstractServiceConfig { + + private static final long serialVersionUID = 3033787999037024738L; + + /** + * The {@link Protocol} implementation with adaptive functionality,it will be different in different scenarios. + * A particular {@link Protocol} implementation is determined by the protocol attribute in the {@link URL}. + * For example: + * + *

  • when the url is registry://224.5.6.7:1234/org.apache.dubbo.registry.RegistryService?application=dubbo-sample, + * then the protocol is RegistryProtocol
  • + * + *
  • when the url is dubbo://224.5.6.7:1234/org.apache.dubbo.config.api.DemoService?application=dubbo-sample, then + * the protocol is DubboProtocol
  • + *

    + * Actually,when the {@link ExtensionLoader} init the {@link Protocol} instants,it will automatically wraps two + * layers, and eventually will get a ProtocolFilterWrapper or ProtocolListenerWrapper + */ + private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + + /** + * A {@link ProxyFactory} implementation that will generate a exported service proxy,the JavassistProxyFactory is its + * default implementation + */ + private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + + /** + * A random port cache, the different protocols who has no port specified have different random port + */ + private static final Map RANDOM_PORT_MAP = new HashMap(); + + /** + * A delayed exposure service timer + */ + private static final ScheduledExecutorService delayExportExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("DubboServiceDelayExporter", true)); + + /** + * The urls of the services exported + */ + private final List urls = new ArrayList(); + + /** + * The exported services + */ + private final List> exporters = new ArrayList>(); + + private final EventDispatcher eventDispatcher = EventDispatcher.getDefaultExtension(); + + /** + * The interface name of the exported service + */ + private String interfaceName; + + /** + * The interface class of the exported service + */ + private Class interfaceClass; + + /** + * The reference of the interface implementation + */ + private T ref; + + /** + * The service name + */ + private String path; + + /** + * The method configuration + */ + private List methods; + + /** + * The provider configuration + */ + private ProviderConfig provider; + + /** + * The providerIds + */ + private String providerIds; + + /** + * Whether the provider has been exported + */ + private transient volatile boolean exported; + + /** + * The flag whether a service has unexported ,if the method unexported is invoked, the value is true + */ + private transient volatile boolean unexported; + + /** + * whether it is a GenericService + */ + private volatile String generic; + + public ServiceConfig() { + } + + public ServiceConfig(Service service) { + appendAnnotation(Service.class, service); + setMethods(MethodConfig.constructMethodConfig(service.methods())); + } + + @Deprecated + private static List convertProviderToProtocol(List providers) { + if (CollectionUtils.isEmpty(providers)) { + return null; + } + List protocols = new ArrayList(providers.size()); + for (ProviderConfig provider : providers) { + protocols.add(convertProviderToProtocol(provider)); + } + return protocols; + } + + @Deprecated + private static List convertProtocolToProvider(List protocols) { + if (CollectionUtils.isEmpty(protocols)) { + return null; + } + List providers = new ArrayList(protocols.size()); + for (ProtocolConfig provider : protocols) { + providers.add(convertProtocolToProvider(provider)); + } + return providers; + } + + @Deprecated + private static ProtocolConfig convertProviderToProtocol(ProviderConfig provider) { + ProtocolConfig protocol = new ProtocolConfig(); + protocol.setName(provider.getProtocol().getName()); + protocol.setServer(provider.getServer()); + protocol.setClient(provider.getClient()); + protocol.setCodec(provider.getCodec()); + protocol.setHost(provider.getHost()); + protocol.setPort(provider.getPort()); + protocol.setPath(provider.getPath()); + protocol.setPayload(provider.getPayload()); + protocol.setThreads(provider.getThreads()); + protocol.setParameters(provider.getParameters()); + return protocol; + } + + @Deprecated + private static ProviderConfig convertProtocolToProvider(ProtocolConfig protocol) { + ProviderConfig provider = new ProviderConfig(); + provider.setProtocol(protocol); + provider.setServer(protocol.getServer()); + provider.setClient(protocol.getClient()); + provider.setCodec(protocol.getCodec()); + provider.setHost(protocol.getHost()); + provider.setPort(protocol.getPort()); + provider.setPath(protocol.getPath()); + provider.setPayload(protocol.getPayload()); + provider.setThreads(protocol.getThreads()); + provider.setParameters(protocol.getParameters()); + return provider; + } + + private static Integer getRandomPort(String protocol) { + protocol = protocol.toLowerCase(); + return RANDOM_PORT_MAP.getOrDefault(protocol, Integer.MIN_VALUE); + } + + private static void putRandomPort(String protocol, Integer port) { + protocol = protocol.toLowerCase(); + if (!RANDOM_PORT_MAP.containsKey(protocol)) { + RANDOM_PORT_MAP.put(protocol, port); + logger.warn("Use random available port(" + port + ") for protocol " + protocol); + } + } + + public URL toUrl() { + return urls.isEmpty() ? null : urls.iterator().next(); + } + + public List toUrls() { + return urls; + } + + @Parameter(excluded = true) + public boolean isExported() { + return exported; + } + + @Parameter(excluded = true) + public boolean isUnexported() { + return unexported; + } + + public void checkAndUpdateSubConfigs() { + // Use default configs defined explicitly on global configs + completeCompoundConfigs(); + // Config Center should always being started first. + startConfigCenter(); + checkDefault(); + checkProtocol(); + checkApplication(); + // if protocol is not injvm checkRegistry + if (!isOnlyInJvm()) { + checkRegistry(); + } + this.refresh(); + checkMetadataReport(); + + if (StringUtils.isEmpty(interfaceName)) { + throw new IllegalStateException(" interface not allow null!"); + } + + if (ref instanceof GenericService) { + interfaceClass = GenericService.class; + if (StringUtils.isEmpty(generic)) { + generic = Boolean.TRUE.toString(); + } + } else { + try { + interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() + .getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e.getMessage(), e); + } + checkInterfaceAndMethods(interfaceClass, methods); + checkRef(); + generic = Boolean.FALSE.toString(); + } + if (local != null) { + if ("true".equals(local)) { + local = interfaceName + "Local"; + } + Class localClass; + try { + localClass = ClassUtils.forNameWithThreadContextClassLoader(local); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e.getMessage(), e); + } + if (!interfaceClass.isAssignableFrom(localClass)) { + throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName); + } + } + if (stub != null) { + if ("true".equals(stub)) { + stub = interfaceName + "Stub"; + } + Class stubClass; + try { + stubClass = ClassUtils.forNameWithThreadContextClassLoader(stub); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e.getMessage(), e); + } + if (!interfaceClass.isAssignableFrom(stubClass)) { + throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName); + } + } + checkStubAndLocal(interfaceClass); + checkMock(interfaceClass); + } + + /** + * Determine if it is injvm + * + * @return + */ + private boolean isOnlyInJvm() { + return getProtocols().size() == 1 && LOCAL_PROTOCOL.equalsIgnoreCase(getProtocols().get(0).getName()); + } + + public synchronized void export() { + checkAndUpdateSubConfigs(); + + if (!shouldExport()) { + return; + } + + if (shouldDelay()) { + delayExportExecutor.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS); + } else { + doExport(); + } + } + + private boolean shouldExport() { + Boolean export = getExport(); + // default value is true + return export == null ? true : export; + } + + @Override + public Boolean getExport() { + return (export == null && provider != null) ? provider.getExport() : export; + } + + private boolean shouldDelay() { + Integer delay = getDelay(); + return delay != null && delay > 0; + } + + @Override + public Integer getDelay() { + return (delay == null && provider != null) ? provider.getDelay() : delay; + } + + protected synchronized void doExport() { + if (unexported) { + throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!"); + } + if (exported) { + return; + } + exported = true; + + if (StringUtils.isEmpty(path)) { + path = interfaceName; + } + doExportUrls(); + + // dispatch a ServiceConfigExportedEvent since 2.7.2 + dispatch(new ServiceConfigExportedEvent(this)); + } + + private void checkRef() { + // reference should not be null, and is the implementation of the given interface + if (ref == null) { + throw new IllegalStateException("ref not allow null!"); + } + if (!interfaceClass.isInstance(ref)) { + throw new IllegalStateException("The class " + + ref.getClass().getName() + " unimplemented interface " + + interfaceClass + "!"); + } + } + + public synchronized void unexport() { + if (!exported) { + return; + } + if (unexported) { + return; + } + if (!exporters.isEmpty()) { + for (Exporter exporter : exporters) { + try { + exporter.unexport(); + } catch (Throwable t) { + logger.warn("Unexpected error occured when unexport " + exporter, t); + } + } + exporters.clear(); + } + unexported = true; + + // dispatch a ServiceConfigUnExportedEvent since 2.7.2 + dispatch(new ServiceConfigUnexportedEvent(this)); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void doExportUrls() { + List registryURLs = loadRegistries(true); + for (ProtocolConfig protocolConfig : protocols) { + String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version); + ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass); + ApplicationModel.initProviderModel(pathKey, providerModel); + doExportUrlsFor1Protocol(protocolConfig, registryURLs); + } + } + + private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) { + String name = protocolConfig.getName(); + if (StringUtils.isEmpty(name)) { + name = DUBBO; + } + + Map map = new HashMap(); + map.put(SIDE_KEY, PROVIDER_SIDE); + + appendRuntimeParameters(map); + appendParameters(map, metrics); + appendParameters(map, application); + appendParameters(map, module); + // remove 'default.' prefix for configs from ProviderConfig + // appendParameters(map, provider, Constants.DEFAULT_KEY); + appendParameters(map, provider); + appendParameters(map, protocolConfig); + appendParameters(map, this); + if (CollectionUtils.isNotEmpty(methods)) { + for (MethodConfig method : methods) { + appendParameters(map, method, method.getName()); + String retryKey = method.getName() + ".retry"; + if (map.containsKey(retryKey)) { + String retryValue = map.remove(retryKey); + if ("false".equals(retryValue)) { + map.put(method.getName() + ".retries", "0"); + } + } + List arguments = method.getArguments(); + if (CollectionUtils.isNotEmpty(arguments)) { + for (ArgumentConfig argument : arguments) { + // convert argument type + if (argument.getType() != null && argument.getType().length() > 0) { + Method[] methods = interfaceClass.getMethods(); + // visit all methods + if (methods != null && methods.length > 0) { + for (int i = 0; i < methods.length; i++) { + String methodName = methods[i].getName(); + // target the method, and get its signature + if (methodName.equals(method.getName())) { + Class[] argtypes = methods[i].getParameterTypes(); + // one callback in the method + if (argument.getIndex() != -1) { + if (argtypes[argument.getIndex()].getName().equals(argument.getType())) { + appendParameters(map, argument, method.getName() + "." + argument.getIndex()); + } else { + throw new IllegalArgumentException("Argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType()); + } + } else { + // multiple callbacks in the method + for (int j = 0; j < argtypes.length; j++) { + Class argclazz = argtypes[j]; + if (argclazz.getName().equals(argument.getType())) { + appendParameters(map, argument, method.getName() + "." + j); + if (argument.getIndex() != -1 && argument.getIndex() != j) { + throw new IllegalArgumentException("Argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType()); + } + } + } + } + } + } + } + } else if (argument.getIndex() != -1) { + appendParameters(map, argument, method.getName() + "." + argument.getIndex()); + } else { + throw new IllegalArgumentException("Argument config must set index or type attribute.eg: or "); + } + + } + } + } // end of methods for + } + + if (ProtocolUtils.isGeneric(generic)) { + map.put(GENERIC_KEY, generic); + map.put(METHODS_KEY, ANY_VALUE); + } else { + String revision = Version.getVersion(interfaceClass, version); + if (revision != null && revision.length() > 0) { + map.put(REVISION_KEY, revision); + } + + String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); + if (methods.length == 0) { + logger.warn("No method found in service interface " + interfaceClass.getName()); + map.put(METHODS_KEY, ANY_VALUE); + } else { + map.put(METHODS_KEY, StringUtils.join(new HashSet(Arrays.asList(methods)), ",")); + } + } + if (!ConfigUtils.isEmpty(token)) { + if (ConfigUtils.isDefault(token)) { + map.put(TOKEN_KEY, UUID.randomUUID().toString()); + } else { + map.put(TOKEN_KEY, token); + } + } + // export service + String host = this.findConfigedHosts(protocolConfig, registryURLs, map); + Integer port = this.findConfigedPorts(protocolConfig, name, map); + URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map); + + if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) + .hasExtension(url.getProtocol())) { + url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) + .getExtension(url.getProtocol()).getConfigurator(url).configure(url); + } + + String scope = url.getParameter(SCOPE_KEY); + // don't export when none is configured + if (!SCOPE_NONE.equalsIgnoreCase(scope)) { + + // export to local if the config is not remote (export to remote only when config is remote) + if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) { + exportLocal(url); + } + // export to remote if the config is not local (export to local only when config is local) + if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) { + if (!isOnlyInJvm() && logger.isInfoEnabled()) { + logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url); + } + if (CollectionUtils.isNotEmpty(registryURLs)) { + for (URL registryURL : registryURLs) { + //if protocol is only injvm ,not register + if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) { + continue; + } + url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY)); + URL monitorUrl = loadMonitor(registryURL); + if (monitorUrl != null) { + url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString()); + } + if (logger.isInfoEnabled()) { + logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL); + } + + // For providers, this is used to enable custom proxy to generate invoker + String proxy = url.getParameter(PROXY_KEY); + if (StringUtils.isNotEmpty(proxy)) { + registryURL = registryURL.addParameter(PROXY_KEY, proxy); + } + + Invoker invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString())); + DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); + + Exporter exporter = protocol.export(wrapperInvoker); + exporters.add(exporter); + } + } else { + Invoker invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); + DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); + + Exporter exporter = protocol.export(wrapperInvoker); + exporters.add(exporter); + } + /** + * @since 2.7.0 + * ServiceData Store + */ + MetadataReportService metadataReportService = null; + if ((metadataReportService = getMetadataReportService()) != null) { + metadataReportService.publishProvider(url); + } + } + } + this.urls.add(url); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + /** + * always export injvm + */ + private void exportLocal(URL url) { + URL local = URLBuilder.from(url) + .setProtocol(LOCAL_PROTOCOL) + .setHost(LOCALHOST_VALUE) + .setPort(0) + .build(); + Exporter exporter = protocol.export( + proxyFactory.getInvoker(ref, (Class) interfaceClass, local)); + exporters.add(exporter); + logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local); + } + + private Optional getContextPath(ProtocolConfig protocolConfig) { + String contextPath = protocolConfig.getContextpath(); + if (StringUtils.isEmpty(contextPath) && provider != null) { + contextPath = provider.getContextpath(); + } + return Optional.ofNullable(contextPath); + } + + protected Class getServiceClass(T ref) { + return ref.getClass(); + } + + /** + * Register & bind IP address for service provider, can be configured separately. + * Configuration priority: environment variables -> java system properties -> host property in config file -> + * /etc/hosts -> default network address -> first available network address + * + * @param protocolConfig + * @param registryURLs + * @param map + * @return + */ + private String findConfigedHosts(ProtocolConfig protocolConfig, List registryURLs, Map map) { + boolean anyhost = false; + + String hostToBind = getValueFromConfig(protocolConfig, DUBBO_IP_TO_BIND); + if (hostToBind != null && hostToBind.length() > 0 && isInvalidLocalHost(hostToBind)) { + throw new IllegalArgumentException("Specified invalid bind ip from property:" + DUBBO_IP_TO_BIND + ", value:" + hostToBind); + } + + // if bind ip is not found in environment, keep looking up + if (StringUtils.isEmpty(hostToBind)) { + hostToBind = protocolConfig.getHost(); + if (provider != null && StringUtils.isEmpty(hostToBind)) { + hostToBind = provider.getHost(); + } + if (isInvalidLocalHost(hostToBind)) { + anyhost = true; + try { + hostToBind = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + logger.warn(e.getMessage(), e); + } + if (isInvalidLocalHost(hostToBind)) { + if (CollectionUtils.isNotEmpty(registryURLs)) { + for (URL registryURL : registryURLs) { + if (MULTICAST.equalsIgnoreCase(registryURL.getParameter("registry"))) { + // skip multicast registry since we cannot connect to it via Socket + continue; + } + try (Socket socket = new Socket()) { + SocketAddress addr = new InetSocketAddress(registryURL.getHost(), registryURL.getPort()); + socket.connect(addr, 1000); + hostToBind = socket.getLocalAddress().getHostAddress(); + break; + } catch (Exception e) { + logger.warn(e.getMessage(), e); + } + } + } + if (isInvalidLocalHost(hostToBind)) { + hostToBind = getLocalHost(); + } + } + } + } + + map.put(Constants.BIND_IP_KEY, hostToBind); + + // registry ip is not used for bind ip by default + String hostToRegistry = getValueFromConfig(protocolConfig, DUBBO_IP_TO_REGISTRY); + if (hostToRegistry != null && hostToRegistry.length() > 0 && isInvalidLocalHost(hostToRegistry)) { + throw new IllegalArgumentException("Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry); + } else if (StringUtils.isEmpty(hostToRegistry)) { + // bind ip is used as registry ip by default + hostToRegistry = hostToBind; + } + + map.put(ANYHOST_KEY, String.valueOf(anyhost)); + + return hostToRegistry; + } + + /** + * Register port and bind port for the provider, can be configured separately + * Configuration priority: environment variable -> java system properties -> port property in protocol config file + * -> protocol default port + * + * @param protocolConfig + * @param name + * @return + */ + private Integer findConfigedPorts(ProtocolConfig protocolConfig, String name, Map map) { + Integer portToBind = null; + + // parse bind port from environment + String port = getValueFromConfig(protocolConfig, DUBBO_PORT_TO_BIND); + portToBind = parsePort(port); + + // if there's no bind port found from environment, keep looking up. + if (portToBind == null) { + portToBind = protocolConfig.getPort(); + if (provider != null && (portToBind == null || portToBind == 0)) { + portToBind = provider.getPort(); + } + final int defaultPort = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(name).getDefaultPort(); + if (portToBind == null || portToBind == 0) { + portToBind = defaultPort; + } + if (portToBind == null || portToBind <= 0) { + portToBind = getRandomPort(name); + if (portToBind == null || portToBind < 0) { + portToBind = getAvailablePort(defaultPort); + putRandomPort(name, portToBind); + } + } + } + + // save bind port, used as url's key later + map.put(Constants.BIND_PORT_KEY, String.valueOf(portToBind)); + + // registry port, not used as bind port by default + String portToRegistryStr = getValueFromConfig(protocolConfig, DUBBO_PORT_TO_REGISTRY); + Integer portToRegistry = parsePort(portToRegistryStr); + if (portToRegistry == null) { + portToRegistry = portToBind; + } + + return portToRegistry; + } + + private Integer parsePort(String configPort) { + Integer port = null; + if (configPort != null && configPort.length() > 0) { + try { + Integer intPort = Integer.parseInt(configPort); + if (isInvalidPort(intPort)) { + throw new IllegalArgumentException("Specified invalid port from env value:" + configPort); + } + port = intPort; + } catch (Exception e) { + throw new IllegalArgumentException("Specified invalid port from env value:" + configPort); + } + } + return port; + } + + private String getValueFromConfig(ProtocolConfig protocolConfig, String key) { + String protocolPrefix = protocolConfig.getName().toUpperCase() + "_"; + String port = ConfigUtils.getSystemProperty(protocolPrefix + key); + if (StringUtils.isEmpty(port)) { + port = ConfigUtils.getSystemProperty(key); + } + return port; + } + + private void completeCompoundConfigs() { + if (provider != null) { + if (application == null) { + setApplication(provider.getApplication()); + } + if (module == null) { + setModule(provider.getModule()); + } + if (registries == null) { + setRegistries(provider.getRegistries()); + } + if (monitor == null) { + setMonitor(provider.getMonitor()); + } + if (protocols == null) { + setProtocols(provider.getProtocols()); + } + if (configCenter == null) { + setConfigCenter(provider.getConfigCenter()); + } + } + if (module != null) { + if (registries == null) { + setRegistries(module.getRegistries()); + } + if (monitor == null) { + setMonitor(module.getMonitor()); + } + } + if (application != null) { + if (registries == null) { + setRegistries(application.getRegistries()); + } + if (monitor == null) { + setMonitor(application.getMonitor()); + } + } + } + + private void checkDefault() { + createProviderIfAbsent(); + } + + private void createProviderIfAbsent() { + if (provider != null) { + return; + } + setProvider( + ConfigManager.getInstance() + .getDefaultProvider() + .orElseGet(() -> { + ProviderConfig providerConfig = new ProviderConfig(); + providerConfig.refresh(); + return providerConfig; + }) + ); + } + + private void checkProtocol() { + if (CollectionUtils.isEmpty(protocols) && provider != null) { + setProtocols(provider.getProtocols()); + } + convertProtocolIdsToProtocols(); + } + + private void convertProtocolIdsToProtocols() { + if (StringUtils.isEmpty(protocolIds) && CollectionUtils.isEmpty(protocols)) { + List configedProtocols = new ArrayList<>(); + configedProtocols.addAll(getSubProperties(Environment.getInstance() + .getExternalConfigurationMap(), PROTOCOLS_SUFFIX)); + configedProtocols.addAll(getSubProperties(Environment.getInstance() + .getAppExternalConfigurationMap(), PROTOCOLS_SUFFIX)); + + protocolIds = String.join(",", configedProtocols); + } + + if (StringUtils.isEmpty(protocolIds)) { + if (CollectionUtils.isEmpty(protocols)) { + setProtocols( + ConfigManager.getInstance().getDefaultProtocols() + .filter(CollectionUtils::isNotEmpty) + .orElseGet(() -> { + ProtocolConfig protocolConfig = new ProtocolConfig(); + protocolConfig.refresh(); + return new ArrayList<>(Arrays.asList(protocolConfig)); + }) + ); + } + } else { + String[] arr = COMMA_SPLIT_PATTERN.split(protocolIds); + List tmpProtocols = CollectionUtils.isNotEmpty(protocols) ? protocols : new ArrayList<>(); + Arrays.stream(arr).forEach(id -> { + if (tmpProtocols.stream().noneMatch(prot -> prot.getId().equals(id))) { + tmpProtocols.add(ConfigManager.getInstance().getProtocol(id).orElseGet(() -> { + ProtocolConfig protocolConfig = new ProtocolConfig(); + protocolConfig.setId(id); + protocolConfig.refresh(); + return protocolConfig; + })); + } + }); + if (tmpProtocols.size() > arr.length) { + throw new IllegalStateException("Too much protocols found, the protocols comply to this service are :" + protocolIds + " but got " + protocols + .size() + " registries!"); + } + setProtocols(tmpProtocols); + } + } + + public Class getInterfaceClass() { + if (interfaceClass != null) { + return interfaceClass; + } + if (ref instanceof GenericService) { + return GenericService.class; + } + try { + if (interfaceName != null && interfaceName.length() > 0) { + this.interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() + .getContextClassLoader()); + } + } catch (ClassNotFoundException t) { + throw new IllegalStateException(t.getMessage(), t); + } + return interfaceClass; + } + + /** + * @param interfaceClass + * @see #setInterface(Class) + * @deprecated + */ + public void setInterfaceClass(Class interfaceClass) { + setInterface(interfaceClass); + } + + public String getInterface() { + return interfaceName; + } + + public void setInterface(Class interfaceClass) { + if (interfaceClass != null && !interfaceClass.isInterface()) { + throw new IllegalStateException("The interface class " + interfaceClass + " is not a interface!"); + } + this.interfaceClass = interfaceClass; + setInterface(interfaceClass == null ? null : interfaceClass.getName()); + } + + public void setInterface(String interfaceName) { + this.interfaceName = interfaceName; + if (StringUtils.isEmpty(id)) { + id = interfaceName; + } + } + + public T getRef() { + return ref; + } + + public void setRef(T ref) { + this.ref = ref; + } + + @Parameter(excluded = true) + public String getPath() { + return path; + } + + public void setPath(String path) { + checkPathName(PATH_KEY, path); + this.path = path; + } + + public List getMethods() { + return methods; + } + + // ======== Deprecated ======== + + @SuppressWarnings("unchecked") + public void setMethods(List methods) { + this.methods = (List) methods; + } + + public ProviderConfig getProvider() { + return provider; + } + + public void setProvider(ProviderConfig provider) { + ConfigManager.getInstance().addProvider(provider); + this.provider = provider; + } + + @Parameter(excluded = true) + public String getProviderIds() { + return providerIds; + } + + public void setProviderIds(String providerIds) { + this.providerIds = providerIds; + } + + public String getGeneric() { + return generic; + } + + public void setGeneric(String generic) { + if (StringUtils.isEmpty(generic)) { + return; + } + if (ProtocolUtils.isGeneric(generic)) { + this.generic = generic; + } else { + throw new IllegalArgumentException("Unsupported generic type " + generic); + } + } + + @Override + public void setMock(Boolean mock) { + throw new IllegalArgumentException("mock doesn't support on provider side"); + } + + @Override + public void setMock(String mock) { + throw new IllegalArgumentException("mock doesn't support on provider side"); + } + + public List getExportedUrls() { + return urls; + } + + /** + * @deprecated Replace to getProtocols() + */ + @Deprecated + public List getProviders() { + return convertProtocolToProvider(protocols); + } + + /** + * @deprecated Replace to setProtocols() + */ + @Deprecated + public void setProviders(List providers) { + this.protocols = convertProviderToProtocol(providers); + } + + @Override + @Parameter(excluded = true) + 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 00000000000..58b871ea7e9 --- /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 00000000000..dcf02cfec77 --- /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 00000000000..e6e9c09a108 --- /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 00000000000..f4914fe8034 --- /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 00000000000..d3e4226767a --- /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 00000000000..30626fd41fc --- /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 d1e02636dd7..f9afb355945 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 147648b67fb..f45727dd325 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; @@ -48,8 +53,9 @@ import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER; import static org.apache.dubbo.common.constants.CommonConstants.SIDE_KEY; import static org.apache.dubbo.rpc.cluster.Constants.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.common.constants.RegistryConstants.EXPORT_KEY; +import static org.apache.dubbo.config.Constants.SHUTDOWN_TIMEOUT_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 +81,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 +143,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 +198,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 00000000000..48f3f38c068 --- /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-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/AnnotationBeanNameBuilder.java b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/AnnotationBeanNameBuilder.java deleted file mode 100644 index 6e18e2a3471..00000000000 --- a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/AnnotationBeanNameBuilder.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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.spring.beans.factory.annotation; - -import org.apache.dubbo.common.constants.CommonConstants; -import org.apache.dubbo.config.annotation.Reference; -import org.apache.dubbo.config.annotation.Service; -import org.apache.dubbo.registry.Registry; - -import org.springframework.core.env.Environment; - -import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_PROTOCOL; -import static org.apache.dubbo.common.constants.RegistryConstants.CONSUMERS_CATEGORY; -import static org.apache.dubbo.common.constants.RegistryConstants.PROVIDERS_CATEGORY; -import static org.apache.dubbo.config.spring.util.AnnotationUtils.resolveInterfaceName; -import static org.springframework.util.StringUtils.arrayToCommaDelimitedString; -import static org.springframework.util.StringUtils.hasText; - -/** - * The Bean Name Builder for the annotations {@link Service} and {@link Reference} - *

    - * The naming rule is consistent with the the implementation {@link Registry} that is based on the service-name aware - * infrastructure, e.g Spring Cloud, Cloud Native and so on. - *

    - * The pattern of bean name : ${category}:${protocol}:${serviceInterface}:${version}:${group}. - *

    - * ${version} and ${group} are optional. - * - * @since 2.6.6 - */ -class AnnotationBeanNameBuilder { - - private static final String SEPARATOR = ":"; - - // Required properties - - private final String category; - - private final String protocol; - - private final String interfaceClassName; - - // Optional properties - - private String version; - - private String group; - - private Environment environment; - - private AnnotationBeanNameBuilder(String category, String protocol, String interfaceClassName) { - this.category = category; - this.protocol = protocol; - this.interfaceClassName = interfaceClassName; - } - - private AnnotationBeanNameBuilder(Service service, Class interfaceClass) { - this(PROVIDERS_CATEGORY, resolveProtocol(service.protocol()), resolveInterfaceName(service, interfaceClass)); - this.group(service.group()); - this.version(service.version()); - } - - private AnnotationBeanNameBuilder(Reference reference, Class interfaceClass) { - this(CONSUMERS_CATEGORY, resolveProtocol(reference.protocol()), resolveInterfaceName(reference, interfaceClass)); - this.group(reference.group()); - this.version(reference.version()); - } - - public static AnnotationBeanNameBuilder create(Service service, Class interfaceClass) { - return new AnnotationBeanNameBuilder(service, interfaceClass); - } - - public static AnnotationBeanNameBuilder create(Reference reference, Class interfaceClass) { - return new AnnotationBeanNameBuilder(reference, interfaceClass); - } - - private static void append(StringBuilder builder, String value) { - if (hasText(value)) { - builder.append(SEPARATOR).append(value); - } - } - - public AnnotationBeanNameBuilder group(String group) { - this.group = group; - return this; - } - - public AnnotationBeanNameBuilder version(String version) { - this.version = version; - return this; - } - - public AnnotationBeanNameBuilder environment(Environment environment) { - this.environment = environment; - return this; - } - - /** - * Resolve the protocol - * - * @param protocols one or more protocols - * @return if protocols == null, it will return - * {@link CommonConstants#DEFAULT_PROTOCOL "dubbo"} as the default protocol - * @see CommonConstants#DEFAULT_PROTOCOL - */ - private static String resolveProtocol(String... protocols) { - String protocol = arrayToCommaDelimitedString(protocols); - return hasText(protocol) ? protocol : DEFAULT_PROTOCOL; - } - - /** - * Build bean name while resolve the placeholders if possible. - * - * @return pattern : ${category}:${protocol}:${serviceInterface}:${version}:${group} - */ - public String build() { - // Append the required properties - StringBuilder beanNameBuilder = new StringBuilder(category); - append(beanNameBuilder, protocol); - append(beanNameBuilder, interfaceClassName); - // Append the optional properties - append(beanNameBuilder, version); - append(beanNameBuilder, group); - String beanName = beanNameBuilder.toString(); - // Resolve placeholders - return environment != null ? environment.resolvePlaceholders(beanName) : beanName; - } -} diff --git a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ReferenceAnnotationBeanPostProcessor.java b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ReferenceAnnotationBeanPostProcessor.java index 7c3a6b9b87d..b40948de95a 100644 --- a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ReferenceAnnotationBeanPostProcessor.java +++ b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ReferenceAnnotationBeanPostProcessor.java @@ -41,6 +41,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import static org.apache.dubbo.config.spring.beans.factory.annotation.ServiceBeanNameBuilder.create; + /** * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation * that Consumer service {@link Reference} annotated fields @@ -178,16 +180,14 @@ protected String buildInjectedObjectCacheKey(Reference reference, Object bean, S return buildReferencedBeanName(reference, injectedType) + "#source=" + (injectedElement.getMember()) + - "#attributes=" + AnnotationUtils.getAttributes(reference,getEnvironment(),true); + "#attributes=" + AnnotationUtils.getAttributes(reference, getEnvironment(), true); } private String buildReferencedBeanName(Reference reference, Class injectedType) { - AnnotationBeanNameBuilder builder = AnnotationBeanNameBuilder.create(reference, injectedType); - - builder.environment(getEnvironment()); + ServiceBeanNameBuilder serviceBeanNameBuilder = create(reference, injectedType, getEnvironment()); - return getEnvironment().resolvePlaceholders(builder.build()); + return serviceBeanNameBuilder.build(); } private ReferenceBean buildReferenceBeanIfAbsent(String referencedBeanName, Reference reference, diff --git a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceAnnotationBeanPostProcessor.java b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceAnnotationBeanPostProcessor.java index 305e3724132..e12db85fa7b 100644 --- a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceAnnotationBeanPostProcessor.java +++ b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceAnnotationBeanPostProcessor.java @@ -60,6 +60,7 @@ import java.util.Map; import java.util.Set; +import static org.apache.dubbo.config.spring.beans.factory.annotation.ServiceBeanNameBuilder.create; import static org.apache.dubbo.config.spring.util.ObjectUtils.of; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; import static org.springframework.context.annotation.AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR; @@ -259,7 +260,7 @@ private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder, Bean buildServiceBeanDefinition(service, interfaceClass, annotatedServiceBeanName); // ServiceBean Bean name - String beanName = generateServiceBeanName(service, interfaceClass, annotatedServiceBeanName); + String beanName = generateServiceBeanName(service, interfaceClass); if (scanner.checkCandidate(beanName, serviceBeanDefinition)) { // check duplicated candidate bean registry.registerBeanDefinition(beanName, serviceBeanDefinition); @@ -285,19 +286,14 @@ private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder, Bean * Generates the bean name of {@link ServiceBean} * * @param service - * @param interfaceClass the class of interface annotated {@link Service} - * @param annotatedServiceBeanName the bean name of annotated {@link Service} + * @param interfaceClass the class of interface annotated {@link Service} * @return ServiceBean@interfaceClassName#annotatedServiceBeanName * @since 2.5.9 */ - private String generateServiceBeanName(Service service, Class interfaceClass, String annotatedServiceBeanName) { - - AnnotationBeanNameBuilder builder = AnnotationBeanNameBuilder.create(service, interfaceClass); - - builder.environment(environment); + private String generateServiceBeanName(Service service, Class interfaceClass) { + ServiceBeanNameBuilder builder = create(service, interfaceClass, environment); return builder.build(); - } private Class resolveServiceInterfaceClass(Class annotatedServiceBeanClass, Service service) { diff --git a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceBeanNameBuilder.java b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceBeanNameBuilder.java new file mode 100644 index 00000000000..5d272515ab3 --- /dev/null +++ b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceBeanNameBuilder.java @@ -0,0 +1,112 @@ +/* + * 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.spring.beans.factory.annotation; + +import org.apache.dubbo.config.annotation.Reference; +import org.apache.dubbo.config.annotation.Service; +import org.apache.dubbo.config.spring.ReferenceBean; +import org.apache.dubbo.config.spring.ServiceBean; + +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +import static org.apache.dubbo.config.spring.util.AnnotationUtils.resolveInterfaceName; + +/** + * Dubbo {@link Service @Service} Bean Builder + * + * @see Service + * @see Reference + * @see ServiceBean + * @see ReferenceBean + * @since 2.6.5 + */ +public class ServiceBeanNameBuilder { + + private static final String SEPARATOR = ":"; + + private final String interfaceClassName; + + private final Environment environment; + + // Optional + private String version; + + private String group; + + private ServiceBeanNameBuilder(String interfaceClassName, Environment environment) { + this.interfaceClassName = interfaceClassName; + this.environment = environment; + } + + private ServiceBeanNameBuilder(Class interfaceClass, Environment environment) { + this(interfaceClass.getName(), environment); + } + + private ServiceBeanNameBuilder(Service service, Class interfaceClass, Environment environment) { + this(resolveInterfaceName(service, interfaceClass), environment); + this.group(service.group()); + this.version(service.version()); + } + + private ServiceBeanNameBuilder(Reference reference, Class interfaceClass, Environment environment) { + this(resolveInterfaceName(reference, interfaceClass), environment); + this.group(reference.group()); + this.version(reference.version()); + } + + public static ServiceBeanNameBuilder create(Class interfaceClass, Environment environment) { + return new ServiceBeanNameBuilder(interfaceClass, environment); + } + + public static ServiceBeanNameBuilder create(Service service, Class interfaceClass, Environment environment) { + return new ServiceBeanNameBuilder(service, interfaceClass, environment); + } + + public static ServiceBeanNameBuilder create(Reference reference, Class interfaceClass, Environment environment) { + return new ServiceBeanNameBuilder(reference, interfaceClass, environment); + } + + private static void append(StringBuilder builder, String value) { + if (StringUtils.hasText(value)) { + builder.append(value).append(SEPARATOR); + } + } + + public ServiceBeanNameBuilder group(String group) { + this.group = group; + return this; + } + + public ServiceBeanNameBuilder version(String version) { + this.version = version; + return this; + } + + public String build() { + StringBuilder beanNameBuilder = new StringBuilder("ServiceBean").append(SEPARATOR); + // Required + append(beanNameBuilder, interfaceClassName); + // Optional + append(beanNameBuilder, version); + append(beanNameBuilder, group); + // Build and remove last ":" + String rawBeanName = beanNameBuilder.substring(0, beanNameBuilder.length() - 1); + // Resolve placeholders + return environment.resolvePlaceholders(rawBeanName); + } +} diff --git a/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/beans/factory/annotation/AnnotationBeanNameBuilderTest.java b/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceBeanNameBuilderTest.java similarity index 67% rename from dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/beans/factory/annotation/AnnotationBeanNameBuilderTest.java rename to dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceBeanNameBuilderTest.java index 2e79109ab33..747fc1e2693 100644 --- a/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/beans/factory/annotation/AnnotationBeanNameBuilderTest.java +++ b/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/beans/factory/annotation/ServiceBeanNameBuilderTest.java @@ -27,18 +27,18 @@ import org.springframework.mock.env.MockEnvironment; import org.springframework.util.ReflectionUtils; -import static org.apache.dubbo.config.spring.beans.factory.annotation.AnnotationBeanNameBuilderTest.GROUP; -import static org.apache.dubbo.config.spring.beans.factory.annotation.AnnotationBeanNameBuilderTest.VERSION; +import static org.apache.dubbo.config.spring.beans.factory.annotation.ServiceBeanNameBuilderTest.GROUP; +import static org.apache.dubbo.config.spring.beans.factory.annotation.ServiceBeanNameBuilderTest.VERSION; /** - * {@link AnnotationBeanNameBuilder} Test + * {@link ServiceBeanNameBuilder} Test * - * @see AnnotationBeanNameBuilder + * @see ServiceBeanNameBuilder * @since 2.6.6 */ @Service(interfaceClass = DemoService.class, group = GROUP, version = VERSION, application = "application", module = "module", registry = {"1", "2", "3"}) -public class AnnotationBeanNameBuilderTest { +public class ServiceBeanNameBuilderTest { @Reference(interfaceClass = DemoService.class, group = "DUBBO", version = "${dubbo.version}", application = "application", module = "module", registry = {"1", "2", "3"}) @@ -58,25 +58,20 @@ public void prepare() { @Test public void testServiceAnnotation() { - Service service = AnnotationUtils.getAnnotation(AnnotationBeanNameBuilderTest.class, Service.class); - AnnotationBeanNameBuilder builder = AnnotationBeanNameBuilder.create(service, INTERFACE_CLASS); - Assert.assertEquals("providers:dubbo:org.apache.dubbo.config.spring.api.DemoService:1.0.0:DUBBO", + Service service = AnnotationUtils.getAnnotation(ServiceBeanNameBuilderTest.class, Service.class); + ServiceBeanNameBuilder builder = ServiceBeanNameBuilder.create(service, INTERFACE_CLASS, environment); + Assert.assertEquals("ServiceBean:org.apache.dubbo.config.spring.api.DemoService:1.0.0:DUBBO", builder.build()); - builder.environment(environment); - Assert.assertEquals("providers:dubbo:org.apache.dubbo.config.spring.api.DemoService:1.0.0:DUBBO", + Assert.assertEquals("ServiceBean:org.apache.dubbo.config.spring.api.DemoService:1.0.0:DUBBO", builder.build()); } @Test public void testReferenceAnnotation() { - Reference reference = AnnotationUtils.getAnnotation(ReflectionUtils.findField(AnnotationBeanNameBuilderTest.class, "INTERFACE_CLASS"), Reference.class); - AnnotationBeanNameBuilder builder = AnnotationBeanNameBuilder.create(reference, INTERFACE_CLASS); - Assert.assertEquals("consumers:dubbo:org.apache.dubbo.config.spring.api.DemoService:${dubbo.version}:DUBBO", - builder.build()); - - builder.environment(environment); - Assert.assertEquals("consumers:dubbo:org.apache.dubbo.config.spring.api.DemoService:1.0.0:DUBBO", + Reference reference = AnnotationUtils.getAnnotation(ReflectionUtils.findField(ServiceBeanNameBuilderTest.class, "INTERFACE_CLASS"), Reference.class); + ServiceBeanNameBuilder builder = ServiceBeanNameBuilder.create(reference, INTERFACE_CLASS, environment); + Assert.assertEquals("ServiceBean:org.apache.dubbo.config.spring.api.DemoService:1.0.0:DUBBO", builder.build()); } diff --git a/dubbo-configcenter/dubbo-configcenter-api/src/main/java/org/apache/dubbo/configcenter/DynamicConfiguration.java b/dubbo-configcenter/dubbo-configcenter-api/src/main/java/org/apache/dubbo/configcenter/DynamicConfiguration.java index 23fc0f5e8bd..0c833af7e82 100644 --- a/dubbo-configcenter/dubbo-configcenter-api/src/main/java/org/apache/dubbo/configcenter/DynamicConfiguration.java +++ b/dubbo-configcenter/dubbo-configcenter-api/src/main/java/org/apache/dubbo/configcenter/DynamicConfiguration.java @@ -19,7 +19,12 @@ import org.apache.dubbo.common.config.Configuration; import org.apache.dubbo.common.config.Environment; +import java.util.Collections; import java.util.Optional; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; import static org.apache.dubbo.common.extension.ExtensionLoader.getExtensionLoader; @@ -28,12 +33,13 @@ *
    * From the use scenario internally in framework, there're mainly three kinds of methods: *

      - *
    • 1. getConfig, get governance rules or single config item from Config Center.
    • - *
    • 2. getConfigFile, get configuration file from Config Center at start up.
    • - *
    • 3. addListener/removeListener, add or remove listeners for governance rules or config items that need to watch.
    • + *
    • 1. getConfig, get governance rules or single config item from Config Center.
    • + *
    • 2. getConfigFile, get configuration file from Config Center at start up.
    • + *
    • 3. addListener/removeListener, add or remove listeners for governance rules or config items that need to watch.
    • *
    */ public interface DynamicConfiguration extends Configuration { + String DEFAULT_GROUP = "dubbo"; /** @@ -113,7 +119,7 @@ default String getConfig(String key, String group) { /** * {@see #getConfig(String, String, long)} - * + *

    * This method are mostly used to get a compound config file, such as a complete dubbo.properties file. */ default String getConfigs(String key, String group) throws IllegalStateException { @@ -122,11 +128,67 @@ default String getConfigs(String key, String group) throws IllegalStateException /** * {@see #getConfig(String, String, long)} - * + *

    * This method are mostly used to get a compound config file, such as a complete dubbo.properties file. */ String getConfigs(String key, String group, long timeout) throws IllegalStateException; + /** + * Publish Config mapped to the given key and the given group. + * + * @param key the key to represent a configuration + * @param group the group where the key belongs to + * @param content the content of configuration + * @return true if success, or false + * @throws UnsupportedOperationException If the under layer does not support + * @since 2.7.2 + */ + default boolean publishConfig(String key, String group, String content) throws UnsupportedOperationException { + throw new UnsupportedOperationException("No support"); + } + + /** + * Get the config keys by the specified group + * + * @param group the specified group + * @return the read-only non-null sorted {@link Set set} of config keys + * @throws UnsupportedOperationException If the under layer does not support + * @since 2.7.2 + */ + default SortedSet getConfigKeys(String group) throws UnsupportedOperationException { + throw new UnsupportedOperationException("No support"); + } + + /** + * Get the {@link SortedMap} with with config keys and contents value by the specified group + * + * @param group the specified group + * @return the read-only non-null sorted {@link SortedMap map} + * @throws UnsupportedOperationException If the under layer does not support + * @since 2.7.2 + */ + default SortedMap getConfigs(String group) throws UnsupportedOperationException { + return getConfigs(group, -1); + } + + /** + * Get the {@link SortedMap} with with config keys and content value by the specified group + * + * @param group the specified group + * @param timeout the millisecond for timeout + * @return the read-only non-null sorted {@link SortedMap map} + * @throws UnsupportedOperationException If the under layer does not support + * @throws IllegalStateException If timeout exceeds + * @since 2.7.2 + */ + default SortedMap getConfigs(String group, long timeout) throws UnsupportedOperationException, + IllegalStateException { + SortedMap configs = new TreeMap<>(); + SortedSet configKeys = getConfigKeys(group); + configKeys.forEach(key -> configs.put(key, getConfig(key, group, timeout))); + return Collections.unmodifiableSortedMap(configs); + } + /** * Find DynamicConfiguration instance * diff --git a/dubbo-configcenter/dubbo-configcenter-api/src/main/java/org/apache/dubbo/configcenter/support/nop/NopDynamicConfiguration.java b/dubbo-configcenter/dubbo-configcenter-api/src/main/java/org/apache/dubbo/configcenter/support/nop/NopDynamicConfiguration.java index dbb91fdffee..52c309a437d 100644 --- a/dubbo-configcenter/dubbo-configcenter-api/src/main/java/org/apache/dubbo/configcenter/support/nop/NopDynamicConfiguration.java +++ b/dubbo-configcenter/dubbo-configcenter-api/src/main/java/org/apache/dubbo/configcenter/support/nop/NopDynamicConfiguration.java @@ -20,6 +20,10 @@ import org.apache.dubbo.configcenter.ConfigurationListener; import org.apache.dubbo.configcenter.DynamicConfiguration; +import java.util.SortedSet; + +import static java.util.Collections.emptySortedSet; + /** * The default extension of {@link DynamicConfiguration}. If user does not specify a config centre, or specifies one * that is not a valid extension, it will default to this one. @@ -30,7 +34,6 @@ public NopDynamicConfiguration(URL url) { // no-op } - @Override public Object getInternalProperty(String key) { return null; @@ -55,4 +58,20 @@ public String getConfig(String key, String group, long timeout) throws IllegalSt public String getConfigs(String key, String group, long timeout) throws IllegalStateException { return null; } + + /** + * @since 2.7.2 + */ + @Override + public boolean publishConfig(String key, String group, String content) { + return true; + } + + /** + * @since 2.7.2 + */ + @Override + public SortedSet getConfigKeys(String group) { + return emptySortedSet(); + } } diff --git a/dubbo-configcenter/dubbo-configcenter-api/src/test/java/org/apache/dubbo/configcenter/support/nop/NopDynamicConfigurationTest.java b/dubbo-configcenter/dubbo-configcenter-api/src/test/java/org/apache/dubbo/configcenter/support/nop/NopDynamicConfigurationTest.java new file mode 100644 index 00000000000..7ba5bbef917 --- /dev/null +++ b/dubbo-configcenter/dubbo-configcenter-api/src/test/java/org/apache/dubbo/configcenter/support/nop/NopDynamicConfigurationTest.java @@ -0,0 +1,70 @@ +/* + * 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.configcenter.support.nop; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link NopDynamicConfiguration} Test + * + * @since 2.7.2 + */ +public class NopDynamicConfigurationTest { + + private NopDynamicConfiguration configuration = new NopDynamicConfiguration(null); + + @Test + public void testGetInternalProperty() { + assertNull(configuration.getInternalProperty(null)); + } + + @Test + public void testGetConfig() { + assertNull(configuration.getConfig(null, null, -1)); + } + + + @Test + public void testGetConfigs() { + assertNull(configuration.getConfigs(null, null, -1)); + } + + @Test + public void testAddListener() { + configuration.addListener(null, null, null); + } + + @Test + public void testRemoveListener() { + configuration.removeListener(null, null, null); + } + + @Test + public void testPublishConfig() { + assertTrue(configuration.publishConfig(null, null, null)); + } + + @Test + public void testGetConfigKeys() { + assertTrue(configuration.getConfigKeys(null).isEmpty()); + } + + +} diff --git a/dubbo-configcenter/dubbo-configcenter-zookeeper/src/main/java/org/apache/dubbo/configcenter/support/zookeeper/ZookeeperDynamicConfiguration.java b/dubbo-configcenter/dubbo-configcenter-zookeeper/src/main/java/org/apache/dubbo/configcenter/support/zookeeper/ZookeeperDynamicConfiguration.java index a06537b432c..5b034ca3740 100644 --- a/dubbo-configcenter/dubbo-configcenter-zookeeper/src/main/java/org/apache/dubbo/configcenter/support/zookeeper/ZookeeperDynamicConfiguration.java +++ b/dubbo-configcenter/dubbo-configcenter-zookeeper/src/main/java/org/apache/dubbo/configcenter/support/zookeeper/ZookeeperDynamicConfiguration.java @@ -27,16 +27,25 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import static java.util.Collections.emptySortedSet; +import static java.util.Collections.unmodifiableSortedSet; +import static org.apache.dubbo.common.utils.CollectionUtils.isEmpty; import static org.apache.dubbo.configcenter.Constants.CONFIG_NAMESPACE_KEY; /** * */ public class ZookeeperDynamicConfiguration implements DynamicConfiguration { + + private static final String EMPTY_STRING = ""; + private static final Logger logger = LoggerFactory.getLogger(ZookeeperDynamicConfiguration.class); private Executor executor; @@ -91,12 +100,53 @@ public void removeListener(String key, String group, ConfigurationListener liste @Override public String getConfig(String key, String group, long timeout) throws IllegalStateException { + String path = buildPath(key, group); + return (String) getInternalProperty(path); + } + + /** + * For zookeeper, {@link #getConfig(String, String, long)} and {@link #getConfigs(String, String, long)} have the same meaning. + * + * @param key + * @param group + * @param timeout + * @return + * @throws IllegalStateException + */ + @Override + public String getConfigs(String key, String group, long timeout) throws IllegalStateException { + return getConfig(key, group, timeout); + } + + @Override + public boolean publishConfig(String key, String group, String content) { + String path = buildPath(key, group); + zkClient.create(path, content, true); + return true; + } + + @Override + public SortedSet getConfigKeys(String group) { + String path = buildPath(group); + List nodes = zkClient.getChildren(path); + return isEmpty(nodes) ? emptySortedSet() : unmodifiableSortedSet(new TreeSet<>(nodes)); + } + + /** + * Build the config node path by the specified key and group + * + * @param key the key to represent a configuration + * @param group the group where the key belongs to + * @return + */ + protected String buildPath(String key, String group) { + String path = null; /** * when group is not null, we are getting startup configs from Config Center, for example: * group=dubbo, key=dubbo.properties */ if (StringUtils.isNotEmpty(group)) { - key = group + "/" + key; + path = group + "/" + key; } /** * when group is null, we are fetching governance rules, for example: @@ -105,23 +155,14 @@ public String getConfig(String key, String group, long timeout) throws IllegalSt */ else { int i = key.lastIndexOf("."); - key = key.substring(0, i) + "/" + key.substring(i + 1); + path = key.substring(0, i) + "/" + key.substring(i + 1); } - - return (String) getInternalProperty(rootPath + "/" + key); + return buildPath(path); } - /** - * For zookeeper, {@link #getConfig(String, String, long)} and {@link #getConfigs(String, String, long)} have the same meaning. - * - * @param key - * @param group - * @param timeout - * @return - * @throws IllegalStateException - */ - @Override - public String getConfigs(String key, String group, long timeout) throws IllegalStateException { - return (String) getConfig(key, group, timeout); + protected String buildPath(String relativePath) { + String path = rootPath + "/" + relativePath; + return path; } + } diff --git a/dubbo-configcenter/dubbo-configcenter-zookeeper/src/test/java/org/apache/dubbo/configcenter/support/zookeeper/ZookeeperDynamicConfigurationTest.java b/dubbo-configcenter/dubbo-configcenter-zookeeper/src/test/java/org/apache/dubbo/configcenter/support/zookeeper/ZookeeperDynamicConfigurationTest.java index 64e0becc64f..df440acc620 100644 --- a/dubbo-configcenter/dubbo-configcenter-zookeeper/src/test/java/org/apache/dubbo/configcenter/support/zookeeper/ZookeeperDynamicConfigurationTest.java +++ b/dubbo-configcenter/dubbo-configcenter-zookeeper/src/test/java/org/apache/dubbo/configcenter/support/zookeeper/ZookeeperDynamicConfigurationTest.java @@ -35,8 +35,14 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.CountDownLatch; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * TODO refactor using mockito */ @@ -122,6 +128,39 @@ public void testAddListener() throws Exception { Assertions.assertEquals("new value2", listener4.getValue()); } + @Test + public void testPublishConfig() { + String key = "user-service"; + String group = "org.apache.dubbo.service.UserService"; + String content = "test"; + + assertTrue(configuration.publishConfig(key, group, content)); + assertEquals("test", configuration.getConfigs(key, group)); + } + + @Test + public void testGetConfigKeysAndContents() { + + String key = "user-service"; + String group = "org.apache.dubbo.service.UserService"; + String content = "test"; + + String key2 = "user-service-1"; + + assertTrue(configuration.publishConfig(key, group, content)); + assertTrue(configuration.publishConfig(key2, group, content)); + + Set configKeys = configuration.getConfigKeys(group); + + assertEquals(new TreeSet(asList(key, key2)), configKeys); + + Map configs = configuration.getConfigs(group); + + assertEquals(configs.keySet(), configKeys); + + configs.forEach((k, value) -> assertEquals(content, value)); + } + private class TestListener implements ConfigurationListener { private CountDownLatch latch; private String value; diff --git a/dubbo-dependencies-bom/pom.xml b/dubbo-dependencies-bom/pom.xml index ef42a6e4517..e250644f506 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 00000000000..017f0e79fde --- /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 00000000000..8989f29a0aa --- /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 00000000000..91282b34c6e --- /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 00000000000..13f2aec16e1 --- /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 00000000000..ab3a2e4998b --- /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 00000000000..92c35b9c882 --- /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 00000000000..591bcebb086 --- /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 00000000000..1857ac85f3f --- /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 00000000000..f7b0f329763 --- /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 00000000000..ecd54edb280 --- /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 00000000000..692b3b6d12b --- /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 00000000000..c3cc7323df2 --- /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 00000000000..785d411f256 --- /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 00000000000..f88b9df3318 --- /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 00000000000..f30327c4852 --- /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 00000000000..b8d57f93992 --- /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 00000000000..eb3d27b02ea --- /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 00000000000..92970343f1c --- /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 00000000000..755d482d2a8 --- /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 00000000000..3cb4b7fdd34 --- /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 00000000000..9a690ae58a5 --- /dev/null +++ b/dubbo-metadata/pom.xml @@ -0,0 +1,66 @@ + + + + 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-config-api + ${project.parent.version} + true + + + + org.apache.dubbo + dubbo-rpc-api + ${project.parent.version} + true + + + + + org.apache.dubbo + dubbo-configcenter-zookeeper + ${project.parent.version} + test + + + + org.apache.curator + curator-test + test + + + + + \ No newline at end of file diff --git a/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/DynamicConfigurationServiceNameMapping.java b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/DynamicConfigurationServiceNameMapping.java new file mode 100644 index 00000000000..4c171bf224a --- /dev/null +++ b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/DynamicConfigurationServiceNameMapping.java @@ -0,0 +1,95 @@ +/* + * 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.logger.Logger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.configcenter.DynamicConfiguration; +import org.apache.dubbo.rpc.model.ApplicationModel; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.apache.dubbo.common.utils.StringUtils.isBlank; + +/** + * The {@link ServiceNameMapping} implementation based on {@link DynamicConfiguration} + */ +public class DynamicConfigurationServiceNameMapping implements ServiceNameMapping { + + private static final String SEPARATOR = ":"; + + private static final String EMPTY = ""; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + public void map(String serviceInterface, String group, String version, String protocol) { + + DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration(); + + // the Dubbo Service Key as group + // the service(application) name as key + // It does matter whatever the content is, we just need a record + String key = ApplicationModel.getApplication(); + String content = String.valueOf(System.currentTimeMillis()); + execute(() -> { + dynamicConfiguration.publishConfig(key, buildGroup(serviceInterface, group, version, protocol), content); + if (logger.isInfoEnabled()) { + logger.info(String.format("The Dubbo service key[%s] mapped to service name[%s] with content : %s", + key, group, content)); + } + }); + } + + @Override + public Set get(String serviceInterface, String group, String version, String protocol) { + + DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration(); + + String key = ApplicationModel.getApplication(); + Set serviceNames = new LinkedHashSet<>(); + execute(() -> { + Set keys = dynamicConfiguration.getConfigKeys(buildGroup(serviceInterface, group, version, protocol)); + serviceNames.addAll(keys); + }); + return Collections.unmodifiableSet(serviceNames); + } + + protected static String buildGroup(String serviceInterface, String group, String version, String protocol) { + StringBuilder groupBuilder = new StringBuilder(serviceInterface) + .append(SEPARATOR).append(defaultString(group)) + .append(SEPARATOR).append(defaultString(version)) + .append(SEPARATOR).append(defaultString(protocol)); + return groupBuilder.toString(); + } + + private static String defaultString(String value) { + return isBlank(value) ? EMPTY : value; + } + + private void execute(Runnable runnable) { + try { + runnable.run(); + } catch (Throwable e) { + if (logger.isWarnEnabled()) { + logger.warn(e.getMessage(), e); + } + } + } +} 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 00000000000..1f90543eb6b --- /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 00000000000..97636338959 --- /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 00000000000..4cbbae57835 --- /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 00000000000..5f756a2b76c --- /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/java/org/apache/dubbo/metadata/ServiceNameMapping.java b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/ServiceNameMapping.java new file mode 100644 index 00000000000..5bbfb23c95c --- /dev/null +++ b/dubbo-metadata/src/main/java/org/apache/dubbo/metadata/ServiceNameMapping.java @@ -0,0 +1,64 @@ +/* + * 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.extension.SPI; + +import java.util.Set; + +import static org.apache.dubbo.common.extension.ExtensionLoader.getExtensionLoader; + +/** + * The interface for Dubbo service name Mapping + * + * @since 2.7.2 + */ +@SPI("default") +public interface ServiceNameMapping { + + /** + * Map the specified Dubbo service interface, group, version and protocol to current Dubbo service name + * + * @param serviceInterface the class name of Dubbo service interface + * @param group the group of Dubbo service interface (optional) + * @param version the version of Dubbo service interface version (optional) + * @param protocol the protocol of Dubbo service interface exported (optional) + */ + void map(String serviceInterface, String group, String version, String protocol); + + /** + * Get the service names from the specified Dubbo service interface, group, version and protocol + * + * @param serviceInterface the class name of Dubbo service interface + * @param group the group of Dubbo service interface (optional) + * @param version the version of Dubbo service interface version (optional) + * @param protocol the protocol of Dubbo service interface exported (optional) + * @return + */ + Set get(String serviceInterface, String group, String version, String protocol); + + + /** + * Get the default extension of {@link ServiceNameMapping} + * + * @return non-null {@link ServiceNameMapping} + * @see DynamicConfigurationServiceNameMapping + */ + static ServiceNameMapping getDefaultExtension() { + return getExtensionLoader(ServiceNameMapping.class).getDefaultExtension(); + } +} 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 00000000000..2af06fde611 --- /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/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.ServiceNameMapping b/dubbo-metadata/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.ServiceNameMapping new file mode 100644 index 00000000000..3975068fc93 --- /dev/null +++ b/dubbo-metadata/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.ServiceNameMapping @@ -0,0 +1 @@ +default=org.apache.dubbo.metadata.DynamicConfigurationServiceNameMapping \ No newline at end of file diff --git a/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/DynamicConfigurationServiceNameMappingTest.java b/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/DynamicConfigurationServiceNameMappingTest.java new file mode 100644 index 00000000000..e341d777b31 --- /dev/null +++ b/dubbo-metadata/src/test/java/org/apache/dubbo/metadata/DynamicConfigurationServiceNameMappingTest.java @@ -0,0 +1,114 @@ +/* + * 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.config.Environment; +import org.apache.dubbo.common.utils.NetUtils; +import org.apache.dubbo.configcenter.DynamicConfiguration; +import org.apache.dubbo.configcenter.DynamicConfigurationFactory; +import org.apache.dubbo.rpc.model.ApplicationModel; + +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.curator.test.TestingServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Set; +import java.util.TreeSet; + +import static java.util.Arrays.asList; +import static org.apache.dubbo.common.extension.ExtensionLoader.getExtensionLoader; +import static org.apache.dubbo.metadata.DynamicConfigurationServiceNameMapping.buildGroup; +import static org.apache.dubbo.metadata.ServiceNameMapping.getDefaultExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link DynamicConfigurationServiceNameMapping} Test + * + * @since 2.7.2 + */ +public class DynamicConfigurationServiceNameMappingTest { + + private static CuratorFramework client; + + private static URL configUrl; + private static int zkServerPort = NetUtils.getAvailablePort(); + private static TestingServer zkServer; + + private final ServiceNameMapping serviceNameMapping = getDefaultExtension(); + + @BeforeAll + public static void setUp() throws Exception { + + zkServer = new TestingServer(zkServerPort, true); + + client = CuratorFrameworkFactory.newClient("localhost:" + zkServerPort, 60 * 1000, 60 * 1000, + new ExponentialBackoffRetry(1000, 3)); + + client.start(); + + configUrl = URL.valueOf("zookeeper://localhost:" + zkServerPort); + + DynamicConfiguration configuration = getExtensionLoader(DynamicConfigurationFactory.class) + .getExtension(configUrl.getProtocol()) + .getDynamicConfiguration(configUrl); + + Environment.getInstance().setDynamicConfiguration(configuration); + } + + @AfterAll + public static void tearDown() throws Exception { + zkServer.stop(); + } + + @Test + public void testBuildGroup() { + assertEquals("test:::", buildGroup("test", null, null, null)); + assertEquals("test:default::", buildGroup("test", "default", null, null)); + assertEquals("test:default:1.0.0:", buildGroup("test", "default", "1.0.0", null)); + assertEquals("test:default:1.0.0:dubbo", buildGroup("test", "default", "1.0.0", "dubbo")); + } + + @Test + public void testMapAndGet() { + + String serviceName = "test"; + String serviceName2 = "test2"; + + ApplicationModel.setApplication(serviceName); + + String serviceInterface = "org.apache.dubbo.service.UserService"; + String group = null; + String version = null; + String protocol = null; + + serviceNameMapping.map(serviceInterface, group, version, protocol); + + ApplicationModel.setApplication(serviceName2); + + serviceNameMapping.map(serviceInterface, group, version, protocol); + + Set serviceNames = serviceNameMapping.get(serviceInterface, group, version, protocol); + + assertEquals(new TreeSet(asList(serviceName, serviceName2)), serviceNames); + + } +} 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 00000000000..835a0d56e3c --- /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 00000000000..937ddce985f --- /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 5a707d0686b..d5689b334e8 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 00000000000..d01891ddb58 --- /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 00000000000..dda1faa680c --- /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 00000000000..f0459281dad --- /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 00000000000..6a1888a4039 --- /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 00000000000..9006681cfe2 --- /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 00000000000..07b1bfceba6 --- /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 00000000000..1bbf768081c --- /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 00000000000..654f069ff02 --- /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 00000000000..eec9d06ac79 --- /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 00000000000..ef1151ad8fa --- /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 00000000000..f359d41376a --- /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 00000000000..3283c3c0c72 --- /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 00000000000..1ed3f203b77 --- /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 00000000000..fb31571aed2 --- /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 00000000000..4a3f61f0b53 --- /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 00000000000..4311d8259fd --- /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 00000000000..30bdd9c9bb3 --- /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-nacos/pom.xml b/dubbo-registry/dubbo-registry-nacos/pom.xml index b8c952d267b..675901e2ec3 100644 --- a/dubbo-registry/dubbo-registry-nacos/pom.xml +++ b/dubbo-registry/dubbo-registry-nacos/pom.xml @@ -92,49 +92,6 @@ - - org.apache.dubbo - dubbo-rpc-rest - ${project.version} - test - - - - org.jboss.resteasy - resteasy-jaxrs - test - - - - org.jboss.resteasy - resteasy-client - test - - - - org.jboss.resteasy - resteasy-netty4 - test - - - - javax.validation - validation-api - test - - - - org.jboss.resteasy - resteasy-jackson-provider - test - - - - org.jboss.resteasy - resteasy-jaxb-provider - test - - org.springframework spring-test diff --git a/dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/demo/consumer/DemoServiceConsumerBootstrap.java b/dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/demo/consumer/DemoServiceConsumerBootstrap.java index b411d4a80f0..f11bdb5df3f 100644 --- a/dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/demo/consumer/DemoServiceConsumerBootstrap.java +++ b/dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/demo/consumer/DemoServiceConsumerBootstrap.java @@ -37,14 +37,10 @@ public class DemoServiceConsumerBootstrap { @Reference(version = "${demo.service.version}") private DemoService demoService; - @Reference(version = "${demo.service.version}", protocol = "rest") - private DemoService restDemoService; - @PostConstruct public void init() throws InterruptedException { for (int j = 0; j < 10; j++) { System.out.println(demoService.sayName("小马哥(mercyblitz)")); - System.out.println(restDemoService.sayName("小马哥(mercyblitz)")); } Thread.sleep(TimeUnit.SECONDS.toMillis(5)); } diff --git a/dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/demo/service/DemoService.java b/dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/demo/service/DemoService.java index 0c808779311..77ac1b60e21 100644 --- a/dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/demo/service/DemoService.java +++ b/dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/demo/service/DemoService.java @@ -16,19 +16,13 @@ */ package org.apache.dubbo.demo.service; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; - /** * DemoService * * @since 2.6.5 */ -@Path("/demo-service") public interface DemoService { - @GET - String sayName(@QueryParam("name") String name); + String sayName(String name); } \ No newline at end of file diff --git a/dubbo-registry/dubbo-registry-nacos/src/test/resources/provider-config.properties b/dubbo-registry/dubbo-registry-nacos/src/test/resources/provider-config.properties index e2dd335b0ed..7e2046b5c40 100644 --- a/dubbo-registry/dubbo-registry-nacos/src/test/resources/provider-config.properties +++ b/dubbo-registry/dubbo-registry-nacos/src/test/resources/provider-config.properties @@ -6,9 +6,6 @@ dubbo.registry.address=127.0.0.1:8848 ## Exports multiple protocols ### Dubbo Protocol using random port dubbo.protocols.dubbo.port=-1 -### REST protocol -dubbo.protocols.rest.port=9090 -dubbo.protocols.rest.server=netty # Provider @Service info demo.service.version=1.0.0 demo.service.name=demoService \ No newline at end of file diff --git a/dubbo-registry/dubbo-registry-zookeeper/pom.xml b/dubbo-registry/dubbo-registry-zookeeper/pom.xml index 3aa13d3fcfd..cf3a1a90e1d 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 00000000000..71cca53a394 --- /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 00000000000..d9d6df0c35f --- /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 00000000000..8e055b65b6d --- /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 00000000000..7e9db4c54e6 --- /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 00000000000..bc466b8dcdd --- /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 00000000000..e74425f9f2a --- /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 e9a1661e033..edc06e12067 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,8 @@ dubbo-metadata-report dubbo-configcenter dubbo-dependencies + dubbo-event + dubbo-metadata