服务引入

服务引入使用reference标签来对要引入的服务进行配置,包括服务的接口 ,名称,init,check等等配置属性。

在DubboNamespaceHandler中,我们可以看到reference标签是通过引入一个ReferenceBean类型的bean实现的,那么我们就以这个bean为入口,一探dubbo服务引入的究竟。

ReferenceBean概述

首先看一下ReferenceBean的继承结构:

  • 继承了ReferenceConfig,用于存放通过配置文件或api设置的一些配置,
  • 实现了若干接口,全部都与spring框架相关,关系到bean的生命周期以及对一些spring基础设施类的感知,
  • 实现FactoryBean。说明是一个工厂bean, 我们将接口作为依赖引入到其他bean中,或者直接调用ApplicationContext.getBean方法时,会通过这个工厂bean获取一个实际类型的bean

    容易想到,这个被引入的服务的引用非获取应该与FactoryBean相关。
  • ApplicationContextAware。Aware接口,目的是为了持有spring容器的引用,以便能够获取其他的依赖的bean
  • InitializingBean。 在spring的bean被实例化后,会一次调用BeanPostProcessor.postProcessBeforeInitialization, InitializingBean.afterPropertiesSet, 自定义的初始化方法(通过init属性配置),BeanPostProcessor.postProcessAfterInitialization,所以实现了InitializingBean接口的bean在实例化时,spring框架会自动调用afterPropertiesSet方法
  • DisposableBean。 bean是一个有声明周期的实体,在spring容器关闭时会自动销毁这个bean

afterPropertiesSet

这个方法主要是做一些配置,比如初始化配置中心bean,消费者配置类ConsumerConfig,全局配置类ApplicationConfig,等等,还有一些其他的配置,大致与服务导出的过程差不多。

FactoryBean.getObject

很显然服务引入的入口就在这个方法中。

兜兜转转,期间经过几个方法调用,忽略中间涉及到的配置部分,我们来到核心方法init

ReferenceConfig.init

public synchronized void destroy() {
if (ref == null) {
return;
}
if (destroyed) {
return;
}
destroyed = true;
try {
invoker.destroy();
} catch (Throwable t) {
logger.warn("Unexpected error occured when destroy invoker of ReferenceConfig(" + url + ").", t);
}
invoker = null;
ref = null;
} private void init() {
// 用一个volatile变量标记是否已经初始化过
if (initialized) {
return;
}
// 这里还是有可能多个线程同时初始化,不如学spring, 直接加锁
initialized = true;
// 检查stub和local合法性
checkStubAndLocal(interfaceClass);
// 检查mock合法性
checkMock(interfaceClass);
// 存放参数
Map<String, String> map = new HashMap<String, String>(); // size属性设为consumer,即消费端
map.put(Constants.SIDE_KEY, Constants.CONSUMER_SIDE);
// 添加运行时的几个参数,之前在分析服务导出 的时候已经讲过
// 1. dubbo协议的版本号
// 2. dubbo框架的发行版本号,可以通过package-info或者jar包名称获取
// 3. 时间戳
// 4. 当前jvm进程号
appendRuntimeParameters(map);
// 对于非泛化服务,添加如下配置
if (!isGeneric()) {
// 修订版本号
String revision = Version.getVersion(interfaceClass, version);
if (revision != null && revision.length() > 0) {
map.put("revision", revision);
} String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
if (methods.length == 0) {
logger.warn("No method found in service interface " + interfaceClass.getName());
map.put("methods", Constants.ANY_VALUE);
} else {
map.put("methods", StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
}
}
// 添加接口名参数
map.put(Constants.INTERFACE_KEY, interfaceName);
// 接下来的几个方法与服务导出中的处理过程类似,都是按照优先级覆盖配置
appendParameters(map, application);
appendParameters(map, module);
appendParameters(map, consumer, Constants.DEFAULT_KEY);
// 最后添加自身的参数配置,即reference标签配置的参数,
// 显然这些配置应该是优先级最高的,所以最后添加以覆盖之前的配置
appendParameters(map, this);
Map<String, Object> attributes = null;
if (CollectionUtils.isNotEmpty(methods)) {
attributes = new HashMap<String, Object>();
for (MethodConfig methodConfig : methods) {
appendParameters(map, methodConfig, methodConfig.getName());
String retryKey = methodConfig.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
// 如果该方法被设置为不重试,那么添加一个参数:方法名.retries=0
if ("false".equals(retryValue)) {
map.put(methodConfig.getName() + ".retries", "0");
}
}
attributes.put(methodConfig.getName(), convertMethodConfig2AyncInfo(methodConfig));
}
} // 通过环境变量或jvm系统变量获取属性DUBBO_IP_TO_REGISTRY,即要发送给注册中心的主机ip地址
String hostToRegistry = ConfigUtils.getSystemProperty(Constants.DUBBO_IP_TO_REGISTRY);
if (StringUtils.isEmpty(hostToRegistry)) {
// 如果从环境变量或jvm系统变量没获取到,那么直接获取本地ip
// 如果获取不到本地ip,最后只有用环回地址
hostToRegistry = NetUtils.getLocalHost();
}
// 添加参数
map.put(Constants.REGISTER_IP_KEY, hostToRegistry); // 关键一步,创建代理
ref = createProxy(map); String serviceKey = URL.buildKey(interfaceName, group, version);
ApplicationModel.initConsumerModel(serviceKey, buildConsumerModel(serviceKey, attributes));
}

