手把手图解教你Java SPI源码分析

 

原创/朱季谦

我在《Java SPI机制总结系列之开发入门实例》一文当中,分享了Java SPI的玩法,但是这只是基于表面的应用。若要明白其中的原理实现,还需深入到底层源码,分析一番。

这里再重温一下SPI机制的概念:SPI,是Service Provider Interface的缩写,即服务提供者接口,单从字面上看,可以这样理解,该机制提供了一种可根据接口类型去动态加载出接口实现类对象的功能。打一个比喻,该机制就类似Spring容器,通过IOC将对象的创建交给Spring容器处理,若需要获取某个类的对象,就从Spring容器里取出使用即可。同理,在SPI机制当中,提供了一个类似Spring容器的角色,叫【服务提供者】,在代码运行过程中,若要使用到实现了某个接口的服务实现类对象,只需要将对应的接口类型交给服务提供者,服务提供者将会动态加载出所有实现了该接口的服务实现类对象,最后给到服务使用者使用。

image

接着前文的分享,可从以下三个步骤目录去深入分析Java SPI机制源码实现——

  1. 创建服务提供者ServiceLoader对象,其内部生成一个可延迟加载接口对应实现类对象的迭代器LazyIterator,主要作用是读取并解析META-INF/services/目录下的配置文件中service类名字,进而通过反射加载生成service类对象。
  2. 调用serviceLoader.iterator()返回一个内部实际是调用LazyIterator迭代器的匿名迭代器对象。
  3. 遍历迭代器,逐行解析接口全类名所对应配置文件中的service实现类的名字,通过反射生成对象缓存到链表,最后返回。
//step 1 创建ServiceLoader对象,其内部生成一个可延迟加载接口对应实现类对象的迭代器LazyIterator,主要作用是读取并解析META-INF/services/目录下的配置文件中service类名字,进而通过反射加载生成service类对象。
ServiceLoader<UserService> serviceLoader = ServiceLoader.load(UserService.class);
//step 2 调用serviceLoader.iterator()返回一个内部实际是调用LazyIterator迭代器的匿名迭代器对象。
Iterator<UserService> serviceIterator = serviceLoader.iterator();
//step 3 遍历迭代器,逐行解析接口全类名所对应配置文件中的service实现类的名字,通过反射生成对象缓存到链表,最后返回。UserService service = serviceIterator.next();service.getName();}
}

整个过程这里先做一个全面概括——ServiceLoader类会延迟加载UserService接口全名对应的META-INF/services/目录下的配置文件com.zhu.service.UserService。当找到对应接口全名文件后,会逐行读取文件里Class类名的字符串,假如存储的是“com.zhu.service.impl.AUserServiceImpl”和“com.zhu.service.impl.BUserServiceImpl”这两个类名,那么就会逐行取出,再通过反射【“Class类名”.newInstance()】,就可以创建出UserService接口对应的服务提供者对象。这些对象会以结构为<实现类名, 实现类对象>的Map形式,存储到LinkedHashMap链表里。该链表将由迭代器循环遍历,取出每一个实现类对象。

画一个流程图说明,大概如下——

image

接下来,基于该全貌流程图,分别对源码作分析。

一、创建服务提供者ServiceLoader对象,其内部生成一个可延迟加载接口对应实现类对象的迭代器LazyIterator,主要作用是读取并解析META-INF/services/目录下的配置文件中service类名字,进而通过反射加载生成service类对象。

先看第一部分代码——

ServiceLoader<UserService> serviceLoader = ServiceLoader.load(UserService.class);

进入到ServiceLoader.load(UserService.class)方法里,里面基于当前线程通Thread.currentThread().getContextClassLoader()创建一个当前上下文的类加载器ClassLoader,该加载器在这里主要是用来加载META-INF.services目录下的文件。

在load方法里,将UserService.class和类加载器ClassLoader当作参数,交给ServiceLoader中的另一个重载方法ServiceLoader.load(service, cl)去做进一步具体实现。

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

进入到ServiceLoader.load(service, cl),该方法里创建了一个ServiceLoader对象,该对象默认执行了参数值分别为UserService.class和ClassLoader的带参构造方法。

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

根据字面意义,可以看出,ServiceLoader是一个专门负责加载服务的对象,在SPI机制里,它充当专门提供接口实现服务对象的角色。

这里就有两个问题,它怎么提供服务对象,它提供的是哪个接口的服务?

针对这两个问题,基于传进来的参数值UserService.class和类加载器ClassLoader,就已经能猜出答案里,它将通过类加载器ClassLoader去加载实现UserService接口的具体服务类对象。

进入到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();
}

这里暂时只需要关注loader和 reload(),而acc是专门用在服务实现类的安全权限访问方面的,本文暂未涉及到acc,后续会考虑专门写一篇文分享下SPI下,如何实现服务实现类的安全权限访问。

传进来的loader如果为空,那么就使用ClassLoader.getSystemClassLoader(),即系统类加载器,可以简单理解,无论如何,都会得到一个非空的类加载器。

接着进入到reload()方法里——

/*** Clear this loader's provider cache so that all providers will be reloaded.* 清除此加载器的提供程序缓存,以便重新加载所有提供程序。* <p> After invoking this method, subsequent invocations of the {@link* #iterator() iterator} method will lazily look up and instantiate providers from scratch, just as is done by a newly-created loader.调用此方法后,后续调用{@link #iterator() iterator}方法将从零开始惰性查找并实例化提供商,就像新创建的加载器一样。** <p> This method is intended for use in situations in which new providers* can be installed into a running Java virtual machine.此方法旨在用于新提供者可以安装到正在运行的Java虚拟机中。*/
public void reload() {providers.clear();lookupIterator = new LazyIterator(service, loader);
}

根据reload() 方法的注释说明,可以看到,该方法做了两件事:

  1. providers是一个Map结构的链表LinkedHashMap,专门存储服务实例(在这里是存储UserService接口实现类对象)的集合,通过clear()方法做了清除,即清空了里面的所有记录。
  2. LazyIterator实现了Iterator迭代器接口,根据类名可以看出,这是一个Lazy懒加载形式的迭代器。

需要额外解释一下延迟加载是什么意思。延迟加载,说明项目启动时不会立马加载,而是需要被用到的时候,才会动态去加载。实现了Iterator迭代器接口的LazyIterator对象,就具备延迟加载的功能。

简单看一下,该LazyIterator的结构——

private class LazyIterator implements Iterator<S>
{//存储服务接口的Class类型Class<S> service;//存储类加载器。ClassLoader loader;//存储服务接口全类名所对应在META-INF.services目录中的配置文件资源路径Enumeration<URL> configs = null;//存储里配置文件中服务类名的迭代器Iterator<String> pending = null;//存储下一个返回的服务提供者类名String nextName = null;private LazyIterator(Class<S> service, ClassLoader loader) {this.service = service;this.loader = loader;}......}

总结这部分源码,主要是创建一个可加载接口服务提供者实例的ServiceLoader类对象,其内部创建一个具有延迟加载功能的迭代器LazyIterator。该LazyIterator迭代器能够延迟去逐行遍历解析出接口全类名所对应配置文件中的Class类名字符串,再将Class类名字符串通过反射生成服务提供者对象,存储到链表,用于外部迭代遍历。

接下来,会基于该延迟加载LazyIterator迭代器,做进一步处理。

到目前为止,只是在ServiceLoader类对象的内部,创建了一个存储接口UserService.class,类加载器loader的LazyIterator迭代器,暂时还没涉及到如何获取接口对应的服务提供者。

简单理解成,菜刀和锅都准备好了,就等切菜和煮菜了。

二、调用serviceLoader.iterator()返回一个内部实际是调用LazyIterator迭代器的匿名迭代器对象

这里通过serviceLoader.iterator()得到了一个类型为UserService的迭代器。

Iterator<UserService> serviceIterator = serviceLoader.iterator();

先进入到serviceLoader.iterator()内部——

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();}public S next() {if (knownProviders.hasNext())return knownProviders.next().getValue();return lookupIterator.next();}public void remove() {throw new UnsupportedOperationException();}};
}

该方法里,return new Iterator() { ... }表示创建一个实现了Iterator接口的匿名内部类实例对象,并返回该实例对象作为一个迭代器。

至于这个匿名对象是叫张三还是李四,都不重要。重要的是,其内部具有能被外部正常调用的hasNext()和next()就可以了。

我画了一幅简单的漫画,举例说明一下,这里为何可以直接返回一个实现Iterator接口的匿名内部类实例对象。

故事是这样的,有一个老板,想要招一个工具人,哦,不对,是打工人(反正都一样......)——
 

image


 

image

image

故事到这里就结束了,这个return new Iterator() { ... }返回的匿名内部类,就像无数籍籍无名的底层打工人一样,或许自始自终都无人知道他们的名字,但他们用自己辛勤的手(hasNext()方法)脚(next()方法),在平凡的岗位上,默默做着不平凡的工作,提供着可以帮助其他人(服务使用者)的服务。

接下来,让我们看看这些打工人那布满皱纹的手和脚——

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();
}

knownProviders是一个包装了LinkedHashMap providers = new LinkedHashMap<>()链表的迭代器。

当调用hasNext()或者next()时,都会判断providers里是否还有可以遍历获取的值,如果空了,就会调用lookupIterator.hasNext()或者lookupIterator.next()。

这个lookupIterator,正是前文创建的LazyIterator迭代器对象的引用。

匿名迭代器对象中的这两个方法,分别是以下两种功能:

  • hasNext()判断迭代器是否存在下一个元素。
  • next()获取迭代器中的下一个元素。

可见,这部分源码调用serviceLoader.iterator()返回一个提供hasNext()和next()方法的匿名迭代器对象,实际上,hasNext()和next()方法内真实调用的是迭代器LazyIterator的hasNext()和next()方法。

三、遍历迭代器,逐行解析接口全类名所对应配置文件中的service实现类的名字,通过反射生成对象缓存到链表,最后返回。

该分析最后的代码了,这里已经到遍历循环迭代器,通过serviceIterator.next()取出存储接口服务提供者对象——

while (serviceIterator.hasNext()) {UserService service = serviceIterator.next();service.getName();}
}

这里的hasNext()和next(),正是前文return new Iterator() { ... }匿名对象里的hasNext()和next()方法。故而在执行serviceIterator.hasNext()或者serviceIterator.next(),将跳转到#ServiceLoader类#iterator() 中,执行该匿名内部类的hasNext()和next()方法。

先来看hasNext()方法——

public boolean hasNext() {if (knownProviders.hasNext())return true;return lookupIterator.hasNext();
}

若是第一次执行时,knownProviders迭代器里的LinkedHashMap链表必定是空的,这时候,就会执行lookupIterator.hasNext()——

public boolean hasNext() {if (acc == null) {//acc为空,执行的是这一步代码return hasNextService();} else {PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc);}
}

这里acc为空,故而执行的是return hasNextService()语句——

private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {//"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;
}

初次调用,configs是null,而类加载器loader非空,故而会执行configs = loader.getResources(fullName)这行代码。

基于该执行步骤,分析一下这里的configs作用是什么,先看以下两个逻辑——

  1. PREFIX的值为private static final String PREFIX = "META-INF/services/",表示正是目录META-INF/services/路径。
  2. service.getName()是获取Class的name值,我们传进来的是UserService.class,故而这里service.getName()获取到的,便是接口全名com.zhu.service.UserService。

两者结合,即代码String fullName = PREFIX + service.getName()得到的,便是“METAINF/services/com.zhu.service.UserService”字符串,表示文件路径名。

这时候,我们的类加载器就开始派上用场了——

configs = loader.getResources(fullName);

没错,到这里已经拿到UserService接口全类名对应的文件路径,就可以通过类加载器读取到该文件资源了。

读取到该文件之后,之后就可以解析存放在文件里的接口的服务实现类信息了,故而具体实现在pending =parse(service, configs.nextElement())这行代码里——

while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}//逐行解析读取配置文件类名,将读取到的类名存储到ArrayList,最后包装成iterator返回赋值给pendingpending = parse(service, configs.nextElement());
}

进入到parse方法里,可以看到,这里开始通过while((lc =parseLine(service, u, r, lc, names))>=0)对文件内容逐行读取,同时创建一个ArrayList names,用来缓存读取出来的类名,具体实现就在parseLine(service, u, r, lc, names))方法里——

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;//遍历文件每一行字符串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);}}//将ArrayList包装成迭代器返回return names.iterator();
}

进入到parseLine(service, u, r, lc, names))方法,代码String ln = r.readLine()表示读取出文件每一行的字符串赋值给ln。

若遇到有#注释符号的就跳过,只读取非#号注释的类名字符串,以names.add(ln)保存到一个ArrayList里。

private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,List<String> names)throws IOException, ServiceConfigurationError
{String ln = r.readLine();if (ln == null) {return -1;}int ci = ln.indexOf('#');if (ci >= 0) ln = ln.substring(0, ci);ln = ln.trim();int n = ln.length();//过滤掉带有#字符的if (n != 0) {if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))fail(service, u, lc, "Illegal configuration-file syntax");int cp = ln.codePointAt(0);if (!Character.isJavaIdentifierStart(cp))fail(service, u, lc, "Illegal provider-class name: " + ln);for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {cp = ln.codePointAt(i);if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))fail(service, u, lc, "Illegal provider-class name: " + ln);}//读取文件里的类名字符串存储到names这个ArrayList里if (!providers.containsKey(ln) && !names.contains(ln))names.add(ln);}return lc + 1;
}

将读取文件里的类名存到ArrayList后,最后return names.iterator()返回一个iterator迭代器,可debug打印看一下,可以看到该ArrayList缓存了从文件里读取出来的类名——

image

该迭代器在解析完成后,会执行一次nextName = pending.next(),表示通过迭代器方式取出ArrayList中的第一个字符串,即“com.zhu.service.impl.AUserServiceImpl”,同时return true。

image

这里nextName = pending.next()和return true就呼应了外部服务使用者的调用,可见serviceIterator.hasNext()内部,若迭代器下一个元素不为空,那么就将下一个元素通过取出,赋值给nextName,同时返回true,让while循环正常遍历下去——

image

前面的nextName = pending.next()将会在serviceIterator.next()里有所体现。

接下来,在next()中,第一次调用,也是lookupIterator.next()方法——

public S next() {if (knownProviders.hasNext())return knownProviders.next().getValue();return lookupIterator.next();
}

进入到lookupIterator.next()方法——

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);}
}

同样,实现的是nextService()——

