我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Apache Dubbo的SPI机制是如何实现的

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Apache Dubbo的SPI机制是如何实现的

一、SPI

1.1 JDK自带SPI实现

从JDK1.6开始引入SPI机制后,可以看到很多使用SPI的案例,比如最常见的数据库驱动实现,在JDK中只定义了java.sql.Driver的接口,具体实现由各数据库厂商来提供。下面一个简单的例子来快速了解下Java SPI的使用方式:

1)定义一个接口


package com.vivo.study
public interface Car {
void getPrice();
}

2)接口实现


package com.vivo.study.impl
 

public class AudiCar implements Car {
    @Override
    public void getPrice() {
        System.out.println("Audi A6L's price is  500000 RMB.");
    }
}
 
package com.vivo.study.impl

public class BydCar implements Car {
    @Override
    public void getPrice() {
        System.out.println("BYD han's price is 220000 RMB.");
    }
}

3)挂载扩展类信息

在META-INF/services目录下以接口全名为文件名的文本文件,对应此处即在META-INF/services目录下创建一个文件名为com.vivo.study.Car的文件,文件内容如下:


com.vivo.study.impl.AudiCar
com.vivo.study.impl.BydCar

4)使用


public class SpiDemo {
public static void main(String[] args) {
        ServiceLoader<Car> load = ServiceLoader.load(Car.class);
        Iterator<Car> iterator = load.iterator();
while (iterator.hasNext()) {
            Car car = iterator.next();
            car.getPrice();
        }
    }
}

上面的例子简单的介绍了JDK SPI机制的使用方式,其中最关键的类为ServiceLoader,通过ServiceLoader类来加载接口的实现类,ServiceLoader是Iterable接口的实现类,对于ServiceLoader加载的详细过程此处不展开。

JDK对SPI的加载实现存在一个较为突出的小缺点,无法按需加载实现类,通过ServiceLoader.load加载时会将文件中的所有实现都进行实例化,如果想要获取具体某个具体的实现类需要进行遍历判断。

1.2 Dubbo SPI

SPI扩展是Dubbo的最大的优点之一,支持协议扩展、调用拦截扩展、引用监听扩展等等。在Dubbo中,根据不同的扩展定位,扩展文件分别被放置在META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/这三个路径下。

Dubbo中有直接使用JDK SPI实现的方式,比如org.apache.dubbo.common.extension.LoadingStrategy放在META-INF/services/路径下,但大多情况下都是使用其自身对JDK SPI的实现的一种优化方式,可称为Dubbo SPI,也就是本文要讲解的点。

相比于JDK的SPI的实现,Dubbo SPI具有以下特点:

配置形式更灵活:支持以key:value的形式在文件里配置类似name:xxx.xxx.xxx.xx,后续可以通过name来进行扩展类按需精准获取。

缓存的使用:使用缓存提升性能,保证一个扩展实现类至多会加载一次。

对扩展类细分扩展:支持扩展点自动包装(Wrapper)、扩展点自动装配、扩展点自适应(@Adaptive)、扩展点自动激活(@Activate)。

Dubbo对扩展点的加载主要由ExtensionLoader这个类展开。

二、加载-ExtensionLoader

ExtensionLoader在Dubbo里的角色类似ServiceLoader在JDK中的存在,用于加载扩展类。在Dubbo源码里,随处都可以见到ExtensionLoader的身影,比如在服务暴露里的关键类ServiceConfig中等,弄清楚ExtensionLoader的实现细节对阅读Dubbo源码有很大的帮助。

2.1 获取ExtensionLoader的实例

ExtensionLoader没有提供共有的构造函数,

只能通过ExtensionLoader.getExtensionLoader(Class type)来获取ExtensionLoader实例。


public
 
    // ConcurrentHashMap缓存,key -> Class value -> ExtensionLoader实例
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);
 
    private ExtensionLoader(Class<?> type) {
        this.type = type;
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
 
    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 an interface!");
        }
 
        // 检查接口是否是被@SPI注解修饰,如果没有则抛出异常
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }
 
        // 从缓存里取,没有则初始化一个并放入缓存EXTENSION_LOADERS中
        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;
    }
}