这个方法大致分为两块,前半部分都是在构建参数的map,最后用这些参数创建一个代理,

添加的参数包括运行时参数,版本号,方法名,按优先级分别添加全局配置,分组配置,消费端配置,以及reference标签的配置,用于注册的ip

ReferenceConfig.createProxy

private T createProxy(Map<String, String> map) {
// 首先判断是不是本地引用,
if (shouldJvmRefer(map)) {
URL url = new URL(Constants.LOCAL_PROTOCOL, Constants.LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
// 创建一个本地服务引用,通过指定的injvm协议创建
invoker = refprotocol.refer(interfaceClass, url);
if (logger.isInfoEnabled()) {
logger.info("Using injvm service " + interfaceClass.getName());
}
} else {
// 用户指定的url,可以是点对点调用,也可以指定注册中心的url
if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address.
// 可以是多个url,以分号(;)号分隔
String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
if (us != null && us.length > 0) {
for (String u : us) {
URL url = URL.valueOf(u);
if (StringUtils.isEmpty(url.getPath())) {
url = url.setPath(interfaceName);
}
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
// refer是注册中心url的参数key名称
urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
} else {
//
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else { // assemble URL from register center's configuration
checkRegistry();
// 用户指定的url,优先用指定的url
// 可以是点对点调用,也可以指定注册中心的url
List<URL> us = loadRegistries(false);
if (CollectionUtils.isNotEmpty(us)) {
for (URL u : us) {
URL monitorUrl = loadMonitor(u);
if (monitorUrl != null) {
map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
}
}
if (urls.isEmpty()) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
} if (urls.size() == 1) {
// 创建Invoker
invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url; // use last registry url
}
}
if (registryURL != null) { // registry url is available
// use RegistryAwareCluster only when register's cluster is available
URL u = registryURL.addParameter(Constants.CLUSTER_KEY, RegistryAwareCluster.NAME);
// The invoker wrap relation would be: RegistryAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, will execute route) -> Invoker
invoker = cluster.join(new StaticDirectory(u, invokers));
} else { // not a registry url, must be direct invoke.
invoker = cluster.join(new StaticDirectory(invokers));
}
}
} if (shouldCheck() && !invoker.isAvailable()) {
// make it possible for consumer to retry later if provider is temporarily unavailable
initialized = false;
throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
}
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
/**
* @since 2.7.0
* ServiceData Store
*/
MetadataReportService metadataReportService = null;
if ((metadataReportService = getMetadataReportService()) != null) {
URL consumerURL = new URL(Constants.CONSUMER_PROTOCOL, map.remove(Constants.REGISTER_IP_KEY), 0, map.get(Constants.INTERFACE_KEY), map);
metadataReportService.publishConsumer(consumerURL);
}
// create service proxy
// 重要的一步,创建代理
return (T) proxyFactory.getProxy(invoker);
}

大致分为三种情况:

  • 如果参数中指明了是本地引用,那么使用InjvmProtocol创建一个本地的Invoker
  • 如果用户在指定了url,那么优先用用户显式指定的url
  • 如果没有显式配置的url,那么就加载所有的注册中心的url

加载完url之后,调用Protocol.refer方法创建一个服务引用,即一个Invoker,

我们分析最普通的情况,即通注册中心引用服务的情况,这种情况是调用RegistryProtocol.refer方法创建Invoker

RegistryProtocol.refer

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
url = URLBuilder.from(url)
// registry属性默认是dubbo
.setProtocol(url.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY))
// 前面protocol属性被设为registry,
// 而原本的protocol属性被保存在registry属性中
// 到这里将protocol设为registry已经完成了他的使命,即将Protocol类型路由到RegistryProtocol中
// 所以这是自然要将protocol属性设回原本的值,而将registry属性丢弃
.removeParameter(REGISTRY_KEY)
.build();
// 这里根据协议决定具体使用哪种Registry
// registryFactory成员属性是通过ExtensionLoader的IOC机制自动注入的,也就是通过ExtensionFactory获取到的
// 对于带有SPI注解的接口,通过IOC方式注入的是自适应的扩展类
// 以常用的zookeeper注册中心为例,这里通过ZookeeperRegistryFactory获取到了一个ZookeeperRegistry
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
} // group="a,b" or group="*"
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
String group = qs.get(Constants.GROUP_KEY);
if (group != null && group.length() > 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
return doRefer(getMergeableCluster(), registry, type, url);
}
}
// 创建Invoker
// 这里的cluster成员属性同样也是通过ExtensionLoader的IOC自动注入的,
// 同样注入的是一个自适应的Cluster
return doRefer(cluster, registry, type, url);
}

对url进行一些处理,然后获取一个注册服务Registry对象,一般常用的有ZookeeperRegistry。

接下来是对分组信息的处理,这里由于不是很常用,我们暂时跳过。

RegistryProtocol.doRefer

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// 创建一个服务目录
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// all attributes of REFER_KEY
Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
// 订阅url
URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url));
// 注册一个消费者
registry.register(directory.getRegisteredConsumerUrl());
}
// 创建路由链
directory.buildRouterChain(subscribeUrl);
// 向注册中心订阅,订阅providers,configurators,routers三个目录的服务
// 接收注册中心的变化信息
directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY)); // 将目录封装成一个Invoker
Invoker invoker = cluster.join(directory);
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
return invoker;
}

这里首先创建了一个服务目录,然后向注册中心注册一个消费者,创建路由链,向注册中心订阅以接收服务变化的通知,

最关键的一步是cluster.join,这一步将服务目录封装成一个Invoker,我们知道从注册中心是可以获取多个服务提供者的。

  • Directory,服务目录,封装了从注册中心发现服务,并感知服务变化的逻辑
  • Cluster,这个类实际上只起到过渡的作用,通过它的join方法返回FailoverClusterInvoker等对象,这些类封装了服务调用过程中的故障转移,重试,负载均衡等逻辑

这两个接口会单独在写文章来分析,本文我们主要是为了理清服务引用的主干逻辑。

ProxyFactory.getProxy

我们回到ReferenceConfig中,通过以上的一些步骤获取到invoker之后,创建服务引用的过程并没有结束。

