java中的SPI机制

news2025/1/20 15:52:24

文章目录

  • SPI 介绍
    • 何谓 SPI?
    • SPI 和 API 有什么区别?
  • 实战演示
    • Service Provider Interface
    • Service Provider
    • 效果展示
  • ServiceLoader
    • ServiceLoader 具体实现
    • 自己实现一个 ServiceLoader
  • 总结

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

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

SPI 介绍

何谓 SPI?

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

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

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

顺口提一句,skywalking中大量使用了SPI。

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
    └─com
        └─zxn
            └─up
                └─spi
                        Logger.java
                        LoggerService.java
                        Main.class

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

package com.zxn.up.spi;

public interface Logger {
    void info(String msg);
    void debug(String msg);
}

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

package com.zxn.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 是所有 ServiceProvider
        loggerList = 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
    ├─com
    │  └─zxn
    │      └─up
    │          └─spi
    │              └─service
    │                      Logback.java
    │
    └─META-INF
        └─services
                com.zxn.up.spi.Logger

新建 Logback 类

package com.zxn.up.spi.service;

import com.zxn.up.spi.Logger;

public class Logback implements Logger {
    @Override
    public void info(String s) {
        System.out.println("Logback info 打印日志:" + s);
    }

    @Override
    public void debug(String s) {
        System.out.println("Logback debug 打印日志:" + s);
    }
}

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

新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。
在这里插入图片描述

再点击 OK 。

在这里插入图片描述

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

实现 Logger 接口,在 src 目录下新建 META-INF/services 文件夹,然后新建文件 com.zxn.up.spi.Logger (SPI 的全类名),文件里面的内容是:com.zxn.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 com.zxn.up.service;

import com.zxn.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 方法被调用时,首先会在 ServiceLoader 的 Provider 缓存中进行查找,如果缓存中没有命中那么则在 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);
            else
                configs = 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.jiangxuan.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 时,会有并发问题。

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

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

相关文章

测试开发备战秋招面试3

努力了那么多年,回头一望,几乎全是漫长的挫折和煎熬。对于大多数人的一生来说,顺风顺水只是偶尔,挫折、不堪、焦虑和迷茫才是主旋律。我们登上并非我们所选择的舞台,演出并非我们所选择的剧本。继续加油吧&#xff01; 目录 1.讲一下redis和mySQL的区别&#xff1f; 2.讲一下…

ChatGPT让现在的软件都土掉渣了

我们家有两个娃&#xff0c;每次我们想要出去时订个酒店时都好麻烦。我在某程上找&#xff0c;我先看有没有家庭房&#xff0c;但家庭房很少&#xff0c;而且有些家庭房实际上只能睡得下两大一小。普通房间能不能睡得下四个人&#xff0c;那可是得查看很多信息&#xff0c;如床…

Redis队列Stream、Redis多线程详解(一)

Redis队列与Stream Redis5.0 最大的新特性就是多出了一个数据结构 Stream&#xff0c;它是一个新的强大的支持多播的可持久化的消息队列&#xff0c;作者声明Redis Stream地借鉴了 Kafka 的设计。 Redis Stream 的结构如上图所示,每一个Stream都有一个消息链表&#xff0c;将所…

Pandas 学习手册中文第二版:1~5

原文&#xff1a;Learning pandas 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 一、Pandas 与数据分析 欢迎来到《Pandas 学习手册》&#xff01; 在本书中&#xff0c;我们将进行一次探索我们学习 Pandas 的旅程&#xff0c;这是一种用于 Python 编程语言的开源数…

Android:启动流程

Android启动流程 第一步&#xff1a;启动电源以及系统启动 当电源按下&#xff0c;引导芯片代码开始从预定义的地方(固化在ROM)开始执行。加载引导程序到RAM&#xff0c;然后 执行 第二步&#xff1a;引导程序 引导程序是在Android操作系统开始运行前的一个小程序。引导程序…

如何防止设备被重复控制

1. 引言 在一个物联网的系统中&#xff0c;主要有三部分组成&#xff1a;云端、WiFi、电控。当用户在APP上控制设备时&#xff0c;其控制下发链路是&#xff1a;云端>>WIFI>> 电控。当电控收到控制指令后&#xff0c;执行设备控制&#xff0c;控制成功后&#xff…

如何使用Midjourney辅助建筑平面设计,常用的建筑平面效果图提示和使用效果展示(内附Midjourney提示词网站)

文章目录一、室内建筑平面设计1.AutoCAD图纸&#xff08;别墅首层图&#xff09;2.平面效果图3.三维平面透视图二、建筑室内设计1.现代简约2.波西米亚风格3.工业风格4.沿海风格5.法国风格6.现代风格7.提示增加颜色倾向8.提示中增加设计师9.其它一些尝试三、好用的Midjourney提示…

Redis 6.x哨兵模式部署(五)

