图文并茂带你理解Java的SPI机制

目录

  • 一、Java的SPI机制
    • 1、什么是Java的SPI ?
    • 2、JavaSPI 代码示例 (使用Maven项目演示)
    • 3、 JavaSPI 机制的核心-ServiceLoader
    • 4、实现自己的ServiceLoader
    • 5、Java中还有哪些SPI实现?

一、Java的SPI机制

1、什么是Java的SPI ?

SPI全称 Service Provider Interface ,字面意思:“服务提供者的接口”,是一种服务发现机制。
用于实现框架或库的扩展点,允许在运行时动态地插入或更换组件实现。它提供了一个框架来发现和加载服务实现,使得软件模块能够灵活地选择和使用不同的服务提供商。SPI鼓励松耦合的设计,因为服务的消费者不需要直接依赖于具体的服务实现。有没有想到面向对象的某个设计原则,SOLID中的 O 开闭原则,对扩展开放对修改关闭。Java允许服务提供者,按照SPI给定的规则实现自己的服务,而Java应用使用通过SPI规则提供的服务时无需进行额外的配置,并且可以随时替换服务,也无需修改业务代码。

SPI可以和我们常用的API对比着理解。

API全称、Application Programming Interface,字面意思:“应用程序编程接口” 。API是一组规则和定义,允许一个软件应用与另一个软件应用、库或操作系统进行交互。它定义了如何进行数据传输、请求服务或执行特定功能的协议和工具。API为开发人员提供了一种标准化的方式来访问和使用预先构建的功能,而无需了解这些功能内部的复杂实现细节。

总结:

名称目的使用者举例
SPI支持可插拔的架构,便于组件和服务的替换与扩展。主要由服务提供者(如库、框架开发者)实现,但也需要应用开发者配置以启用特定的服务实现。(这里说的配置一般就是引入jar或者maven、gradle的坐标即可)Java中,JDBC驱动的加载就是一个典型的SPI应用。
API简化开发过程,提高效率,促进不同系统间的互操作性。通常由应用开发者使用,以集成外部服务或内部模块。语音识别 API、文件上传 API等

个人感觉SPI就是种"设计模式",只是不在常见的23种设计模式之中,Java利用"SPI这种设计模式" 实现灵活的选择服务供应商实现,来达到解耦的目的,以此提高程序的可修改性和灵活性。
SPI和外观设计模式也有相通之处、外观模式(Facade) 定义了一个高层接口,为子系统中的一组接口提供一个一致的界面,从而简化子系统的使用。

简单的图示对比:
拿NASA的重返月球计划举例。
NASA准备把重返月球计划的着陆器设计部分分包给商业航天公司来完成。

如果还按照API的方式来实现,那么可能就是下图中的结果。NASA想替换某个方案就必须修改系统去适配其他公司的API接口。
这样代码的耦合性就大大提高了。并且导致NASA失去了主动性。NASA这个甲方才不愿意这么干。 所以NASA决定采用SPI规则来约束供应商。

在这里插入图片描述

NASA给出了关键性的技术指标。
假设目前有SpaceX 和 Blue Origin两家公司中标 去完成着陆器的设计。 这两家公司需要遵循NASA给定的SPI规则进行设计。
两家公司月球着陆器产品设计完成后,NASA只需要按照SPI规则去加载对应的产品即可。
在这里插入图片描述

下图可以看出,虽然两家公司的火箭设计不一样,但是他们的接口都符合NASA指定的接口规则。这样NASA就可以很轻松的集成两家公司的方案,如果觉得SpaceX的方案不好,直接拿Blue Origin的方案无缝替代即可。
在这里插入图片描述

2、JavaSPI 代码示例 (使用Maven项目演示)

①、NASA先定义SPI接口 并发布到本地仓库供SpaceX和BlueOrigin实现
项目结构:
在这里插入图片描述
pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.nasa</groupId><artifactId>SPI-interface</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties>
</project>

maven打包安装到本地仓库
在这里插入图片描述
打包好的jar名称
在这里插入图片描述

SPI接口:

