当前位置: 首页 > news >正文

Java SPI 机制详解

在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。

为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点类似 IoC 的思想,将装配的控制权移交到了程序之外。

SPI介绍

何谓SPI

SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。

SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。

很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

在这里插入图片描述

SPI 和 API 有什么区别?

那 SPI 和 API 有啥区别?

说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:

在这里插入图片描述

一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。

当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。

当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。

举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。

实战演示

SLF4J (Simple Logging Facade for Java)是 Java 的一个日志门面(接口),其具体实现有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。

在这里插入图片描述
这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。

Service Provider Interface

新建一个 Java 项目 service-provider-interface 目录结构如下:(注意直接新建 Java 项目就好了,不用新建 Maven 项目,Maven 项目会涉及到一些编译配置,如果有私服的话,直接 deploy 会比较方便,但是没有的话,在过程中可能会遇到一些奇怪的问题。)

│  service-provider-interface.iml
│
├─.idea
│  │  .gitignore
│  │  misc.xml
│  │  modules.xml
│  └─ workspace.xml
│
└─src└─edu└─heiyu└─up└─spiLogger.javaLoggerService.javaMain.class

新建 Logger 接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现。

package edu.heiyu.up.spi;public interface Logger {void info(String msg);void debug(String msg);
}

接下来就是 LoggerService 类,这个主要是为服务使用者(调用方)提供特定功能的。这个类也是实现 Java SPI 机制的关键所在,如果存在疑惑的话可以先往后面继续看。

package edu.heiyu.up.spi;import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;public class LoggerService {private static final LoggerService SERVICE = new LoggerService();private final Logger logger;private final List<Logger> loggerList;private LoggerService() {ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);List<Logger> list = new ArrayList<>();for (Logger log : loader) {list.add(log);}// LoggerList 是所有 ServiceProviderloggerList = list;if (!list.isEmpty()) {// Logger 只取一个logger = list.get(0);} else {logger = null;}}public static LoggerService getService() {return SERVICE;}public void info(String msg) {if (logger == null) {System.out.println("info 中没有发现 Logger 服务提供者");} else {logger.info(msg);}}public void debug(String msg) {if (loggerList.isEmpty()) {System.out.println("debug 中没有发现 Logger 服务提供者");}loggerList.forEach(log -> log.debug(msg));}
}

新建 Main 类(服务使用者,调用方),启动程序查看结果。

package org.spi.service;public class Main {public static void main(String[] args) {LoggerService service = LoggerService.getService();service.info("Hello SPI");service.debug("Hello SPI");}
}

程序结果:

info 中没有发现 Logger 服务提供者 debug 中没有发现 Logger 服务提供者

此时我们只是空有接口,并没有为 Logger 接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。

你可以使用命令或者直接使用 IDEA 将整个程序直接打包成 jar 包。

Service Provider

接下来新建一个项目用来实现 Logger 接口,新建项目 service-provider 目录结构如下:

│  service-provider.iml
│
├─.idea
│  │  .gitignore
│  │  misc.xml
│  │  modules.xml
│  └─ workspace.xml
│
├─lib
│      service-provider-interface.jar
|
└─src├─edu│  └─heiyu│      └─up│          └─spi│              └─service│                      Logback.java│└─META-INF└─servicesedu.jiangxuan.up.spi.Logger

新建 Logback 类

package edu.heiyu.up.spi.service;import edu.heiyu.up.spi.Logger;public class Logback implements Logger {@Overridepublic void info(String s) {System.out.println("Logback info 打印日志:" + s);}@Overridepublic void debug(String s) {System.out.println("Logback debug 打印日志:" + s);}
}

将 service-provider-interface 的 jar 导入项目中。

新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。

在这里插入图片描述

再点击ok。

在这里插入图片描述

接下来就可以在项目中导入 jar 包里面的一些类和方法了,就像 JDK 工具类导包一样的。

实现 Logger 接口,在 src 目录下新建 META-INF/services 文件夹,然后新建文件 edu.heiyu.up.spi.Logger (SPI 的全类名),文件里面的内容是:edu.heiyu.up.spi.service.Logback (Logback 的全类名,即 SPI 的实现类的包名 + 类名)。

