Dubbo源码包介绍
当我们从github把Dubbo源码下载下来之后有如下源码包
 

下面来说明每个包的作用,以便我们有目的的阅读代码
dubbo-admin
dubbo管理平台源码包,用来管理dubbo服务的启动、禁用、降权、接口测试等,操作界面如下
 

dubbo-cluster
集群模块,将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。
dubbo-common
公共逻辑模块,包括Util类和通用模型
dubbo-config
配置模块,是Dubbo对外的API,用户通过Config使用Dubbo,隐藏Dubbo所有细节
例如:

<bean id=“xxxService” class=“com.xxx.XxxServiceImpl” /> <!-- 和本地服务一样实现远程服务 -->
<dubbo:service interface=“com.xxx.XxxService” ref=“xxxService” /> <!-- 增加暴露远程服务配置 -->
1
2
dubbo-container
容器模块,是一个Standlone的容器,以简单的Main加载Spring启动,因为服务通常不需要Tomcat/JBoss等Web容器的特性,没必要用Web容器去加载服务
dubbo-demo
dubbo服务provider和consumer例子
dubbo-filter
dubbo扩展过滤器功能,针对dubbo接口,类似Spring的AOP,经常用的并发控制、超时设置、异常设置、tps限流设置等等都是通过filter来实现的。
dubbo-monitor
监控模块,统计服务调用次数,调用时间的,调用链跟踪的服务
dubbo-registry
注册中心模块,基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。
dubbo-remoting
远程通讯模块,相当于Dubbo协议的实现,如果RPC用RMI协议则不需要使用此包
dubbo-rpc
远程调用模块,抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理

dubbo用到的设计模式
工厂模式
Java SPI
在我们分析工厂模式之前我们先了解下Java SPI(service provice interface)。SPI是JDK内置的一种服务发现机制。目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制, 举个例子来说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。

而后把新加的实现描述给JDK即可,Dubbo框架就是基于SPI机制提供扩展功能。
我们看下文件目录
 

各个类的源码如下:

package com.swk.spi;
public interface HelloInterface {
    public void sayHello();
}
package com.swk.spi.impl;
import com.swk.spi.HelloInterface;
public class ImageHello implements HelloInterface{
    @Override
    public void sayHello() {
        System.out.println("image hello");
    }
}
package com.swk.spi.impl;
import com.swk.spi.HelloInterface;
public class TextHello implements HelloInterface{
    @Override
    public void sayHello() {
        System.out.println("text hello");
    }
}
package com.swk.spi;
import java.util.ServiceLoader;
public class SPIMain {

public static void main(String[] args) {
        ServiceLoader<HelloInterface> loaders = ServiceLoader.load(HelloInterface.class);
        for(HelloInterface in:loaders){
            in.sayHello();
        }
    }
}

com.swk.spi.HelloInterface文件内容如下,添加一个实现类的全名就可以调用这个类里面的方法

com.swk.spi.impl.TextHello
com.swk.spi.impl.ImageHello

运行结果如下

text hello
image hello

下面我们看看dubbo的实现,dubbo的扩展机制和java的SPI机制非常相似,但是又增加了如下功能:
1、可以方便的获取某一个想要的扩展实现,java的SPI机制就没有提供这样的功能
2、对于扩展实现IOC依赖注入功能:
现在实现者A1含有setB()方法,会自动注入一个接口B的实现者,此时注入B1还是B2呢?都不是,而是注入一个动态生成的接口B的实现者B$Adpative,该实现者能够根据参数的不同,自动引用B1或者B2来完成相应的功能
3、对扩展采用装饰器模式进行功能增强,类似AOP实现的功能

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

我们以Protocal为例来分析下

@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的实现类有
 

ExtensionLoader中含有一个静态属性,用于缓存所有的扩展加载实例,这里加载Protocol.class,就以Protocol.class为key,创建的ExtensionLoader为value存储到上述EXTENSION_LOADERS中

public class ExtensionLoader<T> {
    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*");

private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
}

我们来看下,ExtensionLoader实例是如何来加载Protocol的实现类的:
1、先解析Protocol上的Extension注解的name,存至String cachedDefaultName属性中,作为默认的实现
2、到类路径下的加载 META-INF/services/com.alibaba.dubbo.rpc.Protocol文件
 

文件内容如下

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol

这里的

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

我们可以理解为一种工厂模式,这种实现方式的优点是可扩展性强,想要扩展实现,只需要在 classpath 下增加个文件就可以了,代码零侵入。另外,像上面的 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态代理,会造成代码调试比较麻烦,需要分析出实际调用的实现类。

