SPI简介

SPI是Service Provider Interface的缩写,即服务提供接口(翻译出来好绕口,还是不翻译的好),实质上是接口,作用是对外提供服务。
SPI是Java的一种插件机制,可以不用修改源代码实现新功能的扩展。
主要有如下几个步骤:

  1. 实现SPI接口
  2. 在项目的META-INF/services文件夹下,新建一个以SPI接口命名的文件, 文件里面配置上SPI接口的实现类
  3. 使用java.util.ServiceLoader加载。
    由于本篇文章主要讲解Dubbo是如何使用SPI的,如果想要具体了解Java的SPI,可以参考下面两篇文章:
  • JavaSPI机制学习笔记
  • Introduction to the Service Provider Interfaces
    当然还可以看 java.util.ServiceLoader 源码,注释中也有详细的说明。

Dubbo SPI

回到正题,SPI在dubbo应用的地方很多,专业一点讲叫做微内核机制;
如下图:

我们拿其中一个标签进行讲解,我们在使用dubbo框架时,会配置标签,告诉dubbo服务的主机、端口、可接收的最大连接数、使用哪个协议,协议的传输控制器(netty,servlet,jetty等)、线程池类型大小等信息。dubbo协议默认使用的是netty网络传输框架,当然还可以使用mina、grizzly,只需要配置transporter、server、client为相应的值即可。那dubbo是如何根据不同的配置使用不同的网络传输框架的呢,当然是通过SPI啦。java spi有一个配置文件,那dubbo是否也有呢?在dubbo-rpc包下的dubbo-rpc-dubbo子包下,发现了一个配置文件

我们来看下配置文件的内容:

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

配置了一个键值对,key为dubbo,值为org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol,在其它几个子包下,也有名称叫做org.apache.dubbo.rpc.Protocol的配置文件,说明Protocol插口有几个对应的插件

可以猜测一下,当<dubbo:protocol />仅仅配置了name="dubbo",port="20880"时,会加载哪一个协议插件呢,根据名称,可以猜测,加载的DubboProtocol插件。那dubbo是怎样做到的呢,我们来一探究竟。

Dubbo为使用SPI做的准备工作:

1. 三个注解

  • SPI:这个注解使用在接口上,标识接口是否是extension(扩展或插口),可以接收一个默认的extension名称
  • Adaptive: 这个注解可以使用在类或方法上,决定加载哪一个extension,值为字符串数组,数组中的字符串是key值,比如new String[]{"key1","key2"};先在URL中寻找key1的值,如果找到,则使用此值加载extension,如果key1没有,则寻找key2的值,如果key2也没有,则使用接口SPI注解的值,如果接口SPI注解,没有配置默认值,则将接口名按照首字母大写分成多个部分,然后以'.'分隔,例如org.apache.dubbo.xxx.YyyInvokerWrapper接口名会变成yyy.invoker.wrapper,然后以此名称做为key到URL寻找,如果仍没有找到,则抛出IllegalStateException异常;Adaptive注解用在类上,表示此类是它实现接口(插口)的自适应插件
  • Activate:这个注解可以使用在类或方法上,用以根据URL的key值判断当前extension是否生效,当一个extension有多个实现时,可以加载特定的extension实现类,例如extension实现类上有注解@Activate("cache, validation"),则当URL上出现"cache”或“validation" key时,当前extension才会生效

2. ExtensionLoader

顾名思义,ExtensionLoader用于加载extension,它的作用有三点:1.自动加载extension;2.自动包装(wrap) extension;3.创建自适应的(adaptive)extension;

旅途开始

先看下上篇文章中Provider端的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"><!-- 提供方应用信息,用于计算依赖关系 --><dubbo:application name="hello-world-app"  /><!-- 使用multicast广播注册中心暴露服务地址 --><dubbo:registry address="multicast://224.5.6.7:1234" /><!-- 用dubbo协议在20880端口暴露服务 --><dubbo:protocol name="dubbo" port="20880" /><!-- 声明需要暴露的服务接口 --><dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" /><!-- 和本地bean一样实现服务 --><bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
</beans>

还是先从ClassPathXmlApplicationContext加载spring配置文件说起,上回我们说到ClassPathXmlApplicationContext会使用XmlBeanDefinitionReader将xml文件解析成BeanDefiniton集合,当解析<dubbo:protocol />标签时,会将其解析成org.apache.dubbo.config.ProtocolConfig对象(为什么?请看上回分解最后,protocol key 实例化DubboBeanDefinitionParser时传入的参数),解析<dubbo:service />时,会将其解析成org.apache.dubbo.config.spring.ServiceBean对象。在解析xml时,会调用AbstractApplicationContext的refresh()方法

ServiceBean是ServiceConfig的子类,所以在创建ServiceBean对象的时候,会去先实例化父类,ServiceConfig中有一个static final成员变量protocol

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

ExtensionLoader终于出场了,想要获取插件,得分两步走,第一步得到Protocol的插件加载对象extensionLoader,然后由这个加载对象获得对应的插件。
先来看第一步:

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {//一些检查的代码,省略ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);if (loader == null) {EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);}return loader;}

EXTENSION_LOADERS保存的是目前已经保存的插口的加载类,显示第一次加载的时候,Protocol还没有自己的插件加载类,那么需要实例化一个。实例化加载对象之后,用这个对象去加载插件。

    public T getAdaptiveExtension() {//从已经缓存的自适应对象中获得,第一次调用时还没有创建自适应类,所以instance为nullObject 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);}}}} else {throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);}}return (T) instance;}

主要关注 instance = createAdaptiveExtension();这句,createAdaptiveExtension()方法是什么样的呢?

    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);}}

getAdaptiveExtensionClass():

    private Class<?> getAdaptiveExtensionClass() {//1.获取所有实现Protocol插口的插件类getExtensionClasses();//2.如果有自适应插件类,则返回if (cachedAdaptiveClass != null) {return cachedAdaptiveClass;}//3.如果没有,则创建插件类return cachedAdaptiveClass = createAdaptiveExtensionClass();}

先来看上面的第1步,getExtensionClasses()

    private Map<String, Class<?>> getExtensionClasses() {//从缓存中获取插件类,第一次肯定没有Map<String, Class<?>> classes = cachedClasses.get();if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {//实际的加载插件类方法classes = loadExtensionClasses();cachedClasses.set(classes);}}}return classes;}//ExtensionLoader中的三个常量,加载插件的目录,第一个熟悉吧,是java spi的默认目录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 Map<String, Class<?>> 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];}}//加载以上三个目录下的实现了相应插口的插件类(本例中插口是Protocol)Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));return extensionClasses;}

调试发现,共取到四个插件(实现Protocol接口的不止这四个类,还有redis、memcache等,不知为啥只取到这四个类?):

再来看上面getAdaptiveExtensionClass方法的第2步,这一句是判断有没有自适应类,在加载配置的插件过程中,会判断此插件类是不是自适应插件类,判断的依据就是插件类上是否有注解@Adaptive,Protocol的这四个插件类上都没有此注解,所以没有自适应插件,则会走到第3步,创建一个自适应插件类

   private Class<?> createAdaptiveExtensionClass() {//生成类代码String code = createAdaptiveExtensionClassCode();ClassLoader classLoader = findClassLoader();//得到编辑器,并将类代码编译成字节码org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();return compiler.compile(code, classLoader);}//来看看生成类代码的过程,以生成Protocol插件类代码为例private String createAdaptiveExtensionClassCode() {StringBuilder codeBuilder = new StringBuilder();//得到Protocol接口所有方法Method[] methods = type.getMethods();boolean hasAdaptiveAnnotation = false;for (Method m : methods) {if (m.isAnnotationPresent(Adaptive.class)) {hasAdaptiveAnnotation = true;break;}}// // 如果方法上没有@Adaptive注解,则不能创建自适应插件类if (!hasAdaptiveAnnotation)throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");codeBuilder.append("package ").append(type.getPackage().getName()).append(";");codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");//类名为Protocol$Adaptive实现了Protocol接口codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {");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) {code.append("throw new UnsupportedOperationException(\"method ").append(method.toString()).append(" of interface ").append(type.getName()).append(" is not adaptive method!\");");} else {int urlTypeIndex = -1;for (int i = 0; i < pts.length; ++i) {if (pts[i].equals(URL.class)) {urlTypeIndex = i;break;}}// 如果发现方法中的参数有一个URL类型if (urlTypeIndex != -1) {// Null Point checkString 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);}//  如果没有发现,则会寻找每一个参数类型中的属性是否有为URL类型的else {String attribMethod = null;// find URL getter methodLBL_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) {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());}// Null point checkString 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);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 keyif (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()};}boolean hasInvocation = false;for (int i = 0; i < pts.length; ++i) {if (pts[i].getName().equals("org.apache.dubbo.rpc.Invocation")) {// Null Point checkString 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);hasInvocation = true;break;}}String defaultExtName = cachedDefaultName;String getNameCode = null;for (int i = value.length - 1; i >= 0; --i) {if (i == value.length - 1) {if (null != defaultExtName) {if (!"protocol".equals(value[i]))if (hasInvocation)getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);elsegetNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);elsegetNameCode = 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);elsegetNameCode = String.format("url.getParameter(\"%s\")", value[i]);elsegetNameCode = "url.getProtocol()";}} else {if (!"protocol".equals(value[i]))//如果方法参数类型名称为"org.apache.dubbo.rpc.Invocation"则从url获取以此参数类型名为key的值,获取不到则取默认扩展名,即Protocol接口上注解SPI的值“dubbo”if (hasInvocation)getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);else//否则,取从url中取以方法上注解adaptive的值为key对应的值getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);elsegetNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);}}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);s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());code.append(s);// return statementif (!rt.equals(void.class)) {code.append("\nreturn ");}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(");");}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}");}codeBuilder.append("\n}");if (logger.isDebugEnabled()) {logger.debug(codeBuilder.toString());}return codeBuilder.toString();}

我们来看下生成的插件类Protocol$Adaptive代码:

package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Protocol ;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.Invoker ;
import org.apache.dubbo.rpc.Exporter;public class Protocol$Adaptive implements Protocol {public void destroy(){throw new UnsupportedOperationException("method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol         is not adaptive method!");}public int getDefaultPort() {throw new UnsupportedOperationException("method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface         org.apache.dubbo.rpc.Protocol is not adaptive method!");}public Invoker refer(Class arg0, URL arg1) throws RpcException {if (arg1 == null) throw new IllegalArgumentException("url == null");URL url = arg1;String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);return extension.refer(arg0, arg1);}public Exporter export(Invoker arg0) throws RpcException {if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");if (arg0.getUrl() == null)throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");org.apache.dubbo.common.URL url = arg0.getUrl();String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);return extension.export(arg0);}
}