package com.nasa;
/*** NASA提供的SPI接口* */
public interface LandingOnTheMoon {/*** 着陆方法*/void land();}

SPI加载实现(稍后会详细介绍):

package com.nasa;import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;/*** 加载具体的服务实现* */
public class LandingOnTheMoonLoader {private static volatile LandingOnTheMoonLoader LOADER;private final LandingOnTheMoon landingOnTheMoon;private final List<LandingOnTheMoon> landingOnTheMoons;/*** 加载服务* */private LandingOnTheMoonLoader() {ServiceLoader<LandingOnTheMoon> loader = ServiceLoader.load(LandingOnTheMoon.class);List<LandingOnTheMoon> list = new ArrayList<>();for (LandingOnTheMoon landingOnTheMoon : loader) {list.add(landingOnTheMoon);}landingOnTheMoons = list;if (!list.isEmpty()) {// 取第一个landingOnTheMoon = list.get(0);} else {landingOnTheMoon = null;}}/*** LandingOnTheMoonLoader 单例加载* */public static LandingOnTheMoonLoader getLOADER() {if (LOADER == null) {synchronized (LandingOnTheMoonLoader.class) {if (LOADER == null) {LOADER = new LandingOnTheMoonLoader();}}}return LOADER;}public void land(){if(landingOnTheMoons.isEmpty()){System.out.println("LandingOnTheMoon服务未加载!");}else {LandingOnTheMoon landingOnTheMoon = landingOnTheMoons.get(0);landingOnTheMoon.land();}}
}

②、SpaceX实现自己的登陆月球方案
先引入NASA 指定的SPI接口
pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.spacex</groupId><artifactId>SpaceX</artifactId><version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>com.nasa</groupId><artifactId>SPI-interface</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties></project>

SpaceX的登陆月球实现:

package com.spacex;import com.nasa.LandingOnTheMoon;/*** SpaceX实现的月球着陆方案* */
public class SpaceXLand implements LandingOnTheMoon {@Overridepublic void land() {System.out.println("SpaceX landing on the moon with StarShip~");}
}

SpaceX的登陆月球实现配置信息:

注意: Maven项目中 META-INF/services/SPI接口全限定名 需要放在resources目录下
在这里插入图片描述

内容为SpaceX实现类的全限定名 :
在这里插入图片描述
③、BlueOrigin实现自己的登陆月球方案

先引入NASA 指定的SPI接口
pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.blue</groupId><artifactId>BlueOrigin</artifactId><version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>com.nasa</groupId><artifactId>SPI-interface</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties></project>

BlueOrigin的登陆月球实现:

package com.blue;import com.nasa.LandingOnTheMoon;/*** 蓝色起源实现的月球着陆方案* */
public class BlueOriginLand implements LandingOnTheMoon {@Overridepublic void land() {System.out.println("BlueOrigin landing on the moon with  NewGlenn~");}
}

BlueOrigin的登陆月球实现配置信息:
Maven项目中 META-INF/services/SPI接口全限定名 需要放在resources目录下
在这里插入图片描述
内容为BlueOrigin实现类的全限定名 :
在这里插入图片描述

编写测试代码:
新建一个Maven项目

pom文件中引入SpaceX的实现坐标

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.nasa</groupId><artifactId>NASA-LANDING</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><!--使用蓝色起源的着陆实现--><!--      <dependency><groupId>com.blue</groupId><artifactId>BlueOrigin</artifactId><version>1.0-SNAPSHOT</version></dependency>--><!--使用SpaceX的着陆实现--><dependency><groupId>com.spacex</groupId><artifactId>SpaceX</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies></project>
package com.nasa;public class LandingShow {public static void main(String[] args) {LandingOnTheMoonLoader service = LandingOnTheMoonLoader.getLOADER();service.land();}
}

执行结果:

SpaceX landing on the moon with StarShip~

此时如果NASA觉得SpaceX的方案不行,需要更换BlueOrigin的方案,操作起来非常简单,只需要注释掉SpaceX的坐标,添加BlueOrigin的坐标,业务代码完全不用改。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.nasa</groupId><artifactId>NASA-LANDING</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><!--使用蓝色起源的着陆实现--><dependency><groupId>com.blue</groupId><artifactId>BlueOrigin</artifactId><version>1.0-SNAPSHOT</version></dependency><!--使用SpaceX的着陆实现-->
<!--        <dependency>-->
<!--            <groupId>com.spacex</groupId>-->
<!--            <artifactId>SpaceX</artifactId>-->
<!--            <version>1.0-SNAPSHOT</version>-->
<!--        </dependency>--></dependencies></project>

执行结果:

BlueOrigin landing on the moon with  NewGlenn~

3、 JavaSPI 机制的核心-ServiceLoader

上面代码中我们使用ServiceLoader 去加载具体的服务实现。
ServiceLoader 是从JDK1.6 开始提供的一个类,用于加载服务提供者。
在这里插入图片描述

我们看下源码:
其中 String PREFIX = “META-INF/services/”;
这个就是JDK的SPI功能规定的具体服务实现的配置信息文件所在的目录 META-INF/services/

JDK的SPI规定 服务实现者需要在 META-INF/services/ 目录下 新建文件名为 SPI接口全限定类名的文件
文件内容为 服务实现者需要被加载的具体类的全限定类名
在这里插入图片描述

我们上面代码在加载服务的时候调用了ServiceLoader.load(LandingOnTheMoon.class);方法

ServiceLoader<LandingOnTheMoon> loader = ServiceLoader.load(LandingOnTheMoon.class);

看下这个方法的源码:

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}

可以看到这个方法 只是获取了当前线程的上下文类加载器 然后又调用了一个load的重载方法

public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){return new ServiceLoader<>(service, loader);}

