# 构建

  • 简单的 Java 项目只包含少量源文件,可用 javac 命令手动编译。
  • 复杂的 Java 项目包含大量源文件、配置文件、依赖库等,手动编译很麻烦,因此通常用 Ant、Maven、Gradle 等工具自动地构建(build),包括编译(compile)、测试(test)、打包(package)等步骤。
  • 构建 Java 项目时,通常将编译生成的类文件,与配置文件、依赖 jar 等一起,打包成一个 ZIP 格式的压缩包,扩展名为 .jar 。
    • 构建 Java Web 项目时,通常打包的扩展名为 .war 。
    • jar 或 war 包是一个独立的 Java 程序,可以被 java 命令直接运行。

# 编译

Java 代码需要先编译才能运行,基本的编译流程如下:

  1. 安装 JDK 。
  2. 编写一个源文件 Hello.java :
    public class Hello {
      public static void main(String[] args) {
          System.out.println("Hello world");
      }
    }
    
  3. 用 javac 命令编译源文件,生成字节码,存储在 .class 类文件中。
    [root@CentOS ~]# ls
    Hello.java
    [root@CentOS ~]# javac Hello.java   # 这会编译生成一个类文件 Hello.class
    [root@CentOS ~]# ls
    Hello.class  Hello.java
    
  4. 用 java 命令运行 .class 类文件:
    [root@CentOS ~]# java Hello
    Hello world
    [root@CentOS ~]# java Hello.class   # 运行类文件时不需要输入扩展名 .class ,否则会被当做类名的一部分
    Error: Could not find or load main class Hello.class
    
  • 字节码(Byte-code)

    • :一种二进制代码,是介于编程语言代码与机器代码之间的中间代码。
  • 类文件

    • :一种扩展名为 .class 的文件,用于存储 .java 源文件编译生成的字节码,因此又称为字节码文件。

# JVM

  • 编译生成的 class 文件,不是机器代码,不能被计算机直接运行,只能被 JVM 程序运行。

    • 用 java 命令运行 Java 程序时,原理是启动一个 JVM 进程,读取 class 文件中的字节码,编译成当前操作系统的机器代码,然后执行。
    • C 代码编译是直接生成当前操作系统的机器代码。而 Java 代码编译是生成字节码,便于移植到不器平台的 JVM 上运行。
  • 为什么 Java 编程语言只能编译成字节码,而不能编译成机器代码?因为字节码便于移植到不同平台的 JVM 上运行。

    • JVM 程序通常开发了多个版本,用于适配不同机器平台、操作系统。
    • 不同平台的 JVM 向上提供相同的接口,用法相同,使得同一个 class 文件可以被不同平台的 JVM 运行。
  • JVM

    • :Java 虚拟机(Java Virtual Machine),是一个负责运行 class 文件的程序。
    • Java 编程语言与 JVM 程序是绑定的。如果用户编写的 Java 代码,使用了最新的语法特性,则只能用最新版本的 JVM 程序运行。
  • 有多家公司研发了不同的 JVM 程序。目前最常用的一款 JVM 程序,是 HotSpot 。

    • HotSpot 采用 C++ 语言开发。
    • HotSpot 采用即时编译(Just In Time,JIT)技术。不会一次性编译全部字节码,而是一边读取字节码,一边解释执行。并且找出经常被执行的某些字节码,称为热点(hot spot),将它们编译成机器码再执行,从而提高效率。

# JDK

  • JDK

    • :Java 开发环境工具包(Java SE Development Kit),包含 JRE 以及开发、调试、监控工具。
  • JRE

    • :Java 运行环境工具包 (Java Runtime Environment),只提供 JVM 、类库。
    • JRE 的体积比 JDK 小,可以节省磁盘空间。
  • 存在哪些 JDK ?

    • 目前 JDK 的官方标准实现是 OpenJDK ,由开源社区维护。
    • 有多家公司基于 OpenJDK 源代码,制作了自己的 JDK 二进制发行版。例如:
      • Oracle JDK :由 Oracle 公司发布,从 2019 年开始收费。
      • Temurin JDK :由 Eclipse 基金会发布,开源。
        • 2001 年,IBM 公司发布了一个名为 Eclipse 的 IDE ,主要用于 Java 开发。
        • 为了与微软公司的 Visual Studio IDE 竞争,IBM 公司开源了 Eclipse ,并成立了 Eclipse 开源软件基金会。