试想,服务引入后,用户是需要在代码中直接调用服务接口中的方法的,而Invoker只有一个invoke方法,显然,我们还需要一个代理,来使的方法调用对用户是透明的,即用户不需要感知到还有Invoker这个东西的存在。所以接下来就分析一下创建代理的过程。

ProxyFactory这个类在服务导出的部分已经接触过。服务导出时,调用ProxyFactory.getInvoker方法获取一个Invoker类,用于将发送过来的调用信息路由到接口的不同方法上。

而在服务引入的过程中,我们需要创建一个代理,将接口中的不同的方法调用转换成Invoker的invoke调用,并进一步转化为网络报文发送给服务提供者,并将返回的结果信息返回给服务调用者。

默认的ProxyFactory是JavassistProxyFactory,继承自AbstractProxyFactory,我们先从AbstractProxyFactory看起

AbstractProxyFactory.getProxy

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}

这个方法通过Proxy.getProxy生成一个Proxy类示例,然后调用Proxy实例的newInstance方法返回代理对象,我们重点分析一下Proxy.getProxy方法

Proxy.getProxy

这个方法就不贴代码了,太长,大概的逻辑是生成两个类的代码,然后调用javassist库编译加载获取Class对象,生成的这两个类一个实现了用户的服务接口的代理类,另一个继承了Proxy,用于生成代理类的实例,对于这部分代码,我认为逐字逐句第分析代码生成部分的逻辑意义不大,不如直接看一下生成后的代码长什么样子,这样能够更加直观地理解代码生成的逻辑。

示例接口:

public interface I2 {
void setName(String name); void hello(String name); int showInt(int v); float getFloat(); void setFloat(float f);
}

生成的代理类代码:

public class proxy0 implements org.apache.dubbo.common.bytecode.I2 {
public static java.lang.reflect.Method[] methods;
private java.lang.reflect.InvocationHandler handler; public proxy0(java.lang.reflect.InvocationHandler arg0) {
handler = $1;
} public float getFloat() {
Object[] args = new Object[0];
Object ret = handler.invoke(this, methods[0], args);
return ret == null ? (float) 0 : ((Float) ret).floatValue();
} public void setName(java.lang.String arg0) {
Object[] args = new Object[1];
args[0] = ($w) $1;
Object ret = handler.invoke(this, methods[1], args);
} public void setFloat(float arg0) {
Object[] args = new Object[1];
args[0] = ($w) $1;
Object ret = handler.invoke(this, methods[2], args);
} public void hello(java.lang.String arg0) {
Object[] args = new Object[1];
args[0] = ($w) $1;
Object ret = handler.invoke(this, methods[3], args);
} public int showInt(int arg0) {
Object[] args = new Object[1];
args[0] = ($w) $1;
Object ret = handler.invoke(this, methods[4], args);
return ret == null ? (int) 0 : ((Integer) ret).intValue();
}
}

生成的Proxy类代码:

public class Proxy0 extends org.apache.dubbo.common.bytecode.Proxy {
public Object newInstance(java.lang.reflect.InvocationHandler h) {
return new org.apache.dubbo.common.bytecode.proxy0($1);
}
}

当然了,上面的代码只是初步的代码,后面肯定要经过一定的处理才能编译,不过这都是javassist库的事情,通过上面生成的代码我们很容易就知道dubbo生成动态代理的逻辑。

从生成的代理类代码可以看出来,代理类缓存了接口的所有方法的Method对象,放到一个数组中,数组下标和方法是严格对应的,这样做的好处是不需要每次调用方法的时候都通过反射去获取Method对象,那样效率太低。代理类调用每个方法的逻辑其实都是一样的,都是调用了InvocationHandler.invoke方法,生成的这个代理类感觉就像是一个门面,唯一的作用就是把所有的方法调用导向invoke调用,并传递参数。

InvokerInvocationHandler.invoke

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
if ("toString".equals(methodName) && parameterTypes.length == 0) {
return invoker.toString();
}
if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
return invoker.hashCode();
}
if ("equals".equals(methodName) && parameterTypes.length == 1) {
return invoker.equals(args[0]);
} return invoker.invoke(createInvocation(method, args)).recreate();
}

