Dubbo SPI源码

news2024/11/15 17:25:55

文章目录

    • Dubbo SPI
    • 使用方式
    • AOP功能
    • 源码剖析
      • @SPI注解
      • 1.获取加载器
      • 2.获取拓展实例对象
      • 3.创建拓展类的实例对象

Dubbo SPI

Dubbo 的 SPI(Service Provider Interface)机制是一种强大的扩展机制,它允许开发者在运行时动态地替换或增加框架的功能。Dubbo 的 SPI 机制与 Java 原生的 SPI 机制有所不同,它提供了更多的灵活性和功能。

在这里插入图片描述SPI机制的核心组件包括:

  • 服务接口:接口定义了服务提供者需要实现的方法,应用程序将使用这个接口与具体的服务实现进行交互。

  • 服务实现:这是实现了服务接口的具体类,第三方可以为服务接口提供多个实现。

  • 服务提供者配置文件:这是一个位于META-INF/dubbo目录下的文件,文件名与服务接口的全限定名相同,该文件包含了服务实现类的全限定名,每行一个接口的具体实现类,在运行时就可以加载这些实现类。

  • ServiceLoader:用于加载服务实现,应用程序可以使用ServiceLoader来获取服务接口的所有具体实现类。

SPI的工作流程如下:

  • 定义服务接口。
  • 实现服务接口,创建具体的服务实现类。
  • 在META-INF/dubbo目录下创建服务提供者配置文件,列出所有服务实现类的全限定名。
  • 使用ServiceLoader加载服务具体实现类,并根据需要使用它们。

总结就是说SPI机制使得应用程序可以在运行时动态地选择和加载服务实现,从而提高了应用程序的可扩展性和灵活性。

使用方式

使用方式和 java的SPI类似,首先通过@SPI注解定义服务接口

@SPI
public interface Greeting {
    public void sayHello();
}

再定义服务实现类实现接口并重写接口中的方法

public class ChineseGreeting implements Greeting {
    @Override
    public void sayHello() {
        System.out.println("你好,世界!");
    }
}
public class EnglishGreeting implements Greeting {
    @Override
    public void sayHello() {
        System.out.println("Hello World!");
    }
}

在META-INF/dubbo目录下创建一个名为com.xydp.dubbo.Greeting的文件,用于存储自定义键名(这里与java的 SPI 不同,需要自定义key的名称,键名随意,主要是为了实现后面按需加载)与具体实现类的全限定名。文件内容如下:
在这里插入图片描述
文件内容

english=com.xydp.dubbo.EnglishGreeting
chinese=com.xydp.dubbo.ChineseGreeting

编写测试类

public class SpiDemo {
    public static void main(String[] args) {
        ExtensionLoader<Greeting> extensionLoader = ExtensionLoader.getExtensionLoader(Greeting.class);
         Greeting englishGreeting = extensionLoader.getExtension("english");
        englishGreeting.sayHello();
        System.out.println(englishGreeting.getClass());
    }
}

输出结果
在这里插入图片描述
Dubbo的 SPI 通过自定义键名的方式按需加载,可以在O(1)的时间复杂度获取具体的服务实现类,而java SPI 需要通过迭代器的方式全局遍历获取某个具体实现类,达到线性时间复杂度O(n)。

Dubbo SPI与Java SPI区别

  • 设计理念:Java SPI 主要关注于服务实现的加载,而 Dubbo SPI 更注重于框架的可扩展性和灵活性。

  • 功能丰富度:Dubbo SPI 提供了更多的功能,如自适应扩展、激活扩展和依赖注入等,而 Java SPI 功能相对有限。

  • 加载方式:Java SPI 采用全局加载的方式,加载特定的实现类时间复杂度达到O(n),性能差,而 Dubbo SPI 采用按需加载的方式,时间复杂度只需O(1),提高了性能。

  • 配置方式:Dubbo SPI 支持通过注解和 URL 参数进行动态配置,使得框架更加灵活;Java SPI 主要通过配置文件进行静态配置。

