From cf3b8b9db0b2de9d578a4cc324285a4273b51af4 Mon Sep 17 00:00:00 2001 From: AmyliaY <471816751@qq.com> Date: Wed, 15 Apr 2020 22:25:13 +0800 Subject: [PATCH] =?UTF-8?q?1.=E5=A2=9E=E5=8A=A0Dubbo=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E5=88=86=E6=9E=90=E6=A8=A1=E5=9D=97=EF=BC=9B=202.Dubbo?= =?UTF-8?q?=E4=B8=8EJava=E7=9A=84SPI=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 + docs/Dubbo/SPI/Dubbo与Java的SPI机制.md | 1242 ++++++++++++++++++++ images/Dubbo/SPI组件目录结构.png | Bin 0 -> 19181 bytes 3 files changed, 1254 insertions(+) create mode 100644 docs/Dubbo/SPI/Dubbo与Java的SPI机制.md create mode 100644 images/Dubbo/SPI组件目录结构.png diff --git a/README.md b/README.md index ac65297..690e013 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,18 @@ - [Netty 高性能之道](docs/Netty/AdvancedFeaturesOfNetty/Netty高性能之道.md) - [Netty 高可靠性设计](docs/Netty/AdvancedFeaturesOfNetty/Netty高可靠性设计.md) +## Dubbo +### 架构设计 + +### SPI +- [Dubbo与Java的SPI机制](docs/Dubbo/SPI/Dubbo与Java的SPI机制.md) + +### RPC + +### Registry + +### Cluster + ## Tomcat - [servlet-api 源码赏析](docs/Tomcat/servlet-api源码赏析.md) - [一个简单的Web服务器]() diff --git a/docs/Dubbo/SPI/Dubbo与Java的SPI机制.md b/docs/Dubbo/SPI/Dubbo与Java的SPI机制.md new file mode 100644 index 0000000..fb3cf29 --- /dev/null +++ b/docs/Dubbo/SPI/Dubbo与Java的SPI机制.md @@ -0,0 +1,1242 @@ +## JDK的SPI思想 +SPI,即Service Provider Interface。在面向对象的设计里面,模块之间推荐基于接口编程,而不是对实现类进行硬编码,这样做也是为了模块设计的可拔插原则。 + +比较典型的应用,如 JDBC,Java 定义了一套 JDBC 的接口,但是 Java 本身并不提供对 JDBC 的实现类,而是开发者根据项目实际使用的数据库来选择驱动程序jar包,比如 mysql,你就将 mysql-jdbc-connector.jar 引入进来;oracle,你就将 oracle-jdbc-connector.jar 引入进来。在系统跑的时候,碰到你使用 jdbc 的接口,他会在底层使用你引入的那个 jar 中提供的实现类。 + +## Dubbo的SPI扩展机制原理 +dubbo自己实现了一套SPI机制,并对 JDK的SPI进行了改进。 +1. JDK标准的SPI只能通过遍历来查找扩展点和实例化,有可能导致一次性加载所有的扩展点,如果不是所有的扩展点都被用到,就会导致资源的浪费。dubbo每个扩展点都有多种实现,例如:com.alibaba.dubbo.rpc.Protocol接口有InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol等实现,如果只是用到其中一个实现,可是加载了全部的实现,会导致资源的浪费。 +2. 对配置文件中扩展实现的格式的修改,例如,META-INF/dubbo/com.xxx.Protocol 里的 com.foo.XxxProtocol格式 改为了 xxx = com.foo.XxxProtocol 这种以键值对的形式,这样做的目的是为了让我们更容易的定位到问题。比如,由于第三方库不存在,无法初始化,导致无法加载扩展点(“A”),当用户配置使用A时,dubbo就会报无法加载扩展点的错误,而不是报哪些扩展点的实现加载失败以及错误原因,**这是因为原来的配置格式没有记录扩展名的id,导致dubbo无法抛出较为精准的异常,这会加大排查问题的难度**。所以改成key-value的形式来进行配置。 +3. dubbo的SPI机制增加了对IOC、AOP的支持,一个扩展点可以直接通过setter注入到其他扩展点。 + +下面我们看一下Dubbo 的 SPI扩展机制实现的结构目录。 + +![avatar](/images/Dubbo/SPI组件目录结构.png) + +### SPI 注解 +首先看一下 SPI注解。在某个接口上加上 @SPI 注解后,表明该接口为可扩展接口。比如,协议扩展接口Protocol,如果使用者在 <dubbo:protocol />、<dubbo:service />、<dubbo:reference /> 都没有指定 protocol属性 的话,那么就默认使用 DubboProtocol 作为接口Protocol的实现,因为在 Protocol 上有 @SPI("dubbo")注解。而这个 protocol属性值 或者默认值会被当作该接口的实现类中的一个key,dubbo 会去 META-INF.dubbo.internal下的com.alibaba.dubbo.rpc.Protocol文件中找该key对应的value,源码如下。 +```java +/** + * 协议接口 + * Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。 + */ +@SPI("dubbo") +public interface Protocol { + + /** + * Get default port when user doesn't config the port. + * + * @return default port + */ + int getDefaultPort(); + + /** + * 暴露远程服务:
+ * 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();
+ * 2. export() 必须是幂等的,也就是暴露同一个 URL 的 Invoker 两次,和暴露一次没有区别。
+ * 3. export() 传入的 Invoker 由框架实现并传入,协议不需要关心。
+ * + * @param 服务的类型 + * @param invoker 服务的执行体 + * @return exporter 暴露服务的引用,用于取消暴露 + * @throws RpcException 当暴露服务出错时抛出,比如端口已占用 + */ + @Adaptive + Exporter export(Invoker invoker) throws RpcException; + + /** + * 引用远程服务:
+ * 1. 当用户调用 refer() 所返回的 Invoker 对象的 invoke() 方法时,协议需相应执行同 URL 远端 export() 传入的 Invoker 对象的 invoke() 方法。
+ * 2. refer() 返回的 Invoker 由协议实现,协议通常需要在此 Invoker 中发送远程请求。
+ * 3. 当 url 中有设置 check=false 时,连接失败不能抛出异常,并内部自动恢复。
+ * + * @param 服务的类型 + * @param type 服务的类型 + * @param url 远程服务的URL地址 + * @return invoker 服务的本地代理 + * @throws RpcException 当连接服务提供方失败时抛出 + */ + @Adaptive + Invoker refer(Class type, URL url) throws RpcException; + + /** + * 释放协议:
+ * 1. 取消该协议所有已经暴露和引用的服务。
+ * 2. 释放协议所占用的所有资源,比如连接和端口。
+ * 3. 协议在释放后,依然能暴露和引用新的服务。
+ */ + void destroy(); +} + +/** + * 扩展点接口的标识。 + * 扩展点声明配置文件,格式修改。 + * 以Protocol示例,配置文件META-INF/dubbo/com.xxx.Protocol内容: + * 由 + * com.foo.XxxProtocol + * com.foo.YyyProtocol + * 改成使用KV格式 + * xxx=com.foo.XxxProtocol + * yyy=com.foo.YyyProtocol + * + * 原因: + * 当扩展点的static字段或方法签名上引用了三方库, + * 如果三方库不存在,会导致类初始化失败, + * Extension标识Dubbo就拿不到了,异常信息就和配置信息对应不起来。 + * + * 比如: + * Extension("mina")加载失败, + * 当用户配置使用mina时,就会报找不到扩展点mina, + * 而不是报加载扩展点失败,等难以定位具体问题的错误。 + * + * @author william.liangf + * @author ding.lid + * @export + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface SPI { + + /** + * default extension name + * + * 默认拓展名 + */ + String value() default ""; +} + +// 配置文件 com.alibaba.dubbo.rpc.Protocol 中的内容 +dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol +``` +value 就是该 Protocol接口 的实现类 DubboProtocol,这样就做到了SPI扩展。 + +### ExtensionLoader +ExtensionLoader 扩展加载器,这是 dubbo 实现 SPI扩展机制 的核心,几乎所有实现的逻辑都被封装在 ExtensionLoader 中,其源码如下。 +```java +/** + * 拓展加载器,Dubbo使用的扩展点获取 + *
    + *
  • 自动注入关联扩展点。
  • + *
  • 自动Wrap上扩展点的Wrap类。
  • + *
  • 缺省获得的的扩展点是一个Adaptive Instance。
  • + *