这个方法的逻辑也很简单,直接调用的Invoker.invoke方法,而Invoker对象是通过构造方法传进来的。所以核心的处理逻辑还是在Invoker对象中,其他的基本都是传参,方法调用的作用。

至于createInvocation方法的逻辑就更简单了,就是把方法名,参数类型列表,调用参数等取出来,然后封装成一个RpcInvocation对象,然后用这个RpcInvocation对象作为参数调用Invoker.invoke方法。

那么Invoker对象又是怎么来的呢?是通过服务目录也就是Directory对象内部生成的,服务目录会监听注册中心,并获取服务提供者的信息,然后生成代表这些服务提供者的Invoker对象,并通过Cluster对象将多个Invoker对象封装在一起,内部实现故障转移,服务路由,负载均衡等逻辑。服务目录,集群,以及负载均衡的内容都比较多,而且模块独立性较强,所以可以分开来看这些模块的代码。

总结

这一节的主要内容是服务引用。服务引用的入口是spring配置文件中的reference标签,这个标签由ReferenceBean处理,ReferenceBean是一个FactoryBean,通过它的getObject方法获取引用,经过一些调用链,最终生成服务接口引用的核心逻辑在ReferenceConfig.init方法中。这个方法的逻辑大致分为三个部分:

  • 参数处理。init方法的大部分代码都是在进行参数的处理,包括一些缓存的逻辑,状态判断,合法性检查等等。

  • 列出所有的url,包括显示指定的url, 注册中心url,通过Protocol接口的refer方法创建Invoker对象,创建出来的Invoker对象已经是经过Cluster对象封装了故障转移,服务路由,负载均衡逻辑的了。

    Invoker对象最主要的功能实际上是封装了通信细节,包括调用参数和返回结果的序列化反序列化,创建TCP连接,发送报文等逻辑。

  • 使用上面生成的Invoker对象生成一个服务接口的代理类,生成的这个代理类负责将对接口方法的调用转化为调用内部的Invoker对象的invoke方法的调用。

    而生成代理类的逻辑封装在ProxyFactory接口中,默认使用javassist生成动态代理,但是代理类代码生成的逻辑仍然是dubbo自己实现,只是用javassist库进行代码编译加载。

    dubbo在生成动态代理是做了一些比较重要的优化:

  • 将被代理的接口的所有方法的Method对象缓存起来,存放到一个数组中,并将方法与数组下标对应起来,这样在方法调用时可以很快获取到Method对象,而不用通过反射再获取一遍Method对象,方法调用的效率大大提升。(PS: 这里我最初的理解错了,实际上jdk动态代理也是差不多的套路,将各个方法的Method对象在类加载是就缓存起来,每次方法调用时不需要再次通过反射获取Methodd对象。)

  • 所以问题是:dubbo实现的动态代理和jdk实现的动态代理有什么区别?dubbo为啥要自己实现??