在重载的load方法中又调用了ServiceLoader的私有构造器

 private ServiceLoader(Class<S> svc, ClassLoader cl) {service = Objects.requireNonNull(svc, "Service interface cannot be null");loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;reload();}

然后调用 reload();方法

  public void reload() {providers.clear();lookupIterator = new LazyIterator(service, loader);}

reload();方法里面 调用了 new LazyIterator(service, loader);

具体的类加载行为就是在LazyIterator内部类中完成的。
这里的 lookupIterator = new LazyIterator(service, loader); 创建LazyIterator 懒加载迭代器对象,并没有马上去加载SPI的具体服务。
当我们调用 for (LandingOnTheMoon landingOnTheMoon : loader) {…} 循环时,会触发迭代器,在ServiceLoader中,迭代器是由Iterable接口的iterator()方法提供的。当第一次调用iterator()时,如果之前没有创建过迭代器,它会创建一个新的LazyIterator实例。

对应下面的源码:

//  lookupIterator 在 调用reload 方法时 创建过了
private LazyIterator lookupIterator;
// 存储已经加载过的服务提供者实例  在 调用reload 方法时   调用了providers.clear(); 清空了这个集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();public Iterator<S> iterator() {return new Iterator<S>() {// 第一次调用时 providers 是空集合 会调用  lookupIterator 也就是 LazyIterator 的实例Iterator<Map.Entry<String,S>> knownProviders= providers.entrySet().iterator();public boolean hasNext() {if (knownProviders.hasNext())return true;return lookupIterator.hasNext();}public S next() {if (knownProviders.hasNext())return knownProviders.next().getValue();return lookupIterator.next();}public void remove() {throw new UnsupportedOperationException();}};}

下面看 LazyIterator 的实现 具体的类加载就在这里:
LazyIterator 的 hasNext 和 next方法 中判断了 acc 是否是null
其中 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; 这段代码是在 ServiceLoader初始化的时候执行的
主要用于判断 当前Java运行环境中是否存在安全管理器(SecurityManager),默认情况下不会配置,如果配置了SecurityManager,某些敏感操作(比如文件访问、网络连接等)可能会受到安全策略的限制或需要相应的权限检查。
这个不重要,我们还是看具体的方法 hasNextService 和 nextService方法。

 public boolean hasNext() {if (acc == null) {return hasNextService();} else {PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc);}}public S next() {if (acc == null) {return nextService();} else {PrivilegedAction<S> action = new PrivilegedAction<S>() {public S run() { return nextService(); }};return AccessController.doPrivileged(action, acc);}}

