9

使用Java编写Cli命令行工具

 1 year ago
source link: https://jasonkayzk.github.io/2023/03/20/%E4%BD%BF%E7%94%A8Java%E7%BC%96%E5%86%99Cli%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

兜兜转转,最近又用回了Java;

最近在写mini-redis的Java版来学习Netty,需要用到Java的命令行工具框架picocli;

发现通过Java来实现命令行还是挺麻烦的,尤其是打包部分,这里简单总结一下;

系列文章:

使用Java编写Cli命令行工具

前言

试了一下,还是不推荐使用 Java 来开发 Cli 的,毕竟不会有人为了这个东西去装 JRE,而且 GraalVM 目前还不能完全支持(各种平台上各种缺动态链接库);

只是用于学习的项目即可!

代码

这里以 picocli 框架提供的 CheckSum 工具为例:

项目的 Maven 配置如下:

<dependencies>
  <dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli</artifactId>
    <version>4.7.1</version>
  </dependency>
</dependencies>

<build>
  <plugins>
    <!-- Enabling Annotation Processor -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <!-- annotationProcessorPaths requires maven-compiler-plugin version 3.5 or higher -->
      <version>3.8.1</version>
      <configuration>
        <annotationProcessorPaths>
          <path>
            <groupId>info.picocli</groupId>
            <artifactId>picocli-codegen</artifactId>
            <version>4.7.1</version>
          </path>
        </annotationProcessorPaths>
        <compilerArgs>
          <arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
        </compilerArgs>
      </configuration>
    </plugin>
  </plugins>
</build>

主要是 picocli 依赖以及注解处理插件;

代码如下:

cli/picocli/a-checksum/src/main/java/io/github/jasonkayzk/CheckSum.java

package io.github.jasonkayzk;

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

import java.io.File;
import java.math.BigInteger;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.concurrent.Callable;

@Command(name = "checksum", mixinStandardHelpOptions = true, version = "checksum 4.0",
        description = "Prints the checksum (SHA-256 by default) of a file to STDOUT.")
public class CheckSum implements Callable<Integer> {

    @Parameters(index = "0", description = "The file whose checksum to calculate.")
    private File file;

    @Option(names = {"-a", "--algorithm"}, description = "MD5, SHA-1, SHA-256, ...")
    private String algorithm = "SHA-256";

    @Override
    public Integer call() throws Exception { // your business logic goes here...
        byte[] fileContents = Files.readAllBytes(file.toPath());
        byte[] digest = MessageDigest.getInstance(algorithm).digest(fileContents);
        System.out.printf("%0" + (digest.length * 2) + "x%n", new BigInteger(1, digest));
        return 0;
    }

    // this example implements Callable, so parsing, error handling and handling user
    // requests for usage help or version help can be done with one line of code.
    public static void main(String... args) {
        int exitCode = new CommandLine(new CheckSum()).execute(args);
        System.exit(exitCode);
    }
}

命令行框架的代码还是很容易理解的;

在 IDEA 中执行的话要配置对应的命令行参数才行;

那么如果打包成 Jar 呢?

Maven配置

指定Jar包主清单属性

如果我们不使用其他的 Maven 插件来打包,打包后执行:

$ java -jar target/a-checksum-1.0-SNAPSHOT.jar \
  --algorithm SHA-256 hello.txt

target/a-checksum-1.0-SNAPSHOT.jar中没有主清单属性

此时会报错:xxx.jar中没有主清单属性

这表示我们没有指定 Jar 包的入口方法,因此这个 Jar 包只能作为一个库来使用而不能成为 Executable Jar;

这个问题是因为:jar包中的META-INF文件夹下的MANIFEST.MF文件缺少定义jar接口类)说白了就是没有指定class类);

这里说明一下MANIFEST.MF就是一个清单文件,通俗点将就相当于WINDOWS中 ini 配置文件,用来配置程序的一些信息;

有两种解决方案:

1、手动编写配置META-INF/MANIFEST.MF

我们可以手动编写这个配置文件,然后打包的时候打包进去,例如:

Manifest-Version: 1.0
Build-Jdk: 1.7.0_67
Main-Class: io.github.jasonkayzk.CheckSum

但是通常我们都是使用 Maven 插件来帮助我们生成!

2、使用 maven-jar-plugin 插件