目录 一、主从复架构搭建 二、哨兵模式搭建 2.1背景 2.2哨兵模式介绍 2.3 Sentinel三大工作任务 1监控&#xff08;Monitoring&#xff09; 2提醒&#xff08;Notification&#xff09; 3自动故障迁移&#xff08;Automatic failover&#xff09; 4核心流程 2.4 安装…

企业如何实现数字化转型?

企业如何实现数字化转型&#xff1f; 首先&#xff0c;我需要先跟各位明确&#xff0c;企业数字化转型中很重要的3个“先行”条件&#xff1a; 第一、企业一把手的眼光和格局 一把手的视野、格局、定力是最重要的因素&#xff0c;没有之一。能不能放下自己过去的执念与经验&a…

信息与计算科学有哪些SCI期刊推荐? - 易智编译EaseEditing

以下是信息与计算科学领域的一些知名SCI期刊推荐&#xff1a; Information Sciences&#xff1a; 该期刊是信息科学领域的重要期刊&#xff0c;涵盖了信息科学、计算科学、人工智能、数据挖掘、模式识别、多媒体技术、网络通信、智能系统等方面的研究。 IEEE Transactions on…

电子文件的线上存储工具,你了解多少?

信息化时代的来临&#xff0c;企业也纷纷跟随时代步伐进入现代化办公。信息时代最显著的特征就是纸质文件到电子文件的转变。企业一天的办公中&#xff0c;可能就会产出无数的电子文件&#xff0c;其中很多文件都是珍贵的业务经验&#xff0c;因此线上存储是企业需要考虑的问题…

网页解析--bs4--01

python爬虫之bs4模块&#xff08;超详细&#xff09; Beautiful Soup 4.4.0 文档 — Beautiful Soup 4.2.0 documentation (crummy.com) 可以看到bs4库将网页文件变成了一个soup的类型&#xff0c; 事实上&#xff0c;bs4库 是解析、遍历、维护、“标签树“的功能库。 通俗一点…

redis基础总结-常用命令

redis常用指令3. 常用指令3.1 key 操作分析3.1.1 key应该设计哪些操作&#xff1f;3.1.2 key 基本操作3.1.3 key 扩展操作&#xff08;时效性控制&#xff09;3.1.4 key 扩展操作&#xff08;查询模式&#xff09;3.2 数据库指令3.2.1 key 的重复问题3.2.2 解决方案3.2.3 数据库…

001:Mapbox GL加载基础的地图

第001个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中加载最基础的 Mapbox GL地图 。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共59行)相关API参考:专栏目标示例效果 配置方式 1)查看基础设置…

无限制翻译软件-中英互译字数无限

翻译软件是我们工作及学习中必不可少的工具&#xff0c;然而许多翻译软件在使用时常常会出现字数限制的问题,这使得用户在处理长文本和大量文本时变得十分麻烦。如果你也遇到了类似的问题&#xff0c;那么哪个翻译软件不限制字数将为您带来全新的翻译体验。 以下是我们的哪个翻…

人人都是ChatGPT prompt 工程师

关于 Prompt ​ 解释这个词之前&#xff0c;首先需要解释 prompt 这个词&#xff1a; 简单的理解它是给 AI 模型的指令。 它可以是一个问题、一段文字描述&#xff0c;甚至可以是带有一堆参数的文字描述。AI 模型会基于 prompt 所提供的信息&#xff0c;生成对应的文本&…

Spark SQL join操作详解

一、 数据准备 本文主要介绍 Spark SQL 的多表连接&#xff0c;需要预先准备测试数据。分别创建员工和部门的 Datafame&#xff0c;并注册为临时视图&#xff0c;代码如下&#xff1a; val spark SparkSession.builder().appName("aggregations").master("lo…

腾讯云服务器CVM标准型S5和S6区别性能评测

腾讯云服务器CVM标准型S5是次新一代云服务器规格&#xff0c;标准型S6是最新一代的云服务器&#xff0c;S6实例的CPU处理器主频性能要高于S5实例&#xff0c;同CPU内存配置下的标准型S6实例要比S5实例性能更好一些&#xff0c;但是目前标准型S5实例活动较多&#xff0c;云服务器…

【ChatGPT】预训练模型微调及其应用(ChatGLM-6B、duckduckgo_search、GPT在科研的应用等)

note instructGPT&#xff08;基于提示学习的系列模型&#xff09;——>GPT3.5&#xff08;大规模预训练语言模型&#xff09;——>ChatGPT模型&#xff08;高质量数据标注反馈学习&#xff09;。chatGPT三大技术&#xff1a;情景学习、思维链、自然指令学习。GPT4飞跃式…

ASEMI代理AD9833BRMZ-REEL原装ADI车规级AD9833BRMZ-REEL

编辑&#xff1a;ll ASEMI代理AD9833BRMZ-REEL原装ADI车规级AD9833BRMZ-REEL 型号&#xff1a;AD9833BRMZ-REEL 品牌&#xff1a;ADI/亚德诺 封装&#xff1a;MSOP-10 批号&#xff1a;2023 引脚数量&#xff1a;10 安装类型&#xff1a;表面贴装型 AD9833BRMZ-REEL汽车…