LazyIterator 的 hasNextService 和 nextService方法
终于到了加载具体服务的地方了。

// 迭代器
Iterator<String> pending = null;private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {// 这里获取 SPI配置文件的文件名  包含 META-INF/services/String fullName = PREFIX + service.getName();//  获取配置if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}// 迭代器如果为空 或者没有值while ((pending == null) || !pending.hasNext()) {// 配置内也没有值if (!configs.hasMoreElements()) {return false;}// 解析配置内的 具体服务名称pending = parse(service, configs.nextElement());}nextName = pending.next();return true;}private S nextService() {if (!hasNextService())throw new NoSuchElementException();// nextName 在hasNextService中已经被赋值了String cn = nextName;nextName = null;Class<?> c = null;try {//  关键点终于来了  最终还是利用Java的反射机制 完成类的加载c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}// 判断下 加载的 Class 的类型 是否实现了 给定的 SPI接口   拿上面NASA登月的例子看,这里的 service 就是  LandingOnTheMoon.classif (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn  + " not a subtype");}try {// 通过反射实例化 SPI的实现类 并且转换成 SPI接口类型S p = service.cast(c.newInstance());// 保存到  LinkedHashMap<String,S> providers 缓存中providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();          // This cannot happen
}

看 具体的parse 方法(解析服务实现者提供的配置文件)

