世界杯奖项

Java 通过命令编译源码并构建可执行 jar 包

2018 / 12 / 26

by John Yuan

Java 通过命令编译源码并构建可执行 jar 包

一般情况下我们会使用构建工具帮助我生成 jar 包,但工具是建立在底层命令的基础之上的,只会用工具而不知道底层实现原理,通常会让我们心里没底。本文主要记录如何使用 Java 自带的命令(javac、java、jar)来实现项目的编译、运行和打包。

一、概述

在本文中,我们将讨论以下内容:

使用 javac 命令编译项目用到的所有 java 文件

使用 java 命令运行项目主函数

使用 jar 命令创建可执行 jar 包

使用 jar 命令解压 jar 包

使用 javac 命令编译项目中所有 java 文件,无论该文件是否被引用

二、项目结构

作为演示,我们项目的结构如下:

demo-project

├── src

│ └── org

│ └── example

│ ├── app

│ │ └── App.java

│ └── service

│ ├── ByeService.java

│ ├── HelloService.java

│ └── impl

│ └── HelloServiceImpl.java

└── target

├── classes # 保存生成的 class 文件

├── demo-project.jar # 保存生成的 jar 包

└── unpackaged # jar 包的解压目录

注意:我们的源代码中一共有 ByeService 和 HelloService 两个接口,但只提供了 HelloService 的实现(HelloServiceImpl)。这是有意为之,后面演示会进行说明。

三、源代码

org/example/app/App.java:

package org.example.app;

import org.example.service.HelloService;

import org.example.service.impl.HelloServiceImpl;

public class App {

public static void main(String[] args) {

HelloService h = new HelloServiceImpl();

h.hello();

}

}

org/example/service/ByService.java:

package org.example.service;

public interface ByeService {

void bye();

}

org/example/service/HelloService:

package org.example.service;

public interface HelloService {

void hello();

}

org/example/service/impl/HelloServiceImpl:

package org.example.service.impl;

import org.example.service.HelloService;

public class HelloServiceImpl implements HelloService {

@Override

public void hello() {

System.out.println("HelloServiceImpl -> HelloService.hello()");

}

}

注意:以上代码中我们没有引用 org.example.service.ByeService 这个类。

四、编译项目

首先确保我们创建了第一节中所描述的目录结构,主要是确保 target/classes 目录存在,因为我们会将该目录作为输出目录。

然后进入 src 目录,使用以下命令进行编译:

# 备注:

# 1) 当前所在目录为 src 目录

# 2) -d 用于指导输出目录

# 3) ../target/classes 目录必须存在才能编译成功

javac org/example/app/App.java -d ../target/classes

以上命令会编译 App.java 以及所有被 App.java 引用(import)的文件,由于 ByeService 没有被引用,所以 ByeService.java 文件没有被编译。

以下为编译之后的文件树形结构:

target

└─ classes

└─ org

└─ example

├─ app

│ └─ App.class

└─ service

├─ HelloService.class

└─ impl

└─ HelloServiceImpl.class

五、运行程序

编译完成后,进入 target/classes 目录,然后执行以下命令以运行程序:

# 指定入口程序的全类名

java org.example.app.App

输出结果为:

HelloServiceImpl -> HelloService.hello()

六、创建可执行 jar 包

使用 jar 命令,我们可以把生成的 class 文件打包成一个 jar 包。首先我们需要进入 target/classes 目录,然后输入以下命令即可进行打包:

# 备注:

# 1) c 代表创建归档文件(即表明:此操作为打包而不是解压)

# 2) v 代表打印日志信息

# 3) f 代表指定输出文件名

# 4) ../demo-project.jar 即目标文件名

# 5) * 表示打包当前路径下的所有文件

jar cvf ../demo-project.jar *

通过上面的命令创建的不是一个这里所说的可执行 jar 包,但我们可以通过以下命令运行 main 方法:

# 备注:

# 1) -cp 代表指定 classpath,此处指定为生成的 jar 包

# 2) org.example.app.App 表示主程序入口

java -cp demo-project.jar org.example.app.App

上面的方式虽然能够执行 jar 包,但步骤稍显麻烦,而且需要我们记住主程序的全类名,比如这里的 org.example.app.App 这一长串。

下面我们要介绍的打包方式可以在打包时记录主程序的全类名,这样在我们执行这个 jar 包的时候,就不用输入全类名了。首先进入 target/classes 目录,然后执行以下命令:

# 备注:

# 1) 此处添加了 e 选项

# 2) 此处指定了主程序全类名 org.example.app.App

jar cvfe ../demo-project.jar org.example.app.App *

打包完成后,进入 target 目录,使用以下命令即可运行 jar 包:

java -jar demo-project.jar

下一节我们将讨论如何解压 jar 包,并说明 jar 包是如何记录主程序全类名的。

七、解压 jar 包

jar 包其实也是一种压缩包格式,我们可以使用 jar 命令解压 jar 包。以上面生成的 demo-project.jar 为例,首先我们进入 target/unpackaged 目录,确保 target/demo-project.jar 文件存在,然后执行以下命令:

# 备注:

# 1) x 代表解压

# 2) v 表示打印日志

# 3) f 指定要解压的文件名

# 4) 当前目录为 unpackaged,解压结果会保存到当前目录

jar xvf ../demo-project.jar

解压完成后,unpackaged 的目录结构如下:

unpackaged

├── META-INF

│ └── MANIFEST.MF

└── org

└── example

├── app

│ └── App.class

└── service

├── HelloService.class

└── impl

└── HelloServiceImpl.class

可以看到,解压后的文件夹中除了我们的 class 文件外,还多了一个 META-INF 文件夹,该文件夹中的文件就是用来保存当前 jar 包的相关信息的。其中 META-INF/MANIFEST.MF 的文件内容如下:

Manifest-Version: 1.0

Created-By: 1.8.0_191 (Oracle Corporation)

Main-Class: org.example.app.App

可以看到,该文件使用 Main-Class 字段保存了主程序的全类名。

八、编译所有源文件

前面我们已经说过,由于我们的程序没有引用 ByeService.java 这个文件,所以我们在编译 App.java 的时候,编译器并不会去编译 ByeService.java 这个文件。但有些时候,我们需要编译源码目录下的所有文件,不管这些文件是否被引用。

要做到这一点,我们需手动告诉编译器我们需要编译哪些文件,比如下面这样:

# 备注:运行时确保我们处于 src 目录下

javac org/example/app/App.java org/example/service/ByeService.java -d ../target/classes

在上面的命令中,我们明确指示编译器要对 org/example/service/ByeService.java 这个文件进行编译。命令完成后,我们可以发现 org/example/service/ByeService.class 已经编译完成。

如果我们的项目很大,里面有无数的 .java 源文件,我们也不能清楚的记得哪些文件被引用了,而哪些文件没有被引用。这个时候,如果要求我们在编译时指定没有被引用的文件,就稍显麻烦了。所以最保险的办法是让编译器编译所有的源文件。也就是说,我们需要在 javac 命令中指定所有 .java 文件的路径。不过由于这个操作太反人类了,我们可以使用以下的 shell 脚本来帮我们完成:

#!/bin/bash

# 备注:

# 请将这个文件保存在项目根目录下(与 src 目录同级)

# 并命名为 build.sh

# 获取脚本文件所在目录

readonly __DIR__=$(cd $(dirname $0) && pwd)

# 源码目录

readonly SRC_DIR="${__DIR__}/src"

# 目标目录

readonly TARGET_DIR="${__DIR__}/target"

# class 文件目录

readonly CLASSES_DIR="${TARGET_DIR}/classes"

# 创建一个临时文件路径

readonly TEMP_FILE=$(mktemp)

# 打印提示信息

echo

echo " SRC_DIR: ${SRC_DIR}"

echo " TARGET_DIR: ${TARGET_DIR}"

echo "CLASSES_DIR: ${CLASSES_DIR}"

echo " TEMP_FILE: ${TEMP_FILE}"

echo

# 如果源码目录不存在,则退出程序

if [ ! -d "${SRC_DIR}" ]

then

echo "ERROR: ${SRC_DIR} is not a directory."

echo

exit 1

fi

# 查找所有 .java 文件,并将文件名保存至临时文件

# 1. 首先进入源码目录

cd $SRC_DIR

# 2. 找到所有以 .java 结尾的文件名,并判断该文件是不是普通文件(排除目录)

for filename in $(find . -name "*.java")

do

# 如果是普通文件,则将文件名保存至临时文件

if [ -f "${filename}" ]

then

echo $filename >> $TEMP_FILE

fi

done

# 将文件名列表保存至变量

readonly FILE_LIST=$(cat $TEMP_FILE)

# 如果文件列表为空,则退出程序

if [ "${FILE_LIST}" == "" ]

then

echo "ERROR: There is no java file in ${SRC_DIR} directory."

echo

exit 1

fi

# 开始编译

# 1. 首先删除原有文件

rm -rf $CLASSES_DIR

# 2. 创建一个空目录

mkdir -p $CLASSES_DIR

# 3. 编译(此处可以根据实际需要添加其他参数)

javac $FILE_LIST -d $CLASSES_DIR

# 打印已经编译的文件名列表

echo "Compiled file list:"

echo

cat $TEMP_FILE

echo

# 删除临时文件

rm -rf $TEMP_FILE

# 编译完成,退出程序

exit

将以上文件内容保存至项目根目录下,并命名为 build.sh,然后执行以下命令给该文件添加可执行权限:

chmod +x build.sh

然后执行 build.sh 文件即可编译所有源文件:

./build.sh

本文完。

Copyright © 2088 中国举办世界杯_世界杯足球场地尺寸 - lchjdj.com All Rights Reserved.
友情链接