【Maven】Maven打包构建完全指南和最佳实践

前言

Apache Maven是一个软件项目管理和构建工具。基于项目对象模型(POM)的概念,Maven可以从一个核心配置文件管理项目的构建,报告和文档。

虽然近年来Gradle作为后起之秀借着Android的东风大肆扩张,Maven作为主流构建工具的地位还是不可动摇的。我们可以看到很多明星项目都是基于Maven构建的,如Spring Boot

Maven的适用范围非常广,可以用来编译、构建、管理依赖、打包、运行测试用例形成测试报告、甚至借助插件直接发布到远程Tomcat等。阅读本文需要你对Maven已经有了大体的了解,本文将从Maven的常见使用场景切入,介绍笔者使用过程中的经验和总结,最终提出最佳实践。

第一个Maven项目

脚手架

安装并配置好Maven后,我们可以用Maven的脚手架命令工具来生成第一个Maven项目。

mvn -B archetype:generate \
  -DarchetypeGroupId=org.apache.Maven.archetypes \
  -DgroupId=com.mycompany.app \
  -DartifactId=my-app

上面的archetype是Maven仓库中内置的脚手架工具,所谓脚手架就是执行这个预置命令就会初始化好一个项目框架,生成一个项目部模板的工具。在模板中可以定义pom.xml预制的依赖项、构建选项等,你也可以自己创建一个Maven项目脚手架发布到Maven仓库中供他人使用。

当然大多数情况下我们用的是IDE的Maven插件来创建的。在Eclipse中我们创建Maven工程时也可以选择预置的脚手架,这通常需要联网下载,由于某些原因国内访问Maven中央仓库非常慢,因此强烈建议配置为阿里云的Maven镜像。

image

最佳实践

笔者在创建项目时一般会在上图勾选create simple project,跳过一些花里胡哨的脚手架模板,自己一步步配置的pom才会真正了如指掌。

在接下来的界面上,Eclipse会提供三个选项:

  • pom,Maven父工程项目,可以向其中添加Module。
  • jar,Java的控制台项目。
  • war,Java的Web项目。

选择你需要的项目类型就可以了。

image

Maven依赖管理

Maven依赖管理可能是最常用的功能之一了,在pom.xml中,通过Maven坐标来引用依赖的jar包,这个坐标的groupId、artifactId和version可唯一确定一个jar包,然后Maven会从配置好的远程仓库中进行下载构建。

<dependencies>
		<dependency>
			<groupId>org.quartz-scheduler</groupId>
			<artifactId>quartz</artifactId>
			<version>2.3.0</version>
			<scope>compile</scope>
		</dependency>
</dependencies>

依赖的作用域

上面代码段中的<scope>选项标识该依赖的作用域,默认scope是compile,scope提供以下选项:

  1. compile (编译范围)

compile是默认的范围;如果没有提供一个范围,那该依赖的范围就是编译范围。编译范围依赖在所有的classpath 中可用,同时它们也会被打包。

  1. provided (已提供范围)

provided 依赖只有在当JDK 或者一个容器已提供该依赖之后才使用。例如, 如果你开发了一个web 应用,你可能在编译 classpath 中需要可用的Servlet API 来编译一个servlet,但是你不会想要在打包好的WAR 中包含这个Servlet API;这个Servlet API jar 由你的应用服务器或者servlet 容器提供。已提供范围的依赖在编译classpath (不是运行时)可用。它们不是传递性的,也不会被打包。

  1. runtime (运行时范围)

runtime 依赖在运行和测试系统的时候需要,但在编译的时候不需要。比如,你可能在编译的时候只需要JDBC API jar,而只有在运行的时候才需要JDBC驱动实现。

  1. test (测试范围)

test范围依赖 在一般的编译和运行时都不需要,它们只有在测试编译和测试运行阶段可用。

  1. system (系统范围)划重点,后面踩过坑

system范围依赖与provided 类似,但是你必须显式的提供一个对于本地系统中jar 文件的路径。这么做是为了允许基于本地对象编译,而这些对象是系统类库的一部分。这样的构件应该是一直可用的,Maven 也不会在仓库中去寻找它。如果你将一个依赖范围设置成系统范围,你必须同时提供一个 systemPath 元素。注意该范围是不推荐使用的(你应该一直尽量去从公共或定制的 Maven 仓库中引用依赖)

  1. import

