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

news2024/11/19 11:23:28

 

原创/朱季谦

我在《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返回赋值给pending
    pending = 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.coloradmin.cn/o/1384590.html

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

相关文章

WSDM 2024 Oral | 港大提出DiffKG:融合知识图谱与扩散模型,为推荐系统赋能

论文链接&#xff1a; https://arxiv.org/abs/2312.16890 代码链接&#xff1a; https://github.com/HKUDS/DiffKG 实验室链接&#xff1a; https://sites.google.com/view/chaoh TLDR 知识图谱已成为提升推荐系统性能的重要资源&#xff0c;能提供丰富的事实信息并揭示实体间的…

Element-Puls Form表单内嵌套el-table表格,根据表格复选框多选或单选动态设置行的验证规则

需求 根据 Table 表格内的复选框来控制当前选中行是否添加必填校验规则 效果图 实现思想 我们需要设置一个 flag 来标识已勾选的行&#xff0c;el-table渲染数据结构是数组对象形式&#xff0c;我们可以在每个对象中手动加如一个标识&#xff0c;例如默认&#xff1a;selected …

Embeddings: What they are and why they matter

embeddings 是什么意思https://simonwillison.net/2023/Oct/23/embeddings/推荐原因&#xff1a;GPT 模型的基础是一种叫做 embeddings 的技术&#xff0c;用来将文本转换成向量&#xff0c;从而可以计算出文本之间的相似度。这篇文章详细地介绍了embeddings及应用 Embeddings…

NLP论文阅读记录 - 2021 | WOS 抽象文本摘要:使用词义消歧和语义内容泛化增强序列到序列模型

文章目录 前言0、论文摘要一、Introduction二.前提三.本文方法3.1 总结为两阶段学习3.1.1 基础系统 3.2 重构文本摘要 四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结思考 前言 Abstractive Text Summarization: Enhancing Sequen…

【CSS】保持元素宽高比

保持元素的宽高比&#xff0c;在视频或图片展示类页面是一个重要功能。 本文介绍其常规的实现方法。 实现效果 当浏览器视口发生变化时&#xff0c;元素的尺寸随之变化&#xff0c;且宽高比不变。 代码实现 我们用最简单的元素结构来演示&#xff0c;实现宽高比为4&#xf…

AI大模型学习笔记二

文章目录 一、Prompt Engineering1&#xff09;环境准备 二、LangChain&#xff08;一个框架名字&#xff09;三、Fine-tuning&#xff08;微调&#xff09; 一、Prompt Engineering 1&#xff09;环境准备 ①安装OpenAI库 pip install --upgrade openai附加 安装来源 pyth…

中职组安全-win20230217-环境-解析

*任务说明&#xff1a; 仅能获取win20230217的IP地址 用户名&#xff1a;test&#xff0c;密码&#xff1a;123456 访问服务器主机,找到主机中管理员名称,将管理员名称作为Flag值提交&#xff1b; john 访问服务器主机,找到主机中补丁信息,将补丁编号作为Flag值提交&#xff…

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…

【OJ】环形链表

目录 1. 环形链表||&#xff08;142&#xff09;1.1 题目描述1.2 题目分析1.3 代码 2. 环形链表&#xff08;141&#xff09;2.1 题目描述2.2 题目分析2.3 代码 1. 环形链表||&#xff08;142&#xff09; 1.1 题目描述 1.2 题目分析 带环链表&#xff1a;尾节点的next指向链…

QLExpress和Groovy对比

原理 Groovy groovy基于JVM运行。 编译时&#xff1a;将源文件编译成class文件后&#xff0c;用java的classLoader加载&#xff1b;运行时&#xff1a;直接用groovy classLoader加载 QLExpress QLExpress将文本解析成AST&#xff0c;用java对象表达后执行。 特点 Groo…

【JaveWeb教程】(27)Mybatis的XML配置文件与Mybatis动态SQL 详细代码示例讲解

目录 2. Mybatis的XML配置文件2.1 XML配置文件规范2.2 XML配置文件实现2.3 MybatisX的使用 3. Mybatis动态SQL3.1 什么是动态SQL3.2 动态SQL-if3.2.1 条件查询3.2.2 更新员工 3.3 动态SQL-foreach3.4 动态SQL-sql&include 2. Mybatis的XML配置文件 Mybatis的开发有两种方式…