# 相关历史

  • 1991 年,Sun 公司的 James Gosling 等人开始研发一种适用于单片机系统的编程语言。
    • 他们将 C++ 语言进行简化,抛弃了多继承、指针等复杂功能,并提高程序的兼容性。
    • 他们将这种语言取名为 Oak ,介绍给硬件厂商,但并没有受到欢迎。
  • 1995 年,Sun 公司发现 Oak 语言在互联网上的应用优势:它容易移植到不同平台上运行。于是将它改名为 Java 重新发布,终于成功推广。
  • Sun 公司设计 Java 编程语言时,最初研发了一款 JVM 程序,但性能差。于是在 1999 年,Sun 公司研发了一款新的 JVM 程序,称为 HotSpot ,性能更好。
  • 2007 年,Sun 公司将 HotSpot 与一些配套软件开源,这个开源项目称为 OpenJDK (opens new window)
  • 2010 年,Sun 公司被 Oracle 公司收购。而 OpenJDK 项目也由 Oracle 公司主导,但依然属于开源社区。

# 安装

  • 用 yum 或 apt 安装 JDK :

    yum install java-1.8.0-openjdk-devel
    
    apt install openjdk-8-jdk
    
  • 或者从官网 (opens new window)下载二进制版:

    wget http://download.oracle.com/otn-pub/java/jdk/8u141-b15/336fa29ff2bb4ef291e347e091f7f4a7/jdk-8u141-linux-x64.tar.gz --header "Cookie: oraclelicense=accept-securebackup-cookie"
    tar -zxvf jdk-8u141-linux-x64.tar.gz -C /usr/local/
    
    # 配置环境变量,从而能定位 java 命令
    echo 'export JAVA_HOME=/usr/local/jdk1.8.0_141' >> /etc/profile
    echo 'export PATH=$PATH:$JAVA_HOME/bin' >> /etc/profile
    source /etc/profile
    
  • 或者用 Docker 部署:

    docker run -it --rm openjdk:8-jdk java
    

    2022 年,Docker Hub 宣布停止维护 openjdk 镜像,用户可以改用 Temurin JDK :

    docker run -it --rm eclipse-temurin:17-jdk-jammy java
    

# 命令

# javac

javac
    *.java                # 编译源文件
    -sourcepath <path>    # 查找源文件的路径
    -classpath <path>     # 用于查找类库的路径。该配置会覆盖环境变量 CLASSPATH
    -d .                  # 编译出的类文件的保存路径
    -encoding utf-8       # 源文件的编码格式
  • path 默认为当前目录。如果指定多个路径,在 Winodws 上用 ; 分隔,在 Linux 上用 : 分隔。

# java

