ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

2021-12-23

2021-12-23 11:04:32  阅读:140  来源: 互联网

标签:12 23 url 代码 2021 Adaptive null method append


2021SC@SDUSC

2.2 自适应拓展类代码生成

createAdaptiveExtensionClassCode 方法代码略多,约有两百行代码。因此本节将会对该方法的代码进行拆分分析,以帮助大家更好的理解代码逻辑。

2.2.1 Adaptive 注解检测

在生成代理类源码之前,createAdaptiveExtensionClassCode 方法首先会通过反射检测接口方法是否包含 Adaptive 注解。对于要生成自适应拓展的接口,Dubbo 要求该接口至少有一个方法被 Adaptive 注解修饰。若不满足此条件,就会抛出运行时异常。相关代码如下:

// 通过反射获取所有的方法
Method[] methods = type.getMethods();
boolean hasAdaptiveAnnotation = false;
// 遍历方法列表
for (Method m : methods) {
    // 检测方法上是否有 Adaptive 注解
    if (m.isAnnotationPresent(Adaptive.class)) {
        hasAdaptiveAnnotation = true;
        break;
    }
}

if (!hasAdaptiveAnnotation)
    // 若所有的方法上均无 Adaptive 注解,则抛出异常
    throw new IllegalStateException("No adaptive method on extension ...");

2.2.2 生成类

通过 Adaptive 注解检测后,即可开始生成代码。代码生成的顺序与 Java 文件内容顺序一致,首先会生成 package 语句,然后生成 import 语句,紧接着生成类名等代码。整个逻辑如下:

// 生成 package 代码:package + type 所在包
codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
// 生成 import 代码:import + ExtensionLoader 全限定名
codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
// 生成类代码:public class + type简单名称 + $Adaptive + implements + type全限定名 + {
codeBuilder.append("\npublic class ")
    .append(type.getSimpleName())
    .append("$Adaptive")
    .append(" implements ")
    .append(type.getCanonicalName())
    .append(" {");

// ${生成方法}

codeBuilder.append("\n}");

这里使用 ${…} 占位符代表其他代码的生成逻辑,该部分逻辑将在随后进行分析。上面代码不是很难理解,下面直接通过一个例子展示该段代码所生成的内容。以 Dubbo 的 Protocol 接口为例,生成的代码如下:

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
    // 省略方法代码
}

2.2.3 生成方法

一个方法可以被 Adaptive 注解修饰,也可以不被修饰。这里将未被 Adaptive 注解修饰的方法称为“无 Adaptive 注解方法”,下面我们先来看看此种方法的代码生成逻辑是怎样的。

2.2.3.1 无 Adaptive 注解方法代码生成逻辑

对于接口方法,我们可以按照需求标注 Adaptive 注解。以 Protocol 接口为例,该接口的 destroy 和 getDefaultPort 未标注 Adaptive 注解,其他方法均标注了 Adaptive 注解。Dubbo 不会为没有标注 Adaptive 注解的方法生成代理逻辑,对于该种类型的方法,仅会生成一句抛出异常的代码。生成逻辑如下:

for (Method method : methods) {
    // 省略无关逻辑
    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    // 如果方法上无 Adaptive 注解,则生成 throw new UnsupportedOperationException(...) 代码
    if (adaptiveAnnotation == null) {
        // 生成的代码格式如下:
        // throw new UnsupportedOperationException(
        //     "method " + 方法签名 + of interface + 全限定接口名 + is not adaptive method!”)
        code.append("throw new UnsupportedOperationException(\"method ")
            .append(method.toString()).append(" of interface ")
            .append(type.getName()).append(" is not adaptive method!\");");
    } else {
        // 省略无关逻辑
    }

    // 省略无关逻辑

以 Protocol 接口的 destroy 方法为例,上面代码生成的内容如下:

throw new UnsupportedOperationException(
            "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");

2.2.3.2 获取 URL 数据

前面说过方法代理逻辑会从 URL 中提取目标拓展的名称,因此代码生成逻辑的一个重要的任务是从方法的参数列表或者其他参数中获取 URL 数据。举例说明一下,我们要为 Protocol 接口的 refer 和 export 方法生成代理逻辑。在运行时,通过反射得到的方法定义大致如下:

Invoker refer(Class arg0, URL arg1) throws RpcException;
Exporter export(Invoker arg0) throws RpcException;
对于 refer 方法,通过遍历 refer 的参数列表即可获取 URL 数据,这个还比较简单。对于 export 方法,获取 URL 数据则要麻烦一些。export 参数列表中没有 URL 参数,因此需要从 Invoker 参数中获取 URL 数据。获取方式是调用 Invoker 中可返回 URL 的 getter 方法,比如 getUrl。如果 Invoker 中无相关 getter 方法,此时则会抛出异常。整个逻辑如下:

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);
    if (adaptiveAnnotation == null) {
        // ${无 Adaptive 注解方法代码生成逻辑}
    } else {
    	int urlTypeIndex = -1;
        // 遍历参数列表,确定 URL 参数位置
        for (int i = 0; i < pts.length; ++i) {
            if (pts[i].equals(URL.class)) {
                urlTypeIndex = i;
                break;
            }
        }
        
        // urlTypeIndex != -1,表示参数列表中存在 URL 参数
        if (urlTypeIndex != -1) {
            // 为 URL 类型参数生成判空代码,格式如下:
            // if (arg + urlTypeIndex == null) 
            //     throw new IllegalArgumentException("url == null");
            String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                                     urlTypeIndex);
            code.append(s);

            // 为 URL 类型参数生成赋值代码,形如 URL url = arg1
            s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);
            code.append(s);
            
        // 参数列表中不存在 URL 类型参数
        } else {
            String attribMethod = null;

            LBL_PTS:
            // 遍历方法的参数类型列表
            for (int i = 0; i < pts.length; ++i) {
                // 获取某一类型参数的全部方法
                Method[] ms = pts[i].getMethods();
                // 遍历方法列表,寻找可返回 URL 的 getter 方法
                for (Method m : ms) {
                    String name = m.getName();
                    // 1. 方法名以 get 开头,或方法名大于3个字符
                    // 2. 方法的访问权限为 public
                    // 3. 非静态方法
                    // 4. 方法参数数量为0
                    // 5. 方法返回值类型为 URL
                    if ((name.startsWith("get") || name.length() > 3)
                        && Modifier.isPublic(m.getModifiers())
                        && !Modifier.isStatic(m.getModifiers())
                        && m.getParameterTypes().length == 0
                        && m.getReturnType() == URL.class) {
                        urlTypeIndex = i;
                        attribMethod = name;
                        
                        // 结束 for (int i = 0; i < pts.length; ++i) 循环
                        break LBL_PTS;
                    }
                }
            }
            if (attribMethod == null) {
                // 如果所有参数中均不包含可返回 URL 的 getter 方法,则抛出异常
                throw new IllegalStateException("fail to create adaptive class for interface ...");
            }

            // 为可返回 URL 的参数生成判空代码,格式如下:
            // if (arg + urlTypeIndex == null) 
            //     throw new IllegalArgumentException("参数全限定名 + argument == null");
            String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                                     urlTypeIndex, pts[urlTypeIndex].getName());
            code.append(s);

            // 为 getter 方法返回的 URL 生成判空代码,格式如下:
            // if (argN.getter方法名() == null) 
            //     throw new IllegalArgumentException(参数全限定名 + argument getUrl() == null);
            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 = argN.getter方法名(),比如 
            // com.alibaba.dubbo.common.URL url = invoker.getUrl();
            s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
            code.append(s);
        }
        
        // 省略无关代码
    }
    
    // 省略无关代码
}

上面代码有点多,需要耐心看一下。这段代码主要目的是为了获取 URL 数据,并为之生成判空和赋值代码。以 Protocol 的 refer 和 export 方法为例,上面的代码为它们生成如下内容(代码已格式化):

refer:
if (arg1 == null) 
    throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;

export:
if (arg0 == null) 
    throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
if (arg0.getUrl() == null) 
    throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
com.alibaba.dubbo.common.URL url = arg0.getUrl();
2.2.3.3 获取 Adaptive 注解值
Adaptive 注解值 value 类型为 String[],可填写多个值,默认情况下为空数组。若 value 为非空数组,直接获取数组内容即可。若 value 为空数组,则需进行额外处理。处理过程是将类名转换为字符数组,然后遍历字符数组,并将字符放入 StringBuilder 中。若字符为大写字母,则向 StringBuilder 中添加点号,随后将字符变为小写存入 StringBuilder 中。比如 LoadBalance 经过处理后,得到 load.balance。

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);
    if (adaptiveAnnotation == null) {
        // ${无 Adaptive 注解方法代码生成逻辑}
    } else {
        // ${获取 URL 数据}
        
        String[] value = adaptiveAnnotation.value();
        // value 为空数组
        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 中添加点号
                        sb.append(".");
                    }
                    // 将字符变为小写,并添加到 sb 中
                    sb.append(Character.toLowerCase(charArray[i]));
                } else {
                    // 添加字符到 sb 中
                    sb.append(charArray[i]);
                }
            }
            value = new String[]{sb.toString()};
        }
        
        // 省略无关代码
    }
    
    // 省略无关逻辑
}

2.2.3.4 检测 Invocation 参数