总之,Java SPI 和 Dubbo SPI 都是用于实现服务发现和实现加载的机制,但 Dubbo SPI 在设计理念、功能和用法上更加灵活和强大,Dubbo SPI 更适合用于构建复杂的分布式系统,而 Java SPI 更适合用于简单的服务加载场景。

AOP功能

在 Dubbo 中,实现 AOP 功能的方式是通过自定义 Wrapper 类实现的,Dubbo 要求实现 AOP 功能的类以 Wrapper 结尾,这是一种约定,以便于识别这些类是用于包装服务实现的,Wrapper 类是 Dubbo 框架的一部分,用于在运行时动态地为服务实现类添加额外的功能,原理和Spring AOP类似,将通知织入所要执行的目标方法前后。

public class GreetingWrapper1 implements  Greeting{
    private Greeting greeting;
    public GreetingWrapper1(Greeting greeting){
        this.greeting = greeting;
    }


    @Override
    public void sayHello() {
        System.out.println("before do someting");
        this.greeting.sayHello();
        System.out.println("after do something");
    }
}

同时需要在配置文件中添加包装类的全限定名
在这里插入图片描述

测试类保持不变

public class SpiDemo {
    public static void main(String[] args) {
        ExtensionLoader<Greeting> extensionLoader = ExtensionLoader.getExtensionLoader(Greeting.class);
         Greeting englishGreeting = extensionLoader.getExtension("english");
        englishGreeting.sayHello();
        System.out.println(englishGreeting.getClass());
    }
}

输出结果
在这里插入图片描述
在这里插入图片描述

从结果可以看出最终执行的是GreetingWrapper1中greeting的sayHello()方法,GreetingWrapper1在这里充当EnglishGreeting的代理对象。

源码剖析

@SPI注解

在 Dubbo 的 SPI 机制中,@SPI 注解用于标记一个接口为可扩展的扩展点,@SPI 注解有两个可选参数:value 和 scope。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
    String value() default "";

    ExtensionScope scope() default ExtensionScope.APPLICATION;
}

value 参数用于指定扩展点的默认实现类,当没有其他扩展实现类被明确指定时,Dubbo 会使用 value 参数指定的key,表示从配置文件中查找对应的实现类。
scope 参数指定扩展实现类的作用域,有以下四种作用域

  • Constants.FRAMEWORK(框架作用域):在Dubbo框架内,实现类只会生成唯一实例,并在整个应用程序内共享。

  • Constants.APPLICATION(应用程序作用域):在应用程序上下文中,实现类仅会被实例化一次,并在整个应用程序中共享,是默认的作用域。

  • Constants.MODULE(模块作用域):在模块上下文中,该实现类将仅创建一个实例,并在该模块内共享。

  • Constants.SELF(自定义作用域):用户可定义实现类的作用范围,涵盖任意范围。

1.获取加载器

当执行获取拓展加载器这行代码时

 ExtensionLoader<Greeting> extensionLoader = ExtensionLoader.getExtensionLoader(Greeting.class);

源码如下

public <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        //检查拓展目录是否被删除
        this.checkDestroyed();
        //校验类型是否为null
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        } else if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        } else if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        } else {
        //如果类型不为null,尝试从缓存中获取加载器loader以及作用域
            ExtensionLoader<T> loader = (ExtensionLoader)this.extensionLoadersMap.get(type);
            ExtensionScope scope = (ExtensionScope)this.extensionScopeMap.get(type);
            //如果缓存中不存在scope,就从SPI注解获取scope,再放入缓存
            if (scope == null) {
                SPI annotation = (SPI)type.getAnnotation(SPI.class);
                scope = annotation.scope();
                this.extensionScopeMap.put(type, scope);
            }
						//如果加载器为null且作用域是SELF,就直接创建loader
            if (loader == null && scope == ExtensionScope.SELF) {
                loader = this.createExtensionLoader0(type);
            }
					//若lader为空,作用域不是SELF且父类加载器不为空,那么尝试从父类加载器去获取loader
            if (loader == null && this.parent != null) {
                loader = this.parent.getExtensionLoader(type);
            }
					//若无法从父类加载器获取loader,那么自己实例化一个loader并放入缓存
            if (loader == null) {
                loader = this.createExtensionLoader(type);
            }
					//返回加载器
            return loader;
        }
    }
    private <T> ExtensionLoader<T> createExtensionLoader(Class<T> type) {
        ExtensionLoader<T> loader = null;
        	//根据SPI注解的属性判断作用域是否等于默认作用域
        if (this.isScopeMatched(type)) {
            loader = this.createExtensionLoader0(type);
        }

        return loader;
    }
    private <T> ExtensionLoader<T> createExtensionLoader0(Class<T> type) {
          //检查拓展目录是否被删除
        this.checkDestroyed();
        //根据类型创建加载器并放入缓存
        this.extensionLoadersMap.putIfAbsent(type, new ExtensionLoader(type, this, this.scopeModel));
        //从缓存获取loader
        ExtensionLoader<T> loader = (ExtensionLoader)this.extensionLoadersMap.get(type);
        //返回loader
        return loader;
    }

