SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。

Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。

1.0 Java SPI 示例

//定义接口
public interface SpiInterface
{ void sayHello(); } 

//实现类
public class SpiImpl01 implements SpiInterface {
@Override
public void sayHello() {
System.out.println("Hello, I am SpiImpl01.");
}
}
public class SpiImpl02 implements SpiInterface{
@Override
public void sayHello() {
System.out.println("Hello, I am SpiImpl02.");
}
}

META-INF/services 文件夹下创建一个文件,名称为Spi的全限定名 接口路径

文件内容:

com.demo.javaSpi.SpiImpl01
com.demo.javaSpi.SpiImpl02
public class JavaSPITest {

    @Test
public void sayHello() throws Exception {
ServiceLoader<SpiInterface> serviceLoader =ServiceLoader.load(SpiInterface.class);
System.out.println("Java SPI");
serviceLoader.forEach(Spi::sayHello);
} }

结果:

2.0 Dubbo SPI 示例

SpiInterface接口加@SPI 注解

public class DubboSpiTest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<SpiInterface> extensionLoader = ExtensionLoader.getExtensionLoader(SpiInterface.class);
SpiInterface spiImpl01 = extensionLoader.getExtension("spiImpl01");
spiImpl01.sayHello();
SpiInterface spiImpl02 = extensionLoader.getExtension("spiImpl02");
spiImpl02.sayHello();
}
}

结果:

Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性

3.0 Dubbo SPI 源码分析

resource目录下配置META-INF/dubbo或者META-INF/dubbo/internal或者META-INF/services,内容是KEY对应Value

加载路径:

下面以加载协议自适应扩展点为例,分析下加载扩展点的流程

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class). getAdaptiveExtension();

ExtensionLoader类:

ExtensionLoader.getExtensionLoader(Protocol.class):代码实现

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if(!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if(!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
//EXTENSION_LOADERS是缓存,首次进来为空
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
//new ExtensionLoader<T>(type)
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
EXTENSION_LOADERS定义:private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

new ExtensionLoader<T>(type):objectFactory是ExtensionFactory的实例,类似一个Spring的工厂,
其中代码ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());,和获取协议扩展点类似。
private ExtensionLoader(Class<?> type) {//Protocol.class
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null :
ExtensionLoader.getExtensionLoader(ExtensionFactory.class).
getAdaptiveExtension());
}
    getAdaptiveExtension()方法
    public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if(createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
//前面都是双重检查锁和缓存检验
//真正方法createAdaptiveExtension()
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;
}
      createAdaptiveExtension方法
    @SuppressWarnings("unchecked")
private T createAdaptiveExtension() {
try {
//可以实现扩展点的注入,类似Spring中set方法注入
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
}
}
    getAdaptiveExtensionClass方法
    //判断当前Protocol这个扩展点是否存在一个自定义的适配器,如果有,则直接返回自定义适配器,否则,就动态创建