java
    <class name>                  # 运行类文件(在前台运行)
    -jar xx.jar                   # 运行 jar 包
    -jar xx.war --httpPort=8080   # 运行 war 包

    -classpath <path>             # 用于查找类库(通常保存为 jar 包)的路径。该配置会覆盖环境变量 CLASSPATH
    -cp <path>                    # 等价于 -classpath 选项
    -D  <property>=<value>        # 配置一个属性的值。可以多次使用该选项。可用 System.out.println(System.getProperty("user.timezone")); 查看一个属性的值
        user.timezone=GMT+08      # 时区
        java.io.tmpdir=/tmp       # 临时目录,供 JVM 创建、保存临时文件

    -version                            # 打印 JDK 或 JRE 的版本信息
    -verbose jar                        # 打印所有 jar 依赖包的文件路径
    -XshowSettings:properties -version  # 打印所有系统属性,例如 java.runtime.version
    -XX:+PrintCommandLineFlags          # 打印当前生效的 java 命令行参数,例如 -XX:MaxHeapSize 容量、采用哪种 GC 垃圾收集器
    -XX:+PrintFlagsFinal                # 打印当前所有 flag 配置参数的取值。每个参数原本会显示默认值,但如果用户修改了该参数的值,则显示的是当前生效的值

    # 关于 Heap 内存
    -Xms??                        # -XX:InitialHeapSize ,堆内存的初始容量(并不是最小容量),默认为主机 RAM 的 1/64 。可配置 -Xmx100M、-Xms1G 等单位
    -Xmx??                        # -XX:MaxHeapSize ,堆内存的最大容量,默认为主机 RAM 的 1/4
    -Xmn??                        # 堆内存中 young 区域的容量,剩下的堆内存则属于 old 区域。配置了该参数时,会自动配置 -XX:NewSize 和 -XX:MaxNewSize
    -XX:NewSize=??                # young 区域的初始容量
    -XX:MaxNewSize=??             # young 区域的最大容量
    -XX:NewRatio=2                # 堆内存中,old 区域容量是 young 的多少倍,默认为 2 ,即让 -Xmn 等于 -Xmx/(2+1)
    -XX:SurvivorRatio=8           # young 区域中,eden space 容量是单个 survivor space 容量的多少倍,默认为 8 。默认有 2 个 survivor space ,因此每个的容量为 NewSize/(8+2)
    -XX:+UseAdaptiveSizePolicy    # 启用自适应大小策略,每次 GC 后,会根据 GC 性能指标自动调整 eden space、survivor space 容量的比例。采用 ParallelGC 时默认启用该功能,但显式配置 SurvivorRatio 时该功能不生效
    -XX:InitialTenuringThreshold=7
    -XX:MaxTenuringThreshold=15   # TenuringThreshold 的最大值

    # 关于 Metaspace 内存。一般不需要配置,因为一般的 Java 程序只占用几十 MB
    -XX:MetaspaceSize=20.8M       # Metaspace 的初始容量,默认为 20.8M
    -XX:MaxMetaspaceSize=??       # Metaspace 的最大容量,默认不限制
        # 当 Metaspace 当前容量写满时,会触发 full GC 。如果 full GC 之后依然内存不足,则会增加当前容量
        # 如果 full GC 之后想增加当前容量,却达到 MaxMetaspaceSize 限制,则会抛出 OutOfMemoryError 错误
        # 如果决定配置 MaxMetaspaceSize ,则建议将 MetaspaceSize 配置为相同的值,避免多次扩容之前的 full GC
    -XX:MinMetaspaceFreeRatio=40  # 最小空闲率。如果 full GC 之后,Metaspace 中空闲内存所占百分比小于该值,则认为空闲内存太少,于是扩容 MetaspaceSize
    -XX:MaxMetaspaceFreeRatio=70  # 最大空闲率。如果 full GC 之后,Metaspace 中空闲内存所占百分比大于该值,则认为空闲内存太多,于是减小 MetaspaceSize

    # 关于其它内存
    -XX:MaxDirectMemorySize=??    # Direct Memory 的最大容量,默认等于 -Xmx 。一般不需要配置,因为一般的 Java 程序只占用几十 MB
    -Xss1M                        # 每个线程的堆栈大小,默认为 1M

    # Java 8u191 新增了几个配置参数,用于适配容器
    -XX:+UseContainerSupport        # 该功能默认启用。用于在容器中运行 JVM 时,自动根据 Cgroup 的 cpu、memory 限额,配置 -XX:ActiveProcessorCount、-Xmx 的默认值
    -XX:ActiveProcessorCount=<int>  # 配置 JVM 可使用的 CPU 核数
    -XX:InitialRAMPercentage=1.5625 # 如果未显式配置 -Xms ,则将 Cgroup memory 限额的该百分比赋值给 -Xms (这里的百分比必须包含小数点)
    -XX:MaxRAMPercentage=25.0       # 如果未显式配置 -Xmx ,将将 Cgroup memory 限额的该百分比赋值给 -Xmx
    -XX:MinRAMPercentage=50.0

    # 关于 ParallelGC
    -XX:+UseParallelGC
    -XX:ParallelGCThreads=??        # 并行工作时,创建多少个 GC 线程。默认等于主机的 CPU 核数

    # 关于 ConcMarkSweepGC
    -XX:+UseConcMarkSweepGC
    -XX:+UseCMSCompactAtFullCollection    # 采用 ConcMarkSweepGC 时默认启用该功能,在 full GC 时自动压缩内存碎片
    -XX:CMSInitiatingOccupancyFraction=-1 # 如果 old 区域内存使用率超过该百分比(比如 70~80),则触发 old GC 。默认为 -1 ,表示由 JVM 自动调整该阈值,一般足够智能
    -XX:+UseCMSInitiatingOccupancyOnly    # 是否保持 CMSInitiatingOccupancyFraction 阈值,不让 JVM 自动调整。默认禁用该功能
    -XX:ConcGCThreads=??                  # 并发标记时,创建多少个 GC 线程。默认等于 (cpu_cores+3)/4 ,至少为 1

    # 关于 G1GC
    # G1GC 的配置参数较少,因为它默认会自动调整大部分配置参数,一般不需要用户干预
    -XX:+UseG1GC
    -XX:G1HeapRegionSize=1M       # 每个 region 的容量,取值范围为 1M~32M ,必须是 2 的幂级数
                                  # 默认会根据 InitialHeapSize 自动决定 G1HeapRegionSize ,使得至少创建 2048 个 region 。region 容量越大,则清理耗时越长
    -XX:MaxGCPauseMillis=200      # 每次 GC 预期的 STW 最大时长,默认为 200 毫秒。该值不应该配置得太小,否则会增加 GC 次数。而且这是一个软限制,不能肯定低于该时长
    -XX:GCTimeRatio=9             # 控制 GC 耗时的比例,取值范围为 1~99 ,默认为 9 。假设 JVM 累计运行时长为 T ,则会限制 GC 累计耗时小于 T/(1+GCTimeRatio)
    -XX:InitiatingHeapOccupancyPercent=45 # 堆内存的使用率超过 45% 时,触发一次 mixed GC

    # 关于 OutOfMemoryError
    -XX:+HeapDumpOnOutOfMemoryError       # 抛出 OutOfMemoryError 错误时,自动生成堆快照文件,保存到磁盘
    -XX:HeapDumpPath=java_pid$PID.hprof   # 堆快照文件的保存路径,支持相对路径
    -XX:ExitOnOutOfMemoryError            # 抛出 OutOfMemoryError 错误时,终止 Java 进程
    -XX:OnOutOfMemoryError="kill -9 %p"   # 抛出 OutOfMemoryError 错误时,执行 shell 命令

    # 关于 GC 日志
    -XX:+PrintGCDetails             # 打印 GC 详细日志。默认不打印 GC 日志
    -XX:+PrintGCDateStamps          # 每行 GC 日志的开头加上日期时间戳
    -XX:+PrintHeapAtGC              # 每次 GC 前后,打印一次当前的 Heap 信息
    -Xloggc:gc.log                  # 将 GC 日志保存到文件中。默认打印到 stdout
    -XX:+UseGCLogFileRotation       # 自动轮换日志文件。默认禁用该功能
    -XX:NumberOfGCLogFiles=0        # 保留多少个日志文件,默认不限制。命名格式为 gc.log.0、gc.log.1 等
    -XX:GCLogFileSize=8K            # 每个日志文件的最大体积

    # 其它配置
    -XX:+OmitStackTraceInFastThrow  # 重复打印一个异常时,省略堆栈信息。默认启用该功能
  • 启动 Java 进程时,一般的命令格式为 java [-options] -jar xx.jar

    • 如果将 -options 放在 -jar xx.jar 之后,则不会生效。
    • 如果重复声明某个配置参数,比如 -Xmx1G -Xmx2G ,则会采用最后配置的值。
    • 可用 -XX:+FLAG 的格式启动一个功能标志,用 -XX:-FLAG 的格式关闭功能。
  • java 启动命令的示例:

    java
        -XX:MaxRAMPercentage=80.0 \
        -XX:InitialRAMPercentage=80.0 \
        -XX:+UseG1GC \
        -XX:ConcGCThreads=8 \
        -XX:ParallelGCThreads=8 \
    
        -XX:+HeapDumpOnOutOfMemoryError \
        -XX:HeapDumpPath=/tmp/heapdump.hprof \
        -XX:+PrintGCDetails \
        -XX:+PrintGCDateStamps \
        -Xloggc:/tmp/gc-%t.log \
        -XX:+UseGCLogFileRotation \
        -XX:NumberOfGCLogFiles=5 \
        -XX:GCLogFileSize=10M \
    
        -Duser.timezone=GMT+08 \
        -jar \
        test.jar
    
  • 建议启用 -XX:+HeapDumpOnOutOfMemoryError 参数,让 JVM 在抛出 OutOfMemoryError 错误时自动生成堆快照文件,方便排查问题。

    • 堆快照(heapdump)是一种二进制文件,记录了所有 Java 对象的内存大小等信息,可用 MAT 等工具分析。
  • 建议监控 Java 进程平时实际占用的内存大小,据此配置内存参数 -Xms 和 -Xmx 。

    • JVM 实际占用的内存可能低于 -Xms ,因为 GC 时可能释放堆内存给操作系统。也可能高于 -Xmx ,因为 DirectMemory、Metaspace 等区域也会占用内存。
    • 建议配置的 -Xms 等于 -Xmx ,让 JVM 启动时就一次向操作系统申请这么多内存,因为频繁申请内存的开销较大。
    • 如果监控到 young GC 次数多,则在不优化 Java 程序代码的情况下,可增加 -Xmx ,或减少 -XX:NewRatio ,从而增加年轻代内存。如果监控到 old GC、full GC 次数多,对策同理。
    • 在 k8s Pod 中部署 Java 进程时,配置的 limits.memory 取值应该比 Java -Xmx 大一些。因为 Java 进程占用的总内存包括 Heap、Direct Memory、Metaspace 等,可能超过 -Xmx ,如果再超过 limits.memory ,就会导致 Pod 被 OOM 杀死,然后自动重启。
      • 可根据 Pod 的 limits.memory ,按比例配置 JVM 内存。例如,将传统的启动命令 java -Xms1G -Xmx1G -jar xx.jar 改为 java -XX:InitialRAMPercentage=80.0 -XX:MaxRAMPercentage=80.0 -jar xx.jar
      • 可在 Pod 中执行 java -XX:MaxRAMPercentage=80.0 -XX:+PrintFlagsFinal 2>&1 | grep HeapSize ,测试此时的 HeapSize 取值是多少。
  • JVM GC 的目的是销毁垃圾对象,一般不会将 committed_memory 中的空闲内存释放给操作系统。

    • 例如 JVM 向操作系统申请了 6G 堆内存,实际只使用 2G 内存,剩下 4G 的空闲内存占着不释放。
    • 采用 Serial GC 时,可调整以下配置参数,强制让 JVM 释放空闲内存,但会增加 CPU 开销。
      -XX:MinHeapFreeRatio=0      # GC 之后,空闲内存的预期最小百分比。如果低于该值,则申请更多空闲内存
      -XX:MaxHeapFreeRatio=100    # GC 之后,空闲内存的预期最大百分比。如果超过该值,则释放一些空闲内存,还给操作系统。默认为 100% ,即不释放
      