基于ssm的理财通的设计与实现+jsp论文

摘 要 在如今社会上&#xff0c;关于信息上面的处理&#xff0c;没有任何一个企业或者个人会忽视&#xff0c;如何让信息急速传递&#xff0c;并且归档储存查询&#xff0c;采用之前的纸张记录模式已经不符合当前使用要求了。所以&#xff0c;对理财信息管理的提升&#xff0c…

DeepFloyd IF:由文本生成图像的强大模型,能够绘制文字的 AI 图像工具

文章目录 一、DeepFloyd IF 简介二、DeepFloyd IF模型架构三、DeepFloyd IF模型生成流程四、DeepFloyd IF 模型定义 一、DeepFloyd IF 简介 DeepFloyd IF&#xff1a;能够绘制文字的 AI 图像工具 之前的 Stable Diffusion 和 Midjourney 都无法生成带有文字的图片&#xff0c;…

09Bean的生命周期/作用域不同管理方式不同/自己new的对象纳入Spring容器管理

Spring其实就是一个管理Bean对象的工厂。它负责对象的创建&#xff0c;对象的销毁等。 所谓的生命周期就是&#xff1a;对象从创建开始到最终销毁的整个过程。 Bean的生命周期之5步 ● 第一步&#xff1a;实例化Bean(无参构造方法执行) ● 第二步&#xff1a;Bean属性赋值(注…

python的库或函数不会用:使用help函数查看函数

help(time) # 查看time这个库 FUNCTIONS #函数&#xff1b;都可以调用asctime(...)asctime([tuple]) -> string #调用这个函数的参数需要一个元组&#xff08;tuple&#xff09;&#xff0c;->&#xff1a;代表返回值是string类型的#下面是简单的介绍Convert a time tup…

强化学习应用(六):基于Q-learning的物流配送路径规划研究(提供Python代码)

一、Q-learning算法简介 Q-learning是一种强化学习算法&#xff0c;用于解决基于马尔可夫决策过程&#xff08;MDP&#xff09;的问题。它通过学习一个值函数来指导智能体在环境中做出决策&#xff0c;以最大化累积奖励。 Q-learning算法的核心思想是使用一个Q值函数来估计每…

LDR伪指令和ADR指令的区别

关于 ldr x5, lable 与 adr x5, label 首先&#xff0c;看ARM官方的手册&#xff1a; 对于 ADR 指令&#xff1a; 看 Operation 即可看出作用&#xff0c;Xd 赋值为 $PC imm 要求 label 和 $PC 位置在 正负1MB的范围内&#xff08;这个数值和上面提到的imm有关&#xff09;…

【2023 我的编程之旅】

前言 转眼 2024 年都过去 14 天了。回顾 2023 有太多技术上的思考以及人生的感悟&#xff0c;接下来趁着 CSDN 官方活动&#xff0c;顺便记录下来。 技术的价值 与现在的年轻人一心只想搞钱不同&#xff0c;刚毕业的时候&#xff0c;我的梦想是进入一家有实力的科技企业&…

光鉴科技的反卷思维,让科技不再难做

文 | 智能相对论 作者 | 陈壹 中国企业的全球竞争力&#xff0c;正从“拼人力、拼产能”转为“拼技术、拼创新”的新阶段。据世界知识产权组织发布的《世界知识产权指标报告》显示&#xff0c;2022年中国专利申请量约160万件&#xff0c;排名世界第一。而在最近发布的全球百强…

【GitHub项目推荐--AI 开源项目/涵盖 OCR、人脸检测、NLP、语音合成多方向】【转载】

今天为大家推荐一个相当牛逼的AI开源项目&#xff0c;当前 Star 3.4k&#xff0c;但是大胆预判&#xff0c;这个项目肯定要火&#xff0c;未来 Star 数应该可以到 10k 甚至 20k&#xff01; 着急的&#xff0c;可以到 GitHub 直接去看源码 传送门&#xff1a;https://github.c…