配置中加入Maven插件:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>3.1.0</version>
      <configuration>
        <archive>
          <manifest>
            <mainClass>io.github.jasonkayzk.CheckSum</mainClass>
          </manifest>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>

在上面的配置中配置了 MainClass 是我们对应的 CheckSum 类;

这样,Maven 在 package 阶段就会自动生成对应的 MANIFEST 文件并打入 Jar 包中;

将依赖加入Jar包

上面配置好了我们的Jar包入口,接下来重新打包并执行:

$ java -jar target/a-checksum-1.0-SNAPSHOT.jar \
  --algorithm SHA-256 hello.txt

Exception in thread "main" java.lang.NoClassDefFoundError: picocli/CommandLine
        at io.github.jasonkayzk.CheckSum.main(CheckSum.java:35)
Caused by: java.lang.ClassNotFoundException: picocli.CommandLine
        at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
        ... 1 more

此时仍然报错:java.lang.NoClassDefFoundError: picocli/CommandLine

这是因为,通常情况下我们打包的 Jar 包是不包含依赖文件的;但是当我们作为 Executable Jar 去运行时,就缺少了我们的依赖;

因此,我们需要将我们的依赖也打入 Jar 包中,即 uber-jar(或叫 fat-jar,胖Jar包);

Maven 提供了两个插件来解决这个问题:

  • maven-assembly-plugin;
  • maven-shade-plugin;

这两个都可以用于将程序和依赖打成一个 uber-jar,尤其是开发sparkstreaming、flink程序,往yarn上提交任务的时候!

两者的区别在于:

maven-assembly-plugin 插件会将依赖和资源文件都打入最终的Jar包,诸如properties文件等,如果项目和依赖中都有相同名称的资源文件时,就会发生冲突,导致项目中的相同名称的文件不会打到最终的Jar包中!如果这个文件是一个关键的配置文件,便会导致问题!

而maven-shade-plugin不存在这样的问题;所以,实际开发项目时候,还是尽量选用maven-shade-plugin!

下面分别来看;

使用maven-assembly-plugin打包

Maven 中加入配置:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-assembly-plugin</artifactId>
  <version>3.1.1</version>
  <configuration>
    <!-- get all project dependencies -->
    <descriptorRefs>
      <descriptorRef>jar-with-dependencies</descriptorRef>
    </descriptorRefs>
    <!-- Main in manifest make executable jar -->
    <archive>
      <manifest>
        <mainClass>io.github.jasonkayzk.CheckSum</mainClass>                        
      </manifest>
    </archive>
  </configuration>
  <executions>
    <execution>
      <id>make-assembly</id>
      <phase>package</phase>
      <goals>
        <goal>single</goal>
      </goals>
    </execution>
  </executions>
</plugin>

然后再次打包并执行:

$ java -jar target/a-checksum-1.0-SNAPSHOT-jar-with-dependencies.jar \
  --algorithm SHA-256 hello.txt

5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03

maven-assembly-plugin 插件会生成两个 Jar 包,一个包含了依赖(如上面的 a-checksum-1.0-SNAPSHOT-jar-with-dependencies.jar),一个不包含;

maven-assembly-plugin 插件使用比较简单,下面来看另外一个插件;

使用maven-shade-plugin打包

加入配置:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <version>3.2.4</version>
  <executions>
    <execution>
      <id>checksum</id>
      <phase>package</phase>
      <goals>
        <goal>shade</goal>
      </goals>
      <configuration>
        <transformers>
          <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <manifestEntries>
              <Main-Class>io.github.jasonkayzk.CheckSum</Main-Class>
            </manifestEntries>
          </transformer>
        </transformers>
      </configuration>
    </execution>
  </executions>
</plugin>

需要注意的是:<id>checksum</id> 是一定要配置的,否则打包时会报错:

Maven – shade for parameter resource: Cannot find ‘resource’ in class org.apache.maven.plugins.shade.resource.ManifestResourceTransformer

重新打包后执行:

$ java -jar target/a-checksum-1.0-SNAPSHOT.jar \
  --algorithm SHA-256 hello.txt

5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03

小结

上文主要讲述了如何编写并打包一个 Executable Jar,打包的方式还是传统的 Jar 包的方式;

实际上,得益于 GraalVM 的发展,目前已经可以直接编译Java 到 Native 了,但是还存在一些坑;

希望以后有机会写关于 GraalVM 的内容~

附录

系列文章:

参考文章:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK