概述

在项目实践过程中,有个需求需要做一个引擎能执行指定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时),可以通过配置改变这一点。

Java项目打包方式分析的更多相关文章

  1. Java学习之道:Java项目打包发布

    Java项目打包发布 如果只想发布为一个可执行的jar包,使用eclipse的Export功能就可以了 使用eclipse的Export功能,将项目中的所有package打包为一个pet.jar文件, ...

  2. Java项目打包发布

    Java项目打包发布 如果只想发布为一个可执行的jar包,使用eclipse的Export功能就可以了 使用eclipse的Export功能,将项目中的所有package打包为一个pet.jar文件, ...

  3. Java项目打包在CMD或者Linux下运行

    Java项目打包在CMD或者Linux下运行 1.在CMD下运行 在eclipse中将项目export成jar包,然后用压缩软件解压

  4. java项目打包成可运行的jar,main方法带参数

    转载 原文地址:http://www.cnblogs.com/neillee/p/6063808.html#commentform 将 java 项目打包成可运行的 jar 包(main 函数带参数) ...

  5. Java项目打包成exe的详细教程

    Java项目打包成exe的详细教程 把Java项目打包成exe共分为以下两步: 1. 利用Eclipse先把Java项目先打成jar包 2. 利用exe4j工具把jar包转成exe 这里以Java项目 ...

  6. Maven将java项目打包生成可运行jar

    Maven将java项目打包生成可运行jar Maven插件配置 <plugins> <plugin> <groupId>org.apache.maven.plug ...

  7. 将Java项目打包成可以独立运行的Jar包

    因为一直也没怎么用过Java的原因,所以以前本科上课的时候,在控制台下运行Java程序都是仅仅运行单个的没有第三方库依赖的情况下运行的 那种情况特别简单,只要输入Javac 文件名.java等到编译完 ...

  8. Eclipse将引用了第三方jar包的Java项目打包成jar文件的两种方法

    方案一:用Eclipse自带的Export功能 步骤1:准备主清单文件 “MANIFEST.MF”, 由于是打包引用了第三方jar包的Java项目,故需要自定义配置文件MANIFEST.MF,在该项目 ...

  9. Java项目打包工具安装失败解决方法

    在学习Java的时候我们打包项目但遇到例如以下情况:(提示没有找到java的执行环境! ) 网上眼下有两中的解决方式: (1)选择本地jdk环境; (2)下载Download 可是第一种选择本地老是失 ...

随机推荐

  1. 爬虫实战:爬虫之 web 自动化终极杀手 ( 上)

    欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~ 作者:陈象 导语: 最近写了好几个简单的爬虫,踩了好几个深坑,在这里总结一下,给大家在编写爬虫时候能给点思路.本次爬虫内容有:静态页面的爬 ...

  2. 02-windows 安装以太坊 ethereum 客户端 (win7-64)-大叔思维

    以太坊(Ethereum)是一个运行智能合约的去中心化平台(Platform for Smart Contract),平台上的应用按程序设定运行,不存在停机.审查.欺诈.第三方人为干预的可能.以太坊平 ...

  3. JAVA设计模式总结之23种设计模式

    上一篇总结了设计模式的六大原则<JAVA设计模式总结之六大设计原则>,这一篇,正式进入到介绍23种设计模式的归纳总结. 一.什么是设计模式                         ...

  4. TCP/IP(七)之玩转HTTP协议

    前言 前面一篇的博文简单的介绍了一下属于应用层的HTTP协议,这一篇我将详细的学习HTTP协议,这也是做Web开发中一定要用到的协议.虽然我是做大数据的,但是多学习一点肯定是 没有坏处的.国庆放假7天 ...

  5. 使用LayUI展示数据

    LayUI是一款免费,开源,轻量级的前端cms框架,适用于企业后端,能快速上手开发,集成了常用的组件,还有完善的文档和社区. 点击查看 文档地址 下载框架 使用: 1.把这个5个文件项都拷贝到项目中 ...

  6. 【技巧】datagrid锁定列后重新加载时出现错位问题的解决

    [问题描述]:有时候datagrid设置了锁定列后,在重新加载datagrid数据时,出现锁定列与非锁定列数据错位的问题,如图: [问题分析]:查看css样式我们发现,锁定的列和非锁定的列属于两个不同 ...

  7. 监控利器 sysdig - 每天5分钟玩转 Docker 容器技术(79)

    sysdig 是一个轻量级的系统监控工具,同时它还原生支持容器.通过 sysdig 我们可以近距离观察 linux 操作系统和容器的行为. Linux 上有很多常用的监控工具,比如 strace,tc ...

  8. ASP.NET没有魔法——为什么使用ASP.NET

    ASP.NET是什么? ASP.NET是一个使用HTML.CSS.Javascript来构建动态网站或者网站应用程序的Web框架,并且也可以使用它来构建web API和实时通信技术如web soket ...

  9. 通过npm写一个cli命令行工具

    前言 如果你想写一个npm插件,如果你想通过命令行来简化自己的操作,如果你也是个懒惰的人,那么这篇文章值得一看. po主的上一篇文章介绍了定制自己的模版,但这样po主还是不满足啊,项目中我们频繁的需要 ...

  10. zabbix部署

    zabbix部署 ----2016年年终总结 二 服务器端安装   yum install zabbix-server 客户端安装 yum install zabbix-agent 配置Server ...