2.获取拓展实例对象

执行这行代码获取拓展类的实例对象

Greeting englishGreeting = extensionLoader.getExtension("english");

源码如下

    public T getExtension(String name) {
    //获取拓展实例对象
        T extension = this.getExtension(name, true);
        if (extension == null) {
            throw new IllegalArgumentException("Not find extension: " + name);
        } else {
        //获取到了返回
            return extension;
        }
    }
public T getExtension(String name, boolean wrap) {
 //检查拓展目录是否被删除
        this.checkDestroyed();
        //若参数为空,抛异常
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }
        //若拓展类名称为true,表示使用SPI注解中声明的默认拓展实现类 
        else if ("true".equals(name)) {
            return this.getDefaultExtension();
        } else {
            String cacheKey = name;
            if (!wrap) {
                cacheKey = name + "_origin";
            }
					//尝试从缓存中获取拓展类实例对象,如果获取不到cacheKey对应的holder
					//则会创建一个空的holder再返回
            Holder<Object> holder = this.getOrCreateHolder(cacheKey);
            Object instance = holder.get();
            //若获取不到,则采用双重检验的单例模式创建实例对象,代码第一次执行是获取不到对象的,
            //此时instance为null
            //第一层判断是为了提高执行速度,防止实例对象不为空时还去竞争锁
            if (instance == null) {
                synchronized(holder) {
                    instance = holder.get();
                    //第二层判断是为了避免创建重复的实例对象
                    if (instance == null) {
                    //获取拓展类实例对象
                        instance = this.createExtension(name, wrap);
                     //将实例对象放入缓存
                        holder.set(instance);
                    }
                }
            }
				//返回实例对象
            return instance;
        }
    }
    private Holder<Object> getOrCreateHolder(String name) {
    //根据推展类名称获取缓存对象holder
        Holder<Object> holder = (Holder)this.cachedInstances.get(name);
        //获取不到,则创建一个空的holder并放入缓存
        if (holder == null) {
            this.cachedInstances.putIfAbsent(name, new Holder());
            holder = (Holder)this.cachedInstances.get(name);
        }
			//返回holder
        return holder;
    }

Holder类是一个缓存对象,用于缓存自定义键名对应的拓展类实例对象

public class Holder<T> {
//volatil的作用是禁止指令重排序
    private volatile T value;

    public Holder() {
    }
	
    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return this.value;
    }
}

可以看到缓存Holder类用volatile修饰变量,这样做是为了禁止指令重排序,避免返回一个未初始化完成的实例对象,创建一个对象分为3步

  1. 为对象分配内存空间
  2. 初始化对象
  3. 将对象指向1所分配的内存空间

若holder没有用volatile修饰,2和3的指令发生顺序颠倒,此时指令的执行顺序为1->3->2,当执行完3时,代码执行到第二层 if 判断,发现instance不为null,此时直接返回instance,返回的是还未初始化的对象(对象的属性未赋值)。

3.创建拓展类的实例对象