dubbo源码阅读之服务引入的更多相关文章

  1. dubbo源码阅读之服务目录

    服务目录 服务目录对应的接口是Directory,这个接口里主要的方法是 List<Invoker<T>> list(Invocation invocation) throws ...

  2. dubbo源码阅读之服务导出

    dubbo服务导出 常见的使用dubbo的方式就是通过spring配置文件进行配置.例如下面这样 <?xml version="1.0" encoding="UTF ...

  3. 【Dubbo源码阅读系列】服务暴露之远程暴露

    引言 什么叫 远程暴露 ?试着想象着这么一种场景:假设我们新增了一台服务器 A,专门用于发送短信提示给指定用户.那么问题来了,我们的 Message 服务上线之后,应该如何告知调用方服务器,服务器 A ...

  4. 【Dubbo源码阅读系列】服务暴露之本地暴露

    在上一篇文章中我们介绍 Dubbo 自定义标签解析相关内容,其中我们自定义的 XML 标签 <dubbo:service /> 会被解析为 ServiceBean 对象(传送门:Dubbo ...

  5. 【Dubbo源码阅读系列】之远程服务调用(上)

    今天打算来讲一讲 Dubbo 服务远程调用.笔者在开始看 Dubbo 远程服务相关源码的时候,看的有点迷糊.后来慢慢明白 Dubbo 远程服务的调用的本质就是动态代理模式的一种实现.本地消费者无须知道 ...

  6. 【Dubbo源码阅读系列】之 Dubbo SPI 机制

    最近抽空开始了 Dubbo 源码的阅读之旅,希望可以通过写文章的方式记录和分享自己对 Dubbo 的理解.如果在本文出现一些纰漏或者错误之处,也希望大家不吝指出. Dubbo SPI 介绍 Java ...

  7. Dubbo源码学习之-服务导出

    前言 忙的时候,会埋怨学习的时间太少,缺少个人的空间,于是会争分夺秒的工作.学习.而一旦繁忙的时候过去,有时间了之后,整个人又会不自觉的陷入一种懒散的状态中,时间也显得不那么重要了,随便就可以浪费掉几 ...

  8. Dubbo源码阅读顺序

    转载: https://blog.csdn.net/heroqiang/article/details/85340958 Dubbo源码解析之配置解析篇,主要内容是<dubbo:service/ ...

  9. Dubbo源码阅读-服务导出

    Dubbo服务导出过程始于Spring容器发布刷新事件,Dubbo在接收到事件后,会立即执行服务导出逻辑.整个逻辑大致可分为三个部分,第一部分是前置工作,主要用于检查参数,组装URL.第二部分是导出服 ...

随机推荐

  1. @Conditional 和 @ConditionalOnProperty

    @ConditionalOnProperty https://blog.csdn.net/dalangzhonghangxing/article/details/78420057 @Condition ...

  2. grandle Project sync failed.please fix your project and try again

    Android Studio导入项目或者新建项目想运行的时候可能会报错Gradle project sync failed. Please fix your project and try again ...

  3. slot 插槽子组件向父组件传值

    slot 插槽要实现子组件向父组件传值,则需要运用 作用域插槽 1.父组件中用 标签加上 slot-scoped 的属性,属性值随性.(旧版本是scope,vue新版本必须用slot-scope) 2 ...

  4. Pandas进阶之DataFrame多级索引

    多级索引:在一个轴上有多个(两个以上)的索引,能够以低维度形式来表示高维度的数据.单级索引是Index对象,多级索引是MultiIndex对象. 一.创建多级索引 方法一:隐式创建,即给DataFra ...

  5. 【原创】MongoDB安装配置详解(标注两个坑)

    1.下载安装 3.4正式版([坑]不要最新版,有可能进度卡在这个位置不动,等了半个小时也没什么反映,) http://downloads.mongodb.org/win32/mongodb-win32 ...

  6. laravel composer 使用阿里云镜像

    使用composer安装错误提示: 即: [Composer\Downloader\TransportException] The "https://packagist.laravel-ch ...

  7. PAT 甲级 1077 Kuchiguse (20 分)(简单,找最大相同后缀)

    1077 Kuchiguse (20 分)   The Japanese language is notorious for its sentence ending particles. Person ...

  8. node.js生成验证码及图片

    示例代码: var svgCaptcha = require('svg-captcha'); var fs = require('fs'); var codeConfig = { size: 5,// ...

  9. [05]Go设计模式:建造者模式(Builder Pattern)

    目录 建造者模式 一.简介 二.代码 三:参考资料 建造者模式 一.简介 建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象.这种类型的设计模式属于创建型模式, ...

  10. GridView树状结构显示

    下面的树形结构代码需要GridVIew中的数据要求是按照上下级关系已经排列好的顺序,比如: GridView ID ParentID Name 1 0 父1 2 1 父1子1 3 1 父1子2 4 3 ...