这是 JDK SPI 机制 ServiceLoader 约定好的标准。

这里先大概解释一下:Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的 META-INF 文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。

所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。

接下来同样将 service-provider 项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。

效果展示

为了更直观的展示效果,我这里再新建一个专门用来测试的工程项目:java-spi-test

然后先导入 Logger 的接口 jar 包,再导入具体的实现类的 jar 包。

新建 Main 方法测试:

package edu.heiyu.up.service;import edu.jiangxuan.up.spi.LoggerService;public class TestJavaSPI {public static void main(String[] args) {LoggerService loggerService = LoggerService.getService();loggerService.info("你好");loggerService.debug("测试Java SPI 机制");}
}

运行结果如下:

Logback info 打印日志:你好 Logback debug 打印日志:测试 Java SPI 机制

说明导入 jar 包中的实现类生效了。

如果我们不导入具体的实现类的 jar 包,那么此时程序运行的结果就会是:

info 中没有发现 Logger 服务提供者 debug 中没有发现 Logger 服务提供者

通过使用 SPI 机制,可以看出服务(LoggerService)和 服务提供者两者之间的耦合度非常低,如果说我们想要换一种实现,那么其实只需要修改 service-provider 项目中针对 Logger 接口的具体实现就可以了,只需要换一个 jar 包即可,也可以有在一个项目里面有多个实现,这不就是 SLF4J 原理吗?

如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的 服务实现(service-provider) 来完成我们需要的操作。

那么接下来我们具体来说说 Java SPI 工作的重点原理—— ServiceLoader

ServiceLoader

ServiceLoader 具体实现

想要使用 Java 的 SPI 机制是需要依赖 ServiceLoader 来实现的,那么我们接下来看看 ServiceLoader 具体是怎么做的:

ServiceLoader 是 JDK 提供的一个工具类, 位于package java.util;包下。

A facility to load implementations of a service.

这是 JDK 官方给的注释:一种加载服务实现的工具。

再往下看,我们发现这个类是一个 final 类型的,所以是不可被继承修改,同时它实现了 Iterable 接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。

public final class ServiceLoader<S> implements Iterable<S>{ xxx...}

可以看到一个熟悉的常量定义:

private static final String PREFIX = "META-INF/services/";

下面是 load 方法:可以发现 load 方法支持两种重载后的入参;

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader) {return new ServiceLoader<>(service, loader);
}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();
}public void reload() {providers.clear();lookupIterator = new LazyIterator(service, loader);
}

根据代码的调用顺序,在 reload() 方法中是通过一个内部类 LazyIterator 实现的。先继续往下面看。

ServiceLoader 实现了 Iterable 接口的方法后,具有了迭代的能力,在这个 iterator 方法被调用时,首先会在 ServiceLoaderProvider 缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator 中进行查找。


public Iterator<S> iterator() {return new Iterator<S>() {Iterator<Map.Entry<String, S>> knownProviders= providers.entrySet().iterator();public boolean hasNext() {if (knownProviders.hasNext())return true;return lookupIterator.hasNext(); // 调用 LazyIterator}public S next() {if (knownProviders.hasNext())return knownProviders.next().getValue();return lookupIterator.next(); // 调用 LazyIterator}public void remove() {throw new UnsupportedOperationException();}};
}

在调用 LazyIterator 时,具体实现如下:


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);}
}private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {//通过PREFIX(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;
}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);}
}private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn + " not a subtype");}try {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();          // This cannot happen
}

可能很多人看这个会觉得有点复杂,没关系,我这边实现了一个简单的 ServiceLoader 的小模型,流程和原理都是保持一致的,可以先从自己实现一个简易版本的开始学:

自己实现一个 ServiceLoader

我先把代码贴出来:

package edu.hieyu.up.service;import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;public class MyServiceLoader<S> {// 对应的接口 Class 模板private final Class<S> service;// 对应实现类的 可以有多个,用 List 进行封装private final List<S> providers = new ArrayList<>();// 类加载器private final ClassLoader classLoader;// 暴露给外部使用的方法,通过调用这个方法可以开始加载自己定制的实现流程。public static <S> MyServiceLoader<S> load(Class<S> service) {return new MyServiceLoader<>(service);}// 构造方法私有化private MyServiceLoader(Class<S> service) {this.service = service;this.classLoader = Thread.currentThread().getContextClassLoader();doLoad();}// 关键方法,加载具体实现类的逻辑private void doLoad() {try {// 读取所有 jar 包里面 META-INF/services 包下面的文件,这个文件名就是接口名,然后文件里面的内容就是具体的实现类的路径加全类名Enumeration<URL> urls = classLoader.getResources("META-INF/services/" + service.getName());// 挨个遍历取到的文件while (urls.hasMoreElements()) {// 取出当前的文件URL url = urls.nextElement();System.out.println("File = " + url.getPath());// 建立链接URLConnection urlConnection = url.openConnection();urlConnection.setUseCaches(false);// 获取文件输入流InputStream inputStream = urlConnection.getInputStream();// 从文件输入流获取缓存BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));// 从文件内容里面得到实现类的全类名String className = bufferedReader.readLine();while (className != null) {// 通过反射拿到实现类的实例Class<?> clazz = Class.forName(className, false, classLoader);// 如果声明的接口跟这个具体的实现类是属于同一类型,(可以理解为Java的一种多态,接口跟实现类、父类和子类等等这种关系。)则构造实例if (service.isAssignableFrom(clazz)) {Constructor<? extends S> constructor = (Constructor<? extends S>) clazz.getConstructor();S instance = constructor.newInstance();// 把当前构造的实例对象添加到 Provider的列表里面providers.add(instance);}// 继续读取下一行的实现类,可以有多个实现类,只需要换行就可以了。className = bufferedReader.readLine();}}} catch (Exception e) {System.out.println("读取文件异常。。。");}}// 返回spi接口对应的具体实现类列表public List<S> getProviders() {return providers;}
}

关键信息基本已经通过代码注释描述出来了,

主要的流程就是:

1、通过 URL 工具类从 jar 包的/META-INF/services目录下面找到对应的文件,
2、读取这个文件的名称找到对应的 spi 接口,
3、通过 InputStream 流将文件里面的具体实现类的全类名读取出来,
4、根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象,
5、将构造出来的实例对象添加到 Providers 的列表中。

总结

其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:我们按照规定将要暴露对外使用的具体实现类在 META-INF/services/ 文件下声明。

另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的反射。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框的理解。

通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:

  • 遍历加载所有的实现类,这样效率还是相对较低的;
  • 当多个 ServiceLoader 同时 load 时,会有并发问题。

相关文章:

Java SPI 机制详解

在面向对象的设计原则中&#xff0c;一般推荐模块之间基于接口编程&#xff0c;通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类&#xff0c;就违反了开闭原则。如果需要替换一种实现&#xff0c;就需要修改代码。 为了实现在模块装…...

腾讯前端经典react面试题(附答案)

React 性能优化在哪个生命周期&#xff1f;它优化的原理是什么&#xff1f; react的父级组件的render函数重新渲染会引起子组件的render方法的重新渲染。但是&#xff0c;有的时候子组件的接受父组件的数据没有变动。子组件render的执行会影响性能&#xff0c;这时就可以使用s…...

Go语言基础(十五):垃圾回收机制(三色标记)

文章目录一、标记清除&#xff08;三色标记&#xff09;大致原理1、标记细节2、root对象二、垃圾回收触发机制垃圾回收&#xff08;Garbage Collection&#xff09;&#xff0c;是一种自动管理内存的机制。传统编程语言&#xff08;如C/C&#xff09;需要开发者对无用内存资源进…...

一文了解build.gradle配置

Gradle 参考官方文档&#xff1a;https://developer.android.com/studio/build?hlzh-cn#groovy settings.gradle 存放于项目根目录下&#xff0c;此设置文件会定义项目级代码库设置&#xff0c;并告知 Gradle 在构建应用时应将哪些模块包含在内 接下来将以一个简单的 settin…...

【Redis 高级】- 持久化 - RDB