上面的代码展示了获取ExtensionLoader实例的过程,可以看出,每一个被@SPI修饰的接口都会对应同一个ExtensionLoader实例,且对应ExtensionLoader只会被初始化一次,并缓存在ConcurresntHashMap中。

2.2 加载扩展类

加载扩展类入口,当使用ExtensionLoader时,getExtensionName、getActivateExtension或是getDefaultExtension都要经过getExtensionClasses方法来加载扩展类,如下图;

getExtensionClasses方法调用的路径如下图,getExtensionClasses是加载扩展类的一个起点,会首先从缓存中获取,如果缓存中没有则通过loadExtensionClasses方法来加载扩展类,所以说实际上的加载逻辑入口在loadExtensionClasses。


getExtensionClasses
  |->loadExtensionClasses
    |->cacheDefaultExtensionName
    |->loadDirectory
      |->loadResource
        |->loadClass

2.2.1 loadExtensionClasses加载扩展类

由于整个加载过程设计的源码较多,因此用一个流程图来进行描述,具体细节可以结合源码进行查看。

loadExtensionClasses主要做了以下这几件事:

默认扩展名:

抽取默认扩展实现名并缓存在ExtensionLoader里的cachedDefaultName,默认扩展名配置通过@SPI注解在接口上配置,如配置@SPI("defaultName")则默认扩展名为defaultName。

加载扩展类信息:

从META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/这三个路径下寻找以类的全路径名命名的文件,并逐行读取文件里的内容。

加载class并缓存:

对扩展类分为自适应扩展实现类(被@Adaptive修饰的实现类)、包装类(拥有一个只有一个为这个接口类型的参数的构造方法)、普通扩展类,其中普通扩展类中又包含自动激活扩展类(被@Activate修饰的类)和真普通的类,对自适应扩展实现类、包装类、自动激活扩展类这三种类型的类分别加载并分别缓存到cachedAdaptiveClass、cachedActivates、cachedWrapperClasses。

返回结果Map<String, Class<?>>:

结果返回Map,其中key对应扩展文件里配置的name,value对应扩展的类class,最后在getExtensionClasses方法里会将此结果放入缓存cachedClasses中。此结果Map中包含除了自适应扩展实现类和包装实现类的其他所用的扩展类名与对应类的映射关系。

通过loadExtensionClasses方法把扩展类(Class对象)都加载到相应的缓存中,是为了方便后面实例化扩展类对象,通过newInstance()等方法来实例化。

2.2.2扩展包装类

什么是扩展包装类?是不是类名结尾包含了Wrapper的类就是扩展包装类?

在Dubbo SPI接口的实现扩展类中,如果此类包含一个此接口作为参数的构造方法,则为扩展包装类。扩展包装类的作用是持有具体的扩展实现类,可以一层一层的被包装,作用类似AOP。

包装扩展类的作用是类似AOP,方便扩展增强。具体实现代码如下:

从代码中可以得出,可以通过boolean wrap选择是否使用包装类,默认情况下为true;如果有扩展包装类,实际的实现类会被包装类按一定的顺序一层一层包起来。

如Protocol的实现类ProtocolFilterWrapper、ProtocolListenerWrapper都是扩展包装类。

2.2.3自适应扩展实现类

2.2.3.1 @Adaptive


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
String[] value() default {};
}

从源码以及源码注释中可以得出以下几点:

Adaptive是一个注解,可以修饰类(接口,枚举)和方法。

此注解的作用是为ExtensionLoader注入扩展实例提供有用的信息。

从注释中理解value的作用:

value可以决定选择使用具体的扩展类。

通过value配置的值key,在修饰的方法的入参org.apache.dubbo.common.URL中通过key获取到对应的值value,根据value作为extensionName去决定使用对应的扩展类。