此范围仅支持<dependencyManagement>中的pom类型。它表示要使用指定POM的<dependencyManagement>中的有效依赖项列表替换的依赖项,即预先定义以依赖列表,按需引用,需要Maven2.9版本以上支持。
示例:

打包构建

开发阶段我们一般都是在IDE里面直接运行Java代码,在项目发布部署时如何导出jar包或war包呢?

非Maven项目

以Eclipse为例,右击项目名,选择Export选择JAR file、Runnable JAR file或WAR file即可

image

Maven项目

Maven的打包策略有很多,在此一打可执行jar包为例进行探讨。首要步骤是需要在pom的<build>节点下添加Maven的打包插件,常见的打包插件有:

  1. 核心插件Maven-compiler-plugin,用于指定编译级别,默认使用jdk1.5编译
  2. 打包可执行jar的Maven-jar-plugin,用于指定本项目生成的jar包中的MANIFEST.MF文件中的配置,如Class-Path和Main-Class
  3. Maven-assembly-plugin,支持定制化打包方式,负责将整个项目按照自定义的目录结构打成最终的压缩包,方便实际部署,可在此处设置打包拷贝路径,配置,以及打包好的jar文件等。
  4. Maven-shade-plugin,用来打可执行包,包含依赖,以及对依赖进行取舍过滤,当你只想将项目打成一个可执行包时,Maven-shade-plugin非常适合。

打包策略选择:

  1. 直接打包,不打包依赖包,仅打包出项目中的代码到jar包中。在POM中添加Maven-compiler-plugin即可,随后执行Maven package。直接打包意义不大,首先Pass。
  2. 将项目和依赖打包成一整个可执行jar包,用到的就是Maven-shade-plugin了。这是常规的做法,由于包含了所有依赖jar包,封装了内部细节,包容一且。
  3. 将依赖jar包输出到lib目录方式,Tomcat就是采用的这种打包策略。这种方案属于将项目结构重新组装,因此要用到的核心打包插件是Maven-assembly-plugin。通过配置,将依赖jar输出到lib目录,与构建后的主项目包分离,若程序需要升级仅需替换主程序jar即可,不必连同lib目录一起替换,大大提高了网络传输效率;结构分离整体上也够直观。

在提出最佳实践之前我们先来了解一下打包策略2需要的配置:

<build>  
    <plugins>  
        <plugin>  
            <groupId>org.apache.Maven.plugins</groupId>  
            <artifactId>Maven-shade-plugin</artifactId>  
            <version>1.4</version>  
            <executions>  
                <execution>  
                    <phase>package</phase>  
                    <goals>  
                        <goal>shade</goal>  
                    </goals>  
                    <configuration>  
                        <filters>  
                            <filter>  
                                <artifact>*:*</artifact>  
                                <excludes>  
                                    <exclude>META-INF/*.SF</exclude>  
                                    <exclude>META-INF/*.DSA</exclude>  
                                    <exclude>META-INF/*.RSA</exclude>  
                                </excludes>  
                            </filter>  
                        </filters>  
                        <transformers>  
                            <transformer  
                                implementation="org.apache.Maven.plugins.shade.resource.ManifestResourceTransformer">  
                                <mainClass>com.cat.Application</mainClass>  
                            </transformer>  
                            <transformer  
                                implementation="org.apache.Maven.plugins.shade.resource.AppendingTransformer">  
                                <resource>META-INF/spring.handlers</resource>  
                            </transformer>  
                            <transformer  
                                implementation="org.apache.Maven.plugins.shade.resource.AppendingTransformer">  
                                <resource>META-INF/spring.schemas</resource>  
                            </transformer>  
                        </transformers>  
                    </configuration>  
                </execution>  
            </executions>  
        </plugin>  
    </plugins>  
</build>  

shade插件绑定的是package生命周期目标,并设置com.cat.Application为Main-Class,以及将META-INF/spring.*文件合并(追加而非覆盖),并过滤掉所有依赖的META/INF中SF,DSA,RSA后缀文件。这里涉及到filter配置和transformer配置。

最佳实践

综上所述,我们选择策略3作为最佳实践(向Tomcat看齐),首先看一下打包后的项目结构:

D:\BANKSERVER //根目录
│  bankServer.jar //主程序包
│  README.md      //说明文档
│  startup.bat    //启动脚本
│  
├─conf //配置文件目录
│      config.properties //配置文件
│      database.properties //数据库配置文件
│        
├─lib //依赖目录
│      aopalliance-1.0.jar
│      bankServer-0.0.1-SNAPSHOT.jar
│      c3p0-0.9.5.2.jar
│      my-Entry-1.0.jar
│      commons-lang3-3.8.1.jar
│      druid-1.1.8.jar

这里用到了三个插件,Maven-compiler-plugin指定编译级别;Maven-jar-plugin主要就是配置了MANIFEST.MF这个文件而已,就是让可执行文件知道自己怎么执行,加载哪些文件执行的描述,剩下的工作交由Maven-assembly-plugin来处理。

贴上来pom.xml的配置:

<build>
		<finalName>bankServer</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.Maven.plugins</groupId>
				<artifactId>Maven-compiler-plugin</artifactId>
				<version>3.7.0</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
					<encoding>utf-8</encoding>
				</configuration>
			</plugin>

			<plugin>
				<groupId>org.apache.Maven.plugins</groupId>
				<artifactId>Maven-jar-plugin</artifactId>
				<configuration>
					<archive>
						<addMavenDescriptor>false</addMavenDescriptor>
						<!-- 指定main信息 -->
						<manifest>
						    <!--指定添加项目中使用的外部jar的classpath项-->
							<addClasspath>true</addClasspath>
							<classpathPrefix>lib/</classpathPrefix>
							<!-- 此处指定main方法入口的class -->
							<mainClass>com.cat.bank.main.Luncher</mainClass>
						</manifest>
					</archive>
				</configuration>
			</plugin>
			<plugin>
				<artifactId>Maven-assembly-plugin</artifactId>
				<configuration>
					<appendAssemblyId>false</appendAssemblyId>
					<descriptors>
					    <!--指定详细的外部配置,读取这个文件,防止pom.xml过于臃肿-->
						<descriptor>packconf/package.xml</descriptor>
					</descriptors>

				</configuration>
				<!--下面是为了使用 mvn package命令,如果不加则使用mvn assembly -->
				<executions>
					<execution>
						<id>make-assembly</id>
						<phase>package</phase>
						<goals>
							<goal>assembly</goal>
						</goals>
					</execution>
				</executions>
			</plugin>

		</plugins>
	</build>

Maven-assembly-plugin引用的package.xml:

<?xml version="1.0" encoding="UTF-8"?>
<assembly>
	<id>bin</id>
	<!-- 最终打包成一个用于发布的zip文件 -->
	<formats>
		<format>zip</format>
	</formats>
	<!-- Adds dependencies to zip package under lib directory -->
	<dependencySets>
		<dependencySet>
			<!-- 不使用项目的artifact,第三方jar不要解压,打包进zip文件的lib目录 -->
			<useProjectArtifact>true</useProjectArtifact>
			<outputDirectory>lib</outputDirectory>
			<unpack>false</unpack>
		</dependencySet>
	</dependencySets>
	<fileSets>
		<!-- 把项目相关的说明文件,打包进zip文件的根目录 -->
		<fileSet>
			<directory>${project.basedir}</directory>
			<outputDirectory>/</outputDirectory>
			<includes>
				<include>README*</include>
				<include>LICENSE*</include>
				<include>NOTICE*</include>
			</includes>
		</fileSet>

		<!-- 把项目的配置文件,打包进zip文件的conf目录 -->
		<fileSet>
			<directory>${project.basedir}/conf</directory>
			<outputDirectory>conf</outputDirectory>
			<includes>
				<include>*</include>
			</includes>
		</fileSet>

		<!-- 把项目的脚本文件目录( src/main/scripts )中的启动脚本文件,打包进zip文件的跟目录 -->
		<fileSet>
			<directory>${project.build.scriptSourceDirectory}</directory>
			<outputDirectory></outputDirectory>
			<includes>
				<include>startup.*</include>
			</includes>
		</fileSet>

		<!-- 把项目自己编译出来的jar文件,打包进zip文件的根目录 -->
		<fileSet>
			<directory>${project.build.directory}</directory>
			<outputDirectory></outputDirectory>
			<includes>
				<include>*.jar</include>
			</includes>
		</fileSet>
	</fileSets>
</assembly>

问题记录:

  1. Java代码读取不到jar包外部配置文件

项目发布的时候通常会修改配置文件,这种情况下就需要把配置文件放到jar外部读取而不能直接打到jar包内。默认的config.properties文件是放在src/main/resources路径,即classpath下的,采用如下代码读取,构建后config.properties文件被打到了conf下,尝试多种方案,即使将"config.properties"加上相对路径也读取不到,若用System.getProperty("user.dir")这种方法在IDE下就不能运行了,因此亟需找到一个两全其美的办法同时支持在IDE下读取和构建后的外部目录读取。

原读取方法:

InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("conf/config.properties");   
Properties p = new Properties();   
try {   
   p.load(inputStream);   
} catch (IOException e1) {   
   e1.printStackTrace();   
}  

解决方案:

1). 将config.properties和database.properties从src/main/resources移到项目根目录的普通文件夹conf

2). 修改读取方式为文件流

try {
	Properties properties = new Properties();
	// 1.加载properties文件
	InputStream is =new FileInputStream("conf/config.properties");
	// 2.加载输入流
	properties.load(is);
} catch (Exception e) {
	logger.error("读取配置文件config.properties错误");
}
  1. 本地jar包打包后Maven无法加载

项目中需要引入一个my-Entry.jar的加密jar包,这个包Maven仓库中是没有的。回忆一下前面提到的依赖的system作用域,因此采用如下方式导入了该jar包,但是打包后调用该包内方法时提示Class Not Found。

<dependency>
	<groupId>com.cat</groupId>
	<artifactId>my-Entry</artifactId>
	<!--注意这个版本号,下面会用到-->
	<version>1.0</version>
	<scope>system</scope>
	<systemPath>${project.basedir}/libs/my-Entry.jar</systemPath>
</dependency>

这是因为scope为system作用域时不参与运行时,解决这个问题有两种方案:

解决方案:

1)将该jar安装到本地仓库

执行以下命令,发布到本地仓库,然后修改pom.xml将scope标签去掉即可。

mvn install:install-file -Dfile=D:\my-Entry.jar -DgroupId=com.cat -DartifactId=ccb-Entry -Dversion=1.0 -Dpackaging=jar

2)将外部包加入主jar包META-INF下的MANIFEST.MF文件

这个MANIFEST.MF里指定了外部依赖的jar包查找路径,需要在在Maven-jar-plugin 里配置Class-Path。

<plugin>
	<groupId>org.apache.Maven.plugins</groupId>
	<artifactId>Maven-jar-plugin</artifactId>
	<configuration>
		<archive>
			<addMavenDescriptor>false</addMavenDescriptor>
			<!-- Manifest specific configuration -->
			<manifest>
				<addClasspath>true</addClasspath>
				<classpathPrefix>lib/</classpathPrefix>
				<!-- 此处指定main方法入口的class -->
				<mainClass>com.cat.bank.main.Luncher</mainClass>
			</manifest>
			<manifestEntries>
			    <!--注意这里要加上-1.0版本号,就是你引入本地依赖时指定的那个version-->
                <Class-Path>lib/my-Entry-1.0.jar</Class-Path>
            </manifestEntries>
		</archive>
	</configuration>
</plugin>

现在用解压软件打开bankServer.jar主jar文件会发现MANIFEST.MF文件中已经加上了my-Entry.jar。但是这时这个my-Entry.jar并没有发布到lib文件夹下,我们需要再配置Maven-assembly-plugin将scope为system的依赖也发布到lib目录,或者可以直接拷贝过去。

在package.xml中追加一行配置,这样就可以正常打包运行啦~

	<dependencySets>
		<dependencySet>
			<!-- 不使用项目的artifact,第三方jar不要解压,打包进zip文件的lib目录 -->
			<useProjectArtifact>true</useProjectArtifact>
			<outputDirectory>lib</outputDirectory>
			<unpack>false</unpack>
		</dependencySet>
		<!--将<scope>system</scope>也发布到lib-->
		<dependencySet>
			<useProjectArtifact>true</useProjectArtifact>
			<outputDirectory>lib</outputDirectory>
			<unpack>false</unpack>
			<scope>system</scope>
		</dependencySet>
	</dependencySets>
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值