可以看到Protocol$Adaptive可以根据url中参数protocol值加载对应的插件,如果url中没有,则加载名为"dubbo"对应的插件,而从前面加载的四个插件可以看出,名称为dubbo的插件类为org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.

写到这里总算将SPI加载的过程大体上讲述了一篇,Dubbo中还有许多类似的插件,原理基本相同;除了有的插口有自适应插件,比如org.apache.dubbo.common.compiler.Compilerorg.apache.dubbo.common.extension.ExtensionFactory,自适应插件类上都有注解@Adaptive,比如Compile的自适应插件AdaptiveCompiler,ExtensionFactory的自适应插件AdaptiveExtensionFactory.

为什么要提供自适应插件,而不是都在运行时生成?
答:
(1)解决鸡生蛋,蛋生鸡的问题,上面createAdaptiveExtensionClass方法中,在第1步生成Protocol$Adaptive类后,会使用编译器将其编译成字节码,但是编译器本身也是插件化的,可以有好几种编译器,所以需要提供一个已经存在的自适应编译器(AdaptiveCompiler),然后在编译的时候,使用此编译器找到Compile接口上SPI注解中配置的默认的编译器进行编译。
(2)解决对象生成方式不同导致的加载问题;Dubbo中对象的生成一类是由Spring容器创建,一类是根据插件文件的配置动态加载;所以要想获取这两部分对象,需要使用不同的方式;而AdaptiveExtensionFactory就是为了解决这个问题,在获取对象时,分别从Spring容器和ExtensionLoader中查找。

转载于:https://www.cnblogs.com/mycodingworld/p/9353833.html