private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {/***nextName即将前文的com.zhu.service.impl.AUserServiceImpl*String cn = nextName*通过Class.forName(cn, false, loader),即可生成AUserServiceImpl的Class类对象*/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 {//既然已经拿到AUserServiceImpl的Class类对象,通过反射c.newInstance()便能生成相应对象S p = service.cast(c.newInstance());//生成的对象会以结构为<实现类名, 实现类对象>的Map形式,存储到LinkedHashMap链表里providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();          // This cannot happen
}

在这里面,主要做了这样几件事:

  1. 将nextName字符串赋值给cn,首次调用时,这里的nextName值为“com.zhu.service.impl.AUserServiceImpl”;
  2. 通过 c = Class.forName(cn,false, loader)生成AUserServiceImpl类的Class对象;
  3. 通过反射通过c.newInstance()生成AUserServiceImpl类实例对象;
  4. 生成的对象会以结构为<实现类名, 实现类对象>的Map形式,存储到LinkedHashMap链表里;
  5. 将生成的对象返回;

因此,在第一次调用完UserService service = serviceIterator.next()后,就能拿到了接口UserService的第一个实现类对象com.zhu.service.impl.AUserServiceImpl,进而就可以执行相应的重写方法service.getName()。

到while的第二次遍历时,执行serviceIterator.hasNext()后,会取出ArrayList中的第二个缓存类名“com.zhu.service.impl.BUserServiceImpl”赋值给nextName,这样在执行UserService service = serviceIterator.next()时,就会重复执行nextService()里的逻辑。一直迭代遍历,直到将配置里的类名都遍历完,serviceIterator才最终结束该UserService接口的服务提供功能。

首次调用就是以上流程,值得提的一个地方是,在反射创建完成的对象后,将以结构为<实现类名, 实现类对象>的Map形式。存储到LinkedHashMap链表里。

这个LinkedHashMap链表缓存的作用是什么呢?

这时回头去看下这行代码,还记得它里面创建了一个匿名内部类吗——

image

这个匿名内部类里,其hasNext()和next()方法,会判断knownProviders是否为空,不为空才去调用knownProviders里的方法。

这里的knownProviders正是使用到了LinkedHashMap链表缓存里的对象。

image

这个链表的作用,就是方便出现重复创建一个匿名迭代器去后去获取接口的服务对象时,直接从LinkedHashMap链表缓存里读取即可,无需再次去解析接口对应的配置文件,起到了查询优化的作用。

类似这样的场景,第二次生成一个迭代器去提供接口的服务功能时,就直接从从LinkedHashMap链表缓存里读取了。

image

以上,就是Java SPI的完整源码分析。

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

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

相关文章

JS实现网页轮播图

轮播图也称为焦点图&#xff0c;是网页中比较常见的网页特效。 1、页面基本结构&#xff1a; 大盒子focus&#xff0c;里面包含 左右按钮ul 包含很多个li &#xff08;每个li里面包含了图片&#xff09;下面有很多个小圆圈 因为我们想要点击按钮&#xff0c;轮播图左右播放&a…

外部晶振、复位按键、唤醒按键、扩展排针原理图详解

前言&#xff1a;本文对外部晶振、复位按键、唤醒按键、扩展排针原理图详解。本文使用的MCU是GD32F103C8T6 目录 外部晶振原理图 复位按键、唤醒按键原理图 扩展排针部分原理图 ​外部晶振原理图 如下图&#xff0c;两个外部晶振&#xff0c;分别是8M&#xff08;主晶振&a…

【InternLM 大模型实战】第三课

基于 InternLM 和 LangChain 搭建你的知识库 大模型开发范式RAG&#xff08;检索增强生成&#xff09;FINETUNE&#xff08;微调&#xff09; LangChain 简介构建向量数据库加载源文件文档分块文档向量化 搭建知识库助手构建检索问答链RAG方案优化建议 Web Demo 部署动手实战In…

让网页自动化测试更简便,流程图设计工具为您解决痛点

在数字化时代&#xff0c;网页自动化测试已经成为提高工作效率、保证项目质量的重要手段。然而&#xff0c;传统的自动化测试往往需要复杂的编程技能&#xff0c;对非专业人员来说门槛较高。为了解决这个问题&#xff0c;我们向您推荐一款创新的设计工具&#xff0c;它可以通过…

使用numpy处理图片——二值图像

大纲 载入图像灰阶处理二值处理 在《使用numpy处理图片——灰阶影像》一文中&#xff0c;我们将彩色图片转换成灰阶图片。本文将在这个基础上将灰阶图片转换成二值图像。 二值图像就是只有黑白两种颜色的图像。像素最终显示黑还是白&#xff0c;需要有一个判断标准。如果图片太…

基于Token认证的登录功能实现

Session 认证和 Token 认证过滤器和拦截器 上篇文章我们讲到了过滤器和拦截器理论知识以及 SpringBoot 集成过滤器和拦截器&#xff0c;本篇文章我们使用过滤器和拦截器去实现基于 Token 认证的登录功能。 一、登录校验 Filter 实现 1.1、Filter 校验流程图 获得请求 url。判…

Gradle的安装及源替换步骤详解

工具介绍 Gradle是一款强大的构建工具&#xff0c;用于管理项目的依赖关系和构建过程。在使用Gradle之前&#xff0c;我们需要先进行安装&#xff0c;并可能需要更改默认的依赖源&#xff0c;以提高下载速度。下面是一步步的Gradle安装及源替换指南。 第一步&#xff1a…

Repo命令与git的关系

Repo命令与git的关系是很密切的。 我们都知道&#xff0c;git是一个开源的版本控制系统&#xff0c;常用在大型项目的管理上。 我们对repo的使用和了解就比较少了。Repo是一个基于Git构建出来的工具&#xff0c;它的出现不是为了取代Git&#xff0c;而是为了更方便开发者使用Gi…

使用PE信息查看工具和Beyond Compare文件比较工具排查dll库文件版本不对的问题

目录 1、问题说明 2、修改了代码&#xff0c;但安装版本还是有问题 3、使用PE信息查看工具查看音视频库文件&#xff08;二进制&#xff09;的时间戳 4、使用Beyond Compare比较两个库文件的差异 5、找到原因 6、最后 C软件异常排查从入门到精通系列教程&#xff08;专栏…

Python 文本处理库之chardet使用详解

概要 当处理文本数据时&#xff0c;经常会遇到各种不同的字符编码。这可能导致乱码和其他问题&#xff0c;因此需要一种方法来准确识别文本的编码。Python中的chardet库就是为了解决这个问题而设计的&#xff0c;它可以自动检测文本数据的字符编码。本文将深入探讨chardet库的…

git 的安装

git 的安装 在我们开始使用 Git 前&#xff0c;需要将它安装在我们的电脑上。即便已经安装&#xff0c;最好将它升级到最新的版本。 我们可以通过软件包或者其它安装程序来安装&#xff0c;或者下载源码编译安装。 本文只介绍通过在 windows 上安装软件包的方式&#xff0c;其…

大模型实战05——LMDeploy大模型量化部署实践

大模型实战05——LMDeploy大模型量化部署实践 1、大模型部署背景 2、LMDeploy简介 3、动手实践环节——安装、部署、量化 注 笔记内容均为截图 笔记课程视频地址&#xff1a;https://www.bilibili.com/video/BV1iW4y1A77P/?spm_id_from333.788&vd_source2882acf8c823ce…

NLP论文阅读记录 - 2022 | WOS 一种新颖的优化的与语言无关的文本摘要技术

文章目录 前言0、论文摘要一、Introduction1.1目标问题1.2相关的尝试1.3本文贡献 二.前提三.本文方法四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结思考 前言 A Novel Optimized Language-Independent Text Summarization Techni…

青动CRM-E售后 售后工单CRM系统 erp系统 带前端小程序全开源可二开

应用介绍 一款基于FastAdminThinkPHP和uniapp开发的CRM售后管理系统&#xff0c;旨在助力企业销售售后全流程精细化、数字化管理&#xff0c;主要功能&#xff1a;客户、合同、工单、任务、报价、产品、库存、出纳、收费&#xff0c;适用于&#xff1a;服装鞋帽、化妆品、机械机…

操作系统复习 七、八章

操作系统复习 七、八章 文章目录 操作系统复习 七、八章第七章 内存管理内存管理的基本要求和原理覆盖与交换连续分配管理方式非连续分配管理方式基本分段存储管理方式段页式管理方式补充 第八章 虚拟内存虚拟内存的基本概念请求分页管理方式易混知识点页面置换算法页面分配策略…

Apollo之原理和使用讲解

文章目录 1 Apollo1.1 简介1.1.1 背景1.1.2 简介1.1.3 特点 1.2 基础模型1.3 Apollo 四个维度1.3.1 application1.3.2 environment1.3.3 cluster1.3.4 namespace 1.4 本地缓存1.5 客户端设计1.5.1 客服端拉取原理1.5.2 配置更新推送实现 1.6 总体设计1.7 可用性考虑 2 操作使用…

Flink-SQL——动态表 (Dynamic Table)

动态表 (Dynamic Table) 文章目录 动态表 (Dynamic Table)DataStream 上的关系查询动态表 & 连续查询(Continuous Query)在流上定义表连续查询更新和追加查询查询限制 表到流的转换总结 SQL 和关系代数在设计时并未考虑流数据。因此&#xff0c;在关系代数(和 SQL)之间几乎…

ubuntu18.04 TensorRT 部署 yolov5-7.0推理

文章目录 1、环境配置2、推理部分2.1、检测2.2、分类2.3、分割2.4、INT8 量化 1、环境配置 链接: TensorRT cuda环境安装 2、推理部分 下载yolov5对应版本的包 https://github.com/wang-xinyu/tensorrtx/tree/master/yolov5 2.1、检测 1、源码模型下载 git clone -b v7.0 …

C# 导出EXCEL 和 导入

使用winfrom简单做个界面 选择导出路径 XLSX起名字 打开导出是XLSX文件 // 创建Excel应用程序对象Excel.Application excelApp new Excel.Application();excelApp.Visible false;// 创建工作簿Excel.Workbook workbook excelApp.Workbooks.Add(Type.Missing);Excel.Works…

F-score 和 Dice Loss 原理及代码

文章目录 1. F-score1. 1 原理1. 2 代码2. Dice Loss2.1 原理2.2 代码 通过看开源图像语义分割库的源码&#xff0c;发现它对 Dice Loss 的实现方式&#xff0c;是直接调用 F-score 函数&#xff0c;换言之&#xff0c;Dice Loss 是 F-score的特殊情况。于是就研究了一下这背后…