【Redis 高级】- 持久化 - RDB &#x1f451;什么是持久化呢&#xff1f; 那当然是够持久呀&#xff0c;这个持久如果在你不主动去删除的情况下&#xff0c;它就一直存在的。 &#x1f3b7;那么这有什么用呢&#xff1f; 举个栗子&#xff1a;我们在用 PowerPoint 在写价值 …...

SpringSecurity的安全认证的详解说明(附完整代码)

SpringSecurity登录认证和请求过滤器以及安全配置详解说明 环境 系统环境&#xff1a;win10 Maven环境&#xff1a;apache-maven-3.8.6 JDK版本&#xff1a;1.8 SpringBoot版本&#xff1a;2.7.8 根据用户名密码登录 根据用户名和密码登录&#xff0c;登录成功后返回Token数据…...

详解制造业业务数据模型

业务数据在企业数字化转型或单体应用的开发中都是至关重要的。站在跨业务跨部门的企业数字化转型角度&#xff0c;离不开业务架构的设计&#xff0c;详细的业务领域和业务数据模型是后续应用架构和数据架构的必要输入。站在单部门单场景的信息化角度&#xff0c;应用程序的需求…...

BigDecimal使用注意避坑

目录一. BigDecimal的初始化精度丢失问题二. BigDecimal在进行除法运算时需设置精度,否则对于除不尽的情况会抛出异常三. 不要使用BigDecimal的equals方法比较大小, 否则可能会因为精度问题导致比较结果和预期的不一致在java.math包中提供了对大数字的操作类&#xff0c;用于进…...

windows环境下,vue启动项目后打开chrome浏览器

前言&#xff1a;关于vue启动后打开chrome浏览器&#xff0c;我查了很多资料&#xff0c;方案如下&#xff1a; 1、增加环境变量BROWSER为chrome&#xff08;试了没效果&#xff09; 2、设置系统的默认浏览器为chrome&#xff08;应该可以&#xff0c;但没试&#xff1b;因为…...

SpringBoot2.X整合ClickHouse项目实战-从零搭建整合(三)

一、ClickHouseSpringBoot2.XMybatisPlus整合搭建 二、需求描述和数据库准备 三、ClickHouse统计SQL编写实战和函数使用 四、ClickHouseSpringBoot2.X案例-基础模块搭建 controller/request层 mapper层 model层 service层 五、ClickHouseSpringBoot2.X案例-数据统计接口 …...

学海记录项目测试报告

⭐️前言⭐️ 本篇文章是博主基于学海记录的个人项目所做的测试报告&#xff0c;用于总结运用自动化测试技术&#xff0c;应用于自己的项目。 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主将持续更新学习记录…...

【1792. 最大平均通过率】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 一所学校里有一些班级&#xff0c;每个班级里有一些学生&#xff0c;现在每个班都会进行一场期末考试。给你一个二维数组 classes &#xff0c;其中 classes[i] [passi, totali] &#xff0c;表示你…...

言简意赅+图解 函数传参问题(传值、传地址 500字解决战斗)

1、传值 2、传地址 不论是传值&#xff0c;还是传地址&#xff0c;形参都是对于实参的一份拷贝 下图为按值传递进行交换&#xff1a; 形参left拷贝一块新空间&#xff0c;形参right拷贝一块新空间 下图为按指针传递进行交换 形参left拷贝一块新的空间&#xff0c;形参right…...

UML-时序图以及PlantUML绘制

介绍 时序图&#xff08;Sequence Diagram&#xff09;&#xff0c;又名序列图、循序图&#xff0c;是一种UML交互图。它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。它可以表示用例的行为顺序&#xff0c;当执行一个用例行为时&#xff0c;其中的每条消息…...

【Redis】Redis 有序集合 Zset 操作 ( 简介 | 查询操作 | 增加操作 | 删除操作 | 修改操作 )

文章目录一、有序集合 Zset二、查询操作1、查询 Zset 所有数据2、查询 Zset 所有数据和评分3、查询指定评分范围的 Zset 数据4、查询指定评分范围的 Zset 数据并从大到小排序5、统计指定评分范围的 Zset 数据个数6、查询指定元素在 Zset 有序集合中的排名三、增加操作1、向 Red…...