Dubbo2.7源码分析-SPI的应用相关推荐

  1. Dubbo 源码分析 - SPI 机制

    1.简介 SPI 全称为 Service Provider Interface,是一种服务发现机制.SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类.这样可以 ...

  2. pixhawk 源码分析-SPI驱动-MS5611

    最近学习了一下SPI的驱动软件,在此将其进行总结. 本文使用的代码为pixhawk 1.5.5版本的源码 源码下载地址 第一步函数入口: 老规矩,所有px4的代码的函数入口都是在启动脚本中,启动脚本地 ...

  3. FPGA 黑金XC6SLX9 08.spi_flash源码分析 spi flash数据的读取擦除写入实验(1)

    Spi flash 数码管显示flash两位数 按键可实现数据写入 流程图 // //                                                         ...

  4. SPI驱动框架源码分析

     SPI驱动框架源码分析 2013-04-12 16:13:08 分类: LINUX SPI驱动框架源码分析 SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设 ...

  5. Dubbo 源码分析 - 集群容错之 LoadBalance

    1.简介 LoadBalance 中文意思为负载均衡,它的职责是将网络请求,或者其他形式的负载"均摊"到不同的机器上.避免集群中部分服务器压力过大,而另一些服务器比较空闲的情况.通 ...

  6. Dubbo 源码分析 - 集群容错之 Cluster

    1.简介 为了避免单点故障,现在的应用至少会部署在两台服务器上.对于一些负载比较高的服务,会部署更多台服务器.这样,同一环境下的服务提供者数量会大于1.对于服务消费者来说,同一环境下出现了多个服务提供 ...

  7. Dubbo 源码分析 - 集群容错之 Router

    1. 简介 上一篇文章分析了集群容错的第一部分 – 服务目录 Directory.服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由.上一篇文章关于服务路由相关逻辑没有细 ...

  8. Linux驱动修炼之道-SPI驱动框架源码分析(上)

    Linux驱动修炼之道-SPI驱动框架源码分析(上)   SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设备可工作于m/s模式.主设备发起数据帧,允许多个从设 ...

  9. JDK源码分析——Java的SPI机制分析与实战

    重点提示:在我博客中的所有的源码分析的实例,我都将会放到github上,感兴趣的朋友可以下载下来调试运行,我相信还是可以有所收获的.我的目的是让所有读到我博客的朋友都可以了解到有价值的东西,学习到ja ...

最新文章

  1. 整图下沉,MindSpore图引擎详解
  2. /usr/include/boost/type_traits/detail/has_binary_operator.hp:50: Parse error at BOOST_JOIN错误
  3. 牛客题霸 NC23 划分链表
  4. Maven内置属性及使用
  5. 【NC30】缺失的第一个正整数
  6. scanf( )函数的返回值
  7. JS/jQuery获取input的值和清空input的value值
  8. 【Flink】Unable to retrieve any partitions with KafkaTopicsDescriptor: Fixed Topics ([xxx)]
  9. Textual Description for Visualization
  10. 人头检测 模型 c++_常熟市房屋建筑检测鉴定服务单位 房屋鉴定中心
  11. PeekMessage和GetMessage的区别
  12. RegExp 误用解析
  13. java总结体会_Java课程总结心得体会
  14. 【饥荒脚本】饥荒控制台代码自动输入
  15. 文章详情页----- 详细步骤
  16. 矩阵顺时针旋转90°、180°、270°
  17. UI设计好学吗?UI设计难不难?
  18. 2022第七届少儿模特明星盛典 全能TOP艺人蒋松廷 T台风采展示
  19. 风格迁移2020 —— 论文汇总——云盘分享
  20. 基于PCA的图像压缩实现

热门文章

  1. 对Moss 2007中访问群体的设置和使用补充
  2. 常见Promise面试题
  3. Linux- 日常运维-w-查看系统负载
  4. hibernate中多对多关系映射时的xml文件
  5. 一个到顶部自动加载更多的ListView
  6. DedeCMS更新文章同步发布到新浪微博
  7. 在iphone上安装多个微信 【微信营销必备】
  8. Oracle网格控制器OMA端安装Yast
  9. matlab 信道模拟 差错概率,移动信道差错序列的分布概率模拟法及门限电平的讨论...
  10. 使用oracle客户端与PLSQL连接ORACLE数据库软件安装过程