private Class<?> getAdaptiveExtensionClass() {
//加载扩展点实现类
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass; //AdaptiveCompiler
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();
} //创建一个适配器扩展点(创建一个动态的字节码文件)
private Class<?> createAdaptiveExtensionClass() {
//生成字节码代码
String code = createAdaptiveExtensionClassCode();
//获得类加载器
ClassLoader classLoader = findClassLoader();
//Compiler默认情况下使用的是javassist
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
//动态编译字节码,加载到JVM中
return compiler.compile(code, classLoader);
}


   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;
}
// 此方法已经getExtensionClasses方法同步过。
private Map<String, Class<?>> loadExtensionClasses() {
//type->Protocol.class
//得到SPI的注解
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if(defaultAnnotation != null) { //如果不等于空.
String value = defaultAnnotation.value();
if(value != null && (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<String, Class<?>> extensionClasses = new HashMap<String, 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/";
loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadFile(extensionClasses, DUBBO_DIRECTORY);
loadFile(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}

loadFile方法:加载文件

  private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> 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 = null;
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf('#');
if (ci >= 0) line = line.substring(0, ci);
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {//文件采用name=value方式,通过i进行分割
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.");
} //判断是否有自定义适配类,如果有,则在前面讲过的获取适配类的时候,直接返回当前的自定义适配类,不需要再动态创建
//@Adaptive如果是加在类上,表示当前类是一个自定义的自适应扩展点
//如果是加在方法级别上,表示需要动态创建一个自适应扩展点,当前就是Protocol$Adaptive
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 {
try {
//cachedWrapperClasses实际上就是对扩展类进行装饰.
//可以在dubbo-rpc-api/internal下找到Protocol文件,发现Protocol配置了3个装饰filter/listener/mock
//Protocol类型的实例会增加对应的装饰器
clazz.getConstructor(type);
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);//包装类 ProtocolFilterWrapper(ProtocolListenerWrapper(Protocol))
} catch (NoSuchMethodException e) {
clazz.getConstructor();
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 activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (! cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
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);
}
}

    createAdaptiveExtensionClassCode方法: 生成字节码
   private String createAdaptiveExtensionClassCode() {
StringBuilder codeBuidler = new StringBuilder();
Method[] methods = type.getMethods();
boolean hasAdaptiveAnnotation = false;
for(Method m : methods) {
if(m.isAnnotationPresent(Adaptive.class)) {
hasAdaptiveAnnotation = true;
break;
}
}
// 完全没有Adaptive方法,则不需要生成Adaptive类
if(! hasAdaptiveAnnotation)
throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!"); codeBuidler.append("package " + type.getPackage().getName() + ";");
codeBuidler.append("\nimport " + ExtensionLoader.class.getName() + ";");
codeBuidler.append("\npublic class " + type.getSimpleName() + "$Adpative" + " 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);
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 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);
}
// 参数没有URL类型
else {
String attribMethod = null; // 找到参数的URL属性
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) {
urlTypeIndex = i;
attribMethod = name;
break LBL_PTS;
}
}
}
if(attribMethod == null) {
throw new IllegalStateException("fail to create adative class for interface " + type.getName()
+ ": not found url parameter or url attribute in parameters of method " + method.getName());
} // 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); s = String.format("%s url = arg%d.%s();",URL.class.getName(), urlTypeIndex, attribMethod);
code.append(s);
} String[] value = adaptiveAnnotation.value();
// 没有设置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()};
} boolean hasInvocation = false;
for (int i = 0; i < pts.length; ++i) {
if (pts[i].getName().equals("com.alibaba.dubbo.rpc.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);
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);
else
getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
else
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
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);
}
}
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 statement
if (!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(");");
} codeBuidler.append("\npublic " + rt.getCanonicalName() + " " + method.getName() + "(");
for (int i = 0; i < pts.length; i ++) {
if (i > 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();
}

Protocol接口

@SPI("dubbo")
public interface Protocol { /**
* 获取缺省端口,当用户没有配置端口时使用。
*
* @return 缺省端口
*/
int getDefaultPort(); /**
* 暴露远程服务:<br>
* 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
* 2. export()必须是幂等的,也就是暴露同一个URL的Invoker两次,和暴露一次没有区别。<br>
* 3. export()传入的Invoker由框架实现并传入,协议不需要关心。<br>
*
* @param <T> 服务的类型
* @param invoker 服务的执行体
* @return exporter 暴露服务的引用,用于取消暴露
* @throws RpcException 当暴露服务出错时抛出,比如端口已占用
*/
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException; /**
* 引用远程服务:<br>
* 1. 当用户调用refer()所返回的Invoker对象的invoke()方法时,协议需相应执行同URL远端export()传入的Invoker对象的invoke()方法。<br>
* 2. refer()返回的Invoker由协议实现,协议通常需要在此Invoker中发送远程请求。<br>
* 3. 当url中有设置check=false时,连接失败不能抛出异常,并内部自动恢复。<br>
*
* @param <T> 服务的类型
* @param type 服务的类型
* @param url 远程服务的URL地址
* @return invoker 服务的本地代理
* @throws RpcException 当连接服务提供方失败时抛出
*/
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; /**
* 释放协议:<br>
* 1. 取消该协议所有已经暴露和引用的服务。<br>
* 2. 释放协议所占用的所有资源,比如连接和端口。<br>
* 3. 协议在释放后,依然能暴露和引用新的服务。<br>
*/
void destroy(); }

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class). getAdaptiveExtension():   执行结束得到的字节码Code