装饰器模式与责任链模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构
责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型对请求的发送者和接收者进行解耦
在我们介绍工厂模式的时候我们也提到了装饰器模式,Dubbo 在启动和调用阶段都大量使用了装饰器模式。
以 Provider 提供的调用链为例,具体的调用链代码是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具体是将注解中含
有 group=provider 的 Filter 实现,按照 order 排序,最后的调用顺序是查看文本打印
EchoFilter-》 ClassLoaderFilter-》 GenericFilter-》 ContextFilter-》 ExceptionFilter-》TimeoutFilter-》 MonitorFilter-》 TraceFilter。更确切地说,这里是装饰器和责任链模式的混合使用。例如, EchoFilter 的作用是判断是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现,源码如下:

/**
 * EchoInvokerFilter
 *
 * @author william.liangf
 */
@Activate(group = Constants.PROVIDER, order = -110000)
public class EchoFilter implements Filter {
    public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
        // 是否是回声测试请求
        if(inv.getMethodName().equals(Constants.$ECHO) && inv.getArguments() != null && inv.getArguments().length == 1 )
            return new RpcResult(inv.getArguments()[0]);
        return invoker.invoke(inv);
    }
}

而像 ClassLoaderFilter则只是在主功能上添加了功能,更改当前线程的 ClassLoader,这是典型的装饰器模式,源码如下

/**
 * ClassLoaderInvokerFilter
 *
 * @author william.liangf
 */
@Activate(group = Constants.PROVIDER, order = -30000)
public class ClassLoaderFilter implements Filter {
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        ClassLoader ocl = Thread.currentThread().getContextClassLoader();
        // 在主功能上添加了功能,更改当前线程的 ClassLoader
        Thread.currentThread().setContextClassLoader(invoker.getInterface().getClassLoader());
        try {
            return invoker.invoke(invocation);
        } finally {
            Thread.currentThread().setContextClassLoader(ocl);
        }
    }
}

观察者模式
当一个对象被修改时,则会自动通知它的依赖对象
在dubbo provider服务启动时候要向注册中心注册自己的服务,在dubbo consumer向注册中心订阅服务时则是一种观察者模式,他开启了一个listener,注册中心会每 5 秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个 notify 消息, provider 接受到 notify 消息后,即运行 NotifyListener 的 notify 方法,执行监听器方法。

public interface NotifyListener {
    /**
     * 当收到服务变更通知时触发。
     *
     * 通知需处理契约:<br>
     * 1. 总是以服务接口和数据类型为维度全量通知,即不会通知一个服务的同类型的部分数据,用户不需要对比上一次通知结果。<br>
     * 2. 订阅时的第一次通知,必须是一个服务的所有类型数据的全量通知。<br>
     * 3. 中途变更时,允许不同类型的数据分开通知,比如:providers, consumers, routers, overrides,允许只通知其中一种类型,但该类型的数据必须是全量的,不是增量的。<br>
     * 4. 如果一种类型的数据为空,需通知一个empty协议并带category参数的标识性URL数据。<br>
     * 5. 通知者(即注册中心实现)需保证通知的顺序,比如:单线程推送,队列串行化,带版本对比。<br>
     *
     * @param urls 已注册信息列表,总不为空,含义同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值。
     */
    void notify(List<URL> urls);
}

动态代理模式
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能
Dubbo 扩展 jdk spi 的类 ExtensionLoader 的 Adaptive 实现是典型的动态代理实现。 Dubbo需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的createAdaptiveExtensionClassCode 方法。代理类的主要逻辑是,获取 URL 参数中指定参数的值作为获取实现类的 key。

/**
     * 根据URL传参获取指定实现类的key
     * 2017年6月4日 上午10:20:25
     * @return
     */
    private String createAdaptiveExtensionClassCode() {
        StringBuilder codeBuidler = new StringBuilder();
        // 根据反射获取方法集合
        Method[] methods = type.getMethods();
        boolean hasAdaptiveAnnotation = false;
        for(Method m : methods) {
            // 判断每个方法是否使用了Adaptive注解
            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的注解信息
            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) {
                    // 如果是URL类型
                    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();
    }
---------------------
作者:孙_悟_空
来源:CSDN
原文:https://blog.csdn.net/fuyuwei2015/article/details/72857722
版权声明:本文为博主原创文章,转载请附上博文链接!

精通Dubbo——dubbo2.0源码中的设计模式与SPI介绍的更多相关文章

  1. Solr4.8.0源码分析(7)之Solr SPI

    Solr4.8.0源码分析(7)之Solr SPI 查看Solr源码时候会发现,每一个package都会由对应的resources. 如下图所示: 一时对这玩意好奇了,看了文档以后才发现,这个serv ...

  2. 访何红辉:谈谈Android源码中的设计模式

    最近Android 6.0版本的源代码开放下载,刚好分析Android源码的技术书籍<Android源码设计模式解析与实战>上市,我们邀请到它的作者何红辉,来谈谈Android源码中的设计 ...

  3. 从Android4.0源码中提取的截图实现(在当前activity中有效,不能全局截图)

    原文:http://blog.csdn.net/xu_fu/article/details/39268771 从这个大神的博客看到了这篇文章,感觉写的挺好的.挺实用的功能.虽然是从源码中提取的,但是看 ...

  4. Android 源码中的设计模式

    最近看了一些android的源码,发现设计模式无处不在啊!感觉有点乱,于是决定要把设计模式好好梳理一下,于是有了这篇文章. 面向对象的六大原则 单一职责原则 所谓职责是指类变化的原因.如果一个类有多于 ...

  5. 【Spark2.0源码学习】-3.Endpoint模型介绍

         Spark作为分布式计算框架,多个节点的设计与相互通信模式是其重要的组成部分.   一.组件概览      对源码分析,对于设计思路理解如下:            RpcEndpoint: ...

  6. 《Prism 5.0源码走读》 设计模式

    Prism或Prism构建的应用程序时会使用大量的设计模式,本文简要列举Prism相关的那些设计模式. Adapter(适配器模式):Prism Library主要在Region和IoC contai ...

  7. Solr4.8.0源码分析(9)之Lucene的索引文件(2)

    Solr4.8.0源码分析(9)之Lucene的索引文件(2) 一. Segments_N文件 一个索引对应一个目录,索引文件都存放在目录里面.Solr的索引文件存放在Solr/Home下的core/ ...

  8. 【Spark2.0源码学习】-1.概述

          Spark作为当前主流的分布式计算框架,其高效性.通用性.易用性使其得到广泛的关注,本系列博客不会介绍其原理.安装与使用相关知识,将会从源码角度进行深度分析,理解其背后的设计精髓,以便后续 ...

  9. Android 图片加载框架Glide4.0源码完全解析(二)

    写在之前 上一篇博文写的是Android 图片加载框架Glide4.0源码完全解析(一),主要分析了Glide4.0源码中的with方法和load方法,原本打算是一起发布的,但是由于into方法复杂性 ...

随机推荐

  1. 抽象类能使用 final 修饰吗?(未完成)

    抽象类能使用 final 修饰吗?(未完成)

  2. 配置linux ftp服务器,客户端报list remote folder fail

    XFTP出现列表文件夹失败.主要是因为FTP模式不对,应该为主动连接模式, 可以在设置主机属性 - 选项页签 - 将默认勾选的“使用被动模式”(使用消极模式)的多选框取消...就可以了...

  3. Aizu - 1382 Black or White (分段决策,单调队列优化dp)

    题意:给定只有黑白两种颜色的序列A,B,每次可以选择一段连续的长度不超过k的区间将其染成同一种颜色,求把序列A变成B所需的最小操作次数. 首先需要找出一些最优解的特征: 1.如果序列A的第一个颜色和B ...

  4. [Svelte 3] Use an onMount lifecycle method to fetch and render data in Svelte 3

    Every Svelte component has a lifecycle that starts when it is created, and ends when it is destroyed ...

  5. Python模块之目录

     1.加密算法有关 hmac模块 hashlib模块 2.进程有关 multiprocessing模块 3.线程有关 threading模块 4.协程有关 asyncio模块 5.系统命令调用 sub ...

  6. istio 安装与bookinfo示例运行

    目的 本文旨在帮助想了解istio安装和运行bookinfo示例的同学快速入门 前置准备 安装k8s和helm 1.k8s安装 修改主机名 hostnamectl set-hostname k8s-m ...

  7. 逆向bfs搜索打表+康拓判重

    HDU 1043八数码问题 八数码,就是1~8加上一个空格的九宫格,这道题以及这个游戏的目标就是把九宫格还原到从左到右从上到下是1~8然后最后是空格. 没了解康托展开之前,这道题怎么想都觉得很棘手,直 ...

  8. plotly绘图

    import plotly.plotly as plt import plotly.offline as pltoff from plotly.graph_objs import * # 生成折线图 ...

  9. 说说如何使用unity Vs来进行断点调试

    转载自:http://dong2008hong.blog.163.com/blog/static/4696882720140293549365/ 大家可以从这下载最新版的unity vs. Unity ...

  10. 简述python中的@staticmethod作用及用法

    关于@staticmethod,这里抛开修饰器的概念不谈,只简单谈它的作用和用法. staticmethod用于修饰类中的方法,使其可以在不创建类实例的情况下调用方法,这样做的好处是执行效率比较高.当 ...