第一次执行时缓存是获取不到实例对象的,所以需要创建,之后就能从缓存中直接获取。

 private T createExtension(String name, boolean wrap) {
 	//解析配置文件,先获取所有的拓展类,再根据类名获取对应的class
        Class<?> clazz = (Class)this.getExtensionClasses().get(name);
        if (clazz != null && !this.unacceptableExceptions.contains(name)) {
            try {
            //根据类对象从缓存获取类实例对象
                T instance = this.extensionInstances.get(clazz);
               //获取不到,则通过反射的方式创建实例对象并放入缓存
                if (instance == null) {
                    this.extensionInstances.putIfAbsent(clazz, this.createExtensionInstance(clazz));
                    instance = this.extensionInstances.get(clazz);
                    //前置处理
                    instance = this.postProcessBeforeInitialization(instance, name);
                    //依赖注入
                    this.injectExtension(instance);
                    //后置处理
                    instance = this.postProcessAfterInitialization(instance, name);
                }
						//包装类的处理,用于实现AOP功能
                if (wrap) {
                    List<Class<?>> wrapperClassesList = new ArrayList();
                    if (this.cachedWrapperClasses != null) {
                        wrapperClassesList.addAll(this.cachedWrapperClasses);
                        wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                        Collections.reverse(wrapperClassesList);
                    }

                    if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                        Iterator var6 = wrapperClassesList.iterator();

                        while(var6.hasNext()) {
                            Class<?> wrapperClass = (Class)var6.next();
                            Wrapper wrapper = (Wrapper)wrapperClass.getAnnotation(Wrapper.class);
                            boolean match = wrapper == null || (ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) && !ArrayUtils.contains(wrapper.mismatches(), name);
                            if (match) {
                                instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance));
                                instance = this.postProcessAfterInitialization(instance, name);
                            }
                        }
                    }
                }
						//生命周期管理
                this.initExtension(instance);
                return instance;
            } catch (Throwable var10) {
                throw new IllegalStateException("Extension instance (name: " + name + ", class: " + this.type + ") couldn't be instantiated: " + var10.getMessage(), var10);
            }
        } else {
            throw this.findException(name);
        }
    }

(1)解析配置文件,获取拓展类

    private Map<String, Class<?>> getExtensionClasses() {
    //从缓存中获取拓展类
        Map<String, Class<?>> classes = (Map)this.cachedClasses.get();
        //缓存获取不到,通过双重检查锁的单例模式创建拓展类
        if (classes == null) {
            synchronized(this.cachedClasses) {
                classes = (Map)this.cachedClasses.get();
                if (classes == null) {
                    try {
                    //解析配置文件,获取类信息并放入缓存
                        classes = this.loadExtensionClasses();
                    } catch (InterruptedException var5) {
                        logger.error("0-15", "", "", "Exception occurred when loading extension class (interface: " + this.type + ")", var5);
                        throw new IllegalStateException("Exception occurred when loading extension class (interface: " + this.type + ")", var5);
                    }

                    this.cachedClasses.set(classes);
                }
            }
        }

        return classes;
    }

    private Map<String, Class<?>> loadExtensionClasses() throws InterruptedException {
        //检查拓展目录是否被销毁
        this.checkDestroyed();
        //判断SPI注解中的默认参数是否合法
        this.cacheDefaultExtensionName();
        Map<String, Class<?>> extensionClasses = new HashMap();
        LoadingStrategy[] var2 = strategies;
        int var3 = var2.length;
			//解析三个配置文件的信息,并将类信息放入缓存
			//三个配置文件 META-INF/dubbo/,META-INF/dubbo/internal/,META-INF/services/
        for(int var4 = 0; var4 < var3; ++var4) {
            LoadingStrategy strategy = var2[var4];
            this.loadDirectory(extensionClasses, strategy, this.type.getName());
            if (this.type == ExtensionInjector.class) {
                this.loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
            }
        }

        return extensionClasses;
    }

(2)实例化对象

 T instance = this.extensionInstances.get(clazz);
                if (instance == null) {
                    this.extensionInstances.putIfAbsent(clazz, this.createExtensionInstance(clazz));
                    instance = this.extensionInstances.get(clazz);
                    }
	//通过反射的方式创建实例对象
    private Object createExtensionInstance(Class<?> type) throws ReflectiveOperationException {
        return this.instantiationStrategy.instantiate(type);
    }