此段逻辑是检测方法列表中是否存在 Invocation 类型的参数,若存在,则为其生成判空代码和其他一些代码。相应的逻辑如下:

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);
    if (adaptiveAnnotation == null) {
        // ${无 Adaptive 注解方法代码生成逻辑}
    } else {
        // ${获取 URL 数据}
        
        // ${获取 Adaptive 注解值}
        
        boolean hasInvocation = false;
        // 遍历参数类型列表
        for (int i = 0; i < pts.length; ++i) {
            // 判断当前参数名称是否等于 com.alibaba.dubbo.rpc.Invocation
            if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
                // 为 Invocation 类型参数生成判空代码
                String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
                code.append(s);
                // 生成 getMethodName 方法调用代码,格式为:
                //    String methodName = argN.getMethodName();
                s = String.format("\nString methodName = arg%d.getMethodName();", i);
                code.append(s);
                
                // 设置 hasInvocation 为 true
                hasInvocation = true;
                break;
            }
        }
    }
    
    // 省略无关逻辑
}

2.2.3.5 生成拓展名获取逻辑

本段逻辑用于根据 SPI 和 Adaptive 注解值生成“获取拓展名逻辑”,同时生成逻辑也受 Invocation 类型参数影响,综合因素导致本段逻辑相对复杂。本段逻辑可能会生成但不限于下面的代码:

String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
或

String extName = url.getMethodParameter(methodName, "loadbalance", "random");
亦或是

String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
本段逻辑复杂之处在于条件分支比较多,大家在阅读源码时需要知道每个条件分支的意义是什么,否则不太容易看懂相关代码。下面开始分析本段逻辑。

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);
    if (adaptiveAnnotation == null) {
        // $无 Adaptive 注解方法代码生成逻辑}
    } else {
        // ${获取 URL 数据}
        
        // ${获取 Adaptive 注解值}
        
        // ${检测 Invocation 参数}
        
        // 设置默认拓展名,cachedDefaultName 源于 SPI 注解值,默认情况下,
        // SPI 注解值为空串,此时 cachedDefaultName = null
        String defaultExtName = cachedDefaultName;
        String getNameCode = null;
        
        // 遍历 value,这里的 value 是 Adaptive 的注解值,2.2.3.3 节分析过 value 变量的获取过程。
        // 此处循环目的是生成从 URL 中获取拓展名的代码,生成的代码会赋值给 getNameCode 变量。注意这
        // 个循环的遍历顺序是由后向前遍历的。
        for (int i = value.length - 1; i >= 0; --i) {
            // 当 i 为最后一个元素的坐标时
            if (i == value.length - 1) {
                // 默认拓展名非空
                if (null != defaultExtName) {
                    // protocol 是 url 的一部分,可通过 getProtocol 方法获取,其他的则是从
                    // URL 参数中获取。因为获取方式不同,所以这里要判断 value[i] 是否为 protocol
                    if (!"protocol".equals(value[i]))
                    	// hasInvocation 用于标识方法参数列表中是否有 Invocation 类型参数
                        if (hasInvocation)
                            // 生成的代码功能等价于下面的代码:
                            //   url.getMethodParameter(methodName, value[i], defaultExtName)
                            // 以 LoadBalance 接口的 select 方法为例,最终生成的代码如下:
                            //   url.getMethodParameter(methodName, "loadbalance", "random")
                            getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                    	else
                    		// 生成的代码功能等价于下面的代码:
	                        //   url.getParameter(value[i], defaultExtName)
	                        getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                    else
                    	// 生成的代码功能等价于下面的代码:
                        //   ( url.getProtocol() == null ? defaultExtName : url.getProtocol() )
                        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);
	                    else
	                    	// 生成的代码功能等价于下面的代码:
	                        //   url.getParameter(value[i])
	                        getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                    else
                    	// 生成从 url 中获取协议的代码,比如 "dubbo"
                        getNameCode = "url.getProtocol()";
                }
            } else {
                if (!"protocol".equals(value[i]))
                    if (hasInvocation)
                        // 生成代码格式同上
                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
	                else
	                	// 生成的代码功能等价于下面的代码:
	                    //   url.getParameter(value[i], getNameCode)
	                    // 以 Transporter 接口的 connect 方法为例,最终生成的代码如下:
	                    //   url.getParameter("client", url.getParameter("transporter", "netty"))
	                    getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                else
                    // 生成的代码功能等价于下面的代码:
                    //   url.getProtocol() == null ? getNameCode : url.getProtocol()
                    // 以 Protocol 接口的 connect 方法为例,最终生成的代码如下:
                    //   url.getProtocol() == null ? "dubbo" : url.getProtocol()
                    getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
            }
        }
        // 生成 extName 赋值代码
        code.append("\nString extName = ").append(getNameCode).append(";");
        // 生成 extName 判空代码
        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);
    }
    
    // 省略无关逻辑
}