如果通过2没有找到对应的扩展,会选择默认的扩展类,通过@SPI配置默认扩展类。

2.2.3.2 @Adaptive简单例子
由于@Adaptive修饰类时比较好理解,这里举一个@Adaptive修饰方法的例子,使用@Adaptive修饰方法的这种情况在Dubbo也是随处可见。



@SPI("impl1")
public interface SimpleExt {
    @Adaptive({"key1", "key2"})
    String yell(URL url, String s);
}

如果调用

ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension().yell(url, s)方法,最终调用哪一个扩展类的实例去执行yell方法的流程大致为:先获取扩展类的名称extName(对应上面说的name:class中的name),然后通过extName来获取对应的类Class,再实例化进行调用。所以关键的步骤在怎么得到extName,上面的这个例子得到extName的流程为:

通过url.getParameters.get("key1")获取,

没有获取到则用url.getParameters.get("key2"),如果还是没有获取到则使用impl1对应的实现类,最后还是没有获取到则抛异常IllegalStateException。

可以看出,@Adaptive的好处就是可以通过方法入参决定具体调用哪一个实现类。下面会对@Adaptive的具体实现进行详细分析。

2.2.3.3 @Adaptive加载流程

流程关键点说明:

1)黄色标记的,cachedAdaptiveClass是在ExtensionLoader#loadClass方法中加载Extension类时缓存的。

2)绿色标记的,如果Extension类中存在被@Adaptive修饰的类时会使用该类来初始化实例。

3)红色标记的,如果Extension类中不存在被@Adaptive修饰的类时,则需要动态生成代码,通过javassist(默认)来编译生成Xxxx$Adaptive类来实例化。

4)实例化后通过injectExtension来将Adaptive实例的Extension注入(属性注入)。

后续围绕上述的关键点3详细展开,关键点4此处不展开。

动态生成Xxx$Adaptive类:下面的代码为动态生成Adaptive类的相关代码,具体生成代码的细节在AdaptiveClassCodeGenerator#generate中


public class ExtensionLoader<T> {
// ...
private Class<?> getAdaptiveExtensionClass() {
// 根据对应的SPI文件加载扩展类并缓存,细节此处不展开
        getExtensionClasses();
// 如果存在被@Adaptive修饰的类则直接返回此类
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
        }
// 动态生成Xxxx$Adaptive类
return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }
private Class<?> createAdaptiveExtensionClass() {
// 生成Xxxx$Adaptive类代码,可自行加日志或断点查看生成的代码
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        ClassLoader classLoader = findClassLoader();
// 获取动态编译器,默认为javassist
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
    }
}

AdaptiveClassCodeGenerator#generate生成code的方式是通过字符串拼接,大量使用String.format,整个代码过程比较繁琐,可通过debug去了解细节。

最关键的的部分是生成被@Adaptive修饰的方法的内容,也就是最终调用实例的@Adaptive方法时,可通过参数来动态选择具体使用哪个扩展实例。下面对此部分进行分析:


public class AdaptiveClassCodeGenerator {
// ...

private String generateMethodContent(Method method) {
// 获取方法上的@Adaptive注解
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
// 方法时没有@Adaptive注解,生成不支持的代码
return generateUnsupported(method);
        } else {
// 方法参数里URL是第几个参数,不存在则为-1
int urlTypeIndex = getUrlTypeIndex(method);
// found parameter in URL type
if (urlTypeIndex != -1) {
// Null Point check
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
// did not find parameter in URL type
                code.append(generateUrlAssignmentIndirectly(method));
            }
// 获取方法上@Adaptive配置的value
// 比如 @Adaptive({"key1","key2"}),则会返回String数组{"key1","key2"}
// 如果@Adaptive没有配置value,则会根据简写接口名按驼峰用.分割,比如SimpleExt对应simple.ext
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
// 参数里是否存在org.apache.dubbo.rpc.Invocation
            boolean hasInvocation = hasInvocationArgument(method);
            code.append(generateInvocationArgumentNullCheck(method));
// 生成String extName = xxx;的代码 ,extName用于获取具体的Extension实例
            code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
            code.append(generateExtNameNullCheck(value));
            code.append(generateExtensionAssignment());
// return statement
            code.append(generateReturnAndInvocation(method));
        }
return code.toString();
    }
}

