0

    jar包天天见,可是你知道它的运行机制吗?

    2023.07.13 | admin | 130次围观

    spi

    spi 是 Java 提供的一套用来被第三方实现或者扩展的 API ,它可以用来启用框架扩展和替换组件。spi 机制是这样的:读取 META-INF/services/ 目录下的元信息,然后 ServiceLoader 根据信息加载对应的类,你可以在自己的代码中使用这个被加载的类。要使用 Java SPI,需要遵循如下约定:

    现在我们来简单的使用一下吧。

    spi 使用示例

    建一个 maven 项目,定义一个接口 ( com.test.SpiTest ),并实现该接口( com.test.SpiTestImpl);然后在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 ( com.test.SpiTest),内容是要应用的实现类( com.test.SpiTestImpl)。

    public
     
    interface
     
    SpiTest
     
    {
     
    void
     test
    ();
    }
    public
     
    class
     
    SpiTestImpl
     
    implements
     
    SpiTest
     
    {
     
    @Override
     
    public
     
    void
     test
    ()
     
    {
     
    System
    .
    out
    .
    println
    (
    "test"
    );
     
    }
    }
    

    然后在我们的应用程序中使用 ServiceLoader来加载配置文件中指定的实现。

    public
     
    static
     
    void
     main
    (
    String
    []
     args
    )
     
    {
     
    ServiceLoader
    <
    SpiTest
    >
     load 
    =
     
    ServiceLoader
    .
    load
    (
    SpiTest
    .
    class
    );
     
    SpiTest
     next 
    =
     load
    .
    iterator
    ().
    next
    ();
     next
    .
    test
    ();
    }
    

    这便是 spi 的使用方式了,简约而不简单。

    spi 技术的应用

    那这一项技术有哪些方面的应用呢?最直接的 jdbc 中我们需要指定数据库驱动的全限定名,这便是 spi 技术。还有不少框架比如 dubbo ,都会预留 spi 扩展点比如:dubbo spi

    为什么要这么做呢?在 Spring 框架中我们注入一个 bean 很容易,通过注解或者 xml 配置即可,然后在其他的地方就能使用这个 bean 。在非 Spring 框架下,我们想要有同样的效果就可以考虑 spi 技术了。

    写过 SpringBoot 的 starter 的都知道,需要在 src/main/resources/ 下建立 /META-INF/spring.factories 文件。这其实也是一种spi技术的变形。

    jar 机制

    通常项目中我们打 jar 包都是通过 maven 来进行的,导致很多人忽略了这个东西的存在,就像很多人不知道 jdb.exe 是啥玩意一样。下面我们不借助任何工具来打一个 jar 包并对 jar 文件结构进行解析。

    命令行打 jar 包

    首先我们建立一个普通的 java 项目,新建几个 class 类,然后在根目录下新建 META-INF/MAINFEST.MF这个文件包含了 jar 的元信息,当我们执行 java -jar 的时候首先会读取该文件的信息做相关的处理。我们来看看这个文件中可以配置哪些信息 :

    定义好元信息之后我们就可以打 jar 包了,以下是打包的一些常用命令

    生成的test.jar中就含test目录和jar自动生成的META-INF目录(内含MAINFEST.MF清单文件)

    jar 
    -
    cvf test
    .
    jar test
    

    jar 
    -
    tvf test
    .
    jar
    

    jar 
    -
    xvf test
    .
    jar
    

    jar 
    -
    xvf test
    .
    jar test\test
    .
    class
    

    追加 MAINFEST.MF 清单文件以外的文件,会追加整个目录结构

    jar 
    -
    uvf test
    .
    jar other\ss
    .
    class
    

    会追加整个目录结构( test.jar 会包含 META-INF 目录)

    jar 
    -
    uMvf test
    .
    jar META
    -
    INF\MAINFEST
    .
    MF
    

    jar 
    -
    cMvf test
    .
    jar test META
    -
    INF
    

    通过 -m 选项配置自定义 MAINFEST.MF 文件时,自定义MAINFEST.MF 文件必须在位于工作目录下才可以

    jar 
    -
    cmvf MAINFEST
    .
    MF test
    .
    jar test
    

    jar 运行的过程

    jar 运行过程和类加载机制有关,而类加载机制又和我们自定义的类加载器有关找不到或无法加载主类,现在我们先来了解一下双亲委派模式。

    java 中类加载器分为三个:

    类的生命周期为:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。

    当我们执行 java -jar 的时候 jar 文件以二进制流的形式被读取到内存,但不会加载到 jvm 中,类会在一个合适的时机加载到虚拟机中。类加载的时机:

    当触发类加载的时候,类加载器也不是直接加载这个类。首先交给 AppClassLoader ,它会查看自己有没有加载过这个类,如果有直接拿出来,无须再次加载,如果没有就将加载任务传递给 ExtClassLoader ,而 ExtClassLoader 也会先检查自己有没有加载过,没有又会将任务传递给 BootstrapClassLoader ,最后 BootstrapClassLoader 会检查自己有没有加载过这个类,如果没有就会去自己要寻找的区域去寻找这个类,如果找不到又将任务传递给 ExtClassLoader ,以此类推最后才是 AppClassLoader 加载我们的类。这样做是确保类只会被加载一次。通常我们的类加载器只识别 classpath (这里的 classpath 指项目根路径,也就是 jar 包内的位置)下 .class 文件。jar 中其他的文件包括 jar 包被当做了资源文件,而不会去读取里面的 .class 文件。但实际上我们可以通过自定义类加载器来实现一些特别的操作

    Tomcat 的类加载器

    Tomcat 的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个 web 应用自己的类加载器(WebAppClassLoader) 会优先加载,加载不到时再交给 commonClassLoader 走双亲委托。

    tomcat 的类加载器:

    我们将一堆 jar 包放到 tomcat 的项目文件夹下, tomcat 运行的时候能加载到这些 jar 包的 class 就是因为这些类加载器对读取到的二进制数据进行处理解析从中拿到了需要的类

    SpringBoot 的 jar 包

    当我们将一个 SpringBoot 项目打好包之后,不妨解压看看里面的结构是什么样子的的

    run
    .
    jar
    |——
    org
    |
     
    |——
    springframework
    |
     
    |——
    boot
    |
     
    |——
    loader
    |
     
    |——
    JarLauncher
    .
    class
    |
     
    |——
    Launcher
    .
    class
    |——
    META
    -
    INF
    |
     
    |——
    MANIFEST
    .
    MF
    |——
    BOOT
    -
    INF
    |
     
    |——
    class
    |
     
    |——
    Main
    .
    class
    |
     
    |——
    Begin
    .
    class
    |
     
    |——
    lib
    |
     
    |——
    commons
    .
    jar
    |
     
    |——
    plugin
    .
    jar
    |
     
    |——
    resource
    |
     
    |——
    a
    .
    jpg
    |
     
    |——
    b
    .
    jpg
    

    classpath 可加载的类只有 JarLauncher.class, Launcher.class, Main.class, Begin.class。在 BOOT-INF/lib 和 BOOT-INF/class 里面的文件不属于 classloader 搜素对象直接访问的话会报 NoClassDefDoundErr 异常。Jar 包里面的资源以 Stream 的形式存在(他们本就处于 Jar 包之中),java 程序时可以访问到的。当 springboot 运行 main 方法时在 main 中会运行 org.springframework.boot.loader.JarLauncher 和 Launcher.class 这两个个加载器(你是否还及得前文提到过得 spi 技术),这个加载器去加载受 stream 中的 jar 包中的 class。这样就实现了加载 jar 包中的 jar 这个功能否则正常的类加载器是无法加载 jar 包中的 jar 的 class 的找不到或无法加载主类,只会根据 MAINFEST.MF 来加载 jar 外部的 jar 来读取里面的 class。

    如何自定义类加载器

    public
     
    class
     
    MyClassLoader
     
    extends
     
    ClassLoader
    {
     
    private
     
    String
     classpath
    ;
     
    public
     
    MyClassLoader
    (
    String
     classpath
    )
     
    {
     
     
    this
    .
    classpath 
    =
     classpath
    ;
     
    }
     
    @Override
     
    protected
     
    Class
    
     findClass
    (
    String
     name
    )
     
    throws
     
    ClassNotFoundException
     
    {
     
    // 该方法是根据一个name加载一个类,我们可以使用一个流来读取path中的文件然后从文件中解析出class来
     
    }
    }
    

    调用 defineClass() 方法加载类

    public
     
    static
     
    void
     main
    (
    String
     
    []
    args
    )
     
    throws
     
    ClassNotFoundException
    ,
     
    InstantiationException
    ,
     
    IllegalAccessException
    ,
     
    NoSuchMethodException
    ,
     
    SecurityException
    ,
     
    IllegalArgumentException
    ,
     
    InvocationTargetException
    {
     
    //自定义类加载器的加载路径
     
    MyClassLoader
     myClassLoader
    =
    new
     
    MyClassLoader
    (
    "D:\\lib"
    );
     
    //包名+类名
     
    Class
     c
    =
    myClassLoader
    .
    loadClass
    (
    "com.test.Test"
    )
     
     
    if
    (
    c
    !=
    null
    ){
     
    // 做点啥
     
    }
    }
    

    总结

    本文从比较基础的层面解读了我们频繁使用却大部分人不是很了解的两个知识点—— spi 和 jar 机制。希望大家看完这篇文章后能对 SpringBoot 中的一些“黑魔法”有更深入的了解,而不是停留在表面。

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论