Java特性之设计模式【策略模式】

一、策略模式 概述 在策略模式&#xff08;Strategy Pattern&#xff09;中&#xff0c;一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式 在策略模式中&#xff0c;我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略…...

IR-CUT 保证摄像机成像效果的滤镜

IR-CUT双滤镜是指在摄像头镜头组里内置了一组滤镜&#xff0c;当镜头外的红外感应点侦测到光线的强弱变化后&#xff0c;内置的IR-CUT自动切换滤镜能够根据外部光线的强弱随之自动切换&#xff0c;使图像达到最 佳效果。也就是说&#xff0c;在白天或黑夜下&#xff0c;双滤光片…...

openpnp - 普通航空插头和PCB的连接要使用线对板连接器

文章目录openpnp - 普通航空插头和PCB的连接要使用线对板连接器概述改进实际效果总结ENDopenpnp - 普通航空插头和PCB的连接要使用线对板连接器 概述 和同学讨论问题, 准备将航空插头连接到PCB上. 航空插头选用GX12-4公头, 拧到开孔的铁板上. 然后航空插头公头再与PCB连接. 铁…...

Python3 错误和异常实例及演示

作为 Python 初学者&#xff0c;在刚学习 Python 编程时&#xff0c;经常会看到一些报错信息&#xff0c;在前面我们没有提及&#xff0c;这章节我们会专门介绍。 Python 有2种错误很容易辨认&#xff1a;语法错误和异常。 Python assert&#xff08;断言&#xff09;用于判断…...

Android 9.0第三方app根据包名设置为横屏显示

1.概述 在android9.0的系统rom定制化开发中,在某些横屏的设备比如平板电脑,tv智能电视,广告机等等设备中,通常系统是默认横批显示的,但是在安装一些竖屏app的时候, 就会旋转为竖屏,这个时候操作app也不方便,所以产品需求要求竖屏也需要根据包名横屏显示出来,这就需要在…...

python/java环境配置

环境变量放一起 python&#xff1a; 1.首先下载Python Python下载地址&#xff1a;Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个&#xff0c;然后自定义&#xff0c;全选 可以把前4个选上 3.环境配置 1&#xff09;搜高级系统设置 2…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容

基于 ​UniApp + WebSocket​实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配​微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

三体问题详解

从物理学角度&#xff0c;三体问题之所以不稳定&#xff0c;是因为三个天体在万有引力作用下相互作用&#xff0c;形成一个非线性耦合系统。我们可以从牛顿经典力学出发&#xff0c;列出具体的运动方程&#xff0c;并说明为何这个系统本质上是混沌的&#xff0c;无法得到一般解…...

tree 树组件大数据卡顿问题优化

问题背景 项目中有用到树组件用来做文件目录&#xff0c;但是由于这个树组件的节点越来越多&#xff0c;导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多&#xff0c;导致的浏览器卡顿&#xff0c;这里很明显就需要用到虚拟列表的技术&…...

排序算法总结(C++)

目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指&#xff1a;同样大小的样本 **&#xff08;同样大小的数据&#xff09;**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案

目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后&#xff0c;迭代器会失效&#xff0c;因为顺序迭代器在内存中是连续存储的&#xff0c;元素删除后&#xff0c;后续元素会前移。 但一些场景中&#xff0c;我们又需要在执行删除操作…...

【HarmonyOS 5】鸿蒙中Stage模型与FA模型详解

一、前言 在HarmonyOS 5的应用开发模型中&#xff0c;featureAbility是旧版FA模型&#xff08;Feature Ability&#xff09;的用法&#xff0c;Stage模型已采用全新的应用架构&#xff0c;推荐使用组件化的上下文获取方式&#xff0c;而非依赖featureAbility。 FA大概是API7之…...

【若依】框架项目部署笔记

参考【SpringBoot】【Vue】项目部署_no main manifest attribute, in springboot-0.0.1-sn-CSDN博客 多一个redis安装 准备工作&#xff1a; 压缩包下载&#xff1a;http://download.redis.io/releases 1. 上传压缩包&#xff0c;并进入压缩包所在目录&#xff0c;解压到目标…...