上面代码比较复杂,不是很好理解。对于这段代码,建议大家写点测试用例,对 Protocol、LoadBalance 以及 Transporter 等接口的自适应拓展类代码生成过程进行调试。这里我以 Transporter 接口的自适应拓展类代码生成过程举例说明。首先看一下 Transporter 接口的定义,如下:

@SPI("netty")
public interface Transporter {
	// @Adaptive({server, transporter})
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY}) 
    Server bind(URL url, ChannelHandler handler) throws RemotingException;

    // @Adaptive({client, transporter})
    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
下面对 connect 方法代理逻辑生成的过程进行分析,此时生成代理逻辑所用到的变量如下:

String defaultExtName = "netty";
boolean hasInvocation = false;
String getNameCode = null;
String[] value = ["client", "transporter"];
下面对 value 数组进行遍历,此时 i = 1, value[i] = “transporter”,生成的代码如下:

getNameCode = url.getParameter("transporter", "netty");
接下来,for 循环继续执行,此时 i = 0, value[i] = “client”,生成的代码如下:

getNameCode = url.getParameter("client", url.getParameter("transporter", "netty"));
for 循环结束运行,现在为 extName 变量生成赋值和判空代码,如下:

String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
if (extName == null) {
    throw new IllegalStateException(
        "Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString()
        + ") use keys([client, transporter])");
}
2.2.3.6 生成拓展加载与目标方法调用逻辑
本段代码逻辑用于根据拓展名加载拓展实例,并调用拓展实例的目标方法。相关逻辑如下:

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);
    if (adaptiveAnnotation == null) {
        // $无 Adaptive 注解方法代码生成逻辑}
    } else {
        // ${获取 URL 数据}
        
        // ${获取 Adaptive 注解值}
        
        // ${检测 Invocation 参数}
        
        // ${生成拓展名获取逻辑}
        
        // 生成拓展获取代码,格式如下:
        // type全限定名 extension = (type全限定名)ExtensionLoader全限定名
        //     .getExtensionLoader(type全限定名.class).getExtension(extName);
        // Tips: 格式化字符串中的 %<s 表示使用前一个转换符所描述的参数,即 type 全限定名
        s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                        type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
        code.append(s);

		// 如果方法返回值类型非 void,则生成 return 语句。
        if (!rt.equals(void.class)) {
            code.append("\nreturn ");
        }

        // 生成目标方法调用逻辑,格式为:
        //     extension.方法名(arg0, arg2, ..., argN);
        s = String.format("extension.%s(", method.getName());
        code.append(s);
        for (int i = 0; i < pts.length; i++) {
            if (i != 0)
                code.append(", ");
            code.append("arg").append(i);
        }
        code.append(");");   
    }
    
    // 省略无关逻辑
}

以 Protocol 接口举例说明,上面代码生成的内容如下:

com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader
    .getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);

2.2.3.7 生成完整的方法

本节进行代码生成的收尾工作,主要用于生成方法定义的代码。相关逻辑如下:

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);
    if (adaptiveAnnotation == null) {
        // $无 Adaptive 注解方法代码生成逻辑}
    } else {
        // ${获取 URL 数据}
        
        // ${获取 Adaptive 注解值}
        
        // ${检测 Invocation 参数}
        
        // ${生成拓展名获取逻辑}
        
        // ${生成拓展加载与目标方法调用逻辑}
    }
}
    
// public + 返回值全限定名 + 方法名 + (
codeBuilder.append("\npublic ")
    .append(rt.getCanonicalName())
    .append(" ")
    .append(method.getName())
    .append("(");

// 添加参数列表代码
for (int i = 0; i < pts.length; i++) {
    if (i > 0) {
        codeBuilder.append(", ");
    }
    codeBuilder.append(pts[i].getCanonicalName());
    codeBuilder.append(" ");
    codeBuilder.append("arg").append(i);
}
codeBuilder.append(")");

// 添加异常抛出代码
if (ets.length > 0) {
    codeBuilder.append(" throws ");
    for (int i = 0; i < ets.length; i++) {
        if (i > 0) {
            codeBuilder.append(", ");
        }
        codeBuilder.append(ets[i].getCanonicalName());
    }
}
codeBuilder.append(" {");
codeBuilder.append(code.toString());
codeBuilder.append("\n}");
以 Protocol 的 refer 方法为例,上面代码生成的内容如下:

public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) {
    // 方法体
}

3.总结

到此,关于自适应拓展的原理,实现就分析完了。总的来说自适应拓展整个逻辑还是很复杂的,并不是很容易弄懂。因此,大家在阅读该部分源码时,耐心一些。同时多进行调试,也可以通过生成好的代码思考代码的生成逻辑。好了,本篇文章就分析到这里。

标签:12,23,url,代码,2021,Adaptive,null,method,append
来源: https://blog.csdn.net/CAIDEYAOSI/article/details/122101892

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有