1.情景展示
在maven诞生之前,我们基于springMVC分层开发思想,会将页面层(View),控制层(Controller),业务层(Bo),数据层(Dao)等通过创建不同的包加以区分。
使用maven创建项目后,为了方便对项目进行分类管理,本着最小依赖原则,我们不再局限于分包,还可以分模块,即:将项目进行模块化拆分管理。
这样一来,虽然管理方便了,项目也变得更加清晰了,但是,项目如何进行打包却成了新手上路的拦路虎。
如何将模块化后的项目,打包进同一个jar包里面?
2.具体分析
SpringBoot项目
使用spring-boot-maven-plugin插件,不需要额外处理,springboot内置插件会自动帮我们完成项目的打包。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
非SpringBoot项目
需要借助插件maven-assembly-plugin来完成,当然,还需要一系列的参数配置。
该插件支持自定义的打包结构,也可以定制依赖项。
后面会详细说明。
二者区别:
spring-boot-maven-plugin:
所在位置:该插件的引用,需要放在需要启动模块所在的pom文件当中(通常是web模块)。
jar包构成:
外部jar包处理方式:项目,所依赖的外部jar包,也会被塞到lib文件夹下。
打包命令:
mvn clean package
maven-assembly-plugin:
所在位置:该插件可以被放到启动模块的pom当中,也可以放置到父级pom.xml当中(父模块)。
jar包构成:
下面会有。
外部jar包处理方式:项目,所依赖的外部jar包会被解压,并将解压后得到的class文件,按照原有目录结构塞到启动模块所在jar包当中。
打包命令:
mvn clean package assembly:single
共同点:项目的所有子模块,都会被打成jar包。
3.基础普及
项目模块化
如何对maven项目进行模块化管理(将maven整体项目拆分多个模块)?
我们可以从上图当中看到,我这只是一个普通的java项目,里面存放的是自己整理的常用工具类。
本文的打包讲解,也以此项目进行实战说明。
将上面每一个包,拆分成一个模块。
选中项目,右键,New-->Module。
在弹出的窗口,可以设置模块所使用的java版本。
在这里,我们可以设置新模块的名称,以及设定它的父模块(这里我使用系统默认,即,将项目名作为父级模块)。
这里有三点需要说明:
第一,关于父模块名称。
父级模块名称=项目名称。(我喜欢这种方式);
父级模块名称=项目名称-parent。
如果喜欢第二种命名方式,我们需要把项目进行重命名。
第二,关于子模块名称。
子模块的名称是跟项目的目录结构息息相关的,一般情况下,按照springMVC的分层架构思想进行模块化;
当然,也要跟自己的实际业务相符,比如消息中间件、缓存、对外部公司提供的接口等都可以划分成单独的模块。
最关键的就是:见名知意。
子模块名称=项目名-有意义的名字。(我喜欢这种)
子模块名称=有意义的名字。
第三,子模块包名路径。
说明:分模块的意思并不是分项目,所以子模块的包名前缀要保持一致。
一般情况下,包名的路径构成为:
com.公司名称.项目.模块名,例如:code.marydon.encapsulation.all。
创建的目录结构如下:
我们可以看到,我是完全按照之前的目录结构,将项目进行了模块化拆分。
但是,现在,还远远没有结束。
多模块的特性
当我们把项目进行模块化拆分后,项目根目录(父目录)下的pom.xml,自动将打包形式设置成了pom。
这也就意味着:
项目在打包时候,不再像之前不分模块时那样,项目只生成一个jar包;
并且,项目的所有classes文件也无法集成到一个jar包当中。
如果,我们强行将父级pom文件当中的打包形式,改成:jar后,那么,在执行mvn命令进行打包时,会报错,提示:
父级pom文件的打包形式只能为:pom。
另外,我们可以看到:
项目模块,会自动被纳入到父级pom.xml当中。
该怎么办?
需要额外创建的模块
既然如此,我们就需要创建一个与子模块同级的模块,该模块不包含任何代码,仅仅是为了解决多个子模块代码不能合并至一个jar包的问题。
关于最终要囊括所有子模块的名称的命名规范
名称1:assembly。翻译成中文叫装配,又因为它使用的插件maven-assembly-plugin。
名称2:core。也有人认为,它囊括了所有模块,是整个项目的核心。
名称3:all。正因为它囊括了所有模块,所以,还有人认为,应该叫all。(我更喜欢这个名字)
项目最终目录结构如下:
小结:
特性1:父级模块,打包形式被强行指定为:pom,并无法改变。
特性2:父级模块,生成的jar包当中,不包含任何有意义的内容。
特性3:子级模块,会被打成一个个独立的jar包(默认是jar包)。
说明:
该模块在父模块的引用当中至于最后一个;
另外,如果模块之间有先后顺序的话(比如:bo模块需要依赖dao模块),被依赖的模块(dao),应该在依赖模块的上面(bo)。
附加模块的pom.xml
在该模块对应的pom.xml当中添加对于与它同级模块的依赖,并遵循先后顺序。
准备工作已经就绪。
如果我们不使用maven-assembly-plugin插件打包,会出现什么效果?
4.使用maven-jar-plugin打包(✖)
说明:无法达到我们想到的结果。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<!-- 存档 -->
<archive>
<!--是否添加maven 描述-->
<!--
true:生成的jar中,包含pom.xml和pom.properties这两个文件
false:最终打的包,没有pom.xml和pom.properties
-->
<addMavenDescriptor>false</addMavenDescriptor>
<!-- 生成MANIFEST.MF的设置 -->
<manifest>
<!--这个属性特别关键,如果没有这个属性,有时候我们引用的包maven库
下面可能会有多个包,并且只有一个是正确的,其余的可能是带时间戳的,
此时会在classpath下面把那个带时间戳的给添加上去,
然后我们 在依赖打包的时候,打的是正确的,所以两头会对不上,报错。 -->
<useUniqueVersions>false</useUniqueVersions>
<!-- 为依赖包添加路径, 这些路径会写在MANIFEST文件的Class-Path下 -->
<addClasspath>true</addClasspath>
<!-- 将要生成的jar包所依赖的jar包,添加classPath的时候的前缀,
如果这个jar本身和依赖包处于同一级目录,则不需要添加 -->
<classpathPrefix>lib/</classpathPrefix>
<!-- jar启动入口类 -->
<mainClass>包名.类名</mainClass>
</manifest>
<manifestEntries>
<!-- 在Class-Path下添加配置文件的路径 -->
<Class-Path>./</Class-Path>
</manifestEntries>
</archive>
<!-- jar包的位置(输出目录) -->
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</plugin>
</plugins>
</build>
我们可以看到:
javaUtils-all模块最终打的jar包当中,有且只有只有一个META-INF,没有项目的源码信息。
但是,子模块还是能够被完整打包的。
所以说,如果我们没有合并模块的需求的话,使用任何一个打包插件,都能完成分模块打包。
但是,我们要的是将项目的代码,最终合到一块,如何实现?
5.使用maven-assembly-plugin进行打包(✓)
传统版:
将插件放置在父pom.xml当中。
<!--build是可以被继承的,这就是为什么将其置于父 POM 中。这样一来,父 POM 中的各个子模块就相当于都使用了此元素,这样在父 POM 中运行打包命令,各个子模块就会各自都进行一次打包。-->
<!--一般来说,父 POM 中是不编写任何有实际意义的 Java 代码的。
在父 POM 中,一般使用 <dependencyManagement>,而不是 <dependencies>。
因此,父 POM 中不会包含任何依赖,所以父 POM 生成的 JAR 包里不会有任何实际的内容。
读者需要自行找到一个通过直接和间接依赖能依赖其它所有有实际内容的模块的“核心模块”,这个“核心模块”中的依赖应该是完整的,且会包含所有完整代码、资源文件。
“核心模块”往往是一种程序入口模块。-->
<build>
<plugins>
<!-- 此插件必须放在父 POM 中 -->
<!--在${project.basedir}目录下(项目根目录),执行cmd命令:
(如果是在idea当中,直接打开Terminal窗口,执行命令就可以了)
打包命令:mvn package assembly:single
先清理再打包:mvn clean package assembly:single
跳过单元测试:mvn clean package assembly:single -Dmaven.test.skip=true
安装到本地仓库:
mvn install assembly:single -Dmaven.test.skip=true
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<!--<finalName>${project.name}-${project.version}</finalName>-->
<!--如果不设为false的话,最终打包的包名将为:javaUtils-all-3.0-SNAPSHOT-jar-with-dependencies.jar-->
<appendAssemblyId>false</appendAssemblyId>
<!-- 设置 JAR 包输出目录
构成:
这里,将打包后的 JAR 包放在每个模块的文件夹 target/maven-assembly-plugin下 -->
<outputDirectory>${project.build.directory}/maven-assembly-plugin</outputDirectory>
<!--要使用maven-assembly-plugin,需要指定至少一个要使用的assembly descriptor 文件-->
<!-- 设置打包后的 JAR 包名称(设置生成的 JAR 包后缀名) -->
<!--JAR 包的名称为:
模块名-模块版本号-descriptorRef标签值-->
<!--descriptorRef必须有,否则打包失败-->
<!--
maven-assembly-plugin内置了几个可以用的assembly descriptor:
bin : 类似于默认打包,会将bin目录下的文件打到包中
jar-with-dependencies : 会将所有依赖都解压打包到生成物中【本次需求正好是将所有依赖也打包】
src :只将源码目录下的文件打包
project : 将整个project资源打包
-->
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<!-- 如果项目没有启动类(程序入口),也就是说打的包不是可执行的,那我们就不必声明<archive>标签 -->
<archive>
<manifest>
<!-- 配置程序运行入口所在的类 -->
<!--
一般指向到程序人口,即main方法
但是这里并没有想要打包成的jar可以直接运行,
所以就不需要为此写一个main方法了,
不会影响打包成的jar包
-->
<mainClass>包名.类名</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<!--
执行本插件的方法为,在主目录下执行如下命令:
mvn package assembly:single
对于 IntelliJ IDEA,生成的 JAR 包位于每个模块下的文件夹 target
-->
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<!-- 只执行一次 -->
<!-- 此处 IntelliJ IDEA 可能会报红,这是正常现象 -->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
我们来到项目所在的目录下;
并执行以下打包命令:
mvn clean package assembly:single -Dmaven.test.skip=true
项目打包成功,会出现:BUID SUCCESS字样。
包含项目完整源码的jar包所在位置:
不是在启动模块的target目录下
target目录下,生成的jar包,没有源码,是因为我们并未在该启动模块写任何代码(打的包并没有错,只不过,不是我们想要的)。
而是在启动模块/target/maven-assembly-plugin目录下
我们可以看到:
启动模块,打成的jar包不仅包含了项目的所有源码;
而且包含了该模块所依赖的所有外部jar包的源码。
其它子模块长啥样子呢?
首先,target目录下的jar包,包含了该模块的源码(然并卵,插件不认)。
其次,maven-assembly-plugin目录下生成的jar包,不仅仅包含了该模块的代码,而且,也包含了该模块所依赖的外部jar包的所有源码文件。
但是,对于该插件来说,当我们执行安装命令时,它并不是将target目录下的jar包安装到了本地仓库,而是采用的maven-assembly-plugin的jar包。
但是,对我们来说,除了模块当中的代码是我们真正需要的之外,模块所需的外部依赖,根本不需要塞进来,怎么办?
进化版,可以解决部分问题。
安装到本地仓库指令:
mvn clean install assembly:single -Dmaven.test.skip=true
进化版:
将插件放置在父pom.xml当中;
并且在除启动模块之外的非启动模块(子模块),排除添加对外部模块所依赖的jar包的装配。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<!--不对该模块单独打包-->
<skipAssembly>true</skipAssembly>
</configuration>
</plugin>
</plugins>
</build>
这样一来,只有启动模块,包含:项目所有依赖的jar包的源码;
其它子模块,将不再产生该插件所特有的jar包(也就是maven-assembly-plugin目录)。
虽然子模块不再单独打包,但是,在执行maven安装到本地仓库命令时,这些模块的jar包,还是能够一个个安装上的(具体原理没搞明白,估计是因为maven在执行安装命令的时候,检索到该启动模块依赖了其它模块,就把其它模块执行了打包命令?)。
安装完成后,我们需要注意的是:(打包package命令,也跟下面一样)
包含项目所有源码的jar包不再是javaUtils目录下的jar包;
这里面囊括多少代码,取决于父pom.xml添加了几个依赖(里面并不包含任何项目源码)。
而是javaUtils-all目录下的jar包。
高级版:
将插件放置在启动模块所在的pom.xml当中;
<build>
<plugins>
<!-- 此插件必须放在启动模块所在 POM 中 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<!-- 配置程序运行入口所在的类 -->
<!--
一般指向到程序人口,即main方法
但是这里并没有想要打包成的jar可以直接运行,
所以就不需要为此写一个main方法了,
不会影响打包成的jar包
-->
<mainClass>包名.类名</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<!--
执行本插件的方法为,在启动模块所在目录下执行如下命令:
mvn package assembly:single
-->
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<!-- 只执行一次 -->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
说明:如果项目没有启动类(程序入口),也就是说打的包不是可执行的,那我们就不必声明<archive>标签。
然后,切换到启动模块所在目录;
并运行maven命令。
mvn clean package assembly:single -Dmaven.test.skip=true
打包完成后,只在启动模块/target/maven-assembly-plugin目录下生成的jar包,包含项目完整代码。
另外,其它子模块将不会被打包。
为什么?
其实,很简单,因为其它子模块没有引入导包插件,自然不会被打包啦。
一般情况下,我们只想拿到项目进行打包,并不想对模块进行单独打包,你可能会说模块单独打包当然有用,我们在更新发布的项目的时候,只需要更新特定的模块就可以啦。
但是,不好意思,这个插件提供的jar-with-dependencies这种打包方式,根本无法满足你说的上述应用场景。
因为,项目最终的jar包,也就是启动模块所打的jar包里面,囊括的是项目所有源码,它是一个整体,我们根本无法替换部分内容,没办法只能全部换掉(重新打包)。
当然了,如果你说,我可以自定义打包方式,那自然是可以的,就需要自行拓展啦。
所以,在运行jar包的时候,这种方式最实用。
但是,无法将该jar包安装到本地仓库,为什么?
因为该启动模块pom.xml当中依赖了其它模块,所以maven会先下载它所依赖的jar包;
由于我们并没有对非启动子模块进行打包,所以maven根本找不到对应的jar包,自然会安装失败;
尽管,启动模块所生成的jar包,已经包含了所有源码文件。
适用场景:
仅使用于项目部署阶段,无法将其发布到本地仓库,除非,我们把它所依赖的其它模块jar包也发布上去。
写在最后
哪位大佬如若发现文章存在纰漏之处或需要补充更多内容,欢迎留言!!!