(3)前置处理
前置处理和Spring的前置处理类似,可以在实例对象初始化之前执行一些自定义的初始化逻辑,例如检查实例对象是否满足某些条件,或者为实例对象添加一些额外的功能。

instance = this.postProcessBeforeInitialization(instance, name);

 private T postProcessBeforeInitialization(T instance, String name) throws Exception {
        ExtensionPostProcessor processor;
        if (this.extensionPostProcessors != null) {
            for(Iterator var3 = this.extensionPostProcessors.iterator(); var3.hasNext(); instance = processor.postProcessBeforeInitialization(instance, name)) {
                processor = (ExtensionPostProcessor)var3.next();
            }
        }

        return instance;
    }

(4)依赖注入
Dubbo的依赖注入只支持Setter方法级别的注入。

this.injectExtension(instance);

 private T injectExtension(T instance) {
        if (this.injector == null) {
            return instance;
        } else {
            try {
                Method[] var2 = instance.getClass().getMethods();
                int var3 = var2.length;

                for(int var4 = 0; var4 < var3; ++var4) {
                    Method method = var2[var4];
                    //方法是setter方法,方法不包含DisableInject注解且instance不是基本数据类型
                    if (this.isSetter(method) && !method.isAnnotationPresent(DisableInject.class) && method.getDeclaringClass() != ScopeModelAware.class && (!(instance instanceof ScopeModelAware) && !(instance instanceof ExtensionAccessorAware) || !ignoredInjectMethodsDesc.contains(ReflectUtils.getDesc(method)))) {
                    //获取方法的参数类型
                        Class<?> pt = method.getParameterTypes()[0];
                        if (!ReflectUtils.isPrimitives(pt)) {
                            try {
                            //获取参数值
                                String property = this.getSetterProperty(method);
                               //根据参数类型和参数值获取依赖对象
                                Object object = this.injector.getInstance(pt, property);
                                if (object != null) {
                                //将依赖对象object注入instance
                                    method.invoke(instance, object);
                                }
                            } catch (Exception var9) {
                                logger.error("0-15", "", "", "Failed to inject via method " + method.getName() + " of interface " + this.type.getName() + ": " + var9.getMessage(), var9);
                            }
                        }
                    }
                }
            } catch (Exception var10) {
                logger.error("0-15", "", "", var10.getMessage(), var10);
            }

            return instance;
        }
    }

(5)后置处理
后置处理的思想与Spring的后置处理类似。

 instance = this.postProcessAfterInitialization(instance, name);
 
    private T postProcessAfterInitialization(T instance, String name) throws Exception {
        if (instance instanceof ExtensionAccessorAware) {
            ((ExtensionAccessorAware)instance).setExtensionAccessor(this.extensionDirector);
        }

        ExtensionPostProcessor processor;
        if (this.extensionPostProcessors != null) {
            for(Iterator var3 = this.extensionPostProcessors.iterator(); var3.hasNext(); instance = processor.postProcessAfterInitialization(instance, name)) {
                processor = (ExtensionPostProcessor)var3.next();
            }
        }

        return instance;
    }

(6)包装类Wrapper处理
Dubbo的AOP功能就是通过Wrapper实现的。

 if (wrap) {
                    List<Class<?>> wrapperClassesList = new ArrayList();
            				//判断wrapper缓存包装类集合是否为空
            				// 不为空则加入list集合中,然后排序之后再翻转
                    if (this.cachedWrapperClasses != null) {
                        wrapperClassesList.addAll(this.cachedWrapperClasses);
                        wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                        Collections.reverse(wrapperClassesList);
                    }
									//判断wrapper包装类集合不为空
                    if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                        Iterator var6 = wrapperClassesList.iterator();
							//遍历集合中的每一个包装类
                        while(var6.hasNext()) {
                            Class<?> wrapperClass = (Class)var6.next();
                            //获取包装类的注解
                            Wrapper wrapper = (Wrapper)wrapperClass.getAnnotation(Wrapper.class);
                            //判断是否符合包装条件
                            boolean match = wrapper == null || (ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) && !ArrayUtils.contains(wrapper.mismatches(), name);
                           //符合包装条件,将当前实例对象添加到包装类中,并做一些后置处理
                            if (match) {
                            //将instance作为参数传给wrapper的构造方法,通过反射的方式创建wrapper实例,
                            //再往wrapper实例注入依赖,然后将wrapper赋值给instance,再对instance做后置处理
                                instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance));
                                instance = this.postProcessAfterInitialization(instance, name);
                            }
                        }
                    }
                }