上述生成Adaptive类的方法内容中最关键的步骤在生成extName的部分,也就是generateExtNameAssignment(value,hasInvocation),此方法if太多了(有点眼花缭乱)。

以下列举几个例子对此方法的实现流程进行简单展示:假设方法中的参数不包含org.apache.dubbo.rpc.Invocation,包含org.apache.dubbo.rpc.Invocation的情况会更加复杂。

1)方法被@Adaptive修饰,没有配置value,且在接口@SPI上配置了默认的实现


@SPI("impl1")
public interface SimpleExt {
@Adaptive
String echo(URL url, String s);
}

对应生成extName的代码为:


String extName = url.getParameter("simple.ext", "impl1")

2)方法被@Adaptive修饰,没有配置value,且在接口@SPI上没有配置默认的实现


@SPI("impl1")
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

对应生成extName的代码为:


String extName = url.getParameter( "simple.ext")

3)方法被@Adaptive修饰,配置了value(假设两个,依次类推),且在接口@SPI上配置了默认的实现


@SPI
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

对应生成extName的代码为:


String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));

4)方法被@Adaptive修饰,配置了value(假设两个,依次类推),且在接口@SPI没有配置默认的实现


@SPI
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

对应生成extName的代码为:


String extName = url.getParameter("key1", url.getParameter("key2"));

完整的生成类可参见附录。

2.2.4自动激活扩展类

如果你有扩展实现过Dubbo的Filter,那么一定会对@Activate很熟悉。@Activate注解的作用是可以通过给定的条件来自动激活扩展实现类,通过ExtensionLoader#getActivateExtension(URL,String, String)方法可以找到指定条件下需要激活的扩展类列表。

下面以一个例子来对@Activate的作用进行说明,在Consumer调用Dubbo接口时,会经过消费方的过滤器链以及提供方的过滤器链,在Provider暴露服务的过程中会拼接需要使用哪些Filter。

对应源码中的位置在ProtocolFilterWrapper#buildInvokerChain(invoker, key, group)方法中。


// export:key-> service.filter ; group-> provider
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
    // 在Provider暴露服务服务export时,会根据获取Url中的service.filter对应的值和group=provider来获取激活对应的Filter
    List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
}

ExtensionLoader#getActivateExtension(URL, String, String)是怎么根据条件来自动激活对应的扩展类列表的可以自行查看该方法的代码,此处不展开。

三、总结

本文主要对Dubbo SPI机制的扩展类加载过程通过ExtensionLoader类源码来进行总结,可以概况为以下几点:

1.Dubbo SPI结合了JDK SPI的实现,并在此基础上进行优化,如精准按需加载扩展类、缓存提升性能。

2.分析ExtensionLoader加载扩展类的过程,加载META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/这三个路径下的文件,并分类缓存在ExtensionLoader实例。

3.介绍扩展包装类及其实现过程,扩展包装类实现了类似AOP的功能。

4.自适应扩展类,分析@Adptive修饰方法时动态生成Xxx$Adaptive类的过程,以及通过参数自适应选择扩展实现类完成方法调用的案例介绍。

简单介绍自动激活扩展类及@Activate的作用。

四、附录

4.1 Xxx$Adaptive完整案例

@SPI接口定义


@SPI("impl1")
public interface SimpleExt {
    // @Adaptive example, do not specify a explicit key.
    @Adaptive
    String echo(URL url, String s);
 
    @Adaptive({"key1", "key2"})
    String yell(URL url, String s);
 
    // no @Adaptive
    String bang(URL url, int i);
}

生成的Adaptive类代码


package org.apache.dubbo.common.extension.ext1;
  
import org.apache.dubbo.common.extension.ExtensionLoader;
  