# jps

jps       # 列出当前主机上运行的所有 Java 进程的 PID 、名称
    -l    # 显示执行的 jar 包名,或主类名

# jmap

jmap [options] <pid>    # 通过 PID 指定一个 Java 进程,显示其信息(比如使用哪种 GC 收集器)
    -heap               # 显示堆内存各个区域的大小
    -dump:format=b,file=heapdump.hprof # 生成堆快照文件

# Ant

:一个 Java 项目的构建工具,可以自动进行 Java 项目的编译、测试、打包。

  • 官方文档 (opens new window)
  • Ant 最初是用于构建 Tomcat ,后来在 2000 年作为一个独立项目发布。
  • 用法:
    1. 为 Java 项目创建一个 build.xml 文件,作为 Ant 的配置文件。
    2. 使用 ant 命令编译:
      ant [target]...
          -f build.xml
      

# 配置

build.xml 的内容示例:

<?xml version="1.0" encoding="UTF-8"?>

<!-- 项目名、项目目录、默认执行的任务 -->
<project name="test" basedir="/opt/test" default="compile">

    <!-- 定义一些属性,属性名由用户自定义,可像变量一样使用 -->
    <property name="src.dir"     value="src"/>
    <property name="lib.dir"     value="lib"/>
    <property name="classes.dir" value="classes"/>  <!-- 编译出的类文件的保存路径 -->
    <property name="build.dir"   value="build"/>
    <property name="jre.dir"     value="/usr/local/jdk1.8.0_101/jre/lib"/>

    <!-- 配置一些路径 -->
    <path id="compile.dependencies">
        <fileset dir="${lib.dir}" includes="*.jar"/>
    </path>
    <path id="java.dependencies">
        <fileset dir="${jre.dir}" includes="*.jar"/>
    </path>

    <!-- 定义一个 target ,作为被 Ant 执行的任务 -->
    <target name="init">
        <delete dir="${classes.dir}"/>
        <delete dir="${build.dir}"/>
        <mkdir  dir="${classes.dir}"/>
        <mkdir  dir="${build.dir}"/>
    </target>

    <target name="compile" depends="init">
        <javac debug="true" includeantruntime="false" srcdir="${src.dir}" destdir="${classes.dir}" encoding="UTF-8">
            <!-- <compilerarg line="-XDignore.symbol.file"/> -->
            <classpath>
                <path refid="compile.dependencies"/>
                <path refid="java.dependencies"/>
            </classpath>
        </javac>
    </target>

    <target name="build_war" depends="compile">
        <war destfile="${build.dir}/${ant.project.name}.war" webxml="${build.dir}/WEB-INF/web.xml">
            <fileset dir="${build.dir}"/>
            <lib     dir="${build.dir}/WEB-INF/lib"/>
            <classes dir="${build.dir}/WEB-INF/classes"/>
        </war>
    </target>

</project>

# Maven

:一个 Java 项目的构建、管理工具,既可以构建 Java 项目,还可以下载、制作、管理 jar 包等构建产物。

  • 官方文档 (opens new window)
  • 读音为 ['meɪv(ə)n]
  • 于 2004 年发布,比 Ant 的功能更多。
  • 基于 POM(Project Object Model ,项目对象模型)管理 Java 项目。
  • 用法:
    1. 调整 Java 项目的目录结构,以符合 POM 的规范。
    2. 使用 mvn 命令编译。

# 安装

  • 用 yum 安装:
    yum install maven
    
  • 或者下载二进制版:
    VERSION=3.8.4
    wget https://dlcdn.apache.org/maven/maven-3/$VERSION/binaries/apache-maven-$VERSION-bin.tar.gz
    tar -zxvf apache-maven-$VERSION-bin.tar.gz -C /usr/local/
    echo "export MAVEN_HOME=/usr/local/apache-maven-$VERSION" >> /etc/profile
    echo "export PATH=$PATH:$MAVEN_HOME/bin" >> /etc/profile
    source /etc/profile
    
  • 或者用 Docker 部署:
    docker run -it --rm \
        -v maven-repo:/root/.m2 \   # 挂载本地仓库
        -v $PWD:/data \             # 挂载项目目录
        --workdir /data \
        maven:3.8.4-jdk-8 \
        mvn clean package
    

# 命令

mvn
    compile           # 编译当前项目
    test              # 在 compile 的基础上,测试项目。这会先编译出测试用例的类文件,然后执行它们
    package           # 在 test    的基础上,打包项目。一般是生成 jar 包或 war 包
    install           # 在 package 的基础上,将构建产物安装到本地仓库
    deploy            # 在 install 的基础上,将构建产物上传到远程仓库

    clean             # 清理项目目录,比如删除 target 目录下的文件
    clean package     # 先执行 clean ,再执行另一个操作(也可以换成 install 等操作)

    -D  <property>=<value>    # 配置一个属性的值。可以多次使用该选项
        maven.test.skip=true  # 跳过 test 步骤
        file.encoding=UTF-8   # 配置编码格式
    -P  dev,test              # 激活一些配置文件(profiles),用逗号分隔

    -pl module1,module2,...   # --projects ,只构建指定的模块。默认构建当前项目的所有模块
    -am                       # --also-make ,指定了 -pl 时,也构建它们依赖的模块

    -v                # 显示版本信息
    -X                # 显示调试信息
    dependency:list   # 列出项目的所有依赖
    dependency:tree   # 以树形结构列出项目的所有依赖,可以看出它们之间的依赖关系
  • 执行 mvn deploy 时,会依次执行多个步骤:resources、compile、testResources、testCompile、test、jar、install、deploy 。
    • 执行 mvn package 时,只是执行到 jar 打包步骤就停止。
    • 执行 mvn clean deploy 时,会先执行 clean 步骤。
  • 执行 mvn 命令时,会创建一个 java 子进程来进行 Maven 构建。
    • 建议声明环境变量 export MAVEN_OPTS=-Xmx1g 来限制其占用的内存。默认会占用较多内存,却并不能明显提高编译速度。
  • 例:创建一个 webapp 类型的项目
    mvn archetype:generate
        -DgroupId=com.example     # 组织名(网址倒序)
        -DartifactId=web_demo     # 项目名
        -DarchetypeArtifactId=maven-archetype-webapp  # 使用的项目模板
        -DinteractiveMode=false   # 是否进入交互模式
    
  • 例:安装指定路径的 jar 包到本地仓库
    mvn install:install-file -Dfile=targets/demo.jar \
        -DgroupId=com.test -DartifactId=demo -Dversion=0.0.1 -Dpackaging=jar
    
  • 例:下载指定的 jar 包到本地仓库
    mvn org.apache.maven.plugins:maven-dependency-plugin:2.10:get \
        -Dartifact=com.test:demo:0.0.1
        # -DremoteRepositories=http://repo1.maven.org/maven2/   # 指定远程仓库,这会覆盖 pom.xml 和 settings.xml 的配置
        # -Dtransitive=true                                     # 是否下载依赖的其它包
    

# 目录结构

  • Maven 项目的目录结构示例:

    web_demo
    ├── pom.xml                 # 该 Maven 项目的配置文件
    ├── src
    │   ├── main                # 存放源代码
    │   │    ├── java           # 存放代码文件
    │   │    ├── resources      # 存放资源文件
    │   │    └── webapp
    │   └── test                # 存放测试代码
    │       ├── java
    │       └── resources
    └── target                  # 存放 Maven 的编译产物
        ├── classes
        ├── maven-archiver
        │   └── pom.properties
        ├── web_demo            # 编译后的 webapp 目录,可以打包成 war 包
        │   ├── index.jsp
        │   ├── META-INF
        │   └── WEB-INF
        │       ├── classes
        │       └── web.xml
        └── web_demo.war        # war 包
    
  • Maven 项目支持创建多个模块(module)。

    • 每个模块相当于一个子 Maven 项目,位于一个子目录中。分别有一个 pom.xml ,继承父 pom.xml 。
    • 目录结构示例:
      web_demo
      ├── pom.xml
      ├── module1
      │   ├── pom.xml
      │   └── src
      └── module2
          ├── pom.xml
          └── src
      

# pom.xml

配置文件 pom.xml 示例:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/SETTINGS/1.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

    <!-- 声明 pom.xml 的格式版本 -->
    <modelVersion>4.0.0</modelVersion>

    <!-- 该 Maven 项目的首要信息,用于在编译后打包成一个工件 -->
    <name>test_project</name>
    <description>The project is for test</description>
    <groupId>in.freewind</groupId>
    <artifactId>test_project</artifactId>
    <version>0.0.1</version>
    <packaging>jar</packaging>

    <!-- 声明该项目的所有模块。会自动根据它们之间的依赖关系,确定编译顺序 -->
    <modules>
      <module>module1</module>
      <module>module2</module>
    </modules>

    <!-- 声明构建项目时的配置。建议采用默认配置 -->
    <build>
      <sourceDirectory> ${basedir}/src/main/java </sourceDirectory>
      <outputDirectory> ${basedir}/target/classes </outputDirectory>
      <testSourceDirectory> ${basedir}/src/test/java </testSourceDirectory>
      <testOutputDirectory> ${basedir}/target/test-classes </testOutputDirectory>
    </build>

    <!-- 声明一些属性,相当于全局变量,可以在 pom.xml 文件的其它位置用 ${propertie} 的方式调用 -->
    <properties>
      <!-- JDK 的版本 -->
      <java.version>1.8</java.version>
      <maven.compiler.source>1.8</maven.compiler.source>
      <maven.compiler.target>1.8</maven.compiler.target>
      <!-- 编码格式 -->
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <!-- 声明用于下载工件的远程仓库。可以声明多个,依次尝试从其中下载工件。如果都不可用,则默认采用中央仓库 central -->
    <repositories>
      <repository>
        <id>releases</id>
        <url>http://nexus.test.com/repository/maven-releases/</url>
        <!-- 仓库默认允许下载 releases 工件,不允许下载 snapshots 工件 -->
        <!--
        <releases>
          <enabled>true</enabled>
        </releases>
        <snapshots>
          <enabled>false</enabled>
        </snapshots>
        -->
      </repository>
      <repository>
        <id>snapshots</id>
        <url>http://nexus.test.com/repository/maven-snapshots/</url>
        <snapshots>
          <enabled>true</enabled>
          <!-- 更新工件的频率,即下载相同名称、版本,但修改时间更加新的工件。默认为 daily -->
          <updatePolicy>always</updatePolicy>
        </snapshots>
      </repository>
    </repositories>

    <!-- 声明用于上传工件的远程仓库 -->
    <distributionManagement>
      <repository>
        <id>releases</id>
        <url>http://nexus.test.com/repository/maven-releases/</url>
      </repository>
      <!-- 如果声明了 snapshotRepository ,则采用它上传 snapshot 工件,否则采用 repository -->
      <snapshotRepository>
        <id>snapshots</id>
        <url>http://nexus.test.com/repository/maven-snapshots/</url>
      </snapshotRepository>
    </distributionManagement>

    <!-- 声明该项目依赖的所有工件 -->
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>1.1.5</version>
        <!-- <type>jar</type> -->
      </dependency>
      <dependency>
        <groupId>com.test</groupId>
        <artifactId>test</artifactId>
        <version>1.0.0-SNAPSHOT</version>
      </dependency>
    </dependency>

    <!-- 可以定义一些可选配置(profile),用于覆盖当前的 POM 配置。可以通过 mvn -P 选项主动激活,也可以通过 activation 条件自动激活 -->
    <profiles>
      <profile>
        <id>dev</id>
        <properties>
          <env>dev</env>
        </properties>
        <activation>
          <activeByDefault>false</activeByDefault>
          <jdk>1.8</jdk>
        </activation>
      </profile>
      <profile>
        <id>test</id>
        <repositories>...</repositories>
      </profile>
    </profiles>

</project>
  • 官方文档 (opens new window)
  • Artifact 的命名格式为 groupId:artifactId:version[:packaging[:classifier]]
    • groupId :制作者的团体名称,一般使用倒序的域名。
    • artifactId :工件的名称,在同一个 groupId 下唯一。
    • version :工件的版本。分为两种格式:
      • releases :正式版本,比如 1.0.0 。构建项目时,先尝试从本地仓库获取相同名称、版本的依赖工件,如果不存在,再从远程仓库下载到本地仓库,缓存起来。
      • snapshots :快照版本,比如 1.0.0-SNAPSHOT 。构建项目时,总是尝试从远程仓库获取相同名称、版本,但修改时间更加新的依赖工件,覆盖本地仓库的缓存。
    • packaging :打包格式,默认为 jar 。
    • classifier :分类器,可以是一个任意字符串。
  • Maven 仓库用于存储 Artifact 文件,分为两种:
    • 本地仓库
      • :位于本机某个目录下(默认为 ~/.m2/repository/),用于缓存从远程仓库下载的依赖。
    • 远程仓库
      • :位于其它主机上的仓库,可用于下载、上传工件。比如用 Nexus 搭建的私有仓库。
      • Maven 社区维护了一个远程仓库,称为中央仓库(central)。可在 https://search.maven.org/ 网页搜索,或浏览其文件列表 https://repo1.maven.org/maven2/
      • 在 pom.xml 中可以配置当前项目使用的远程仓库,在 settings.xml 中可以配置全局的远程仓库。

# settings.xml

执行 mvn 命令时,还会读取以下位置的配置文件:

~/.m2/settings.xml              # 当前用户的配置文件
$MAVEN_HOME/conf/settings.xml   # 全局的配置文件

settings.xml 示例:

<?xml version="1.0" encoding="UTF-8"?>

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

  <!-- 配置远程仓库的访问账号 -->
  <servers>
    <server>
        <id>releases</id>
        <username>***</username>
        <password>***</password>
    </server>
    <server>
        <id>mirror1</id>
        <username>***</username>
        <password>***</password>
    </server>
  </servers>

  <!-- 声明镜像仓库 -->
  <mirrors>
    <mirror>
      <id>mirror1</id>
      <!-- 用该镜像仓库替换哪些 id 的远程仓库 -->
      <mirrorOf>*</mirrorOf>
      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
    </mirror>
  </mirrors>

</settings>
  • 镜像仓库(mirror):一些更高优先级的远程仓库。可以拦截发向普通远程仓库的请求,交给镜像仓库处理。
    • 只会拦截下载工件的请求,不会拦截上传工件的请求。
    • 如果一个远程仓库匹配多个镜像仓库,则只采用第一个镜像仓库。
    • 如果镜像仓库不可访问,并不会再尝试访问原来的远程仓库。
    • mirrorOf 的取值示例:
      *             # 匹配所有远程仓库
      central       # 匹配中央仓库
      repo1,repo2   # 匹配指定 id 的远程仓库,不支持匹配 mirror id
      !repo1,*      # 排除 repo1 ,然后匹配其它远程仓库
      

# Gradle

:一个 Java 项目的构建工具,于 2012 年发布。