public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
public void 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!");
} public int getDefaultPort() {
throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
} public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws com.alibaba.dubbo.rpc.RpcException {
if (arg1 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([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);
} public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException {
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();
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
}
}
参考Dubbo官方文档:http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html

理解SPI的更多相关文章

  1. JDK源码系列(一) ------ 深入理解SPI机制

    什么是SPI机制 最近我建了另一个文章分类,用于扩展JDK中一些重要但不常用的功能. SPI,全名Service Provider Interface,是一种服务发现机制.它可以看成是一种针对接口实现 ...

  2. 深入理解SPI机制-服务发现机制

    https://www.jianshu.com/p/3a3edbcd8f24 SPI ,全称为 Service Provider Interface,是一种服务发现机制.它通过在ClassPath路径 ...

  3. 深入理解SPI机制

    一.什么是SPI SPI ,全称为 Service Provider Interface,是一种服务发现机制.它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加 ...

  4. Arduino 与 SPI 结合使用 以及SPI 深层理解

    本文主要讲解两部分内容,不做任何转发,仅个人学习记录: 一. Arduino 与 SPI 结合使用  : 二. SPI 深层理解 有价值的几个好的参考: 1. 中文版: https://blog.cs ...

  5. spi子系统之驱动SSD1306 OLED

    spi子系统之驱动SSD1306 OLED 接触Linux之前,曾以为读源码可以更快的学习软件,于是前几个博客都是一边读源码一边添加注释,甚至精读到每一行代码,实际上效果并不理想,看过之后就忘记了.主 ...

  6. Java的SPI机制与简单的示例

    一.SPI机制 这里先说下SPI的一个概念,SPI英文为Service Provider Interface单从字面可以理解为Service提供者接口,正如从SPI的名字去理解SPI就是Service ...

  7. STM32F10X SPI操作flash MX25L64读写数据(转)

    源:STM32F10X SPI操作flash MX25L64读写数据 前一段时间在弄SPI,之前没接触过嵌入式外围应用,就是单片机也只接触过串口通信,且也是在学校的时候了.从离开手机硬件测试岗位后,自 ...

  8. FPGA构造spi时序——AD7176为例(转)

    reference:https://blog.csdn.net/fzhykx/article/details/79490330 项目中用到了一种常见的低速接口(spi),于是整理了一下关于spi相关的 ...

  9. JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制

    代码托管在:https://github.com/fabe2ry/classloaderDemo 初始化数据库 如果你写过操作数据库的程序的话,可能会注意,有的代码会在程序的开头,有Class.for ...

随机推荐

  1. P3968 [TJOI2014]电源插排

    P3968 [TJOI2014]电源插排 线段树维护最长空区间及左端点位置,这个和$nlongn$的动态最大子序和差不多,就不多解释了 $n$较大哈希优化空间 My complete code: #i ...

  2. webpack笔记1

    1.设置多个入口起点 多用于提取公共类库 a.利用commonChunkPlugin const webpack= require('webpack'); const path = require(' ...

  3. smokeping再次部署遇到的问题记录

    问题1: Can't locate Sys/Syslog.pm in @INC (@INC contains: /opt/smokeping_workspace/thirdparty/lib/perl ...

  4. iOS Assertion failure in -[UITableView _classicHeightForRowAtIndexPath:]

    Assertion failure in -[UITableView _classicHeightForRowAtIndexPath:], /SourceCache/UIKit_Sim/UIKit-3 ...

  5. matlab之flipud()函数

    此函数实现矩阵的上下翻转.fliplw()实现左右旋转. 举例: a =[1 2;3 4;5 6] flipud(a)的结果: 5 6 3 4 1 2 fliplr(a)的结果: 2 1 4 3 6 ...

  6. 按钮滚动到指定位置(JS)

    function intval(v) { v = parseInt(v); return isNaN(v) ? 0 : v; } function getPos(e) { var l = 0; var ...

  7. css3线性渐变兼容

    火狐浏览器: background:-moz-linear-gradient(top, red, rgba(0, 0, 255, 0.5)); 谷歌: .l6{background: -webkit- ...

  8. listen 73

    Give Time to Feel Less Time-Squeeze Meetings, calls, kids, dogs, errands, exercise—and all those ema ...

  9. bjwc Day0 大型签到日

    1.18期末考试 1.19试卷讲评 1.20我开始了bjwc愉快的冬眠之旅 上午先是颁发noip一等奖 我在台下笑得像个没有一等奖的孩子/手动微笑 然后去机房试了一下机 坐在鸡神边上,键盘竟然是坏的, ...

  10. Codefoeces 734F. Anton and School 数学

    Codefoeces 734F 题目大意: 给定两个正整数序列\(b,c\)构造一个正整数序列\(a\)使其满足 \[ \left\{ \begin{array}{} b_i=(a_i\text{ a ...