命令行开发原创
# 一、Java 命令行开发方案
# 什么是命令行程序?
命令行程序俗称 CLI(Command Line Interface),是指通过命令行界面运行的应用程序。通过终端窗口接收用户输入的 纯文本 命令,并执行相应的任务。
一些常见的命令行环境包括 Unix/Linux 的终端、Windows 的命令提示符和 PowerShell 等。学编程的同学可能没有开发过命令行程序,但一定都接触过终端!
# 命令的结构
一图胜千言,输入给命令行的命令通常包含:
- command:命令类型,具体要做的事
- option:选项,用于改变命令的行为
- parameter:参数,传递给命令行工具的值
# 为什么要开发命令行?
命令行程序的几个优点:
- 不依赖于特定的图形界面,非常轻量
- 通常可以直接在操作系统自带的终端环境中运行
- 可以和用户交互、给用户输入引导和帮助手册
- 内置一些快捷操作(比如查看历史命令、上下切换命令)
还有一个最大的优点 —— 简单直接,比如复制粘贴别人写好的命令就能执行,而不用像使用网页一样点击多次,非常符合程序员的使用(偷懒)习惯,less is more!
# 命令行的作用
回归到我们的项目,命令行的作用是什么呢?
可以使用命令行程序来和用户交互,引导用户输入代码生成的定制参数,并将输入参数封装为配置对象,然后 “喂” 给之前编写好的代码生成器来生成文件。
比如之间我们的动态模板配置 MainTemplateConfig
中包含 loop、author、outputText 这 3 个参数,那么可以让用户输入下列完整命令,来给模板配置传值:
generate --loop --author yupi --outputText good
或者更人性化一些,允许用户交互式输入,比如先输入 generate
,然后按照系统的提示依次输入其他参数,如下图:
# 实现方案
了解了命令行的作用后,我们如何用 Java 语言开发命令行程序呢?
# 1、自主实现
最直接的方案就是使用 Java 内置的类库自主实现,比如通过 Scanner 读取用户的输入,再配合 while、if ... else 等流程控制语句实现多次输入和提示。
示例代码如下:
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
// 读取整数
int number = scanner.nextInt();
}
2
3
4
5
6
这种方式虽然简单,但缺点也很多:
1)需要自主解析用户的输入
前面已经介绍了命令的结构,可以看到一句命令可能非常复杂,包含各种选项和参数,而且用户还可能乱序输入不同的参数。
如何从这么一句复杂的命令中提取出需要的值,是一个难题。
2)需要自主编写一套获取参数的交互流程
比如用户输入了异常值怎么办?是中断程序还是给出提示?给出什么提示?这些都要开发者自己考虑。
3)需要自主实现更多高级功能
比如基本所有命令行工具都有的帮助手册命令(--help)、颜色高亮等。
如果这些和业务无关的功能如果都需要自己开发,那真的是太浪费时间了!
所以建议直接用现成的第三方命令行开发库。
# 2、第三方库
这里鱼皮做了充分的调研,收集了几种经典的 Java 命令行开发相关库,简单分为 3 类:
1)命令行工具开发框架
专门用于开发命令行工具的框架
⭐️ Picocli(https://github.com/remkop/picocli):优点是 GitHub 的 star 数多(4k+)、持续更新,支持颜色高亮、自动补全、子命令、帮助手册等,最推荐。
2)控制台输入处理库
能够对用户在控制台的输入进行处理的库
JLine(https://github.com/jline/jline3):支持自动补全、行编辑、查看命令历史等,但官方文档内容略少、学习成本高。参考教程:https://zhuanlan.zhihu.com/p/43835406
3)命令行解析库
支持对命令行进行解析取值的库
⭐️ JCommander(https://github.com/cbeust/jcommander):注解驱动,可以直接把命令映射到对象上,从而大幅简化代码。GitHub 上近 2k star,比较推荐。
Apache Commons CLI(https://github.com/apache/commons-cli):简单易用,但是功能不够多。参考教程:https://blog.csdn.net/liuxiangke0210/article/details/78141887
Jopt Simple(https://github.com/jopt-simple/jopt-simple):不推荐。冷门、很久没维护、star 数少、生态不好。
综上,最推荐的是专业的命令行开发框架 Picocli,下面从 0 开始带大家入门和实战这个框架。
# 二、Picocli 命令行框架入门
网上有关 Picocli 框架的教程非常少,最推荐的入门方式除了看鱼皮的教程外,就是阅读官方文档了。
官方文档:https://picocli.info/
推荐从官方提供的快速入门教程开始:https://picocli.info/quick-guide.html
一般我们学习新技术的步骤是:先跑通入门 Demo,再学习该技术的用法和特性。
# 入门 Demo
1)在 yuzi-generator-basic
项目的 pom.xml
文件中引入 picocli 的依赖:
<!-- https://picocli.info -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.5</version>
</dependency>
2
3
4
5
6
然后我们在 com.yupi
包下新建 cli.example
包,用于存放所有和 Picocli 入门有关的示例代码。
2)复制官方快速入门教程中的示例代码到 com.yupi.cli.example
包下,并略微修改 run 方法中的代码,打印参数的值。
完整代码如下:
package com.yupi.cli.example;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "ASCIIArt", version = "ASCIIArt 1.0", mixinStandardHelpOptions = true)
public class ASCIIArt implements Runnable {
@Option(names = { "-s", "--font-size" }, description = "Font size")
int fontSize = 19;
@Parameters(paramLabel = "<word>", defaultValue = "Hello, picocli",
description = "Words to be translated into ASCII art.")
private String[] words = { "Hello,", "picocli" };
@Override
public void run() {
// 自己实现业务逻辑
System.out.println("fontSize = " + fontSize);
System.out.println("words = " + String.join(",", words));
}
public static void main(String[] args) {
int exitCode = new CommandLine(new ASCIIArt()).execute(args);
System.exit(exitCode);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
看不懂这段代码没关系,官方文档已经给了非常详细的解释:
帮大家翻译一下:
- 创建一个实现
Runnable
或Callable
接口的类,这就是一个命令。 - 使用
@Command
注解标记该类并为其命名,mixinStandardHelpOptions
属性设置为 true 可以给应用程序自动添加--help
和--version
选项。 - 通过
@Option
注解将字段设置为命令行选项,可以给选项设置名称和描述。 - 通过
@Parameters
注解将字段设置为命令行参数,可以指定默认值、描述等信息。 - Picocli 会将命令行参数转换为强类型值,并自动注入到注解字段中。
- 在类的
run
或call
方法中定义业务逻辑,当命令解析成功(用户敲了回车)后被调用。 - 在
main
方法中,通过CommandLine
对象的execute
方法来处理用户输入的命令,剩下的就交给 Picocli 框架来解析命令并执行业务逻辑啦~ CommandLine.execute
方法返回一个退出代码。可以调用System.exit
并将该退出代码作为参数,从而向调用进程表示成功或失败。
3)让我们更改主程序的执行参数(args)来测试程序,能够成功看到输出结果,如下图:
通过这个入门 Demo,我们可以简单总结一个命令的开发流程:
- 创建命令
- 设置选项和参数
- 编写命令执行的业务逻辑
- 通过 CommandLine 对象接受输入并执行命令
在跑通了入门 Demo 后,我们来学习一些 Picocli 开发命令行的实用功能。
# 实用功能
# 1、帮助手册
通过给类添加的 @Command
注解参数 mixinStandardHelpOptions
设置为 true 来开启:
@Command(name = "ASCIIArt", mixinStandardHelpOptions = true)
然后将主程序的输入参数设置为 --help
就能打印出命令的帮助手册信息了,如下图:
可以看到,Picocli 生成的帮助手册不仅规范、而且清晰完整。
# 2、命令解析
Picocli 最核心的能力就是命令解析,能够从一句完整的命令中解析选项和参数,并填充到对象的属性中。
Picocli 使用注解的方式实现命令解析,不需要自己编写代码,整个类看起来非常清晰。
最核心的 2 个注解其实在入门 Demo 中我们已经使用到了:
@Option
注解用于解析选项@Parameters
注解用于解析参数
示例代码如下:
@Option(names = { "-s", "--font-size" }, description = "Font size")
int fontSize = 19;
@Parameters(paramLabel = "<word>", defaultValue = "Hello, picocli",
description = "Words to be translated into ASCII art.")
private String[] words = { "Hello,", "picocli" };
2
3
4
5
6
可以给这些注解指定参数,比较常用的参数有:
1)@Option 注解的 names 参数:指定选项英文名称。
2)description 参数:指定描述信息,从而让生成的帮助手册和提示信息更清晰。
3)@Parameters 注解的 paramLabel 参数:参数标签,作用类似于描述信息。
4)@Parameters 注解的 defaultValue 参数:默认值,参考文档:https://picocli.info/#_default_values
5)required 参数:要求必填,参考文档:https://picocli.info/#_required_arguments
示例代码如下:
class RequiredOption {
@Option(names = "-a", required = true)
String author;
}
2
3
4
5
此外,命令解析天然支持 多值选项 ,只需要把对象属性的类型设置为数组类型即可,比如:
@Option(names = "-option")
int[] values;
2
具体可以参考官方文档:https://picocli.info/#_multiple_values
更多关于选项和参数注解的用法,也可以阅读官方文档学习:https://picocli.info/quick-guide.html#_options_and_parameters
# 3、交互式输入
所谓的交互式输入就是允许用户像跟程序聊天一样,在程序的指引下一个参数一个参数地输入。
如下图:
Picocli 为交互式输入提供了很好的支持,我梳理了大概 4 种交互式输入的模式。
# 1)基本能力
交互式输入的一个典型应用场景就是:用户要登录时,引导 ta 输入密码。
官方已经为我们提供了一段交互式输入的示例代码,鱼皮对它进行了简化,示例代码如下:
参考官方文档:https://picocli.info/#_interactive_password_options
package com.yupi.cli.example;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import java.util.concurrent.Callable;
public class Login implements Callable<Integer> {
@Option(names = {"-u", "--user"}, description = "User name")
String user;
@Option(names = {"-p", "--password"}, description = "Passphrase", interactive = true)
String password;
public Integer call() throws Exception {
System.out.println("password = " + password);
return 0;
}
public static void main(String[] args) {
new CommandLine(new Login()).execute("-u", "user123", "-p");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
让我们分析下上面的代码,主要包含 4 个部分:
1)首先命令类需要实现 Callable
接口
public class Login implements Callable<Integer> {
...
}
2
3
2)将 @Option
注解的 interactive
参数设置为 true,表示该选项支持交互式输入
@Option(names = {"-p", "--password"}, interactive = true)
String password;
2
3)在所有参数都输入完成后,会执行 call
方法,可以在该方法中编写具体的业务逻辑:
public Integer call() throws Exception {
System.out.println("password = " + password);
return 0;
}
2
3
4
4)在 Main 方法中执行命令并传入参数:
java
new CommandLine(new Login()).execute("-u", "user123", "-p");
2
3
执行上述代码,看到程序提示我们输入密码:
注意,如果以 jar 包方式运行上述程序,用户的输入默认是不会显示在控制台的(类似输入密码时的体验)。从 Picocli 4.6 版本开始,可以通过指定 @Option
注解的 echo
参数为 true 来显示用户的输入,并通过 prompt
参数指定引导用户输入的提示语。
# 2)多个选项交互式
Picocli 支持在一个命令中指定多个交互式输入的选项,会按照顺序提示用户并接收输入。
在上述代码中再增加一个 checkPassword 选项,同样开启交互式输入,代码如下:
public class Login implements Callable<Integer> {
@Option(names = {"-u", "--user"}, description = "User name")
String user;
@Option(names = {"-p", "--password"}, description = "Passphrase", interactive = true)
String password;
@Option(names = {"-cp", "--checkPassword"}, description = "Check Password", interactive = true)
String checkPassword;
public Integer call() throws Exception {
System.out.println("password = " + password);
System.out.println("checkPassword = " + checkPassword);
return 0;
}
public static void main(String[] args) {
new CommandLine(new Login()).execute("-u", "user123", "-p");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
但运行上述代码我们会发现,怎么只提示我输入了密码,没提示我输入确认密码呢?
这是由于 Picocli 框架的规则,用户必须在命令中指定需要交互式输入的选项(比如 -p
),才会引导用户输入。
所以我们需要修改上述代码中的 main 方法,给命令输入补充 -cp
参数:
public static void main(String[] args) {
new CommandLine(new Login()).execute("-u", "user123", "-p", "-cp");
}
2
3
再次执行,这下程序会依次提醒我们输入两个选项啦:
根据实际使用情况,又可以将交互式输入分为 2 种模式:
- 可选交互式:用户可以直接在整行命令中输入选项,而不用给用户提示信息。
- 强制交互式:用户必须获得提示并输入某个选项,不允许不填写。
下面分别讲解这两种模式。
# 3)可选交互式
默认情况下,是无法直接在命令中给交互式选项指定任何参数的,只能通过交互式输入,比如命令中包含 -p xxx
会报错。
可选交互式官方文档:https://picocli.info/#_optionally_interactive
让我们测试一下,给上面的示例代码输入以下参数:
new CommandLine(new Login()).execute("-u", "user123", "-p", "xxx", "-cp");
执行效果如下图,出现了参数不匹配的报错:
官方提供了可选交互式的解决方案,通过调整 @Option
注解中的 arity
属性来指定每个选项可接受的参数个数,就能解决这个问题。
arity 官方介绍:https://picocli.info/#_arity
示例代码如下:
@Option(names = {"-p", "--password"}, arity = "0..1", description = "Passphrase", interactive = true)
String password;
2
然后可以直接在完整命令中给交互式选项设置值:
new CommandLine(new Login()).execute("-u", "user123", "-p", "123", "-cp");
2
执行结果如图,不再提示让用户输入 password 选项,而是直接读取了命令中的值:
这里鱼皮推荐一个最佳实践:建议给所有需要交互式输入的选项都增加 arity
参数(一般是 arity = "0..1"
),这样用户既可以在完整命令中直接给选项填充参数,也可以选择交互式输入。
示例代码如下:
public class Login implements Callable<Integer> {
@Option(names = {"-u", "--user"}, description = "User name")
String user;
// 设置了 arity 参数,可选交互式
@Option(names = {"-p", "--password"}, arity = "0..1", description = "Passphrase", interactive = true)
String password;
// 设置了 arity 参数,可选交互式
@Option(names = {"-cp", "--checkPassword"}, arity = "0..1", description = "Check Password", interactive = true)
String checkPassword;
public Integer call() throws Exception {
System.out.println("password = " + password);
System.out.println("checkPassword = " + checkPassword);
return 0;
}
public static void main(String[] args) {
new CommandLine(new Login()).execute("-u", "user123", "-p", "123", "-cp", "456");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 4)强制交互式
在之前已经提到,如果用户不在命令中输入交互式选项(比如 -p
),那么系统不会提示用户输入这个选项,属性的值将为默认值(比如 null)。
举个例子,下列命令中不带 -p
选项:
java
new CommandLine(new Login()).execute("-u", "user123");
2
3
执行就会发现,程序不会提示用户输入 -p
选项的参数,而是直接输出结果,值为 null:
但有些时候,我们要求用户必须输入某个选项,而不能使用默认的空值,怎么办呢?
官方给出了强制交互式的解决方案,参考文档:https://picocli.info/#_forcing_interactive_input
但是,官方的解决方案是需要自己定义业务逻辑的。原理是在命令执行后对属性进行判断,如果用户没有输入指定的参数,那么再通过 System.console().readLine
等方式提示用户输入,示例代码如下:
@Command
public class Main implements Runnable {
@Option(names = "--interactive", interactive = true)
String value;
public void run() {
if (value == null && System.console() != null) {
// 主动提示用户输入
value = System.console().readLine("Enter value for --interactive: ");
}
System.out.println("You provided value '" + value + "'");
}
public static void main(String[] args) {
new CommandLine(new Main()).execute(args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
个人不是很喜欢这种方案,因为要额外编写提示代码,感觉又回到自主实现了。
鱼皮想出的一种方案是,编写一段通用的校验程序,如果用户的输入命令中没有包含交互式选项,那么就自动为输入命令补充该选项即可,这样就能强制触发交互式输入。
说通俗一点,检测 args 数组中是否存在对应选项,不存在则为数组增加选项元素。
该思路作为一个小扩展点,实现起来并不复杂,大家可以自行实现。(小提示:可以利用反射自动读取必填的选项名称)
# 4、子命令
子命令是指命令中又包含一组命令,相当于命令的分组嵌套,适用于功能较多、较为复杂的命令行程序,比如 git、docker 命令等。
官方文档:https://picocli.info/#_subcommands
在 Picocli 中,提供了两种设置子命令的方式。
# 1)声明式
通过 @Command
注解的 subcommands
属性来给命令添加子命令,优点是更直观清晰。
示例代码如下:
@Command(subcommands = {
GitStatus.class,
GitCommit.class,
GitAdd.class,
GitBranch.class,
GitCheckout.class,
GitClone.class,
GitDiff.class,
GitMerge.class,
GitPush.class,
GitRebase.class,
GitTag.class
})
public class Git { /* ... */ }
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2)编程式
在创建 CommandLine
对象时,调用 addSubcommand
方法来绑定子命令,优点是更灵活。
示例代码如下:
CommandLine commandLine = new CommandLine(new Git())
.addSubcommand("status", new GitStatus())
.addSubcommand("commit", new GitCommit())
.addSubcommand("add", new GitAdd())
.addSubcommand("branch", new GitBranch())
.addSubcommand("checkout", new GitCheckout())
.addSubcommand("clone", new GitClone())
.addSubcommand("diff", new GitDiff())
.addSubcommand("merge", new GitMerge())
.addSubcommand("push", new GitPush())
.addSubcommand("rebase", new GitRebase())
.addSubcommand("tag", new GitTag());
2
3
4
5
6
7
8
9
10
11
12
# 实践
让我们编写一个示例程序,支持增加、删除、查询 3 个子命令,并传入不同的 args 来测试效果。
完整代码如下:
package com.yupi.cli.example;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@Command(name = "main", mixinStandardHelpOptions = true)
public class SubCommandExample implements Runnable {
@Override
public void run() {
System.out.println("执行主命令");
}
@Command(name = "add", description = "增加", mixinStandardHelpOptions = true)
static class AddCommand implements Runnable {
public void run() {
System.out.println("执行增加命令");
}
}
@Command(name = "delete", description = "删除", mixinStandardHelpOptions = true)
static class DeleteCommand implements Runnable {
public void run() {
System.out.println("执行删除命令");
}
}
@Command(name = "query", description = "查询", mixinStandardHelpOptions = true)
static class QueryCommand implements Runnable {
public void run() {
System.out.println("执行查询命令");
}
}
public static void main(String[] args) {
// 执行主命令
String[] myArgs = new String[] { };
// 查看主命令的帮助手册
// String[] myArgs = new String[] { "--help" };
// 执行增加命令
// String[] myArgs = new String[] { "add" };
// 执行增加命令的帮助手册
// String[] myArgs = new String[] { "add", "--help" };
// 执行不存在的命令,会报错
// String[] myArgs = new String[] { "update" };
int exitCode = new CommandLine(new SubCommandExample())
.addSubcommand(new AddCommand())
.addSubcommand(new DeleteCommand())
.addSubcommand(new QueryCommand())
.execute(myArgs);
System.exit(exitCode);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
测试运行,发现当输入 --help
参数时,打印出了主命令和所有的子命令信息,证明子命令绑定成功:
# 5、其他功能
除了上面演示的功能外,还有一些可能有用的功能,大家感兴趣了解下就好,不要作为重点学习。
比如:
- 参数分组:https://picocli.info/#_argument_groups
- 错误处理:https://picocli.info/#_handling_errors
- 颜色高亮:https://picocli.info/#_ansi_colors_and_styles
# 更多学习资源
关于 Picocli 的学习资源极少,还是以官方文档为主。
分享一篇还算完整的博客:
- Picocli 中文入门学习:https://blog.csdn.net/it_freshman/article/details/125458116
# 三、命令模式
已经入门了 Picocli 命令行框架,在正式运用它来开发代码生成器项目前,我们要先来学习一种经典的设计模式 —— 命令模式,这样有利于理解后面的代码开发。
# 什么是命令模式?
命令模式是一种行为类设计模式,核心是将每种请求或操作封装为一个独立的对象,从而可以集中管理这些请求或操作,比如将请求队列化依次执行、或者对操作进行记录和撤销。
命令模式通过将请求的发送者(客户端)和接收者(执行请求的对象)解耦,提供了更大的灵活性和可维护性。
听不懂上面这句话很正常,我来举个例子。
我们在生活中都用过电视机,我们
就相当于客户端,要操作电视来换台;而 电视
就是执行请求的对象,要根据我们的操作来换台。但是我们一般不会直接按电视上的按钮来换台,而是用一个 遥控器
,通过点击遥控器上的 操作按钮
来控制电视。
这样就相当于把我们和电视解耦了。哪怕遥控器丢了,再换一个遥控器就好了;而且现在手机都能作为万能的电视遥控器,我们可以同时遥控多个品牌的设备,不用关心设备的具体品牌型号,提供了更大的方便。
# 命令模式的优点和应用场景
正如上面的例子,命令模式最大的优点就是解耦请求发送者和接受者,让系统更加灵活、可扩展。
由于每个操作都是一个独立的命令类,所以我们需要新增命令操作时,不需要改动现有代码。
命令模式典型的应用场景:
- 系统需要统一处理多种复杂的操作,比如操作排队、记录操作历史、撤销重做等。
- 系统需要持续增加新的命令、或者要处理复杂的组合命令(子命令),使用命令模式可以实现解耦。
本项目要开发的命令行工具,就符合这两个应用场景。
# 命令模式的要素和实现
通过上面用户使用遥控器来操作电视机设备的例子,带大家理解命令模式的关键要素和实现代码。
以下所有示例代码放到
com.yupi.cli.pattern
包中
# 1)命令
相当于遥控器操作按钮的制作规范
命令是一个抽象类或接口,它定义了执行操作的方法,通常是 execute
,该方法封装了具体的操作。
代码如下:
public interface Command {
void execute();
}
2
3
# 2)具体命令
相当于遥控器的某个操作按钮
具体命令是命令接口的具体实现类,它负责将请求传递给接收者(设备)并执行具体的操作。
比如定义一个关闭设备命令,代码如下:
public class TurnOffCommand implements Command {
private Device device;
public TurnOffCommand(Device device) {
this.device = device;
}
public void execute() {
device.turnOff();
}
}
2
3
4
5
6
7
8
9
10
11
还可以定义开启设备命令,代码如下:
public class TurnOnCommand implements Command {
private Device device;
public TurnOnCommand(Device device) {
this.device = device;
}
public void execute() {
device.turnOn();
}
}
2
3
4
5
6
7
8
9
10
11
# 3)接受者
相当于被遥控的设备
接收者是最终执行命令的对象,知道如何执行具体的操作。
比如定义一个设备类,代码如下:
public class Device {
private String name;
public Device(String name) {
this.name = name;
}
public void turnOn() {
System.out.println(name + " 设备打开");
}
public void turnOff() {
System.out.println(name + " 设备关闭");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 4)调用者
相当于遥控器
作用是接受客户端的命令并执行。
比如定义遥控器类,代码如下:
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
2
3
4
5
6
7
8
9
10
11
以上只是最基础的调用者类,还可以给遥控器类增加更多能力,比如存储历史记录、撤销重做等。
# 5)客户端
相当于使用遥控器的人
客户端的作用是创建命令对象并将其与接收者关联(绑定设备),然后将命令对象传递给调用者(按遥控器),从而触发执行。
示例客户端代码如下:
public class Client {
public static void main(String[] args) {
// 创建接收者对象
Device tv = new Device("TV");
Device stereo = new Device("Stereo");
// 创建具体命令对象,可以绑定不同设备
TurnOnCommand turnOn = new TurnOnCommand(tv);
TurnOffCommand turnOff = new TurnOffCommand(stereo);
// 创建调用者
RemoteControl remote = new RemoteControl();
// 执行命令
remote.setCommand(turnOn);
remote.pressButton();
remote.setCommand(turnOff);
remote.pressButton();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在这个示例中,命令模式将遥控器按钮的按下操作与实际设备的开关操作解耦,从而实现了灵活的控制和可扩展性。
整个程序的 UML 类图如下:
视频教程中有给大家演示使用 IDEA 来生成 UML 图的方法。
# 推荐学习
鱼皮独立开发的 yuindex 网页终端项目,就是以命令模式为核心实现的,前后端同学都可以学习:https://github.com/liyupi/yuindex
# 四、Picocli 命令行代码生成器开发
学习了 Picocli 框架的用法和命令模式后,我们可以运用它们来开发一款命令行代码生成器啦。
首先明确我们的需求,这个命令行程序需要支持 3 种子命令:
- generate 子命令:生成文件
- list 子命令:查看要生成的原始文件列表信息
- config 子命令:查看允许用户传入的动态参数信息
为了简化使用,要求能同时支持通过完整命令和交互式输入的方式来设置动态参数。
整个开发过程分为 6 个步骤:
- 创建命令执行器(主命令)
- 分别实现每种子命令
- 提供项目的全局调用入口
- 构建程序 jar 包
- 测试使用
- 简化使用(封装脚本)
# 1、创建命令执行器
首先在 com.yupi.cli.command
包下新建 3 个子命令类,和需求对应:
然后在 com.yupi.cli
包下创建命令执行器 CommandExecutor
类,负责绑定所有子命令,并且提供执行命令的方法。
完整代码如下:
@Command(name = "yuzi", mixinStandardHelpOptions = true)
public class CommandExecutor implements Runnable {
private final CommandLine commandLine;
{
commandLine = new CommandLine(this)
.addSubcommand(new GenerateCommand())
.addSubcommand(new ConfigCommand())
.addSubcommand(new ListCommand());
}
@Override
public void run() {
// 不输入子命令时,给出友好提示
System.out.println("请输入具体命令,或者输入 --help 查看命令提示");
}
/**
* 执行命令
*
* @param args
* @return
*/
public Integer doExecute(String[] args) {
return commandLine.execute(args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 2、子命令实现
# generate 子命令
这是代码生成器的核心命令,作用是接受参数并生成代码。
实现步骤如下:
1)定义参数选项。和之前动态生成代码定义的数据模型 MainTemplateConfig
属性一致即可。使用 Picocli 提供的注解来交互式获取参数信息(interactive = true),并且对用户显示输入信息(echo = true)。
2)使用 BeanUtil.copyProperties
快速将通过命令接受到的属性复制给 MainTemplateConfig
配置对象。
3)调用之前开发好的 MainGenerator
代码生成类来生成代码。
完整代码如下:
@Command(name = "generate", description = "生成代码", mixinStandardHelpOptions = true)
@Data
public class GenerateCommand implements Callable<Integer> {
@Option(names = {"-l", "--loop"}, arity = "0..1", description = "是否循环", interactive = true, echo = true)
private boolean loop;
@Option(names = {"-a", "--author"}, arity = "0..1", description = "作者", interactive = true, echo = true)
private String author = "yupi";
@Option(names = {"-o", "--outputText"}, arity = "0..1", description = "输出文本", interactive = true, echo = true)
private String outputText = "sum = ";
public Integer call() throws Exception {
MainTemplateConfig mainTemplateConfig = new MainTemplateConfig();
BeanUtil.copyProperties(this, mainTemplateConfig);
System.out.println("配置信息:" + mainTemplateConfig);
MainGenerator.doGenerate(mainTemplateConfig);
return 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# list 子命令
list 是一个辅助命令,作用是遍历输出所有要生成的文件列表。
此处由于我们要生成的项目文件都封装在了 acm-template
目录下,所以直接用 Hutool 库提供的 FileUtil.loopFiles(inputPath)
方法来遍历该目录下的所有文件即可。
完整代码如下:
@Command(name = "list", description = "查看文件列表", mixinStandardHelpOptions = true)
public class ListCommand implements Runnable {
public void run() {
String projectPath = System.getProperty("user.dir");
// 整个项目的根路径
File parentFile = new File(projectPath).getParentFile();
// 输入路径
String inputPath = new File(parentFile, "yuzi-generator-demo-projects/acm-template").getAbsolutePath();
List<File> files = FileUtil.loopFiles(inputPath);
for (File file : files) {
System.out.println(file);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# config 子命令
config 是一个辅助命令,作用是输出允许用户传入的动态参数的信息(也就是本项目 MainTemplateConfig
类的字段信息)。
如何输出呢?
最简单粗暴的方法是直接自己手写打印信息,比较灵活。但是如果配置类的属性字段发生修改,也要同步修改 config 命令的代码,不利于维护。
更推荐的方式是通过 Java 的反射机制,在程序运行时动态打印出对象属性的信息。又有 2 种方法:
1)JDK 原生反射语法
示例代码如下:
Class<?> myClass = MainTemplateConfig.class;
// 获取类的所有字段
Field[] fields = myClass.getDeclaredFields();
2
3
2)Hutool 的反射工具类(只需一行代码,更推荐)
示例代码如下:
Field[] fields = ReflectUtil.getFields(MainTemplateConfig.class);
完整代码如下:
@Command(name = "config", description = "查看参数信息", mixinStandardHelpOptions = true)
public class ConfigCommand implements Runnable {
public void run() {
// 实现 config 命令的逻辑
System.out.println("查看参数信息");
// // 获取要打印属性信息的类
// Class<?> myClass = MainTemplateConfig.class;
// // 获取类的所有字段
// Field[] fields = myClass.getDeclaredFields();
Field[] fields = ReflectUtil.getFields(MainTemplateConfig.class);
// 遍历并打印每个字段的信息
for (Field field : fields) {
System.out.println("字段名称:" + field.getName());
System.out.println("字段类型:" + field.getType());
// System.out.println("Modifiers: " + java.lang.reflect.Modifier.toString(field.getModifiers()));
System.out.println("---");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3、全局调用入口
在项目的根包 com.yupi
下创建 Main 类,作为整个代码生成器项目的全局调用入口。作用是接受用户的参数、创建命令执行器并调用执行。
代码如下:
public class Main {
public static void main(String[] args) {
CommandExecutor commandExecutor = new CommandExecutor();
commandExecutor.doExecute(args);
}
}
2
3
4
5
6
7
接下来我们需要对命令进行测试。建议直接在 main 方法中给 args 参数设置值来完成测试,比较灵活:
public class Main {
public static void main(String[] args) {
args = new String[]{"generate", "-l", "-a", "-o"};
// args = new String[]{"config"};
// args = new String[]{"list"};
CommandExecutor commandExecutor = new CommandExecutor();
commandExecutor.doExecute(args);
}
}
2
3
4
5
6
7
8
9
10
1)测试 generate 命令,执行并得到输出结果:
成功查看到生成的代码:
2)测试 config 命令,输出结果如下图:
3)测试 list 命令,输出结果如下图:
# 4、jar 包构建
虽然命令行程序已经开发完成,但是不能每次都让用户在 Main 方法里修改参数吧?
我们可以将代码生成器打成 jar 包,支持用户执行并使用命令行工具动态输入参数。
构建 jar 包的流程并不复杂:
1)先修改 Main.java
主类,不再强制指定 args 参数,而是通过执行参数获取:
public class Main {
public static void main(String[] args) {
CommandExecutor commandExecutor = new CommandExecutor();
commandExecutor.doExecute(args);
}
}
2
3
4
5
6
7
2)使用 Maven 打包构建
需要在 pom.xml
文件中引入 maven-assembly-plugin
插件,从而将依赖库一起打入 jar 包,并且指定 mainClass 路径为 com.yupi.Main
。
代码如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.hello.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
然后执行 mvn package
打包命令,即可构建 jar 包:
可以在项目根目录下看到生成的 jar 包:
# 5、测试使用
得到 jar 包后,我们就可以通过 java -jar
命令运行 jar 包了。
参考命令格式如下:
java -jar <jar 包名> generate [..args]
1)让我们打开终端并进入到 target 目录中,输入下列命令交互式生成代码:
java -jar yuzi-generator-basic-1.0-SNAPSHOT-jar-with-dependencies.jar generate -l -o -a
执行结果:
2)或者直接在命令中指定部分参数,比如作者为 liyupi:
java -jar yuzi-generator-basic-1.0-SNAPSHOT-jar-with-dependencies.jar generate -l -o -a liyupi
执行结果,不会再提示让我们输入作者(因为已经在命令中指定):
# 6、封装脚本
虽然命令行工具已经可以成功使用,但是要输入的命令也太长、太复杂了!
怎么简化调用呢?
可以把命令的调用封装在一个 bash 脚本或者 windows 批处理文件,像封装一个函数一样,简化命令。
# Linux Bash 脚本
适用于 Linux 和 Mac 系统
在项目 yuzi-generator-basic
的根目录下创建 generator
文件,输入脚本信息:
#!/bin/bash
java -jar target/yuzi-generator-basic-1.0-SNAPSHOT-jar-with-dependencies.jar "$@"
2
然后输入命令 chmod a+x generator
,给该文件添加可执行权限。
然后就可以简化使用了:
查看帮助提示,很方便!
# Windows 批处理文件
如果是 Windows 系统,在项目根目录下创建 generator.bat
文件,输入脚本信息:
@echo off
java -jar target/yuzi-generator-basic-1.0-SNAPSHOT-jar-with-dependencies.jar %*
2
在上述批处理文件中:
- @echo off 用于禁止在命令执行时显示命令本身。
- java -jar <jar 包路径> %* 执行 Java 命令,其中 %* 用于将批处理文件接收到的所有参数传递给 Java 应用程
然后就可以在终端中使用啦~
# 命令模式的巧妙运用
不知道大家有没有发现,其实在我们的代码中,已经浑然天成地运用了命令模式!
命令模式的几个要素:
- 命令:
GenerateCommand
等子命令中实现的 Runnable(或 Callable)接口 - 具体命令:每个子命令类
- 调用方:
CommandExecutor
命令执行器类 - 接受者:代码生成器
MainGenerator
类(实际执行功能的类) - 客户端:主程序
Main
所以如果面试官问到:你的项目中哪里用到了设计模式?
应该就能很好地回答啦~
- 01
- 模板生成工具 原创02-18
- 02
- RTC实时时钟 原创02-12
- 03
- keil调试 原创01-21
- 04
- GPIO概述与配置 原创01-20
- 05
- element-plus多文件手动上传 原创11-03