在项目实践过程当中,有个需求须要作一个引擎能执行指定jar包的指定main方法。node
起初咱们以一个简单的spring-boot项目进行测试,使用spring-boot-maven-plugin
进行打包,使用java -cp demo.jar <package>.<MainClass>
执行,结果报错找不到对应的类。web
我分析了spring-boot-maven-plugin
打包的结构,又回头复习了java原生jar
命令打包的结果,以及其余Maven打包插件打包的结果,而后写成这边文章。spring
这篇文章里会简单介绍java原生的打包方式,maven原生的打包方式,使用maven shade插件将项目打成一个大一统的jar包的方式,使用spring-boot-maven-plugin
将项目打成一个大一统的jar包的方式,并比较它们的差别,给出使用建议。apache
为了简单起见,假设咱们的项目只有一个HelloWorld.java
,不使用Maven。假设当前目录为.
,初始目录下没有任何内容。api
首先,咱们在当前目录新建文件HelloWorld.java
。为了演示如何让编译的class文件自动放置到与package
对应的目录结构中,特意添加package
命令。app
package com.hikvision.demo; public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } }
在当前目录新建target
子目录,此时,目录结构以下:maven
./ ├─ HelloWorld.java ├─ target/
命令:javac HelloWorld.java -d target
。目录结构变为:ide
./ ├─ HelloWorld.java ├─ target/ ├─ com/hikvision/demo/ ├─ HelloWorld.class
命令:jar cvf demo-algorithm.jar -C target/ .
。目录结构变为:spring-boot
./ ├─ 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包结构。
我通常使用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打包插件应该不止一种,这里使用的是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
的作法,应该是大谬的吧。
java -cp
的方式执行,通常没法直接使用java -jar
的方式执行。java -cp
的方式执行,可使用java -jar
的方式执行。mvn install
时),能够经过配置改变这一点。