private Iterator<String> parse(Class<?> service, URL u)throws ServiceConfigurationError{InputStream in = null;BufferedReader r = null;// 保存解析配置文件内的 全限定类名ArrayList<String> names = new ArrayList<>();try {in = u.openStream();r = new BufferedReader(new InputStreamReader(in, "utf-8"));int lc = 1;// 一行一行的读取配置信息 并将每行解析出的全限定类名 保存到 names // parseLine 方法不用看了 就是解析字符串的while ((lc = parseLine(service, u, r, lc, names)) >= 0);} catch (IOException x) {fail(service, "Error reading configuration file", x);} finally {try {if (r != null) r.close();if (in != null) in.close();} catch (IOException y) {fail(service, "Error closing configuration file", y);}}// 返回 拥有所有实现SPI接口的服务提供者全限定类名集合的迭代器return names.iterator();}

绕了半天 通过源码发现 最终类的加载还是通过 Java反射机制实现的。

那了解了ServiceLoader的具体实现之后,我们也可以实现一个自己的 服务加载器。
ServiceLoader只不过在实现了核心的SPI服务加载功能的基础上增加了一些额外的功能,比如通过LazyIterator 实现延迟加载,
可以调用reload() 在应用运行时实现服务的动态加载(不用重启应用就能加载服务),访问控制(比如判断 当前Java运行环境中是否存在安全管理器),还有按照加载顺序保存已加载的服务等等。不过ServiceLoader也是线程不安全的这点需要注意。

4、实现自己的ServiceLoader

我们实现一个最简单的ServiceLoader只考虑加载服务的功能,其他的安全、性能之类的都不考虑。
我们也仿照ServiceLoader类的结构,和SPI配置类规定的路径来实现。

package com.nasa;import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;public class MySimpleServiceLoader<T> {// 直接copy ServiceLoader的规则private static final String PREFIX = "META-INF/services/";// 定义的SPI接口private final Class<T> service;// 类加载器private final ClassLoader loader;// 保存已加载的服务实例private final LinkedHashMap<String, T> providers = new LinkedHashMap<>();// 构造方法Tprivate MySimpleServiceLoader(Class<T> svc, ClassLoader cl) {service = Objects.requireNonNull(svc, "Service interface cannot be null");loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;// 加载服务realLoad();}public static <T> MySimpleServiceLoader<T> load(Class<T> service) {ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();return new MySimpleServiceLoader<>(service, contextClassLoader);}private void realLoad() {String fullName = PREFIX + service.getName();try {// 获取配置文件Enumeration<URL> resources = loader.getResources(fullName);// 读取配置文件while (resources.hasMoreElements()) {URL url = resources.nextElement();InputStream inputStream = url.openStream();BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));// 我们就不做解析错误之类的逻辑了  默认给的配置文件都是对的String name = reader.readLine();// 通过反射创建对象Class<?> aClass = Class.forName(name, false, loader);// 判断 服务实现类是否实现了 给定的SPI接口if (service.isAssignableFrom(aClass)) {T cast = service.cast(aClass.newInstance());providers.put(name, cast);}}} catch (Exception e) {System.out.println("出现异常!");e.printStackTrace();}}// 返回加载好的 服务实现public LinkedHashMap<String, T> getProviders() {return providers;}
}

使用自定义的服务加载器 MySimpleServiceLoader

package com.nasa;import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;/*** 加载具体的服务实现* */
public class LandingOnTheMoonLoader {private static volatile LandingOnTheMoonLoader LOADER;private final LandingOnTheMoon landingOnTheMoon;private final List<LandingOnTheMoon> landingOnTheMoons = new ArrayList<>();/*** 加载服务* */private LandingOnTheMoonLoader() {// 通过自定义的服务加载器 去加载服务实现MySimpleServiceLoader<LandingOnTheMoon> mySimpleServiceLoader = MySimpleServiceLoader.load(LandingOnTheMoon.class);LinkedHashMap<String, LandingOnTheMoon> providers = mySimpleServiceLoader.getProviders();providers.forEach((k,v)->{System.out.println(k);landingOnTheMoons.add(v);});if (!landingOnTheMoons.isEmpty()) {// 取第一个landingOnTheMoon = landingOnTheMoons.get(0);} else {landingOnTheMoon = null;}}/*** LandingOnTheMoonLoader 单例加载* */public static LandingOnTheMoonLoader getLOADER() {if (LOADER == null) {synchronized (LandingOnTheMoonLoader.class) {if (LOADER == null) {LOADER = new LandingOnTheMoonLoader();}}}return LOADER;}public void land(){if(landingOnTheMoons.isEmpty()){System.out.println("LandingOnTheMoon服务未加载!");}else {LandingOnTheMoon landingOnTheMoon = landingOnTheMoons.get(0);landingOnTheMoon.land();}}}

测试:

public static void main(String[] args) {LandingOnTheMoonLoader service = LandingOnTheMoonLoader.getLOADER();service.land();}

使用SpaceX的实现,结果如下:

com.spacex.SpaceXLand
SpaceX landing on the moon with StarShip~

5、Java中还有哪些SPI实现?

  • 1、JDBC (Java Database Connectivity): JDBC驱动的加载就是SPI的一个经典应用。各个数据库厂商提供自己的JDBC驱动实现,应用程序无需直接引用具体驱动的实现类,只需将驱动JAR包放入类路径,Java SPI机制会自动发现并加载合适的驱动。

  • 2、日志框架: 如SLF4J (Simple Logging Facade for Java) 和Logback、Log4j等,它们利用SPI机制来发现和加载具体的日志实现。用户可以根据需要选择或更换日志实现,而无需修改应用程序代码。

  • 3、Spring框架: 虽然Spring框架本身更倾向于使用其自身的bean工厂和依赖注入机制来管理组件和服务,但Spring也支持SPI机制,特别是在某些扩展点和与第三方库集成时。

  • 4、Dubbo: Apache Dubbo是一个高性能的RPC框架,它大量使用SPI机制来实现其插件体系,允许用户轻松替换或扩展序列化、负载均衡、集群容错等核心组件。

  • 5、JNDI (Java Naming and Directory Interface): JNDI服务提供者也是通过SPI机制注册和发现的,允许应用程序访问不同的命名和目录服务。

  • 6、JAX-WS (Java API for XML Web Services) 和 JAX-RS (Java API for RESTful Web Services): 这些Java Web服务技术框架使用SPI来发现和加载实现特定功能的服务提供者,比如SOAP绑定和HTTP连接器。

  • 7、JavaBeans Activation Framework (JAF): 用于处理MIME类型的邮件附件等,通过SPI来发现数据处理器。

  • 8、JavaMail: 用于发送和接收电子邮件的API,利用SPI来加载传输协议和其他服务提供者。

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

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

相关文章

中秋快到了,要给哪些国外客户送祝福(附贺卡模板)

马上就要中秋节了&#xff0c;在这里提前祝小伙伴们中秋节快乐&#xff0c;身体健康&#xff0c;阖家团圆&#xff0c;业绩越来越好&#xff0c;公司越来越好&#xff0c;一切都越来越好&#xff01; 中秋节是我们非常重要的几个传统节日之一了&#xff0c;除了我们自己庆祝之…

计算机网络练级第一级————认识网络

目录 网络搁哪&#xff1f; 网络的发展史&#xff08;了解&#xff09; 独立模式&#xff1a; 网络互联&#xff1a; 局域网时期&#xff1a; 广域网时期&#xff1a; 什么是协议 TCP/IP五层/四层模型 用官话来说&#xff1a; 我自己的话来说 第一层应用层&#xff1…

Python+selenium自动化元素定位防踩坑(建议收藏)

踩坑一&#xff1a;StaleElementReferenceException selenium.common.exceptions.StaleElementReferenceException: Message: stale element reference: element is not attached to the page document 异常原因&#xff1a; 意思是&#xff0c;引用的元素已过期。原因是页面…

soup.find(‘div‘)获取的数据长度为3,为什么1和3都是空的?

用beautifulSoup中的find&#xff08;‘div’&#xff09;可以获取一个div数据&#xff0c;为什么用len&#xff08;&#xff09;计数是显示长度为3&#xff1f; 实际在打印输出时&#xff0c;1和3又没有内容输出&#xff1f;用print&#xff08;div【0】&#xff09;和print&…

Java小白一文讲清Java中集合相关的知识点(七)

LinkedHashSet LinkedHashSet是HashSet的子类 LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组双向链表 而在之前讲的HashSet中的链表是单向的哈&#xff0c;注意区分&#xff01; LinkedHashSet根据元素的hashcode值来决定元素的存储位置&#xff0c;同时使用链表…

极限编程XP例题

答案&#xff1a;D 解析&#xff1a; 结对编程&#xff0c;一个人写代码&#xff0c;一个人看&#xff0c;由于是两个或两个以上的人负责&#xff0c;因此选项A 支持共同代码拥有和共同对系统负责是正确的 选项B 由于是一个人写一个人看&#xff0c;变相实现了代码审查 选项…

深入了解 GROW with SAP:它究竟是什么?

GROW with SAP 是一套综合全面的产品组合&#xff0c;包含一系列解决方案、加速采用服务、社区支持和学习资源&#xff0c;能够确保各种规模的企业成功采用 ERP 云软件。部署 GROW with SAP 后&#xff0c;企业可以采用 SAP S/4HANA Cloud Public Edition [ERP 公有云版]。在 S…

4 路由模式

路由模式 逻辑图 如果我们将生产环境的日志进行处理&#xff0c;而日志是分等级的&#xff0c;我们就按照 error waring info三个等级来讲解 一个消费者是处理【所有】&#xff08;info&#xff0c;error&#xff0c;warning&#xff09;的日志&#xff0c;用于做数据仓库&am…

Redis搭建集成

图示 正常来讲配置一主两从需要三台服务器,博主内存告急,就使用一台进行操作了,使用多台跟一台操作没有区别,只是多台不需要新建太多配置文件 一. 准备配置文件 如果你跟我一样是在一台服务器里面进行配置主从服务的,跟我一起操作即可 找到redis目录 在bin目录同位置创建一…

Linux驱动.之驱动开发思维,设备,驱动,总线分析思想,驱动的分类(字符设备,块设备,网络设备)

在stm32&#xff0c;裸机开发时&#xff0c;偏底层&#xff0c;跟寄存器打交道&#xff0c;有些MCU提供了库&#xff0c;库也还是操作寄存器的&#xff0c;通过配置寄存器&#xff0c; 配置各种工作模式&#xff0c;时钟&#xff0c;等等&#xff0c;交换数据等等。 Linux下驱…

Unity笔记之静态/动态合批

借用博主链接 一、静态合批 1、首先项目设置里面需要勾选静态合批 2、添加静态合批选项 3、至此就完成了&#xff0c;至于成功没有就要去分析器里面看了。 静态合批注意问题&#xff1a; 二、动态合批 1、首先项目设置里面需要勾选动态合批 2、调用 StaticBatchingUtilit…

给大家推荐好用的AI网站

地址&#xff1a;https://ai.ashuiai.com/auth/register?inviteCode8E8DIC1QCR 个人觉得挺好用的&#xff0c;可以免费&#xff0c;免费有限制次数&#xff0c;也可以会员升级200永久免费&#xff0c;我用的200永久免费。 可以在国内使用以下ai模型 回答问题更智能&#xff…

IBM中国研发部裁员:全球化背景下的IT产业变局与应对之道

裁员风波中的思考与机遇 前言了解霍尼韦尔的“东方服务东方”施耐德电气的“中国中心”战略对比与分析 中国信息技术(IT)行业展现出蓬勃发展的前景**政府支持与政策导向****技术创新与应用****市场规模与需求****人才培养与就业**国际化与开放合作总结 前言 如何看待IBM中国研发…

“冰山之下”:谁在成为车企的真正智能助手?

“其实我们一直扮演的角色就是数字化助手&#xff0c;也就是别人可以去挖金&#xff0c;我们给大家提供铲子&#xff0c;这是我们扮演的角色&#xff0c;而现在我们希望给大家提供最好的铲子。” 作者| 皮爷 出品|产业家 如果说AI发展的最鲜明印痕是什么&#xff1f;有人…

【原创教程】自动化工程案例01:8工位插针装配机03-程序解读

在前面两篇文章中&#xff0c;我们介绍了8工位设备每个工位的情况&#xff0c;然后我们介绍了触摸屏的情况&#xff0c;接着我们来看一下程序。关于一些实物照片不宜公开发表&#xff0c;需要的可以私信。 程序系统块设置 系统块中的模块实际上是我们所使用的的硬件设施 符号…

本地Linux服务器使用docker搭建DashDot并实现公网实时监测服务器信息

文章目录 前言1. 本地环境检查1.1 安装docker1.2 下载Dashdot镜像 2. 部署DashDot应用3. 本地访问DashDot服务4. 安装cpolar内网穿透5. 固定DashDot公网地址 前言 本篇文章我们将使用Docker在本地部署DashDot服务器仪表盘&#xff0c;并且结合cpolar内网穿透工具可以实现公网实…

HC-SR501人体红外传感器详解(STM32)

目录 一、介绍 二、传感器原理 1.原理图 2.引脚描述 3.工作原理介绍 三、程序设计 main.c文件 body_hw.h文件 body_hw.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 HC-SR501人体红外模块是基于红外线技术的自动控制模块&#xff0c;采用德国原装进口LHI77…

USB总线开关量DIO采集卡,24路数字量输入输出及32位计数器卡USB5801

阿尔泰科技 型号&#xff1a;USB5801 概述&#xff1a; 产品应用&#xff1a; 指标参数&#xff1a; 数字量 通道数 24路&#xff0c;每8路可配置成输入或输出 电气标准 TTL兼容 数字量输入 高电平的低电压&#xff1a;2V 低电平的高电压&#xff1a;0.8V 数字量输出 …

数据结构基础讲解(七)——数组和广义表专项练习

本文数据结构讲解参考书目&#xff1a; 通过网盘分享的文件&#xff1a;数据结构 C语言版.pdf 链接: https://pan.baidu.com/s/159y_QTbXqpMhNCNP_Fls9g?pwdze8e 提取码: ze8e 数据结构基础讲解&#xff08;六&#xff09;——串的专项练习-CSDN博客 个人主页&#xff1a;樱娆…

替代区块链

随着比特币的成功&#xff0c;人们逐渐意识到区块链技术的潜力&#xff0c;并随之出现了迅速的发展&#xff0c;各种区块链协议、应用程序和平台相应产生。 需要指出的是&#xff0c;在这种多元的局面下&#xff0c;很多项目迅速失去了它们的吸引力。事实上&#xff0c;有不少项…