第三章 命令行开发

154 阅读5分钟

第三章 命令行开发

前言

本笔记主要用途是学习up 主程序员鱼皮的项目:代码生成器时的一些学习心得。

代码地址:github.com/Liucc-123/y…

项目教程:www.codefather.cn/course/1790…

上一章节内容:juejin.cn/post/747372…

本节重点

  • 第一阶段:开发本地代码生成器。
  • 重点内容:
    1. Java命令行开发方案
    2. Picocli命令行框架入门
    3. 命令模式讲解
    4. Picocli命令行代码生成器开发

一、Java命令行开发方案

  1. 什么是命令行程序?
    • CLI(Command Line Interface):通过终端窗口接收纯文本命令并执行任务。
    • 常见环境:Unix/Linux终端、Windows命令提示符、PowerShell等。
  2. 命令的结构
    • command:命令类型(如generate)。
    • option:选项(如-loop)。
    • parameter:参数(如author yupi)。
  3. 为什么要开发命令行?
    • 轻量级,不依赖图形界面。
    • 可直接在操作系统终端运行。
    • 支持用户交互和帮助手册。
    • 简单直接,符合程序员使用习惯。
  4. 命令行的作用
    • 与用户交互,引导输入代码生成的定制参数。
    • 封装参数为配置对象,传递给代码生成器。
  5. 实现方案
    • 自主实现:使用Java内置类库(如Scanner)读取输入,但需要手动解析命令和实现交互。
    • 第三方库:推荐使用Picocli框架,支持命令解析、交互式输入、帮助手册等功能。

二、Picocli命令行框架入门

  1. 入门Demo
    • pom.xml中引入Picocli依赖:

      <dependency>
          <groupId>info.picocli</groupId>
          <artifactId>picocli</artifactId>
          <version>4.7.5</version>
      </dependency>
      
    • 示例代码:

      @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. 实用功能
    • 帮助手册:通过mixinStandardHelpOptions = true自动生成。
    • 命令解析:使用@Option@Parameters注解解析命令行参数。
    • 交互式输入:通过interactive = true实现。
    • 子命令:支持命令嵌套,适用于复杂工具(如gitdocker)。
  3. 更多学习资源

三、命令模式

  1. 什么是命令模式?

    • 将请求封装为对象,解耦请求发送者和接收者。
    • 适用于操作队列化、记录操作历史、撤销重做等场景。
  2. 命令模式的要素

    • 命令:定义执行操作的接口(如execute())。
    • 具体命令:实现命令接口,调用接收者的操作。
    • 接收者:执行具体操作的对象。
    • 调用者:执行命令的对象。
    • 客户端:创建命令对象并触发执行。
  3. 示例代码

    // 命令接口
    public interface Command {
        void execute();
    }
    
    // 具体命令
    public class TurnOnCommand implements Command {
        private Device device;
    
        public TurnOnCommand(Device device) {
            this.device = device;
        }
    
        @Override
        public void execute() {
            device.turnOn();
        }
    }
    
    // 接收者
    public class Device {
        public void turnOn() {
            System.out.println("设备打开");
        }
    }
    
    // 调用者
    public class RemoteControl {
        private Command command;
    
        public void setCommand(Command command) {
            this.command = command;
        }
    
        public void pressButton() {
            command.execute();
        }
    }
    
    // 客户端
    public class Client {
        public static void main(String[] args) {
            Device tv = new Device();
            Command turnOn = new TurnOnCommand(tv);
            RemoteControl remote = new RemoteControl();
            remote.setCommand(turnOn);
            remote.pressButton();
        }
    }
    
    

四、Picocli命令行代码生成器开发

  1. 需求分析

    • 支持3种子命令:
      • generate:生成代码。
      • list:查看要生成的原始文件列表信息。
      • config:允许用户查看动态参数信息。
    • 支持完整命令和交互式输入。
  2. 开发步骤

    • 创建命令执行器(CommandExecutor)。

      package com.liucc.cli;
      
      import com.liucc.cli.command.ConfigCommand;
      import com.liucc.cli.command.GenerateCommand;
      import com.liucc.cli.command.ListCommand;
      import picocli.CommandLine;
      
      @CommandLine.Command(name = "yuzi", mixinStandardHelpOptions = true)
      public class CommandExecutor implements Runnable{
          private final CommandLine commandLine;
          {
              commandLine = new CommandLine(this)
                      .addSubcommand(new ConfigCommand())
                      .addSubcommand(new ListCommand())
                      .addSubcommand(new GenerateCommand());
          }
      
          @Override
          public void run() {
              // 不输入子命令时,给出友好提示
              System.out.println("请输入具体命令,或者输入 --help 查看命令");
          }
      
          /**
           * 执行命令
           * @param args
           * @return
           */
          public Integer doExecute(String[] args) {
              return commandLine.execute(args);
          }
      }
      
    • 实现每种子命令(GenerateCommandListCommandConfigCommand)。

    • 提供全局调用入口(Main类)。

    • 构建jar包并测试。

    • 封装脚本简化使用。

  3. 子命令实现

    • generate命令
      • 使用Picocli注解定义参数。
      • 调用MainGenerator类生成代码。
    • list命令
      • 遍历acm-template目录下的文件。
    • config命令
      • 使用反射动态打印MainTemplateConfig类的字段信息。
    // 配置命令
    @CommandLine.Command(name = "config", description = "查看参数信息", mixinStandardHelpOptions = true)
    public class ConfigCommand implements Runnable {
    
        public void run() {
            // 实现 config 命令的逻辑
            System.out.println("查看参数信息");
    
            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("---");
            }
        }
    }
    
    // 列表命令
    @CommandLine.Command(name =  "list", description = "查看文件列表", mixinStandardHelpOptions = true)
    public class ListCommand implements Runnable {
    
        public void run() {
            // 整个项目的根路径
            String projectPath = System.getProperty("user.dir");
            // 输入路径
            String inputPath = new File(projectPath, "yuzi-generator-demo-projects/acm-template").getAbsolutePath();
            List<File> files = FileUtil.loopFiles(inputPath);
            System.out.println("模板文件列表:");
            for (File file : files) {
                System.out.println(file);
            }
        }
    }
    
    // 生成动态文件命令
    @CommandLine.Command(name = "generate", description = "生成代码", mixinStandardHelpOptions = true)
    @Data
    public class GenerateCommand implements Callable<Integer> {
    
        @CommandLine.Option(names = {"-l", "--loop"}, arity = "0..1", description = "是否循环", interactive = true, echo = true)
        private boolean loop;
    
        @CommandLine.Option(names = {"-a", "--author"}, arity = "0..1", description = "作者", interactive = true, echo = true)
        private String author = "yupi";
    
        @CommandLine.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;
        }
    }
    
  4. 全局调用入口

    public class Main {
        public static void main(String[] args) {
            CommandExecutor executor = new CommandExecutor();
            executor.doExecute(args);
        }
    }
    
    
  5. jar包构建

    • 使用maven-assembly-plugin插件将依赖打入jar包。

      <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.yupi.Main</mainClass> <!-- 替换为你的主类的完整类名 -->
                          </manifest>
                      </archive>
                  </configuration>
                  <executions>
                      <execution>
                          <phase>package</phase>
                          <goals>
                              <goal>single</goal>
                          </goals>
                      </execution>
                  </executions>
              </plugin>
          </plugins>
      </build>
      
      
    • 执行mvn package命令生成jar包。

      在这里插入图片描述

      可以看到指定 jar 包已生成

  6. 测试使用

    • 使用java -jar命令运行jar包。

    • 示例命令:

      java -jar yuzi-generator-basic-1.0-SNAPSHOT-jar-with-dependencies.jar generate -l -o -a liyupi
      
  7. 封装脚本

    • Linux Bash脚本

      #!/bin/bash
      java -jar target/yuzi-generator-basic-1.0-SNAPSHOT-jar-with-dependencies.jar "$@"
      
    • Windows批处理文件

      @echo off
      java -jar target/yuzi-generator-basic-1.0-SNAPSHOT-jar-with-dependencies.jar %*
      
    • 使用脚本

      liuchuangchuang@liuchuangchuangdeMacBook-Air yuzi-generator % ./generate generate -a -l -o
      Enter value for --author (作者): lcc22
      Enter value for --loop (是否循环): true
      Enter value for --outputText (输出文本): 嘿嘿
      配置信息:MainTemplateConfig(author=lcc22, loop=true, outputText=嘿嘿)
      acm-template
      src
      com
      liucc
      acm
      liuchuangchuang@liuchuangchuangdeMacBook-Air yuzi-generator % 
      
      

      可以根据实际 jar 包所在的实际位置,动态调整脚本内容!

五、总结

  • 命令模式的巧用
    • 命令:Runnable 接口中的run()方法
    • 具体命令:ConfigCommandListCommandGenerateCommand
    • 接收方:比如MainGeneratordoGenerate()方法
    • 调用方:CommandExecutor 具体指向
    • 客户端:主程序Main
  • 通过脚本封装对 jar 包的复杂调用