public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {
  
    public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");
        org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
        return extension.yell(arg0, arg1);
    }
  
    public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("simple.ext", "impl1");
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");
        org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
        return extension.echo(arg0, arg1);
    }
  
    public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {
        throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!");
    }
  
}

以上就是Apache Dubbo的SPI机制是如何实现的的详细内容,更多关于Apache Dubbo的SPI机制的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Apache Dubbo的SPI机制是如何实现的

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

如何理解Java中的SPI机制

本篇内容介绍了“如何理解Java中的SPI机制”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!SPI的概念SPI在Java中的全称为Servi
2023-06-15

java反射机制是如何实现的

Java反射机制是通过Java的`java.lang.reflect`包中的类和接口来实现的。该包中包含了一些重要的类,如`Class`、`Method`、`Field`等,它们提供了对类的结构、方法和字段的访问和操作。Java反射机制的实
2023-10-08

Apache如何实现基于IP的访问控制

Apache可以通过使用mod_authz_host模块来实现基于IP的访问控制。您可以通过编辑Apache的配置文件来配置基于IP的访问控制规则。以下是一个示例配置,只允许特定IP地址范围内的用户访问您的网站:
Apache如何实现基于IP的访问控制
2024-07-05

Apache如何实现基于组的访问控制

Apache可以通过使用基于组的访问控制模块来实现基于组的访问控制。其中,最常用的模块是mod_authz_groupfile模块和mod_authz_dbd模块。mod_authz_groupfile模块允许管理员使用一个简单的文本文件来
Apache如何实现基于组的访问控制
2024-07-05

python的import机制如何实现

本篇内容主要讲解“python的import机制如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“python的import机制如何实现”吧!import 机制功能Python 的 impo
2023-06-30

使用spring-boot如何实现整合dubbo中的Spring-boot-dubbo-starter

使用spring-boot如何实现整合dubbo中的Spring-boot-dubbo-starter?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。在application.p
2023-05-31

Apache如何实现基于用户名的访问控制

Apache可以通过使用htpasswd文件和.htaccess文件来实现基于用户名的访问控制。创建htpasswd文件:首先要创建一个包含用户名和密码的htpasswd文件。可以使用htpasswd命令行工具来创建该文件。例如,运行以下命
Apache如何实现基于用户名的访问控制
2024-07-05

java的反射机制如何实现

Java的反射机制是指在运行时动态获取和操作类的相关信息的能力。通过反射,可以在运行时获取类的属性、方法、构造函数等信息,还可以动态调用方法、获取和设置属性的值。Java的反射机制是通过`java.lang.reflect`包中的类和接口来
2023-08-31

Flume的内部机制是如何实现数据流动的

Flume是一个分布式,可靠,高可用的系统,用于收集,聚合和移动大量的日志数据。它的内部机制是基于三个主要组件:Source,Channel和Sink。Source:Source是Flume的数据输入端,负责从不同的数据源收集数据,并将数据
Flume的内部机制是如何实现数据流动的
2024-03-08

ahooks控制时机的hook如何实现

本篇内容主要讲解“ahooks控制时机的hook如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“ahooks控制时机的hook如何实现”吧!Function Component VS Cl
2023-07-02

如何理解Spring AOP的实现机制

这篇文章将为大家详细讲解有关如何理解Spring AOP的实现机制,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。AOP(Aspect Orient Programming),一般称为面向切面
2023-06-16

如何实现Redis的LRU缓存机制

这篇文章给大家分享的是有关如何实现Redis的LRU缓存机制的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。前言最近在逛博客的时候看到了有关Redis方面的面试题,其中提到了Redis在内存达到最大限制的时候会使用
2023-06-14

Apache的负载均衡如何实现

Apache的负载均衡可以通过以下几种常见的方式来实现:使用Apache的内置模块mod_proxy和mod_proxy_balancer来实现负载均衡。mod_proxy_balancer模块可以将请求分发到多个后端服务器上,实现负载均衡
Apache的负载均衡如何实现
2024-07-05

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录