zoukankan      html  css  js  c++  java
  • Java项目打包方式分析

    概述

    在项目实践过程中,有个需求需要做一个引擎能执行指定jar包的指定main方法。

    起初我们以一个简单的spring-boot项目进行测试,使用spring-boot-maven-plugin进行打包,使用java -cp demo.jar <package>.<MainClass>执行,结果报错找不到对应的类。

    我分析了spring-boot-maven-plugin打包的结构,又回头复习了java原生jar命令打包的结果,以及其他Maven打包插件打包的结果,然后写成这边文章。

    这篇文章里会简单介绍java原生的打包方式,maven原生的打包方式,使用maven shade插件将项目打成一个大一统的jar包的方式,使用spring-boot-maven-plugin将项目打成一个大一统的jar包的方式,并比较它们的差异,给出使用建议。

    Java原生打包

    为了简单起见,假设我们的项目只有一个HelloWorld.java,不使用Maven。假设当前目录为.,初始目录下没有任何内容。

    首先,我们在当前目录新建文件HelloWorld.java。为了演示如何让编译的class文件自动放置到与package对应的目录结构中,特地添加package命令。

    package com.hikvision.demo;
    
    public class HelloWorld {
    	public static void main(String[] args) {
    		System.out.println("Hello World");
    	}
    }
    

    在当前目录新建target子目录,此时,目录结构如下:

     ./
      ├─ HelloWorld.java
      ├─ target/
    

    编译

    命令:javac HelloWorld.java -d target。目录结构变为:

     ./
      ├─ HelloWorld.java
      ├─ target/
            ├─ com/hikvision/demo/
                  ├─ HelloWorld.class
    

    打包

    命令:jar cvf demo-algorithm.jar -C target/ .。目录结构变为:

     ./
      ├─ HelloWorld.java
      ├─ target/
      │     └─ com/hikvision/demo/
      │           └─ HelloWorld.class
      ├─ demo-algorithm.jar
    

    打包的结果demo-algorithm.jar,其内部结构为:

    demo-algorithm.jar
      ├─ com
      │   └─ hikvision
      │       └─ demo
      │           └─ HelloWorld.class
      └─ META-INF
          └─ MANIFEST.MF
    

    其中,MANIFEST.MF的内容为:

    Manifest-Version: 1.0
    Created-By: 1.8.0_144 (Oracle Corporation)
    

    运行

    命令:java -cp demo-algorithm.jar com.hikvision.demo.HelloWorld

    留意上面的jar包的结构,如果我们希望以java -cp的方式运行jar包中的某一个类的main方法,class的package必须对应jar包内部的一级目录。

    这种结构我们称之为java标准jar包结构。

    Maven原生打包

    我一般使用mvn clean package命令打包。

    maven打包的结果,jar包名称是根据artifactId和version来生成的,比如对于com.hikvision.algorithm:demo-algorithm:0.0.1-SNAPSHOT的打包结果是:demo-algorithm-0.0.1-SNAPSHOT.jar

    分析这个jar包的结构:

    .
    ├─com
    │  └─hikvision
    │      └─algorithm
    │          └─HelloWorld.class
    ├─META-INF
    │   ├─maven
    │   │   └─com.hikvision.algorithm
    │   │       └─demo-algorithm
    │   │           ├─pom.properties
    │   │           └─pom.xml
    │   └─MANIFEST.MF
    └─application.properties
    

    除META-INF目录之外,其他的都是class path,这一点符合java标准jar结构。不同的是META-INF有一级子目录maven,放置项目的maven信息。

    对于maven原生的打包结果,可以使用java -cp的方式执行其中某个主类。但是需要注意它并没有包含所以来的jar包,这需要另外提供。

    使用Maven shade插件打包

    Maven打包插件应该不止一种,这里使用的是maven-shade-plugin

    在pom文件中添加插件配置:

    <plugin>
    	<groupId>org.apache.maven.plugins</groupId>
    	<artifactId>maven-shade-plugin</artifactId>
    	<version>2.4.3</version>
    	<executions>
    		<execution>
    			<phase>package</phase>
    			<goals>
    				<goal>shade</goal>
    			</goals>
    		</execution>
    	</executions>
    </plugin>
    

    根据上面的配置,在package阶段,会自动执行插件的shade目标,这个目标负责将项目的class文件,以及项目依赖的class文件都会统一打到一个jar包里。

    我们可以执行mvn clean package来自动触发shade,或者直接执行mvn shade:shade

    target目录会生成2个jar包,一个是maven原生的jar包,一个是插件的jar包:

    target/
     ├─ original-demo-algorithm-0.0.1-SNAPSHOT.jar (4KB)
     └─ demo-algorithm-0.0.1-SNAPSHOT.jar (6.24MB)
    

    original-demo-algorithm-0.0.1-SNAPSHOT.jar是原生的jar包,不包含任何依赖,只有4KB。demo-algorithm-0.0.1-SNAPSHOT.jar是包含依赖的jar包,有6.24MB。

    对照上文可以猜测shade插件对maven原生打包结果进行重命名之后,使用这个名字又打出一个集成了依赖的jar包。

    注意,这表示如果执行了mvn install,最终被安装到本地仓库的是插件打出的jar包,而不是maven原生的打包结果。可以配置插件,修改打包结果的名称:

    <plugin>
    	<groupId>org.apache.maven.plugins</groupId>
    	<artifactId>maven-shade-plugin</artifactId>
    	<version>2.4.3</version>
    	<executions>
    		<execution>
    			<phase>package</phase>
    			<goals>
    				<goal>shade</goal>
    			</goals>
    			<configuration>
    				<finalName>demo-algorithm-0.0.1-SNAPSHOT-assembly</finalName>
    			</configuration>
    		</execution>
    	</executions>
    </plugin>
    

    使用这个配置,最终的打包结果:

    target/
     ├─ demo-algorithm-0.0.1-SNAPSHOT.jar (4KB)
     └─ demo-algorithm-0.0.1-SNAPSHOT-assembly.jar (6.24MB)
    

    此时,demo-algorithm-0.0.1-SNAPSHOT.jar是maven原生的打包结果,demo-algorithm-0.0.1-SNAPSHOT-assembly.jar是插件的打包结果。

    插件打包结果的内部结构如下:

    ├─ch
    │  └─qos
    │      └─logback
    │          ├─classic
    │          │  ├─boolex
    │          │  ├─db
    │          │  │  ├─names
    │          │  │  └─script
    │          │  ├─encoder
    │          │  └─util
    │          └─core
    │              ├─boolex
    │              ├─db
    │              │  └─dialect
    │              ├─encoder
    │              ├─joran
    │              │  ├─action
    │              │  ├─conditional
    │              │  ├─event
    │              │  │  └─stax
    │              │  ├─node
    │              │  ├─spi
    │              │  └─util
    │              │      └─beans
    │              ├─subst
    │              └─util
    ├─com
    │  └─hikvision
    │      └─algorithm
    ├─META-INF
    │  ├─maven
    │  │  ├─ch.qos.logback
    │  │  │  ├─logback-classic
    │  │  │  └─logback-core
    │  │  ├─com.hikvision.algorithm
    │  │  │  └─demo-algorithm
    │  │  ├─org.slf4j
    │  │  │  ├─jcl-over-slf4j
    │  │  │  ├─jul-to-slf4j
    │  │  │  ├─log4j-over-slf4j
    │  │  │  └─slf4j-api
    │  │  ├─org.springframework.boot
    │  │  │  ├─spring-boot
    │  │  │  ├─spring-boot-autoconfigure
    │  │  │  ├─spring-boot-starter
    │  │  │  └─spring-boot-starter-logging
    │  │  └─org.yaml
    │  │      └─snakeyaml
    │  ├─org
    │  │  └─apache
    │  │      └─logging
    │  │          └─log4j
    │  │              └─core
    │  │                  └─config
    │  │                      └─plugins
    │  └─services
    └─org
        ├─apache
        │  ├─commons
        │  │  └─logging
        │  │      └─impl
        │  └─log4j
        │      ├─helpers
        │      ├─spi
        │      └─xml
        ├─slf4j
        │  ├─bridge
        │  ├─event
        │  ├─helpers
        │  ├─impl
        │  └─spi
        ├─springframework
        │  ├─boot
        │  │  ├─admin
        │  │  ├─ansi
        │  │  ├─web
        │  │  │  ├─client
        │  │  │  ├─filter
        │  │  │  ├─servlet
        │  │  │  └─support
        │  │  └─yaml
        │  └─validation
        │      ├─annotation
        │      ├─beanvalidation
        │      └─support
        └─yaml
            └─snakeyaml
                ├─error
                ├─tokens
                └─util
    

    这里省略了所有的文件,以及大部分的子目录。

    META-INF目录外的其他所有目录,都是classpath,结构和Maven原生的打包结构相同。不同的是shade插件将所有的依赖jar解压缩之后,和项目的class文件一起重新打成jar包;并且在META-INF/maven下包含了项目本身及所依赖的项目的pom信息。

    如果在pom文件中,声明某个依赖是provided的,它就不会被集成到jar包里。

    总的来说,使用maven-shade-plugin打出的jar包的结构依然符合java标准jar包结构,所以我们可以通过java -cp的方式运行jar包中的某一个类的main方法。

    使用spring-boot-maven-plugin插件打包

    项目首先必须是spring-boot项目,即项目直接或间接继承了org.springframework.boot:spring-boot-starter-parent

    在pom文件中配置spring-boot-maven-plugin插件:

    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    

    这个插件默认将打包绑定在了maven生命周期的package阶段,即执行package命令会自动触发插件打包。

    插件会将Maven原生的打包结果重命名,然后将自己的打包结果使用之前那个名字。比如:

    target/
      ├─ ...
      ├─ demo-algorithm-0.0.1-SNAPSHOT.jar.original
      └─ demo-algorithm-0.0.1-SNAPSHOT.jar
    

    如上,demo-algorithm-0.0.1-SNAPSHOT.jar.original是Maven原生的打包结果,被重命名之后追加了.original后缀。demo-algorithm-0.0.1-SNAPSHOT.jar是插件的打包结果。

    这里需要注意,如果运行了mvn install,会将这个大一统的jar包安装到本地仓库。这一点可以配置,使用下面的插件配置,可以确保安装到本地仓库的是原生的打包结果:

    <plugin>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <!--将原始的包作为install和deploy的对象,而不是包含了依赖的包-->
            <attach>false</attach>
        </configuration>
    </plugin>
    

    spring-boot-maven-plugin打包的结构如下:

    .
    ├─BOOT-INF
    │  ├─classes
    │  │  └─com
    │  │      └─hikvision
    │  │          └─algorithm
    │  └─lib
    ├─META-INF
    │  └─maven
    │      └─com.hikvision.algorithm
    │          └─demo-algorithm
    └─org
        └─springframework
            └─boot
                └─loader
                    ├─archive
                    ├─data
                    ├─jar
                    └─util
    

    这里忽略了所有的文件。

    分析这个结构,spring-boot插件将项目本身的class放到了目录BOOT-INF/classes下,将所有依赖的jar放到了BOOT-INF/lib下。在jar包的顶层有一个子目录org,是spring-boot loader相关的classes。

    所以,这个与java标准jar包结构是不同的,和maven原生的打包结构也是不同的。

    另外,需要注意的是,即使设置为provided的依赖,依然会被集成到jar包里,这一点与上文的shade插件不同。

    分析META-INF/MANIFEST.MF文件内容:

    Manifest-Version: 1.0
    Implementation-Title: demo-algorithm
    Implementation-Version: 0.0.1-SNAPSHOT
    Archiver-Version: Plexus Archiver
    Built-By: lijinlong9
    Implementation-Vendor-Id: com.hikvision.algorithm
    Spring-Boot-Version: 1.5.8.RELEASE
    Implementation-Vendor: Pivotal Software, Inc.
    Main-Class: org.springframework.boot.loader.JarLauncher
    Start-Class: com.hikvision.algorithm.HelloWorld
    Spring-Boot-Classes: BOOT-INF/classes/
    Spring-Boot-Lib: BOOT-INF/lib/
    Created-By: Apache Maven 3.3.9
    Build-Jdk: 1.8.0_144
    Implementation-URL: http://projects.spring.io/spring-boot/demo-algorithm/
    

    注意,这里配置了Main-Class,这表示我们可以以java -jar的方式执行这个jar包。Main-Class对应的值为org.springframework.boot.loader.JarLauncher,这表示具体的加载过程是由spring-boot定义的。

    这里有一篇文章分析spring boot
    jar的启动过程
    。我简单看了下这篇文章,并没有细读,我大概猜测到spring-boot实现了一套自己的加载机制,与这个机制相对应的,spring-boot也自定义了一套自己的jar包结构。对我这说,目前了解到这个程度就够了。

    因为不符合Java标准jar包结构,所以无法通过java -cp <package>.<MainClass>的方式运行jar包里的某个类,因为按照标准的jar包结构是找不到这个类的。

    从这一点来看,我们需要重新思考什么样的项目或者module应该做成spring-boot项目?到目前为止,我认为只有完整、可运行的项目或module才需要做成spring-boot项目,比如对外提供rest服务的module。而像common类的module,对外提供公共类库,其本身无法独立运行,则不应该作为spring-boot项目。

    更何况对于多module的项目,将最顶层的module定义为spring-boot项目,而让所有的子module都通过继承顶层module来间接继承spring-boot-starter-parent的做法,应该是大谬的吧。

    总结

    1. Java原生打包、Maven原生打包、shade插件打包的结果,其结构都是一致的。可以使用java -cp的方式执行,一般无法直接使用java -jar的方式执行。
    2. 使用spring-boot插件打包,其结构和上述的结构不同。不能使用java -cp的方式执行,可以使用java -jar的方式执行。
    3. shade插件会忽略provided的依赖,不集成到jar包里;spring-boot插件会将所有的依赖都集成到jar包里。
    4. 默认的情况下,shade插件和spring-boot插件的打包结果,会代替Maven原生打包结果被安装到本地仓库(执行mvn install时),可以通过配置改变这一点。
  • 相关阅读:
    智能推荐算法演变及学习笔记(三):CTR预估模型综述
    从中国农业银行“雅典娜杯”数据挖掘大赛看金融行业数据分析与建模方法
    智能推荐算法演变及学习笔记(二):基于图模型的智能推荐(含知识图谱/图神经网络)
    (设计模式专题3)模板方法模式
    (设计模式专题2)策略模式
    (设计模式专题1)为什么要使用设计模式?
    关于macOS上常用操作命令(持续更新)
    记录下关于RabbitMQ常用知识点(持续更新)
    EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
    SpringCloud教程二:Ribbon(Finchley版)
  • 原文地址:https://www.cnblogs.com/ywjy/p/7771042.html
Copyright © 2011-2022 走看看