(7)生命周期管理

  this.initExtension(instance);
   
    private void initExtension(T instance) {
        if (instance instanceof Lifecycle) {
            Lifecycle lifecycle = (Lifecycle)instance;
            lifecycle.initialize();
        }

    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2138948.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2-95 基于matlab的模板定位

基于matlab的模板定位。利用①相关匹配&#xff08;Correlation Matching&#xff09;、②基于Hausdorff距离匹配方法 及③考虑对场景图象距离变换&#xff08;Distance Transform&#xff09;的Hausdorff距离匹配方法,实现模板目标在场景图象中的定位。程序已调通&#xff0c;…

XShell快速连接虚拟机(Ubuntu系统)

目录 前言 一 (XShell)(虚拟机 )(Ubuntu)下载 二 虚拟机的ip查找 三 虚拟机中安装连接环境 四 开启ssh-server服务 五 验证是Ubuntu是否开启ssh-server服务 六 连接XShell软件 前言 对于刚开始探索 Linux 世界的新手来说&#xff0c;拥有一台自己的服务器可能并不现实。幸运的…

linux服务器配置及服务器资源命令使用查看

在做i性能压测之前&#xff0c;所了解的服务器配置&#xff1a;CPU、内存、硬盘、网络 一、查看cpu信息 常用命令&#xff1a;cat /proc/cpuinfo或者lscpu、pidstat等 需要关注的&#xff1a; Architecture: x86_64 # 架构信息&#xff0c;表示系统的CPU架构为x86_64&#…

业务资源管理模式语言14

第三节&#xff1a;在前面讨论的Resource Transcations&#xff08;资源事务&#xff09;中有许多共同的行为。其中一个行为可以包含多个项目&#xff0c;每个项目对应一个不同的资源&#xff08;ItemizeTheResourceTransaction&#xff08;11&#xff09;。事务可以产生一些报…

GitLab权限及设置

之前很少关注这些&#xff0c;项目的权限&#xff0c;一般由专门的管理人员设置。 但自己创建的项目自己可以设置权限。下面是一些笔记。 GitLab中用户权限_gitlab 权限-CSDN博客 开发中遇到要将自己这块的代码上传到Git&#xff0c;由其他组的同事拉取后继续开发。上传代码后…

【JVM】概述

前言 Java的技术体系主要由支撑Java程序运行的虚拟机、提供各开发领域接口支持的Java类库、Java编程语言及许许多多的第三方Java框架&#xff08;如Spring、MyBatis等&#xff09;构成。在国内&#xff0c;有关Java类库API、Java语言语法及第三方框架的技术资料和书籍非常丰富&…

Oracle从入门到放弃

Oracle从入门到放弃 左连接和右连接Where子查询单行子查询多行子查询 from子句的子查询select子句的子查询oracle分页序列序列的应用 索引PL/SQL变量声明与赋值select into 赋值变量属性类型 异常循环游标存储函数存储过程不带传出参数的存储过程带传出参数的存储过程 左连接和…

【爬虫软件】批量采集抖音主页已发布作品

一、背景介绍 以下xx代表你猜中的部分。 1.1 爬取目标 用python开发的xx爬虫采集软件&#xff0c;可自动按博主抓取其已发布视频。 为什么有了源码还开发界面软件呢&#xff1f;方便不懂编程代码的小白用户使用&#xff0c;无需安装python&#xff0c;无需改代码&#xff0c;…

瞳孔检测系统源码分享

瞳孔检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vision …

CS61C 2020计算机组成原理Lecture01-数字表示,溢出

1. 原码 原码就是符号化的数值&#xff0c;其编码规则简单直观&#xff1a;正数符号位用0表示&#xff0c;负数符号位用1表示&#xff0c;数值位保持不变。 x0.1101&#xff0c;则[x]原0.1101&#xff1b;x1101&#xff0c;则[x]原01101x -0.1111&#xff0c;则[x]原1.1111&…

《Mesh 组网和 AC+AP 组网的优缺点》

Mesh 组网和 ACAP 组网的优缺点。 Mesh 组网的优点&#xff1a; 1. 部署灵活&#xff1a;节点之间可以通过无线方式连接&#xff0c;新增节点比较方便&#xff0c;无需事先规划布线。 2. 自我修复和优化&#xff1a;如果某个节点出现故障&#xff0c;网络可以自动重新路由数据&…

MyBatis 数据处理:主键获取、批量删除与动态表名

目录 MyBatis 数据处理&#xff1a;主键获取、批量删除与动态表名 1.主键获取 1&#xff09;mapper接口 2&#xff09;mapper.xml 3&#xff09;测试代码 4&#xff09;测试结果 2.批量删除 1&#xff09;mapper接口 1-使用手动拼接字符串数组的方法 2-使用mybatis中的foreach标…

【数据结构与算法 | 每日一题 | 力扣篇】力扣1184

1. 力扣1184&#xff1a;公交站间的距离 1.1 题目&#xff1a; 环形公交路线上有 n 个站&#xff0c;按次序从 0 到 n - 1 进行编号。我们已知每一对相邻公交站之间的距离&#xff0c;distance[i] 表示编号为 i 的车站和编号为 (i 1) % n 的车站之间的距离。 环线上的公交车…

微信支付开发-程序开发

一、操作流程图 二、后端代码实现 1、题库实现 a、列表、所有、详情、保存、启禁用、导入答题 b、获取奖品信息、保存奖品信息、 class Question extends Base {// 列表public function getList(){$param $this->request->param();$where [];if(!empty($param[title])…

spdlog二次封装

这里写自定义目录标题 封装的原因封装的思想初始化接口的封装对日志输出接口进行宏的封装 封装的原因 1.避免单例的锁冲突&#xff0c;因此直接创建全局的线程安全的日志器进行使用 2.因为日志输出没有文件名行号&#xff0c;因此使用宏进行二次封装输出日志的文件名和行号 3.…

【JavaScript】LeetCode:707设计链表

文章目录 题目内容题目分析(1) 获取第n个节点的值(2) 头部插入节点(3) 尾部插入节点(4) 第n个节点前插入节点(5) 删除第n个节点 完整代码 题目内容 题目分析 添加哨兵节点dummy。在第n个节点前插入节点时&#xff0c;应该找到第n - 1个节点&#xff08;即前一个节点&#xff0…

国内人工智能产业发展现状及对策研究

一、引言 人工智能作为新时代科技革命和产业变革的核心力量&#xff0c;正深刻改变着全球经济格局。我国政府高度重视人工智能产业发展&#xff0c;将其列为国家战略性新兴产业。在此背景下&#xff0c;本文旨在分析我国人工智能产业发展现状&#xff0c;探讨面临的挑战&#x…

java基础面试题总结

java基础面试题总结 目录 前言 1. JVM vs JDK vs JRE的了解 2. 谈谈你对编程、编译、运行的理解 3. 什么是字节码?采用字节码的好处是什么? 5. java中的注解有几种&#xff0c;分别是什么&#xff1f; 6. 字符型常量和字符串常量 7.标识符和关键字的认识 8. 泛型&#xff…

初识Verilog HDL其二

文章目录 运算符往期回顾 运算符 往期回顾 初识Verilog HDL其一

储能运维管理云平台解决方案EMS能量管理系统

在储能行业蓬勃发展的今天&#xff0c;储能运维管理的重要性日益凸显。而储能运维管理云平台的出现&#xff0c;正为储能系统的稳定运行和高效管理注入了新的活力。 一、储能运维管理面临的挑战 传统的储能运维管理方式往往依赖人工巡检和现场操作&#xff0c;存在诸多问题。比…