+ * + * 另外,该类同时是 ExtensionLoader 的管理容器,例如 {@link #EXTENSION_INSTANCES} 、{@link #EXTENSION_INSTANCES} 属性。 + */ +@SuppressWarnings("deprecation") +public class ExtensionLoader { + + private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class); + + private static final String SERVICES_DIRECTORY = "META-INF/services/"; + + private static final String DUBBO_DIRECTORY = "META-INF/dubbo/"; + + private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/"; + + private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*"); + + // ============================== 静态属性 ============================== + + /** + * 拓展加载器集合 + * + * key:拓展接口 + */ + private static final ConcurrentMap, ExtensionLoader> EXTENSION_LOADERS = new ConcurrentHashMap, ExtensionLoader>(); + /** + * 拓展实现类集合 + * + * key:拓展实现类 + * value:拓展对象。 + * + * 例如,key 为 Class + * value 为 AccessLogFilter 对象 + */ + private static final ConcurrentMap, Object> EXTENSION_INSTANCES = new ConcurrentHashMap, Object>(); + + // ============================== 对象属性 ============================== + + /** + * 拓展接口。 + * 例如,Protocol + */ + private final Class type; + /** + * 对象工厂 + * + * 用于调用 {@link #injectExtension(Object)} 方法,向拓展对象注入依赖的属性。 + * + * 例如,StubProxyFactoryWrapper 中有 `Protocol protocol` 属性。 + */ + private final ExtensionFactory objectFactory; + /** + * 缓存的拓展名与拓展类的映射。 + * + * 和 {@link #cachedClasses} 的 KV 对调。 + * + * 通过 {@link #loadExtensionClasses} 加载 + */ + private final ConcurrentMap, String> cachedNames = new ConcurrentHashMap, String>(); + /** + * 缓存的拓展实现类集合。 + * + * 不包含如下两种类型: + * 1. 自适应拓展实现类。例如 AdaptiveExtensionFactory + * 2. 带唯一参数为拓展接口的构造方法的实现类,或者说拓展 Wrapper 实现类。例如,ProtocolFilterWrapper 。 + * 拓展 Wrapper 实现类,会添加到 {@link #cachedWrapperClasses} 中 + * + * 通过 {@link #loadExtensionClasses} 加载 + */ + private final Holder>> cachedClasses = new Holder>>(); + + /** + * 拓展名与 @Activate 的映射 + * + * 例如,AccessLogFilter。 + * + * 用于 {@link #getActivateExtension(URL, String)} + */ + private final Map cachedActivates = new ConcurrentHashMap(); + /** + * 缓存的拓展对象集合 + * + * key:拓展名 + * value:拓展对象 + * + * 例如,Protocol 拓展 + * key:dubbo value:DubboProtocol + * key:injvm value:InjvmProtocol + * + * 通过 {@link #loadExtensionClasses} 加载 + */ + private final ConcurrentMap> cachedInstances = new ConcurrentHashMap>(); + /** + * 缓存的自适应( Adaptive )拓展对象 + */ + private final Holder cachedAdaptiveInstance = new Holder(); + /** + * 缓存的自适应拓展对象的类 + * + * {@link #getAdaptiveExtensionClass()} + */ + private volatile Class cachedAdaptiveClass = null; + /** + * 缓存的默认拓展名 + * + * 通过 {@link SPI} 注解获得 + */ + private String cachedDefaultName; + /** + * 创建 {@link #cachedAdaptiveInstance} 时发生的异常。 + * + * 发生异常后,不再创建,参见 {@link #createAdaptiveExtension()} + */ + private volatile Throwable createAdaptiveInstanceError; + + /** + * 拓展 Wrapper 实现类集合 + * + * 带唯一参数为拓展接口的构造方法的实现类 + * + * 通过 {@link #loadExtensionClasses} 加载 + */ + private Set> cachedWrapperClasses; + + /** + * 拓展名 与 加载对应拓展类发生的异常 的 映射 + * + * key:拓展名 + * value:异常 + * + * 在 {@link #loadFile(Map, String)} 时,记录 + */ + private Map exceptions = new ConcurrentHashMap(); + + private ExtensionLoader(Class type) { + this.type = type; + objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); + } + + /** + * 是否包含 @SPI 注解 + * + * @param type 类 + * @param 泛型 + * @return 是否包含 + */ + private static boolean withExtensionAnnotation(Class type) { + return type.isAnnotationPresent(SPI.class); + } + + /** + * 根据拓展点的接口,获得拓展加载器 + * + * @param type 接口 + * @param 泛型 + * @return 加载器 + */ + @SuppressWarnings("unchecked") + public static ExtensionLoader getExtensionLoader(Class type) { + if (type == null) + throw new IllegalArgumentException("Extension type == null"); + // 必须是接口 + if (!type.isInterface()) { + throw new IllegalArgumentException("Extension type(" + type + ") is not interface!"); + } + // 必须包含 @SPI 注解 + if (!withExtensionAnnotation(type)) { + throw new IllegalArgumentException("Extension type(" + type + + ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!"); + } + + // 获得接口对应的拓展点加载器 + ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + if (loader == null) { + EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type)); + loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + } + return loader; + } + + private static ClassLoader findClassLoader() { + return ExtensionLoader.class.getClassLoader(); + } + + public String getExtensionName(T extensionInstance) { + return getExtensionName(extensionInstance.getClass()); + } + + public String getExtensionName(Class extensionClass) { + return cachedNames.get(extensionClass); + } + + public List getActivateExtension(URL url, String key) { + return getActivateExtension(url, key, null); + } + + public List getActivateExtension(URL url, String[] values) { + return getActivateExtension(url, values, null); + } + + /** + * 获得符合自动激活条件的拓展对象数组 + */ + public List getActivateExtension(URL url, String key, String group) { + // 从 Dubbo URL 获得参数值 + String value = url.getParameter(key); + // 获得符合自动激活条件的拓展对象数组 + return getActivateExtension(url, value == null || value.length() == 0 ? null : Constants.COMMA_SPLIT_PATTERN.split(value), group); + } + + /** + * 获得符合自动激活条件的拓展对象数组 + */ + public List getActivateExtension(URL url, String[] values, String group) { + List exts = new ArrayList(); + List names = values == null ? new ArrayList(0) : Arrays.asList(values); + // 处理自动激活的拓展对象们 + // 判断不存在配置 `"-name"` 。例如, ,代表移除所有默认过滤器。 + if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) { + // 获得拓展实现类数组 + getExtensionClasses(); + // 循环 + for (Map.Entry entry : cachedActivates.entrySet()) { + String name = entry.getKey(); + Activate activate = entry.getValue(); + if (isMatchGroup(group, activate.group())) { // 匹配分组 + // 获得拓展对象 + T ext = getExtension(name); + if (!names.contains(name) // 不包含在自定义配置里。如果包含,会在下面的代码处理。 + && !names.contains(Constants.REMOVE_VALUE_PREFIX + name) // 判断是否配置移除。例如 ,则 MonitorFilter 会被移除 + && isActive(activate, url)) { // 判断是否激活 + exts.add(ext); + } + } + } + // 排序 + Collections.sort(exts, ActivateComparator.COMPARATOR); + } + // 处理自定义配置的拓展对象们。例如在 ,代表需要加入 DemoFilter (这个是笔者自定义的)。 + List usrs = new ArrayList(); + for (int i = 0; i < names.size(); i++) { + String name = names.get(i); + if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX) && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) { // 判断非移除的 + // 将配置的自定义在自动激活的拓展对象们前面。例如, ,则 DemoFilter 就会放在默认的过滤器前面。 + if (Constants.DEFAULT_KEY.equals(name)) { + if (!usrs.isEmpty()) { + exts.addAll(0, usrs); + usrs.clear(); + } + } else { + // 获得拓展对象 + T ext = getExtension(name); + usrs.add(ext); + } + } + } + // 添加到结果集 + if (!usrs.isEmpty()) { + exts.addAll(usrs); + } + return exts; + } + + /** + * 匹配分组 + * + * @param group 过滤的分组条件。若为空,无需过滤 + * @param groups 配置的分组 + * @return 是否匹配 + */ + private boolean isMatchGroup(String group, String[] groups) { + // 为空,无需过滤 + if (group == null || group.length() == 0) { + return true; + } + // 匹配 + if (groups != null && groups.length > 0) { + for (String g : groups) { + if (group.equals(g)) { + return true; + } + } + } + return false; + } + + /** + * 是否激活,通过 Dubbo URL 中是否存在参数名为 `@Activate.value` ,并且参数值非空。 + * + * @param activate 自动激活注解 + * @param url Dubbo URL + * @return 是否 + */ + private boolean isActive(Activate activate, URL url) { + String[] keys = activate.value(); + if (keys.length == 0) { + return true; + } + for (String key : keys) { + for (Map.Entry entry : url.getParameters().entrySet()) { + String k = entry.getKey(); + String v = entry.getValue(); + if ((k.equals(key) || k.endsWith("." + key)) + && ConfigUtils.isNotEmpty(v)) { + return true; + } + } + } + return false; + } + + /** + * 返回扩展点实例,如果没有指定的扩展点或是还没加载(即实例化)则返回null。 + * 注意:此方法不会触发扩展点的加载。 + * 一般应该调用{@link #getExtension(String)}方法获得扩展,这个方法会触发扩展点加载。 + */ + @SuppressWarnings("unchecked") + public T getLoadedExtension(String name) { + if (name == null || name.length() == 0) + throw new IllegalArgumentException("Extension name == null"); + Holder holder = cachedInstances.get(name); + if (holder == null) { + cachedInstances.putIfAbsent(name, new Holder()); + holder = cachedInstances.get(name); + } + return (T) holder.get(); + } + + /** + * 返回已经加载的扩展点的名字。 + * 一般应该调用 getSupportedExtensions() 方法获得扩展,这个方法会返回所有的扩展点。 + */ + public Set getLoadedExtensions() { + return Collections.unmodifiableSet(new TreeSet(cachedInstances.keySet())); + } + + /** + * 返回指定名字的扩展对象。如果指定名字的扩展不存在,则抛异常 {@link IllegalStateException}. + * + * @param name 拓展名 + * @return 拓展对象 + */ + @SuppressWarnings("unchecked") + public T getExtension(String name) { + if (name == null || name.length() == 0) + throw new IllegalArgumentException("Extension name == null"); + // 查找 默认的 拓展对象 + if ("true".equals(name)) { + return getDefaultExtension(); + } + // 从 缓存中 获得对应的拓展对象 + Holder holder = cachedInstances.get(name); + if (holder == null) { + cachedInstances.putIfAbsent(name, new Holder()); + holder = cachedInstances.get(name); + } + Object instance = holder.get(); + if (instance == null) { + synchronized (holder) { + instance = holder.get(); + // 从 缓存中 未获取到,进行创建缓存对象。 + if (instance == null) { + instance = createExtension(name); + // 设置创建对象到缓存中 + holder.set(instance); + } + } + } + return (T) instance; + } + + /** + * 返回缺省的扩展,如果没有设置则返回null。 + */ + public T getDefaultExtension() { + getExtensionClasses(); + // 如果为 true ,不能继续调用 `#getExtension(true)` 方法,会形成死循环。 + if (null == cachedDefaultName || cachedDefaultName.length() == 0 + || "true".equals(cachedDefaultName)) { + return null; + } + return getExtension(cachedDefaultName); + } + + public boolean hasExtension(String name) { + if (name == null || name.length() == 0) + throw new IllegalArgumentException("Extension name == null"); + try { + return getExtensionClass(name) != null; + } catch (Throwable t) { + return false; + } + } + + public Set getSupportedExtensions() { + Map> clazzes = getExtensionClasses(); + return Collections.unmodifiableSet(new TreeSet(clazzes.keySet())); + } + + /** + * 返回缺省的扩展点名,如果没有设置缺省则返回null。 + */ + public String getDefaultExtensionName() { + getExtensionClasses(); + return cachedDefaultName; + } + + /** + * 编程方式添加新扩展点。 + * + * @param name 扩展点名 + * @param clazz 扩展点类 + * @throws IllegalStateException 要添加扩展点名已经存在。 + */ + public void addExtension(String name, Class clazz) { + getExtensionClasses(); // load classes + + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Input type " + + clazz + "not implement Extension " + type); + } + if (clazz.isInterface()) { + throw new IllegalStateException("Input type " + + clazz + "can not be interface!"); + } + + if (!clazz.isAnnotationPresent(Adaptive.class)) { + if (StringUtils.isBlank(name)) { + throw new IllegalStateException("Extension name is blank (Extension " + type + ")!"); + } + if (cachedClasses.get().containsKey(name)) { + throw new IllegalStateException("Extension name " + + name + " already existed(Extension " + type + ")!"); + } + + cachedNames.put(clazz, name); + cachedClasses.get().put(name, clazz); + } else { + if (cachedAdaptiveClass != null) { + throw new IllegalStateException("Adaptive Extension already existed(Extension " + type + ")!"); + } + + cachedAdaptiveClass = clazz; + } + } + + /** + * 编程方式添加替换已有扩展点。 + * + * @param name 扩展点名 + * @param clazz 扩展点类 + * @throws IllegalStateException 要添加扩展点名已经存在。 + * @deprecated 不推荐应用使用,一般只在测试时可以使用 + */ + @Deprecated + public void replaceExtension(String name, Class clazz) { + getExtensionClasses(); // load classes + + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Input type " + + clazz + "not implement Extension " + type); + } + if (clazz.isInterface()) { + throw new IllegalStateException("Input type " + + clazz + "can not be interface!"); + } + + if (!clazz.isAnnotationPresent(Adaptive.class)) { + if (StringUtils.isBlank(name)) { + throw new IllegalStateException("Extension name is blank (Extension " + type + ")!"); + } + if (!cachedClasses.get().containsKey(name)) { + throw new IllegalStateException("Extension name " + + name + " not existed(Extension " + type + ")!"); + } + + cachedNames.put(clazz, name); + cachedClasses.get().put(name, clazz); + cachedInstances.remove(name); + } else { + if (cachedAdaptiveClass == null) { + throw new IllegalStateException("Adaptive Extension not existed(Extension " + type + ")!"); + } + + cachedAdaptiveClass = clazz; + cachedAdaptiveInstance.set(null); + } + } + + /** + * 获得自适应拓展对象 + * + * @return 拓展对象 + */ + @SuppressWarnings("unchecked") + public T getAdaptiveExtension() { + // 从缓存中,获得自适应拓展对象 + Object instance = cachedAdaptiveInstance.get(); + if (instance == null) { + // 若之前未创建报错, + if (createAdaptiveInstanceError == null) { + synchronized (cachedAdaptiveInstance) { + instance = cachedAdaptiveInstance.get(); + if (instance == null) { + try { + // 创建自适应拓展对象 + instance = createAdaptiveExtension(); + // 设置到缓存 + cachedAdaptiveInstance.set(instance); + } catch (Throwable t) { + // 记录异常 + createAdaptiveInstanceError = t; + throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t); + } + } + } + // 若之前创建报错,则抛出异常 IllegalStateException + } else { + throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError); + } + } + return (T) instance; + } + + /** + * 获得拓展名不存在时的异常 + * + * @param name 拓展名 + * @return 异常 + */ + private IllegalStateException findException(String name) { + // 在 `#loadFile(...)` 方法中,加载时,发生异常 + for (Map.Entry entry : exceptions.entrySet()) { + if (entry.getKey().toLowerCase().contains(name.toLowerCase())) { + return entry.getValue(); + } + } + // 生成不存在该拓展类实现的异常。 + StringBuilder buf = new StringBuilder("No such extension " + type.getName() + " by name " + name); + int i = 1; + for (Map.Entry entry : exceptions.entrySet()) { + if (i == 1) { + buf.append(", possible causes: "); + } + buf.append("\r\n("); + buf.append(i++); + buf.append(") "); + buf.append(entry.getKey()); + buf.append(":\r\n"); + buf.append(StringUtils.toString(entry.getValue())); + } + return new IllegalStateException(buf.toString()); + } + + /** + * 创建拓展名的拓展对象,并缓存。 + * + * @param name 拓展名 + * @return 拓展对象 + */ + @SuppressWarnings("unchecked") + private T createExtension(String name) { + // 获得拓展名对应的拓展实现类 + Class clazz = getExtensionClasses().get(name); + if (clazz == null) { + throw findException(name); // 抛出异常 + } + try { + // 从缓存中,获得拓展对象。 + T instance = (T) EXTENSION_INSTANCES.get(clazz); + if (instance == null) { + // 当缓存不存在时,创建拓展对象,并添加到缓存中。 + EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); + instance = (T) EXTENSION_INSTANCES.get(clazz); + } + // 注入依赖的属性 + injectExtension(instance); + // 创建 Wrapper 拓展对象 + Set> wrapperClasses = cachedWrapperClasses; + if (wrapperClasses != null && !wrapperClasses.isEmpty()) { + for (Class wrapperClass : wrapperClasses) { + instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); + } + } + return instance; + } catch (Throwable t) { + throw new IllegalStateException("Extension instance(name: " + name + ", class: " + + type + ") could not be instantiated: " + t.getMessage(), t); + } + } + + /** + * 注入依赖的属性 + * + * @param instance 拓展对象 + * @return 拓展对象 + */ + private T injectExtension(T instance) { + try { + if (objectFactory != null) { + for (Method method : instance.getClass().getMethods()) { + if (method.getName().startsWith("set") + && method.getParameterTypes().length == 1 + && Modifier.isPublic(method.getModifiers())) { // setting && public 方法 + // 获得属性的类型 + Class pt = method.getParameterTypes()[0]; + try { + // 获得属性 + String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; + // 获得属性值 + Object object = objectFactory.getExtension(pt, property); + // 设置属性值 + if (object != null) { + method.invoke(instance, object); + } + } catch (Exception e) { + logger.error("fail to inject via method " + method.getName() + + " of interface " + type.getName() + ": " + e.getMessage(), e); + } + } + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return instance; + } + + private Class getExtensionClass(String name) { + if (type == null) + throw new IllegalArgumentException("Extension type == null"); + if (name == null) + throw new IllegalArgumentException("Extension name == null"); + // 获得拓展实现类 + Class clazz = getExtensionClasses().get(name); + if (clazz == null) + throw new IllegalStateException("No such extension \"" + name + "\" for " + type.getName() + "!"); + return clazz; + } + + /** + * 获得拓展实现类数组 + * + * @return 拓展实现类数组 + */ + private Map> getExtensionClasses() { + // 从缓存中,获得拓展实现类数组 + Map> classes = cachedClasses.get(); + if (classes == null) { + synchronized (cachedClasses) { + classes = cachedClasses.get(); + if (classes == null) { + // 从配置文件中,加载拓展实现类数组 + classes = loadExtensionClasses(); + // 设置到缓存中 + cachedClasses.set(classes); + } + } + } + return classes; + } + + /** + * 加载拓展实现类数组 + * + * @return 拓展实现类数组 + */ + private Map> loadExtensionClasses() { + // 通过 @SPI 注解,获得默认的拓展实现类名 + final SPI defaultAnnotation = type.getAnnotation(SPI.class); + if (defaultAnnotation != null) { + String value = defaultAnnotation.value(); + if ((value = value.trim()).length() > 0) { + String[] names = NAME_SEPARATOR.split(value); + if (names.length > 1) { + throw new IllegalStateException("more than 1 default extension name on extension " + type.getName() + + ": " + Arrays.toString(names)); + } + if (names.length == 1) cachedDefaultName = names[0]; + } + } + + // 从配置文件中,加载拓展实现类数组 + Map> extensionClasses = new HashMap>(); + loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY); + loadFile(extensionClasses, DUBBO_DIRECTORY); + loadFile(extensionClasses, SERVICES_DIRECTORY); + return extensionClasses; + } + + /** + * 从一个配置文件中,加载拓展实现类数组。 + * + * @param extensionClasses 拓展类名数组 + * @param dir 文件名 + */ + private void loadFile(Map> extensionClasses, String dir) { + // 完整的文件名 + String fileName = dir + type.getName(); + try { + Enumeration urls; + // 获得文件名对应的所有文件数组 + ClassLoader classLoader = findClassLoader(); + if (classLoader != null) { + urls = classLoader.getResources(fileName); + } else { + urls = ClassLoader.getSystemResources(fileName); + } + // 遍历文件数组 + if (urls != null) { + while (urls.hasMoreElements()) { + java.net.URL url = urls.nextElement(); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8")); + try { + String line; + while ((line = reader.readLine()) != null) { + // 跳过当前被注释掉的情况,例如 #spring=xxxxxxxxx + final int ci = line.indexOf('#'); + if (ci >= 0) line = line.substring(0, ci); + line = line.trim(); + if (line.length() > 0) { + try { + // 拆分,key=value 的配置格式 + String name = null; + int i = line.indexOf('='); + if (i > 0) { + name = line.substring(0, i).trim(); + line = line.substring(i + 1).trim(); + } + if (line.length() > 0) { + // 判断拓展实现,是否实现拓展接口 + Class clazz = Class.forName(line, true, classLoader); + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Error when load extension class(interface: " + + type + ", class line: " + clazz.getName() + "), class " + + clazz.getName() + "is not subtype of interface."); + } + // 缓存自适应拓展对象的类到 `cachedAdaptiveClass` + if (clazz.isAnnotationPresent(Adaptive.class)) { + if (cachedAdaptiveClass == null) { + cachedAdaptiveClass = clazz; + } else if (!cachedAdaptiveClass.equals(clazz)) { + throw new IllegalStateException("More than 1 adaptive class found: " + + cachedAdaptiveClass.getClass().getName() + + ", " + clazz.getClass().getName()); + } + } else { + // 缓存拓展 Wrapper 实现类到 `cachedWrapperClasses` + try { + clazz.getConstructor(type); + Set> wrappers = cachedWrapperClasses; + if (wrappers == null) { + cachedWrapperClasses = new ConcurrentHashSet>(); + wrappers = cachedWrapperClasses; + } + wrappers.add(clazz); + // 缓存拓展实现类到 `extensionClasses` + } catch (NoSuchMethodException e) { + clazz.getConstructor(); + // 未配置拓展名,自动生成。例如,DemoFilter 为 demo 。主要用于兼容 Java SPI 的配置。 + if (name == null || name.length() == 0) { + name = findAnnotationName(clazz); + if (name == null || name.length() == 0) { + if (clazz.getSimpleName().length() > type.getSimpleName().length() + && clazz.getSimpleName().endsWith(type.getSimpleName())) { + name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase(); + } else { + throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + url); + } + } + } + // 获得拓展名,可以是数组,有多个拓展名。 + String[] names = NAME_SEPARATOR.split(name); + if (names != null && names.length > 0) { + // 缓存 @Activate 到 `cachedActivates` 。 + Activate activate = clazz.getAnnotation(Activate.class); + if (activate != null) { + cachedActivates.put(names[0], activate); + } + for (String n : names) { + // 缓存到 `cachedNames` + if (!cachedNames.containsKey(clazz)) { + cachedNames.put(clazz, n); + } + // 缓存拓展实现类到 `extensionClasses` + Class c = extensionClasses.get(n); + if (c == null) { + extensionClasses.put(n, clazz); + } else if (c != clazz) { + throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName()); + } + } + } + } + } + } + } catch (Throwable t) { + // 发生异常,记录到异常集合 + IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + url + ", cause: " + t.getMessage(), t); + exceptions.put(line, e); + } + } + } // end of while read lines + } finally { + reader.close(); + } + } catch (Throwable t) { + logger.error("Exception when load extension class(interface: " + + type + ", class file: " + url + ") in " + url, t); + } + } // end of while urls + } + } catch (Throwable t) { + logger.error("Exception when load extension class(interface: " + + type + ", description file: " + fileName + ").", t); + } + } + + @SuppressWarnings("deprecation") + private String findAnnotationName(Class clazz) { + com.alibaba.dubbo.common.Extension extension = clazz.getAnnotation(com.alibaba.dubbo.common.Extension.class); + if (extension == null) { + String name = clazz.getSimpleName(); + if (name.endsWith(type.getSimpleName())) { + name = name.substring(0, name.length() - type.getSimpleName().length()); + } + return name.toLowerCase(); + } + return extension.value(); + } + + /** + * 创建自适应拓展对象 + * + * @return 拓展对象 + */ + @SuppressWarnings("unchecked") + private T createAdaptiveExtension() { + try { + return injectExtension((T) getAdaptiveExtensionClass().newInstance()); + } catch (Exception e) { + throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e); + } + } + + /** + * @return 自适应拓展类 + */ + private Class getAdaptiveExtensionClass() { + getExtensionClasses(); + if (cachedAdaptiveClass != null) { + return cachedAdaptiveClass; + } + return cachedAdaptiveClass = createAdaptiveExtensionClass(); + } + + /** + * 自动生成自适应拓展的代码实现,并编译后返回该类。 + * + * @return 类 + */ + private Class createAdaptiveExtensionClass() { + // 自动生成自适应拓展的代码实现的字符串 + String code = createAdaptiveExtensionClassCode(); + // 编译代码,并返回该类 + ClassLoader classLoader = findClassLoader(); + com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); + return compiler.compile(code, classLoader); + } + + /** + * 自动生成自适应拓展的代码实现的字符串 + * + * @return 代码字符串 + */ + private String createAdaptiveExtensionClassCode() { + StringBuilder codeBuidler = new StringBuilder(); + // 遍历方法数组,判断有 @Adaptive 注解 + Method[] methods = type.getMethods(); + boolean hasAdaptiveAnnotation = false; + for (Method m : methods) { + if (m.isAnnotationPresent(Adaptive.class)) { + hasAdaptiveAnnotation = true; + break; + } + } + // no need to generate adaptive class since there's no adaptive method found. + // 完全没有Adaptive方法,则不需要生成Adaptive类 + if (!hasAdaptiveAnnotation) + throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!"); + + // 生成代码:package 和 import + codeBuidler.append("package " + type.getPackage().getName() + ";"); + codeBuidler.append("\nimport " + ExtensionLoader.class.getName() + ";"); + // 生成代码:类名 + codeBuidler.append("\npublic class " + type.getSimpleName() + "$Adaptive" + " implements " + type.getCanonicalName() + " {"); + + // 循环方法 + for (Method method : methods) { + Class rt = method.getReturnType(); // 返回类型 + Class[] pts = method.getParameterTypes(); // 参数类型数组 + Class[] ets = method.getExceptionTypes(); // 异常类型数组 + + Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); + StringBuilder code = new StringBuilder(512); // 方法体的代码 + // 非 @Adaptive 注解,生成代码:生成的方法为直接抛出异常。因为,非自适应的接口不应该被调用。 + if (adaptiveAnnotation == null) { + code.append("throw new UnsupportedOperationException(\"method ") + .append(method.toString()).append(" of interface ") + .append(type.getName()).append(" is not adaptive method!\");"); + // @Adaptive 注解,生成方法体的代码 + } else { + // 寻找 Dubbo URL 参数的位置 + int urlTypeIndex = -1; + for (int i = 0; i < pts.length; ++i) { + if (pts[i].equals(URL.class)) { + urlTypeIndex = i; + break; + } + } + // found parameter in URL type + // 有类型为URL的参数,生成代码:生成校验 URL 非空的代码 + if (urlTypeIndex != -1) { + // Null Point check + String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");", + urlTypeIndex); + code.append(s); + + s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex); + code.append(s); + } + // did not find parameter in URL type + // 参数没有URL类型 + else { + String attribMethod = null; + + // find URL getter method + // 找到参数的URL属性 。例如,Invoker 有 `#getURL()` 方法。 + LBL_PTS: + for (int i = 0; i < pts.length; ++i) { + Method[] ms = pts[i].getMethods(); + for (Method m : ms) { + String name = m.getName(); + if ((name.startsWith("get") || name.length() > 3) + && Modifier.isPublic(m.getModifiers()) + && !Modifier.isStatic(m.getModifiers()) + && m.getParameterTypes().length == 0 + && m.getReturnType() == URL.class) { // pubic && getting 方法 + urlTypeIndex = i; + attribMethod = name; + break LBL_PTS; + } + } + } + // 未找到,抛出异常。 + if (attribMethod == null) { + throw new IllegalStateException("fail to create adaptive class for interface " + type.getName() + + ": not found url parameter or url attribute in parameters of method " + method.getName()); + } + + // 生成代码:校验 URL 非空 + // Null point check + String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");", + urlTypeIndex, pts[urlTypeIndex].getName()); + code.append(s); + s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");", + urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod); + code.append(s); + + // 生成 `URL url = arg%d.%s();` 的代码 + s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod); + code.append(s); + } + + String[] value = adaptiveAnnotation.value(); + // value is not set, use the value generated from class name as the key + // 没有设置Key,则使用“扩展点接口名的点分隔 作为Key + if (value.length == 0) { + char[] charArray = type.getSimpleName().toCharArray(); + StringBuilder sb = new StringBuilder(128); + for (int i = 0; i < charArray.length; i++) { + if (Character.isUpperCase(charArray[i])) { + if (i != 0) { + sb.append("."); + } + sb.append(Character.toLowerCase(charArray[i])); + } else { + sb.append(charArray[i]); + } + } + value = new String[]{sb.toString()}; + } + + // 判断是否有 Invocation 参数 + boolean hasInvocation = false; + for (int i = 0; i < pts.length; ++i) { + if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) { + // 生成代码:校验 Invocation 非空 + // Null Point check + String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i); + code.append(s); + + // 生成代码:获得方法名 + s = String.format("\nString methodName = arg%d.getMethodName();", i); + code.append(s); + + // 标记有 Invocation 参数 + hasInvocation = true; + break; + } + } + + // 默认拓展名 + String defaultExtName = cachedDefaultName; + // 获得最终拓展名的代码字符串,例如: + // 【简单】1. url.getParameter("proxy", "javassist") + // 【复杂】2. url.getParameter(key1, url.getParameter(key2, defaultExtName)) + String getNameCode = null; + for (int i = value.length - 1; i >= 0; --i) { // 倒序的原因,因为是顺序获取参数,参见【复杂】2. 的例子 + if (i == value.length - 1) { + if (null != defaultExtName) { + if (!"protocol".equals(value[i])) + if (hasInvocation) // 当【有】 Invocation 参数时,使用 `URL#getMethodParameter()` 方法。 + getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); + else // 当【非】 Invocation 参数时,使用 `URL#getParameter()` 方法。 + getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName); + else // 当属性名是 "protocol" ,使用 `URL#getProtocl()` 方法获取。 + getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName); + } else { + if (!"protocol".equals(value[i])) + if (hasInvocation) + getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); // 此处的 defaultExtName ,可以去掉的。 + else + getNameCode = String.format("url.getParameter(\"%s\")", value[i]); + else + getNameCode = "url.getProtocol()"; + } + } else { + if (!"protocol".equals(value[i])) + if (hasInvocation) + getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); + else + getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode); + else + getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode); + } + } + + // 生成代码:获取参数的代码。例如:String extName = url.getParameter("proxy", "javassist"); + code.append("\nString extName = ").append(getNameCode).append(";"); + // check extName == null? + String s = String.format("\nif(extName == null) " + + "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");", + type.getName(), Arrays.toString(value)); + code.append(s); + + // 生成代码:拓展对象,调用方法。例如 + // `com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.ProxyFactory.class) + // .getExtension(extName);` 。 + s = String.format("\n%s extension = (% 0) { + codeBuidler.append(", "); + } + codeBuidler.append(pts[i].getCanonicalName()); + codeBuidler.append(" "); + codeBuidler.append("arg" + i); + } + codeBuidler.append(")"); + if (ets.length > 0) { + codeBuidler.append(" throws "); // 异常 + for (int i = 0; i < ets.length; i++) { + if (i > 0) { + codeBuidler.append(", "); + } + codeBuidler.append(ets[i].getCanonicalName()); + } + } + codeBuidler.append(" {"); + codeBuidler.append(code.toString()); + codeBuidler.append("\n}"); + } + + // 生成类末尾的 `}` + codeBuidler.append("\n}"); + + // 调试,打印生成的代码 + if (logger.isDebugEnabled()) { + logger.debug(codeBuidler.toString()); + } + return codeBuidler.toString(); + } + + @Override + public String toString() { + return this.getClass().getName() + "[" + type.getName() + "]"; + } +} +``` \ No newline at end of file diff --git a/images/Dubbo/SPI组件目录结构.png b/images/Dubbo/SPI组件目录结构.png new file mode 100644 index 0000000000000000000000000000000000000000..cd2102b9fe2fb4b873416b65c5cc902501efa1bb GIT binary patch literal 19181 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