Repository: skiptests/astraea
Pull request #263 (2022-03-01 15:28:57)

Introduce  to integrate converter and validator
1. 引入了新的介面`Field`整合`converter`和`validator`
2. 重整了所有轉換（除了一些特殊用途），盡量讓大部分的檢查有一致的邏輯
3. 將`ArgumentWithProps`整合到了`Argument`


Patch:
diff --git a/app/src/main/java/org/astraea/argument/Argument.java b/app/src/main/java/org/astraea/argument/Argument.java
new file mode 100644
index 00000000..b8ea149a
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/Argument.java
@@ -0,0 +1,74 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.UnixStyleUsageFormatter;
+import java.io.*;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import org.apache.kafka.clients.CommonClientConfigs;
+
+/** This basic argument defines the common property used by all kafka clients. */
+public abstract class Argument {
+
+  /**
+   * Side effect: parse args into toolArgument
+   *
+   * @param toolArgument An argument object that the user want.
+   * @param args Command line arguments that are put into main function.
+   */
+  public static <T> T parse(T toolArgument, String[] args) {
+    JCommander jc = JCommander.newBuilder().addObject(toolArgument).build();
+    jc.setUsageFormatter(new UnixStyleUsageFormatter(jc));
+    try {
+      jc.parse(args);
+    } catch (ParameterException pe) {
+      var sb = new StringBuilder();
+      jc.getUsageFormatter().usage(sb);
+      throw new ParameterException(pe.getMessage() + "\n" + sb);
+    }
+    return toolArgument;
+  }
+
+  @Parameter(
+      names = {"--bootstrap.servers"},
+      description = "String: server to connect to",
+      validateWith = NonEmptyStringField.class,
+      required = true)
+  public String brokers;
+
+  /**
+   * @param propsFile file path containing the properties to be passed to kafka
+   * @return the kafka props consists of bootstrap servers and all props from file (if it is
+   *     existent)
+   */
+  Map<String, Object> properties(String propsFile) {
+    var props = new Properties();
+    if (propsFile != null) {
+      try (var input = new FileInputStream(propsFile)) {
+        props.load(input);
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      }
+    }
+    props.setProperty(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, brokers);
+    return props.entrySet().stream()
+        .collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue));
+  }
+
+  @Parameter(
+      names = {"--prop.file"},
+      description = "the file path containing the properties to be passed to kafka admin",
+      validateWith = NonEmptyStringField.class)
+  public String propFile;
+
+  /**
+   * @return the kafka props consists of bootstrap servers and all props from file (if it is
+   *     existent)
+   */
+  public Map<String, Object> props() {
+    return properties(propFile);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/ArgumentUtil.java b/app/src/main/java/org/astraea/argument/ArgumentUtil.java
deleted file mode 100644
index 70a0a300..00000000
--- a/app/src/main/java/org/astraea/argument/ArgumentUtil.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.astraea.argument;
-
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.UnixStyleUsageFormatter;
-
-/*
- * A tool used to parse command line arguments.
- *
- * To add new option, add new option in the corresponding file.
- * @Parameter(names={"--option"}, description="")
- * public <optionType> <optionName>;
- * */
-
-public class ArgumentUtil {
-  // Do not instantiate.
-  private ArgumentUtil() {}
-
-  /**
-   * Side effect: parse args into toolArgument
-   *
-   * @param toolArgument An argument object that the user want.
-   * @param args Command line arguments that are put into main function.
-   */
-  public static <T> T parseArgument(T toolArgument, String[] args) {
-    JCommander jc = JCommander.newBuilder().addObject(toolArgument).build();
-    jc.setUsageFormatter(new UnixStyleUsageFormatter(jc));
-    try {
-      jc.parse(args);
-    } catch (ParameterException pe) {
-      var sb = new StringBuilder();
-      jc.getUsageFormatter().usage(sb);
-      throw new ParameterException(pe.getMessage() + "\n" + sb);
-    }
-    return toolArgument;
-  }
-}
diff --git a/app/src/main/java/org/astraea/argument/BasicArgument.java b/app/src/main/java/org/astraea/argument/BasicArgument.java
deleted file mode 100644
index 5d1c9c8d..00000000
--- a/app/src/main/java/org/astraea/argument/BasicArgument.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.astraea.argument;
-
-import com.beust.jcommander.Parameter;
-import java.io.*;
-import java.util.Map;
-import java.util.Properties;
-import java.util.stream.Collectors;
-import org.apache.kafka.clients.CommonClientConfigs;
-import org.astraea.argument.validator.NotEmptyString;
-
-/** This basic argument defines the common property used by all kafka clients. */
-public abstract class BasicArgument {
-  @Parameter(
-      names = {"--bootstrap.servers"},
-      description = "String: server to connect to",
-      validateWith = NotEmptyString.class,
-      required = true)
-  public String brokers;
-
-  /**
-   * @param propsFile file path containing the properties to be passed to kafka
-   * @return the kafka props consists of bootstrap servers and all props from file (if it is
-   *     existent)
-   */
-  protected Map<String, Object> properties(String propsFile) {
-    var props = new Properties();
-    if (propsFile != null) {
-      try (var input = new FileInputStream(propsFile)) {
-        props.load(input);
-      } catch (IOException e) {
-        throw new UncheckedIOException(e);
-      }
-    }
-    props.setProperty(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, brokers);
-    return props.entrySet().stream()
-        .collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue));
-  }
-}
diff --git a/app/src/main/java/org/astraea/argument/BasicArgumentWithPropFile.java b/app/src/main/java/org/astraea/argument/BasicArgumentWithPropFile.java
deleted file mode 100644
index 4414fb48..00000000
--- a/app/src/main/java/org/astraea/argument/BasicArgumentWithPropFile.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.astraea.argument;
-
-import com.beust.jcommander.Parameter;
-import java.util.Map;
-import org.astraea.argument.validator.NotEmptyString;
-
-public abstract class BasicArgumentWithPropFile extends BasicArgument {
-  @Parameter(
-      names = {"--prop.file"},
-      description = "the file path containing the properties to be passed to kafka admin",
-      validateWith = NotEmptyString.class)
-  public String propFile;
-
-  /**
-   * @return the kafka props consists of bootstrap servers and all props from file (if it is
-   *     existent)
-   */
-  public Map<String, Object> props() {
-    return properties(propFile);
-  }
-}
diff --git a/app/src/main/java/org/astraea/argument/BooleanField.java b/app/src/main/java/org/astraea/argument/BooleanField.java
new file mode 100644
index 00000000..86165c35
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/BooleanField.java
@@ -0,0 +1,17 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.ParameterException;
+
+public class BooleanField implements NonEmptyField<Boolean> {
+  @Override
+  public Boolean convert(String value) {
+    return Boolean.parseBoolean(value);
+  }
+
+  @Override
+  public void validate(String name, String value) throws ParameterException {
+    NonEmptyField.super.validate(name, value);
+    if (!("false".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value)))
+      throw new ParameterException(value + " is not boolean type");
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/CompressionField.java b/app/src/main/java/org/astraea/argument/CompressionField.java
new file mode 100644
index 00000000..f5df778e
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/CompressionField.java
@@ -0,0 +1,34 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.ParameterException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.kafka.common.record.CompressionType;
+
+public class CompressionField implements NonEmptyField<CompressionType> {
+  /**
+   * @param value Name of compression type. Accept lower-case name only ("none", "gzip", "snappy",
+   *     "lz4", "zstd").
+   */
+  @Override
+  public CompressionType convert(String value) {
+    try {
+      // `CompressionType#forName` accept lower-case name only.
+      return CompressionType.forName(value);
+    } catch (IllegalArgumentException e) {
+      throw new ParameterException(
+          "the "
+              + value
+              + " is unsupported. The supported algorithms are "
+              + Stream.of(CompressionType.values())
+                  .map(CompressionType::name)
+                  .collect(Collectors.joining(",")));
+    }
+  }
+
+  @Override
+  public void validate(String name, String value) throws ParameterException {
+    NonEmptyField.super.validate(name, value);
+    convert(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/converter/DurationConverter.java b/app/src/main/java/org/astraea/argument/DurationField.java
similarity index 91%
rename from app/src/main/java/org/astraea/argument/converter/DurationConverter.java
rename to app/src/main/java/org/astraea/argument/DurationField.java
index 3afdbd40..6049a478 100644
--- a/app/src/main/java/org/astraea/argument/converter/DurationConverter.java
+++ b/app/src/main/java/org/astraea/argument/DurationField.java
@@ -1,7 +1,5 @@
-package org.astraea.argument.converter;
+package org.astraea.argument;
 
-import com.beust.jcommander.IParameterValidator;
-import com.beust.jcommander.IStringConverter;
 import com.beust.jcommander.ParameterException;
 import java.time.Duration;
 import java.util.regex.Matcher;
@@ -42,7 +40,7 @@ import java.util.regex.Pattern;
  *   <li><b>(doesn't work)</b> {@code "0.5" to {@code Duration.ofMillis(500)}}
  * </ul>
  */
-public class DurationConverter implements IStringConverter<Duration>, IParameterValidator {
+public class DurationField implements Field<Duration> {
 
   static final Pattern TIME_PATTERN =
       Pattern.compile("^(?<value>[0-9]+)(?<unit>days|day|h|m|s|ms|us|ns|)$");
diff --git a/app/src/main/java/org/astraea/argument/Field.java b/app/src/main/java/org/astraea/argument/Field.java
new file mode 100644
index 00000000..1b281594
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/Field.java
@@ -0,0 +1,21 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.IParameterValidator;
+import com.beust.jcommander.IStringConverter;
+import com.beust.jcommander.ParameterException;
+
+public interface Field<T> extends IStringConverter<T>, IParameterValidator {
+
+  /** @return an object of type T created from the parameter value. */
+  @Override
+  T convert(String value);
+
+  /**
+   * Validate the parameter.
+   *
+   * @param name The name of the parameter (e.g. "-host").
+   * @param value The value of the parameter that we need to validate
+   * @throws ParameterException Thrown if the value of the parameter is invalid.
+   */
+  void validate(String name, String value) throws ParameterException;
+}
diff --git a/app/src/main/java/org/astraea/argument/IntegerSetField.java b/app/src/main/java/org/astraea/argument/IntegerSetField.java
new file mode 100644
index 00000000..8344d81c
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/IntegerSetField.java
@@ -0,0 +1,12 @@
+package org.astraea.argument;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class IntegerSetField implements SetField<Integer> {
+  @Override
+  public Set<Integer> convert(String value) {
+    return Stream.of(value.split(SEPARATOR)).map(Integer::valueOf).collect(Collectors.toSet());
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/NonEmptyField.java b/app/src/main/java/org/astraea/argument/NonEmptyField.java
new file mode 100644
index 00000000..4c89054d
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/NonEmptyField.java
@@ -0,0 +1,12 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.ParameterException;
+
+public interface NonEmptyField<T> extends Field<T> {
+
+  @Override
+  default void validate(String name, String value) throws ParameterException {
+    if (value == null || value.isBlank())
+      throw new ParameterException(name + " should not be empty.");
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/NonEmptyStringField.java b/app/src/main/java/org/astraea/argument/NonEmptyStringField.java
new file mode 100644
index 00000000..0837b85d
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/NonEmptyStringField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class NonEmptyStringField implements NonEmptyField<String> {
+  @Override
+  public String convert(String value) {
+    return value;
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/NonNegativeDoubleField.java b/app/src/main/java/org/astraea/argument/NonNegativeDoubleField.java
new file mode 100644
index 00000000..f063a4f9
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/NonNegativeDoubleField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class NonNegativeDoubleField implements PositiveNumberField<Double> {
+  @Override
+  public Double convert(String value) {
+    return Double.parseDouble(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/NonNegativeIntegerField.java b/app/src/main/java/org/astraea/argument/NonNegativeIntegerField.java
new file mode 100644
index 00000000..2b6b87ac
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/NonNegativeIntegerField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class NonNegativeIntegerField implements PositiveNumberField<Integer> {
+  @Override
+  public Integer convert(String value) {
+    return Integer.parseInt(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/NonNegativeLongField.java b/app/src/main/java/org/astraea/argument/NonNegativeLongField.java
new file mode 100644
index 00000000..ff4f7566
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/NonNegativeLongField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class NonNegativeLongField implements PositiveNumberField<Long> {
+  @Override
+  public Long convert(String value) {
+    return Long.parseLong(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/NonNegativeNumberField.java b/app/src/main/java/org/astraea/argument/NonNegativeNumberField.java
new file mode 100644
index 00000000..84da9188
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/NonNegativeNumberField.java
@@ -0,0 +1,12 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.ParameterException;
+
+public interface NonNegativeNumberField<T extends Number> extends NonEmptyField<T> {
+
+  @Override
+  default void validate(String name, String value) throws ParameterException {
+    NonEmptyField.super.validate(name, value);
+    if (Long.parseLong(value) < 0) throw new ParameterException(name + " should not be negative.");
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/NonNegativeShortField.java b/app/src/main/java/org/astraea/argument/NonNegativeShortField.java
new file mode 100644
index 00000000..57ea3273
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/NonNegativeShortField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class NonNegativeShortField implements PositiveNumberField<Short> {
+  @Override
+  public Short convert(String value) {
+    return Short.parseShort(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/PositiveDoubleField.java b/app/src/main/java/org/astraea/argument/PositiveDoubleField.java
new file mode 100644
index 00000000..0a97144d
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/PositiveDoubleField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class PositiveDoubleField implements PositiveNumberField<Double> {
+  @Override
+  public Double convert(String value) {
+    return Double.parseDouble(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/PositiveIntegerField.java b/app/src/main/java/org/astraea/argument/PositiveIntegerField.java
new file mode 100644
index 00000000..38669743
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/PositiveIntegerField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class PositiveIntegerField implements PositiveNumberField<Integer> {
+  @Override
+  public Integer convert(String value) {
+    return Integer.parseInt(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/PositiveLongField.java b/app/src/main/java/org/astraea/argument/PositiveLongField.java
new file mode 100644
index 00000000..79d5cdcd
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/PositiveLongField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class PositiveLongField implements PositiveNumberField<Long> {
+  @Override
+  public Long convert(String value) {
+    return Long.parseLong(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/PositiveNumberField.java b/app/src/main/java/org/astraea/argument/PositiveNumberField.java
new file mode 100644
index 00000000..d6ac1c1c
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/PositiveNumberField.java
@@ -0,0 +1,11 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.ParameterException;
+
+public interface PositiveNumberField<T extends Number> extends Field<T> {
+
+  @Override
+  default void validate(String name, String value) throws ParameterException {
+    if (Long.parseLong(value) <= 0) throw new ParameterException(name + " should be positive.");
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/PositiveShortField.java b/app/src/main/java/org/astraea/argument/PositiveShortField.java
new file mode 100644
index 00000000..e9f04ba8
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/PositiveShortField.java
@@ -0,0 +1,8 @@
+package org.astraea.argument;
+
+public class PositiveShortField implements PositiveNumberField<Short> {
+  @Override
+  public Short convert(String value) {
+    return Short.parseShort(value);
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/SetField.java b/app/src/main/java/org/astraea/argument/SetField.java
new file mode 100644
index 00000000..e543ff75
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/SetField.java
@@ -0,0 +1,14 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.ParameterException;
+import java.util.Set;
+
+public interface SetField<T> extends Field<Set<T>> {
+  String SEPARATOR = ",";
+
+  @Override
+  default void validate(String name, String value) throws ParameterException {
+    if (value == null || value.isBlank() || Set.of(value.split(SEPARATOR)).isEmpty())
+      throw new ParameterException("set type can't be empty");
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/StringSetField.java b/app/src/main/java/org/astraea/argument/StringSetField.java
new file mode 100644
index 00000000..5f74f694
--- /dev/null
+++ b/app/src/main/java/org/astraea/argument/StringSetField.java
@@ -0,0 +1,11 @@
+package org.astraea.argument;
+
+import java.util.Set;
+
+public class StringSetField implements SetField<String> {
+
+  @Override
+  public Set<String> convert(String value) {
+    return Set.of(value.split(SEPARATOR));
+  }
+}
diff --git a/app/src/main/java/org/astraea/argument/converter/IntegerSetConverter.java b/app/src/main/java/org/astraea/argument/converter/IntegerSetConverter.java
deleted file mode 100644
index e87c21b2..00000000
--- a/app/src/main/java/org/astraea/argument/converter/IntegerSetConverter.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.astraea.argument.converter;
-
-import com.beust.jcommander.IStringConverter;
-import com.beust.jcommander.ParameterException;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-public class IntegerSetConverter implements IStringConverter<Set<Integer>> {
-  @Override
-  public Set<Integer> convert(String value) {
-    if (value.isEmpty()) throw new ParameterException("array type can't be empty");
-    return Stream.of(value.split(",")).map(Integer::valueOf).collect(Collectors.toSet());
-  }
-}
diff --git a/app/src/main/java/org/astraea/argument/converter/ShortConverter.java b/app/src/main/java/org/astraea/argument/converter/ShortConverter.java
deleted file mode 100644
index b8390649..00000000
--- a/app/src/main/java/org/astraea/argument/converter/ShortConverter.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.astraea.argument.converter;
-
-import com.beust.jcommander.IStringConverter;
-
-public class ShortConverter implements IStringConverter<Short> {
-  @Override
-  public Short convert(String value) {
-    return Short.valueOf(value);
-  }
-}
diff --git a/app/src/main/java/org/astraea/argument/converter/StringSetConverter.java b/app/src/main/java/org/astraea/argument/converter/StringSetConverter.java
deleted file mode 100644
index 8acf4979..00000000
--- a/app/src/main/java/org/astraea/argument/converter/StringSetConverter.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.astraea.argument.converter;
-
-import com.beust.jcommander.IStringConverter;
-import com.beust.jcommander.ParameterException;
-import java.util.Set;
-
-public class StringSetConverter implements IStringConverter<Set<String>> {
-  @Override
-  public Set<String> convert(String value) {
-    if (value.isEmpty()) throw new ParameterException("array type can't be empty");
-    return Set.of(value.split(","));
-  }
-}
diff --git a/app/src/main/java/org/astraea/argument/validator/NonNegativeLong.java b/app/src/main/java/org/astraea/argument/validator/NonNegativeLong.java
deleted file mode 100644
index 5a01a432..00000000
--- a/app/src/main/java/org/astraea/argument/validator/NonNegativeLong.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.astraea.argument.validator;
-
-import com.beust.jcommander.IParameterValidator;
-import com.beust.jcommander.ParameterException;
-
-public class NonNegativeLong implements IParameterValidator {
-  @Override
-  public void validate(String name, String value) throws ParameterException {
-    if (Long.parseLong(value) < 0) throw new ParameterException(name + " should not be negative.");
-  }
-}
diff --git a/app/src/main/java/org/astraea/argument/validator/NotEmptyString.java b/app/src/main/java/org/astraea/argument/validator/NotEmptyString.java
deleted file mode 100644
index 078169a3..00000000
--- a/app/src/main/java/org/astraea/argument/validator/NotEmptyString.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.astraea.argument.validator;
-
-import com.beust.jcommander.IParameterValidator;
-import com.beust.jcommander.ParameterException;
-
-public class NotEmptyString implements IParameterValidator {
-  @Override
-  public void validate(String name, String value) throws ParameterException {
-    if (value == null || value.equals(""))
-      throw new ParameterException(name + " should not be empty.");
-  }
-}
diff --git a/app/src/main/java/org/astraea/argument/validator/PositiveLong.java b/app/src/main/java/org/astraea/argument/validator/PositiveLong.java
deleted file mode 100644
index c41fc60d..00000000
--- a/app/src/main/java/org/astraea/argument/validator/PositiveLong.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.astraea.argument.validator;
-
-import com.beust.jcommander.IParameterValidator;
-import com.beust.jcommander.ParameterException;
-
-public class PositiveLong implements IParameterValidator {
-  @Override
-  public void validate(String name, String value) throws ParameterException {
-    if (Long.parseLong(value) <= 0) throw new ParameterException(name + " should be positive.");
-  }
-}
diff --git a/app/src/main/java/org/astraea/automation/Automation.java b/app/src/main/java/org/astraea/automation/Automation.java
index d89ee564..8a0679d3 100644
--- a/app/src/main/java/org/astraea/automation/Automation.java
+++ b/app/src/main/java/org/astraea/automation/Automation.java
@@ -10,8 +10,7 @@ import java.util.concurrent.ExecutionException;
 import org.apache.kafka.clients.admin.AdminClient;
 import org.apache.kafka.clients.admin.AdminClientConfig;
 import org.apache.kafka.clients.admin.KafkaAdminClient;
-import org.astraea.argument.ArgumentUtil;
-import org.astraea.argument.validator.NotEmptyString;
+import org.astraea.argument.NonEmptyStringField;
 import org.astraea.performance.Performance;
 
 /**
@@ -42,7 +41,7 @@ public class Automation {
   public static void main(String[] args) {
     try {
       var properties = new Properties();
-      var arg = ArgumentUtil.parseArgument(new automationArgument(), args);
+      var arg = org.astraea.argument.Argument.parse(new Argument(), args);
       properties.load(new FileInputStream(arg.address));
       var whetherDeleteTopic = properties.getProperty("--whetherDeleteTopic").equals("true");
       var bootstrap = properties.getProperty("--bootstrap.servers");
@@ -57,7 +56,7 @@ public class Automation {
       while (i < times) {
         var str =
             Performance.execute(
-                ArgumentUtil.parseArgument(
+                org.astraea.argument.Argument.parse(
                     new Performance.Argument(), performanceArgs(properties)));
         i++;
         if (whetherDeleteTopic) {
@@ -88,11 +87,11 @@ public class Automation {
     return args.toArray(strings);
   }
 
-  private static class automationArgument {
+  private static class Argument {
     @Parameter(
         names = {"--file"},
         description = "String: automation.properties address",
-        validateWith = NotEmptyString.class)
+        validateWith = NonEmptyStringField.class)
     String address = "";
   }
 }
diff --git a/app/src/main/java/org/astraea/metrics/MetricExplorer.java b/app/src/main/java/org/astraea/metrics/MetricExplorer.java
index 0459e306..1ba15b0e 100644
--- a/app/src/main/java/org/astraea/metrics/MetricExplorer.java
+++ b/app/src/main/java/org/astraea/metrics/MetricExplorer.java
@@ -1,11 +1,14 @@
 package org.astraea.metrics;
 
 import com.beust.jcommander.IParameterValidator;
-import com.beust.jcommander.IStringConverter;
 import com.beust.jcommander.Parameter;
 import com.beust.jcommander.ParameterException;
 import java.net.MalformedURLException;
-import java.util.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -13,8 +16,7 @@ import java.util.stream.IntStream;
 import java.util.stream.Stream;
 import javax.management.openmbean.CompositeDataSupport;
 import javax.management.remote.JMXServiceURL;
-import org.astraea.argument.ArgumentUtil;
-import org.astraea.argument.validator.NotEmptyString;
+import org.astraea.argument.NonEmptyField;
 import org.astraea.metrics.jmx.BeanObject;
 import org.astraea.metrics.jmx.BeanQuery;
 import org.astraea.metrics.jmx.MBeanClient;
@@ -164,7 +166,7 @@ public class MetricExplorer {
   }
 
   public static void main(String[] args) {
-    var arguments = ArgumentUtil.parseArgument(new Argument(), args);
+    var arguments = org.astraea.argument.Argument.parse(new Argument(), args);
 
     try (var mBeanClient = MBeanClient.of(arguments.jmxServer)) {
       execute(mBeanClient, arguments);
@@ -176,8 +178,8 @@ public class MetricExplorer {
         names = {"--jmx.server"},
         description =
             "The JMX server address to connect to, support [hostname:port] style or JMX URI format",
-        validateWith = NotEmptyString.class,
-        converter = Argument.JmxServerUrlConverter.class,
+        validateWith = JmxServerUrlField.class,
+        converter = JmxServerUrlField.class,
         required = true)
     JMXServiceURL jmxServer;
 
@@ -204,7 +206,7 @@ public class MetricExplorer {
         description = "Show the list view of MBeans' domain name & properties")
     boolean viewObjectNameList = false;
 
-    public static class JmxServerUrlConverter implements IStringConverter<JMXServiceURL> {
+    public static class JmxServerUrlField implements NonEmptyField<JMXServiceURL> {
 
       /** This regex used to test if a string look like a JMX URL */
       static Pattern patternOfJmxUrlStart = Pattern.compile("^service:jmx:rmi://");
diff --git a/app/src/main/java/org/astraea/performance/Distribution.java b/app/src/main/java/org/astraea/performance/Distribution.java
index bbfc5955..0942b253 100644
--- a/app/src/main/java/org/astraea/performance/Distribution.java
+++ b/app/src/main/java/org/astraea/performance/Distribution.java
@@ -1,18 +1,18 @@
 package org.astraea.performance;
 
-import com.beust.jcommander.IStringConverter;
 import com.beust.jcommander.ParameterException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
 import java.util.stream.IntStream;
+import org.astraea.argument.NonEmptyField;
 
 /** Randomly generate a long number with respect to some distribution */
 public interface Distribution {
 
   long get();
 
-  class DistributionConverter implements IStringConverter<Distribution> {
+  class DistributionField implements NonEmptyField<Distribution> {
     @Override
     public Distribution convert(String rawArgument) {
       var args = rawArgument.split(":");
diff --git a/app/src/main/java/org/astraea/performance/ExeTime.java b/app/src/main/java/org/astraea/performance/ExeTime.java
index 45062436..07bc7f6e 100644
--- a/app/src/main/java/org/astraea/performance/ExeTime.java
+++ b/app/src/main/java/org/astraea/performance/ExeTime.java
@@ -1,12 +1,11 @@
 package org.astraea.performance;
 
-import com.beust.jcommander.IParameterValidator;
-import com.beust.jcommander.IStringConverter;
 import com.beust.jcommander.ParameterException;
 import java.time.Duration;
 import java.util.function.BiFunction;
 import java.util.regex.Pattern;
-import org.astraea.argument.converter.DurationConverter;
+import org.astraea.argument.DurationField;
+import org.astraea.argument.NonEmptyField;
 
 /**
  * Two kind of running modes. One runs for a duration of time. The other runs for a number of
@@ -21,7 +20,7 @@ interface ExeTime {
       final long records = Long.parseLong(exeTime.replace("records", ""));
       return ExeTime.of((completeRecords, ignore) -> 100D * completeRecords / records, exeTime);
     }
-    final Duration duration = new DurationConverter().convert(exeTime);
+    final Duration duration = new DurationField().convert(exeTime);
     return ExeTime.of((ignore, elapsedTime) -> 100D * elapsedTime / duration.toMillis(), exeTime);
   }
 
@@ -39,14 +38,7 @@ interface ExeTime {
     };
   }
 
-  class Converter implements IStringConverter<ExeTime> {
-    @Override
-    public ExeTime convert(String value) {
-      return ExeTime.of(value);
-    }
-  }
-
-  class Validator implements IParameterValidator {
+  class Field implements NonEmptyField<ExeTime> {
     static final Pattern PATTERN = Pattern.compile("^([0-9]+)(days|day|h|m|s|ms|us|ns|records)$");
 
     @Override
@@ -56,5 +48,10 @@ interface ExeTime {
             "Invalid ExeTime format. valid format example: \"1m\" or \"89242records\"");
       }
     }
+
+    @Override
+    public ExeTime convert(String value) {
+      return ExeTime.of(value);
+    }
   }
 }
diff --git a/app/src/main/java/org/astraea/performance/Performance.java b/app/src/main/java/org/astraea/performance/Performance.java
index 96a5978c..4ada2f00 100644
--- a/app/src/main/java/org/astraea/performance/Performance.java
+++ b/app/src/main/java/org/astraea/performance/Performance.java
@@ -1,8 +1,6 @@
 package org.astraea.performance;
 
-import com.beust.jcommander.IStringConverter;
 import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
 import java.io.IOException;
 import java.time.Duration;
 import java.util.Collection;
@@ -17,19 +15,17 @@ import java.util.concurrent.Future;
 import java.util.function.BiConsumer;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
-import java.util.stream.Stream;
 import org.apache.kafka.clients.producer.ProducerConfig;
 import org.apache.kafka.clients.producer.internals.DefaultPartitioner;
 import org.apache.kafka.common.TopicPartition;
 import org.apache.kafka.common.errors.WakeupException;
 import org.apache.kafka.common.record.CompressionType;
 import org.astraea.Utils;
-import org.astraea.argument.ArgumentUtil;
-import org.astraea.argument.BasicArgumentWithPropFile;
-import org.astraea.argument.converter.ShortConverter;
-import org.astraea.argument.validator.NonNegativeLong;
-import org.astraea.argument.validator.NotEmptyString;
-import org.astraea.argument.validator.PositiveLong;
+import org.astraea.argument.CompressionField;
+import org.astraea.argument.NonEmptyStringField;
+import org.astraea.argument.NonNegativeLongField;
+import org.astraea.argument.PositiveLongField;
+import org.astraea.argument.PositiveShortField;
 import org.astraea.concurrent.ThreadPool;
 import org.astraea.consumer.Consumer;
 import org.astraea.producer.Producer;
@@ -67,7 +63,7 @@ public class Performance {
   /** Used in Automation, to achieve the end of one Performance and then start another. */
   public static void main(String[] args)
       throws InterruptedException, IOException, ExecutionException {
-    execute(ArgumentUtil.parseArgument(new Argument(), args));
+    execute(org.astraea.argument.Argument.parse(new Argument(), args));
   }
 
   public static Future<String> execute(final Argument param)
@@ -233,37 +229,39 @@ public class Performance {
     return param.specifyBroker.stream().allMatch(broker -> broker >= 0);
   }
 
-  public static class Argument extends BasicArgumentWithPropFile {
+  public static class Argument extends org.astraea.argument.Argument {
 
     @Parameter(
         names = {"--topic"},
         description = "String: topic name",
-        validateWith = NotEmptyString.class)
+        validateWith = NonEmptyStringField.class)
     String topic = "testPerformance-" + System.currentTimeMillis();
 
     @Parameter(
         names = {"--partitions"},
         description = "Integer: number of partitions to create the topic",
-        validateWith = PositiveLong.class)
+        validateWith = PositiveLongField.class)
     int partitions = 1;
 
     @Parameter(
         names = {"--replicas"},
         description = "Integer: number of replica to create the topic",
-        validateWith = PositiveLong.class,
-        converter = ShortConverter.class)
+        validateWith = PositiveShortField.class,
+        converter = PositiveShortField.class)
     short replicas = 1;
 
     @Parameter(
         names = {"--producers"},
         description = "Integer: number of producers to produce records",
-        validateWith = PositiveLong.class)
+        validateWith = PositiveShortField.class,
+        converter = PositiveShortField.class)
     int producers = 1;
 
     @Parameter(
         names = {"--consumers"},
         description = "Integer: number of consumers to consume records",
-        validateWith = NonNegativeLong.class)
+        validateWith = NonNegativeLongField.class,
+        converter = NonNegativeLongField.class)
     int consumers = 1;
 
     @Parameter(
@@ -273,8 +271,8 @@ public class Performance {
                 + " The duration formats accepted are (a number) + (a time unit)."
                 + " The time units can be \"days\", \"day\", \"h\", \"m\", \"s\", \"ms\","
                 + " \"us\", \"ns\"",
-        validateWith = ExeTime.Validator.class,
-        converter = ExeTime.Converter.class)
+        validateWith = ExeTime.Field.class,
+        converter = ExeTime.Field.class)
     ExeTime exeTime = ExeTime.of("1000records");
 
     @Parameter(
@@ -285,20 +283,20 @@ public class Performance {
     @Parameter(
         names = {"--record.size"},
         description = "DataSize: size of each record. e.g. \"500KiB\"",
-        converter = DataSize.Converter.class)
+        converter = DataSize.Field.class)
     DataSize recordSize = DataUnit.KiB.of(1);
 
     @Parameter(
         names = {"--jmx.servers"},
         description =
             "String: server to get jmx metrics <jmx_server>@<broker_id>[,<jmx_server>@<broker_id>]*",
-        validateWith = NotEmptyString.class)
+        validateWith = NonEmptyStringField.class)
     String jmxServers = "";
 
     @Parameter(
         names = {"--partitioner"},
         description = "String: the full class name of the desired partitioner",
-        validateWith = NotEmptyString.class)
+        validateWith = NonEmptyStringField.class)
     String partitioner = DefaultPartitioner.class.getName();
 
     public Map<String, Object> producerProps() {
@@ -317,44 +315,21 @@ public class Performance {
         names = {"--compression"},
         description =
             "String: the compression algorithm used by producer. Available algorithm are none, gzip, snappy, lz4, and zstd",
-        converter = CompressionArgument.class)
+        converter = CompressionField.class)
     CompressionType compression = CompressionType.NONE;
 
     @Parameter(
         names = {"--key.distribution"},
         description =
             "String: Distribution name. Available distribution names: \"fixed\" \"uniform\", \"zipfian\", \"latest\". Default: uniform",
-        converter = Distribution.DistributionConverter.class)
+        converter = Distribution.DistributionField.class)
     Distribution distribution = Distribution.uniform();
 
     @Parameter(
         names = {"--specify.broker"},
         description =
             "String: Used with SpecifyBrokerPartitioner to specify the brokers that partitioner can send.",
-        validateWith = NotEmptyString.class)
+        validateWith = NonEmptyStringField.class)
     List<Integer> specifyBroker = List.of(-1);
   }
-
-  static class CompressionArgument implements IStringConverter<CompressionType> {
-
-    /**
-     * @param value Name of compression type. Accept lower-case name only ("none", "gzip", "snappy",
-     *     "lz4", "zstd").
-     */
-    @Override
-    public CompressionType convert(String value) {
-      try {
-        // `CompressionType#forName` accept lower-case name only.
-        return CompressionType.forName(value);
-      } catch (IllegalArgumentException e) {
-        throw new ParameterException(
-            "the "
-                + value
-                + " is unsupported. The supported algorithms are "
-                + Stream.of(CompressionType.values())
-                    .map(CompressionType::name)
-                    .collect(Collectors.joining(",")));
-      }
-    }
-  }
 }
diff --git a/app/src/main/java/org/astraea/topic/ReplicaCollie.java b/app/src/main/java/org/astraea/topic/ReplicaCollie.java
index 6ebb658e..8f702973 100644
--- a/app/src/main/java/org/astraea/topic/ReplicaCollie.java
+++ b/app/src/main/java/org/astraea/topic/ReplicaCollie.java
@@ -1,16 +1,18 @@
 package org.astraea.topic;
 
 import com.beust.jcommander.Parameter;
-import com.beust.jcommander.converters.BooleanConverter;
 import java.io.IOException;
-import java.util.*;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
 import org.apache.kafka.common.TopicPartition;
-import org.astraea.argument.ArgumentUtil;
-import org.astraea.argument.BasicArgumentWithPropFile;
-import org.astraea.argument.converter.IntegerSetConverter;
-import org.astraea.argument.converter.StringSetConverter;
-import org.astraea.argument.validator.NotEmptyString;
+import org.astraea.argument.BooleanField;
+import org.astraea.argument.IntegerSetField;
+import org.astraea.argument.StringSetField;
 
 public class ReplicaCollie {
   static final String UNKNOWN = "unknown";
@@ -276,7 +278,7 @@ public class ReplicaCollie {
   }
 
   public static void main(String[] args) throws IOException {
-    var argument = ArgumentUtil.parseArgument(new Argument(), args);
+    var argument = org.astraea.argument.Argument.parse(new Argument(), args);
     try (var admin = TopicAdmin.of(argument.props())) {
       execute(admin, argument)
           .forEach(
@@ -297,49 +299,49 @@ public class ReplicaCollie {
     }
   }
 
-  static class Argument extends BasicArgumentWithPropFile {
+  static class Argument extends org.astraea.argument.Argument {
     @Parameter(
         names = {"--topics"},
         description = "Those topics' partitions will get reassigned. Empty means all topics",
-        validateWith = NotEmptyString.class,
-        converter = StringSetConverter.class)
+        validateWith = StringSetField.class,
+        converter = StringSetField.class)
     public Set<String> topics = Collections.emptySet();
 
     @Parameter(
         names = {"--from"},
         description = "Those brokers won't hold any replicas of topics (defined by --topics)",
-        validateWith = NotEmptyString.class,
-        converter = IntegerSetConverter.class,
+        validateWith = IntegerSetField.class,
+        converter = IntegerSetField.class,
         required = true)
     Set<Integer> fromBrokers;
 
     @Parameter(
         names = {"--to"},
         description = "The replicas of topics (defined by --topic) will be moved to those brokers",
-        validateWith = NotEmptyString.class,
-        converter = IntegerSetConverter.class)
+        validateWith = IntegerSetField.class,
+        converter = IntegerSetField.class)
     Set<Integer> toBrokers = Collections.emptySet();
 
     @Parameter(
         names = {"--partitions"},
         description = "all partitions that will be moved",
-        validateWith = NotEmptyString.class,
-        converter = IntegerSetConverter.class)
+        validateWith = IntegerSetField.class,
+        converter = IntegerSetField.class)
     Set<Integer> partitions = Collections.emptySet();
 
     @Parameter(
         names = {"--path"},
         description = "The partition that will be moved to",
-        validateWith = NotEmptyString.class,
-        converter = StringSetConverter.class)
+        validateWith = StringSetField.class,
+        converter = StringSetField.class)
     Set<String> path = Collections.emptySet();
 
     @Parameter(
         names = {"--verify"},
         description =
             "True if you just want to see the new assignment instead of executing the plan",
-        validateWith = NotEmptyString.class,
-        converter = BooleanConverter.class)
+        validateWith = BooleanField.class,
+        converter = BooleanField.class)
     boolean verify = false;
   }
 }
diff --git a/app/src/main/java/org/astraea/topic/ReplicaSyncingMonitor.java b/app/src/main/java/org/astraea/topic/ReplicaSyncingMonitor.java
index de62b154..d77b10bd 100644
--- a/app/src/main/java/org/astraea/topic/ReplicaSyncingMonitor.java
+++ b/app/src/main/java/org/astraea/topic/ReplicaSyncingMonitor.java
@@ -13,10 +13,8 @@ import java.util.stream.Stream;
 import org.apache.kafka.common.TopicPartition;
 import org.apache.kafka.common.TopicPartitionReplica;
 import org.astraea.Utils;
-import org.astraea.argument.ArgumentUtil;
-import org.astraea.argument.BasicArgumentWithPropFile;
-import org.astraea.argument.converter.DurationConverter;
-import org.astraea.argument.validator.NotEmptyString;
+import org.astraea.argument.DurationField;
+import org.astraea.argument.NonEmptyStringField;
 import org.astraea.utils.DataRate;
 import org.astraea.utils.DataSize;
 import org.astraea.utils.DataUnit;
@@ -24,7 +22,7 @@ import org.astraea.utils.DataUnit;
 public class ReplicaSyncingMonitor {
 
   public static void main(String[] args) {
-    Argument argument = ArgumentUtil.parseArgument(new Argument(), args);
+    Argument argument = org.astraea.argument.Argument.parse(new Argument(), args);
     try (TopicAdmin topicAdmin = TopicAdmin.of(argument.props())) {
       execute(topicAdmin, argument);
     }
@@ -295,12 +293,12 @@ public class ReplicaSyncingMonitor {
         .collect(Collectors.joining(", ", "[", "]"));
   }
 
-  static class Argument extends BasicArgumentWithPropFile {
+  static class Argument extends org.astraea.argument.Argument {
 
     @Parameter(
         names = {"--topics"},
         description = "String: topics to track, use all non-synced topics by default",
-        validateWith = NotEmptyString.class)
+        validateWith = NonEmptyStringField.class)
     public Set<String> topics = Set.of();
 
     @Parameter(
@@ -314,8 +312,8 @@ public class ReplicaSyncingMonitor {
         description =
             "Time: the time interval between replica state check, support multiple time unit like 10s, 500ms and 100us. "
                 + "If no time unit specified, second unit will be used.",
-        validateWith = DurationConverter.class,
-        converter = DurationConverter.class)
+        validateWith = DurationField.class,
+        converter = DurationField.class)
     public Duration interval = Duration.ofSeconds(1);
   }
 }
diff --git a/app/src/main/java/org/astraea/topic/TopicExplorer.java b/app/src/main/java/org/astraea/topic/TopicExplorer.java
index acb801cb..e0b9d1c8 100644
--- a/app/src/main/java/org/astraea/topic/TopicExplorer.java
+++ b/app/src/main/java/org/astraea/topic/TopicExplorer.java
@@ -11,10 +11,8 @@ import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 import org.apache.kafka.common.TopicPartition;
-import org.astraea.argument.ArgumentUtil;
-import org.astraea.argument.BasicArgumentWithPropFile;
-import org.astraea.argument.converter.StringSetConverter;
-import org.astraea.argument.validator.NotEmptyString;
+import org.astraea.argument.NonEmptyStringField;
+import org.astraea.argument.StringSetField;
 import org.astraea.utils.DataSize;
 import org.astraea.utils.DataUnit;
 
@@ -92,7 +90,7 @@ public class TopicExplorer {
   }
 
   public static void main(String[] args) throws IOException {
-    var argument = ArgumentUtil.parseArgument(new Argument(), args);
+    var argument = org.astraea.argument.Argument.parse(new Argument(), args);
     try (var admin = TopicAdmin.of(argument.props())) {
       var result = execute(admin, argument.topics.isEmpty() ? admin.topicNames() : argument.topics);
       TreeOutput.print(result, System.out);
@@ -422,12 +420,12 @@ public class TopicExplorer {
     }
   }
 
-  static class Argument extends BasicArgumentWithPropFile {
+  static class Argument extends org.astraea.argument.Argument {
     @Parameter(
         names = {"--topics"},
         description = "the topics to show all offset-related information. Empty means all topics",
-        validateWith = NotEmptyString.class,
-        converter = StringSetConverter.class)
+        validateWith = NonEmptyStringField.class,
+        converter = StringSetField.class)
     public Set<String> topics = Collections.emptySet();
   }
 }
diff --git a/app/src/main/java/org/astraea/topic/cost/PartitionScore.java b/app/src/main/java/org/astraea/topic/cost/PartitionScore.java
index 40def4f6..9cae91b4 100644
--- a/app/src/main/java/org/astraea/topic/cost/PartitionScore.java
+++ b/app/src/main/java/org/astraea/topic/cost/PartitionScore.java
@@ -1,14 +1,15 @@
 package org.astraea.topic.cost;
 
-import static org.astraea.argument.ArgumentUtil.parseArgument;
-
 import com.beust.jcommander.Parameter;
-import com.beust.jcommander.converters.BooleanConverter;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.kafka.common.TopicPartition;
-import org.astraea.argument.BasicArgument;
-import org.astraea.argument.validator.NotEmptyString;
+import org.astraea.argument.BooleanField;
 import org.astraea.topic.TopicAdmin;
 
 public class PartitionScore {
@@ -79,26 +80,26 @@ public class PartitionScore {
   }
 
   public static void main(String[] args) {
-    var argument = parseArgument(new Argument(), args);
+    var argument = org.astraea.argument.Argument.parse(new Argument(), args);
     var admin = TopicAdmin.of(argument.brokers);
     var score = execute(argument, admin);
     printScore(score, argument);
   }
 
-  static class Argument extends BasicArgument {
+  static class Argument extends org.astraea.argument.Argument {
     @Parameter(
         names = {"--exclude.internal.topics"},
         description =
             "True if you want to ignore internal topics like _consumer_offsets while counting score.",
-        validateWith = NotEmptyString.class,
-        converter = BooleanConverter.class)
+        validateWith = BooleanField.class,
+        converter = BooleanField.class)
     boolean excludeInternalTopic = false;
 
     @Parameter(
         names = {"--hide.balanced"},
         description = "True if you want to hide topics and partitions thar already balanced.",
-        validateWith = NotEmptyString.class,
-        converter = BooleanConverter.class)
+        validateWith = BooleanField.class,
+        converter = BooleanField.class)
     boolean hideBalanced = false;
   }
 }
diff --git a/app/src/main/java/org/astraea/utils/DataSize.java b/app/src/main/java/org/astraea/utils/DataSize.java
index f37f1053..ac3f15fc 100644
--- a/app/src/main/java/org/astraea/utils/DataSize.java
+++ b/app/src/main/java/org/astraea/utils/DataSize.java
@@ -1,6 +1,5 @@
 package org.astraea.utils;
 
-import com.beust.jcommander.IStringConverter;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.MathContext;
@@ -11,6 +10,7 @@ import java.util.Comparator;
 import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.astraea.argument.NonEmptyField;
 
 /** Data size class */
 public class DataSize implements Comparable<DataSize> {
@@ -215,7 +215,7 @@ public class DataSize implements Comparable<DataSize> {
     return Objects.hash(bits);
   }
 
-  public static class Converter implements IStringConverter<DataSize> {
+  public static class Field implements NonEmptyField<DataSize> {
     // Parse number and DataUnit
     private static final Pattern DATA_SIZE_PATTERN =
         Pattern.compile("(?<measurement>[0-9]+)\\s?(?<dataUnit>[a-zA-Z]+)");


Test patch:
diff --git a/app/src/test/java/org/astraea/argument/ArgumentUtilTest.java b/app/src/test/java/org/astraea/argument/ArgumentUtilTest.java
deleted file mode 100644
index 2a3ed8d0..00000000
--- a/app/src/test/java/org/astraea/argument/ArgumentUtilTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-package org.astraea.argument;
-
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import java.time.Duration;
-import java.util.Set;
-import java.util.function.Supplier;
-import java.util.stream.Stream;
-import org.astraea.argument.converter.DurationConverter;
-import org.astraea.argument.converter.StringSetConverter;
-import org.astraea.argument.validator.NonNegativeLong;
-import org.astraea.argument.validator.NotEmptyString;
-import org.astraea.argument.validator.PositiveLong;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.CsvSource;
-import org.junit.jupiter.params.provider.MethodSource;
-
-public class ArgumentUtilTest {
-  public static class FakeParameter {
-    @Parameter(
-        names = {"--require"},
-        validateWith = NotEmptyString.class,
-        required = true)
-    public String require;
-
-    @Parameter(
-        names = {"--longPositive"},
-        validateWith = PositiveLong.class)
-    public long longPositive;
-
-    @Parameter(
-        names = {"--longNotNegative"},
-        validateWith = NonNegativeLong.class)
-    public int longNotNegative = 1;
-
-    @Parameter(
-        names = {"--durationConvert"},
-        converter = DurationConverter.class)
-    public Duration durationConvert;
-
-    @Parameter(
-        names = {"--setConverter"},
-        converter = StringSetConverter.class,
-        variableArity = true)
-    public Set<String> setConverter;
-  }
-
-  @Test
-  public void testParse() {
-    var param =
-        ArgumentUtil.parseArgument(new FakeParameter(), new String[] {"--require", "require"});
-    Assertions.assertEquals("require", param.require);
-  }
-
-  @Test
-  public void testRequired() {
-    Assertions.assertThrows(
-        ParameterException.class,
-        () -> ArgumentUtil.parseArgument(new FakeParameter(), new String[] {}));
-  }
-
-  @Test
-  public void testLongPositive() {
-    var param =
-        ArgumentUtil.parseArgument(
-            new FakeParameter(), new String[] {"--require", "require", "--longPositive", "1000"});
-
-    Assertions.assertEquals(1000, param.longPositive);
-    Assertions.assertThrows(
-        ParameterException.class,
-        () ->
-            ArgumentUtil.parseArgument(
-                new FakeParameter(), new String[] {"--require", "require", "--longPositive", "0"}));
-  }
-
-  @Test
-  public void testNotNegative() {
-    FakeParameter param =
-        ArgumentUtil.parseArgument(
-            new FakeParameter(),
-            new String[] {"--require", "require", "--longNotNegative", "1000"});
-
-    Assertions.assertEquals(1000, param.longNotNegative);
-    Assertions.assertThrows(
-        ParameterException.class,
-        () ->
-            ArgumentUtil.parseArgument(
-                new FakeParameter(),
-                new String[] {"--require", "require", "--longNotNegative", "-1"}));
-  }
-
-  @Test
-  public void testDurationConvert() {
-    FakeParameter param =
-        ArgumentUtil.parseArgument(
-            new FakeParameter(),
-            new String[] {"--require", "require", "--durationConvert", "1000"});
-
-    Assertions.assertEquals(Duration.ofSeconds(1000), param.durationConvert);
-  }
-
-  @Test
-  public void testSetConverter() {
-    FakeParameter param =
-        ArgumentUtil.parseArgument(
-            new FakeParameter(),
-            new String[] {"--require", "require", "--setConverter", "1", "1", "2"});
-
-    Assertions.assertEquals(Set.of("1", "2"), param.setConverter);
-  }
-
-  private static Stream<Arguments> testDurationConvertorTestcases() {
-    return Stream.of(
-        Arguments.of("1", Duration.ofSeconds(1)),
-        Arguments.of("0", Duration.ZERO),
-        Arguments.of("60s", Duration.ofSeconds(60)),
-        Arguments.of("30m", Duration.ofMinutes(30)),
-        Arguments.of("24h", Duration.ofHours(24)),
-        Arguments.of("7day", Duration.ofDays(7)),
-        Arguments.of("7days", Duration.ofDays(7)),
-        Arguments.of("100ms", Duration.ofMillis(100)),
-        Arguments.of("500us", Duration.ofNanos(500 * 1000)),
-        Arguments.of("1ns", Duration.ofNanos(1)));
-  }
-
-  @ParameterizedTest(name = "[{index}] time string \"{0}\" will match duration \"{1}\"")
-  @MethodSource("testDurationConvertorTestcases")
-  public void testDurationConvertorConvert(String timeString, Duration expectedDuration) {
-    var durationConverter = new DurationConverter();
-
-    Assertions.assertEquals(expectedDuration, durationConverter.convert(timeString));
-  }
-
-  @ParameterizedTest()
-  @CsvSource(
-      delimiterString = ",",
-      value = {
-        // input string, is_legal, test-purpose
-        "             0, true    , valid unit",
-        "             1, true    , valid unit",
-        "          5566, true    , valid unit",
-        "        1234ns, true    , valid unit",
-        "        4321us, true    , valid unit",
-        "        1234ms, true    , valid unit",
-        "        12000s, true    , valid unit",
-        "           60m, true    , valid unit",
-        "           60h, true    , valid unit",
-        "        365day, true    , valid unit",
-        "       365days, true    , valid unit",
-        "   0010100days, true    , valid unit",
-        "         -1234, false   , currently no negative number allowed",
-        "       -1234ms, false   , currently no negative number allowed",
-        "      -365days, false   , currently no negative number allowed",
-        "          0.5s, false   , currently no floating value allowed",
-        "         hello, false   , illegal time/unit",
-        "            ms, false   , illegal time/unit",
-        "           day, false   , illegal time/unit",
-        "             h, false   , illegal time/unit",
-      })
-  public void testDurationConvertorValidate(String timeString, boolean isLegal) {
-    var execution =
-        (Supplier<Boolean>)
-            () -> {
-              try {
-                var durationConverter = new DurationConverter();
-                durationConverter.validate("key", timeString);
-                return true;
-              } catch (ParameterException ignored) {
-                return false;
-              }
-            };
-
-    Assertions.assertEquals(isLegal, execution.get());
-  }
-}
diff --git a/app/src/test/java/org/astraea/argument/BasicArgumentTest.java b/app/src/test/java/org/astraea/argument/BasicArgumentTest.java
index c97fa798..f4e8ef7a 100644
--- a/app/src/test/java/org/astraea/argument/BasicArgumentTest.java
+++ b/app/src/test/java/org/astraea/argument/BasicArgumentTest.java
@@ -16,12 +16,13 @@ public class BasicArgumentTest {
       output.write("key2=value2");
     }
     var argument =
-        ArgumentUtil.parseArgument(new DumbArgument(), new String[] {"--bootstrap.servers", "abc"});
+        org.astraea.argument.Argument.parse(
+            new DumbArgument(), new String[] {"--bootstrap.servers", "abc"});
     Assertions.assertEquals(3, argument.properties(file.toString()).size());
     Assertions.assertEquals("abc", argument.brokers);
     Assertions.assertEquals("value1", argument.properties(file.toString()).get("key1").toString());
     Assertions.assertEquals("value2", argument.properties(file.toString()).get("key2").toString());
   }
 
-  private static class DumbArgument extends BasicArgument {}
+  private static class DumbArgument extends Argument {}
 }
diff --git a/app/src/test/java/org/astraea/argument/CompressionFieldTest.java b/app/src/test/java/org/astraea/argument/CompressionFieldTest.java
new file mode 100644
index 00000000..be6c69e4
--- /dev/null
+++ b/app/src/test/java/org/astraea/argument/CompressionFieldTest.java
@@ -0,0 +1,34 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import java.util.stream.Stream;
+import org.apache.kafka.common.record.CompressionType;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class CompressionFieldTest {
+
+  private static class FakeParameter {
+    @Parameter(
+        names = {"--field"},
+        converter = CompressionField.class,
+        validateWith = CompressionField.class)
+    public CompressionType value;
+  }
+
+  @Test
+  void testConversion() {
+    var arg = new CompressionField();
+    Stream.of(CompressionType.values())
+        .forEach(type -> Assertions.assertEquals(type, arg.convert(type.name)));
+    Assertions.assertThrows(ParameterException.class, () -> arg.convert("aaa"));
+  }
+
+  @Test
+  void testParse() {
+    var arg =
+        org.astraea.argument.Argument.parse(new FakeParameter(), new String[] {"--field", "gzip"});
+    Assertions.assertEquals(CompressionType.GZIP, arg.value);
+  }
+}
diff --git a/app/src/test/java/org/astraea/argument/DurationFieldTest.java b/app/src/test/java/org/astraea/argument/DurationFieldTest.java
new file mode 100644
index 00000000..1082746c
--- /dev/null
+++ b/app/src/test/java/org/astraea/argument/DurationFieldTest.java
@@ -0,0 +1,77 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.ParameterException;
+import java.time.Duration;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class DurationFieldTest {
+  @ParameterizedTest()
+  @CsvSource(
+      delimiterString = ",",
+      value = {
+        // input string, is_legal, test-purpose
+        "             0, true    , valid unit",
+        "             1, true    , valid unit",
+        "          5566, true    , valid unit",
+        "        1234ns, true    , valid unit",
+        "        4321us, true    , valid unit",
+        "        1234ms, true    , valid unit",
+        "        12000s, true    , valid unit",
+        "           60m, true    , valid unit",
+        "           60h, true    , valid unit",
+        "        365day, true    , valid unit",
+        "       365days, true    , valid unit",
+        "   0010100days, true    , valid unit",
+        "         -1234, false   , currently no negative number allowed",
+        "       -1234ms, false   , currently no negative number allowed",
+        "      -365days, false   , currently no negative number allowed",
+        "          0.5s, false   , currently no floating value allowed",
+        "         hello, false   , illegal time/unit",
+        "            ms, false   , illegal time/unit",
+        "           day, false   , illegal time/unit",
+        "             h, false   , illegal time/unit",
+      })
+  public void testDurationConvertorValidate(String timeString, boolean isLegal) {
+    var execution =
+        (Supplier<Boolean>)
+            () -> {
+              try {
+                var durationConverter = new DurationField();
+                durationConverter.validate("key", timeString);
+                return true;
+              } catch (ParameterException ignored) {
+                return false;
+              }
+            };
+
+    Assertions.assertEquals(isLegal, execution.get());
+  }
+
+  @ParameterizedTest(name = "[{index}] time string \"{0}\" will match duration \"{1}\"")
+  @MethodSource("testDurationConvertorTestcases")
+  public void testDurationConvertorConvert(String timeString, Duration expectedDuration) {
+    var durationConverter = new DurationField();
+
+    Assertions.assertEquals(expectedDuration, durationConverter.convert(timeString));
+  }
+
+  private static Stream<Arguments> testDurationConvertorTestcases() {
+    return Stream.of(
+        Arguments.of("1", Duration.ofSeconds(1)),
+        Arguments.of("0", Duration.ZERO),
+        Arguments.of("60s", Duration.ofSeconds(60)),
+        Arguments.of("30m", Duration.ofMinutes(30)),
+        Arguments.of("24h", Duration.ofHours(24)),
+        Arguments.of("7day", Duration.ofDays(7)),
+        Arguments.of("7days", Duration.ofDays(7)),
+        Arguments.of("100ms", Duration.ofMillis(100)),
+        Arguments.of("500us", Duration.ofNanos(500 * 1000)),
+        Arguments.of("1ns", Duration.ofNanos(1)));
+  }
+}
diff --git a/app/src/test/java/org/astraea/argument/NonNegativeFieldTest.java b/app/src/test/java/org/astraea/argument/NonNegativeFieldTest.java
new file mode 100644
index 00000000..5ef681d1
--- /dev/null
+++ b/app/src/test/java/org/astraea/argument/NonNegativeFieldTest.java
@@ -0,0 +1,29 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class NonNegativeFieldTest {
+
+  private static class FakeParameter {
+    @Parameter(
+        names = {"--field"},
+        validateWith = NonNegativeLongField.class)
+    public int value = 1;
+  }
+
+  @Test
+  public void testNotNegative() {
+    var param =
+        org.astraea.argument.Argument.parse(new FakeParameter(), new String[] {"--field", "1000"});
+
+    Assertions.assertEquals(1000, param.value);
+    Assertions.assertThrows(
+        ParameterException.class,
+        () ->
+            org.astraea.argument.Argument.parse(
+                new FakeParameter(), new String[] {"--field", "-1"}));
+  }
+}
diff --git a/app/src/test/java/org/astraea/argument/PositiveFieldTest.java b/app/src/test/java/org/astraea/argument/PositiveFieldTest.java
new file mode 100644
index 00000000..9408d02b
--- /dev/null
+++ b/app/src/test/java/org/astraea/argument/PositiveFieldTest.java
@@ -0,0 +1,34 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class PositiveFieldTest {
+
+  private static class FakeParameter {
+    @Parameter(
+        names = {"--field"},
+        validateWith = PositiveLongField.class)
+    public long value = 1;
+  }
+
+  @Test
+  public void testNotNegative() {
+    var param =
+        org.astraea.argument.Argument.parse(new FakeParameter(), new String[] {"--field", "1000"});
+
+    Assertions.assertEquals(1000, param.value);
+    Assertions.assertThrows(
+        ParameterException.class,
+        () ->
+            org.astraea.argument.Argument.parse(
+                new FakeParameter(), new String[] {"--field", "0"}));
+    Assertions.assertThrows(
+        ParameterException.class,
+        () ->
+            org.astraea.argument.Argument.parse(
+                new FakeParameter(), new String[] {"--field", "-1"}));
+  }
+}
diff --git a/app/src/test/java/org/astraea/argument/SetFieldTest.java b/app/src/test/java/org/astraea/argument/SetFieldTest.java
new file mode 100644
index 00000000..911719a7
--- /dev/null
+++ b/app/src/test/java/org/astraea/argument/SetFieldTest.java
@@ -0,0 +1,24 @@
+package org.astraea.argument;
+
+import com.beust.jcommander.Parameter;
+import java.util.Set;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class SetFieldTest {
+  private static class FakeParameter {
+    @Parameter(
+        names = {"--field"},
+        converter = StringSetField.class,
+        variableArity = true)
+    public Set<String> value;
+  }
+
+  @Test
+  public void testSetConverter() {
+    var param =
+        org.astraea.argument.Argument.parse(new FakeParameter(), new String[] {"--field", "1,2,3"});
+
+    Assertions.assertEquals(Set.of("1", "2", "3"), param.value);
+  }
+}
diff --git a/app/src/test/java/org/astraea/metrics/MetricExplorerTest.java b/app/src/test/java/org/astraea/metrics/MetricExplorerTest.java
index 7aa324c6..ca04b1d5 100644
--- a/app/src/test/java/org/astraea/metrics/MetricExplorerTest.java
+++ b/app/src/test/java/org/astraea/metrics/MetricExplorerTest.java
@@ -10,7 +10,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.astraea.argument.ArgumentUtil;
 import org.astraea.metrics.jmx.BeanObject;
 import org.astraea.metrics.jmx.MBeanClient;
 import org.junit.jupiter.api.function.Executable;
@@ -33,7 +32,7 @@ class MetricExplorerTest {
       })
   void testPatternJmxUrlStart(String url, String expected) {
     // arrange
-    Pattern pattern = Argument.JmxServerUrlConverter.patternOfJmxUrlStart;
+    Pattern pattern = Argument.JmxServerUrlField.patternOfJmxUrlStart;
 
     // act
     Matcher matcher = pattern.matcher(url);
@@ -73,7 +72,7 @@ class MetricExplorerTest {
       })
   void executeDoesPrintSomething(String args) {
     // arrange
-    var argument = ArgumentUtil.parseArgument(new Argument(), args.split(" "));
+    var argument = org.astraea.argument.Argument.parse(new Argument(), args.split(" "));
     var mockMBeanClient = mock(MBeanClient.class);
     when(mockMBeanClient.queryBeans(any()))
         .thenReturn(List.of(new BeanObject("example.com", Map.of("key", "value"), Map.of())));
@@ -111,7 +110,7 @@ class MetricExplorerTest {
     String[] arguments = argumentString.split(" ");
 
     // act
-    Executable doParsing = () -> ArgumentUtil.parseArgument(new Argument(), arguments);
+    Executable doParsing = () -> org.astraea.argument.Argument.parse(new Argument(), arguments);
 
     // assert
     if (outcome.equals("ok")) {
diff --git a/app/src/test/java/org/astraea/performance/CompressionArgumentTest.java b/app/src/test/java/org/astraea/performance/CompressionArgumentTest.java
deleted file mode 100644
index 30d96a54..00000000
--- a/app/src/test/java/org/astraea/performance/CompressionArgumentTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.astraea.performance;
-
-import com.beust.jcommander.ParameterException;
-import java.util.stream.Stream;
-import org.apache.kafka.clients.producer.ProducerConfig;
-import org.apache.kafka.common.record.CompressionType;
-import org.astraea.argument.ArgumentUtil;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-public class CompressionArgumentTest {
-
-  @Test
-  void testConversion() {
-    var arg = new Performance.CompressionArgument();
-    Stream.of(CompressionType.values())
-        .forEach(type -> Assertions.assertEquals(type, arg.convert(type.name)));
-    Assertions.assertThrows(ParameterException.class, () -> arg.convert("aaa"));
-  }
-
-  @Test
-  void testParse() {
-    var arg =
-        ArgumentUtil.parseArgument(
-            new Performance.Argument(),
-            new String[] {"--bootstrap.servers", "aa", "--compression", "gzip"});
-    Assertions.assertEquals(CompressionType.GZIP, arg.compression);
-    Assertions.assertEquals(
-        "gzip", arg.producerProps().get(ProducerConfig.COMPRESSION_TYPE_CONFIG));
-    Assertions.assertNull(arg.props().get(ProducerConfig.COMPRESSION_TYPE_CONFIG));
-  }
-
-  @Test
-  void testJmxServer() {
-    var arg =
-        ArgumentUtil.parseArgument(
-            new Performance.Argument(),
-            new String[] {"--bootstrap.servers", "aa", "--jmx.servers", "aaa"});
-    Assertions.assertEquals("aaa", arg.producerProps().get("jmx_servers"));
-  }
-}
diff --git a/app/src/test/java/org/astraea/performance/ExeTimeTest.java b/app/src/test/java/org/astraea/performance/ExeTimeTest.java
index 04cd1e0e..1cf7bb0c 100644
--- a/app/src/test/java/org/astraea/performance/ExeTimeTest.java
+++ b/app/src/test/java/org/astraea/performance/ExeTimeTest.java
@@ -5,34 +5,30 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 public class ExeTimeTest {
-  private final ExeTime.Converter converter = new ExeTime.Converter();
-  private final ExeTime.Validator validator = new ExeTime.Validator();
+  private final ExeTime.Field field = new ExeTime.Field();
 
   @Test
   void testRecords() {
-    Assertions.assertDoesNotThrow(() -> validator.validate("--run.until", "1000records"));
-    var exeTime = converter.convert("1000records");
+    Assertions.assertDoesNotThrow(() -> field.validate("--run.until", "1000records"));
+    var exeTime = field.convert("1000records");
     Assertions.assertEquals(0, exeTime.percentage(0, 10));
     Assertions.assertEquals(100D, exeTime.percentage(1000, 10));
   }
 
   @Test
   void testDuration() {
-    Assertions.assertDoesNotThrow(() -> validator.validate("--run.until", "100ms"));
-    var exeTime = converter.convert("100ms");
+    Assertions.assertDoesNotThrow(() -> field.validate("--run.until", "100ms"));
+    var exeTime = field.convert("100ms");
     Assertions.assertEquals(0, exeTime.percentage(1000, 0));
     Assertions.assertEquals(100D, exeTime.percentage(1000, 100));
   }
 
   @Test
   void testUnknownArgument() {
+    Assertions.assertThrows(ParameterException.class, () -> field.validate("--run.until", "aaa"));
     Assertions.assertThrows(
-        ParameterException.class, () -> validator.validate("--run.until", "aaa"));
-    Assertions.assertThrows(
-        ParameterException.class, () -> validator.validate("--run.until", "10record"));
-    Assertions.assertThrows(
-        ParameterException.class, () -> validator.validate("--run.until", "1000"));
-    Assertions.assertThrows(
-        ParameterException.class, () -> validator.validate("--run.until", "10Ms"));
+        ParameterException.class, () -> field.validate("--run.until", "10record"));
+    Assertions.assertThrows(ParameterException.class, () -> field.validate("--run.until", "1000"));
+    Assertions.assertThrows(ParameterException.class, () -> field.validate("--run.until", "10Ms"));
   }
 }
diff --git a/app/src/test/java/org/astraea/topic/ReplicaSyncingMonitorIntegrationTest.java b/app/src/test/java/org/astraea/topic/ReplicaSyncingMonitorIntegrationTest.java
index a70e7e9b..6b4c6e03 100644
--- a/app/src/test/java/org/astraea/topic/ReplicaSyncingMonitorIntegrationTest.java
+++ b/app/src/test/java/org/astraea/topic/ReplicaSyncingMonitorIntegrationTest.java
@@ -8,7 +8,6 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.IntStream;
 import org.apache.kafka.common.TopicPartition;
-import org.astraea.argument.ArgumentUtil;
 import org.astraea.producer.Producer;
 import org.astraea.producer.Sender;
 import org.astraea.service.RequireBrokerCluster;
@@ -53,7 +52,7 @@ class ReplicaSyncingMonitorIntegrationTest extends RequireBrokerCluster {
                 topicAdmin.migrator().partition(TOPIC_NAME, 0).moveTo(Set.of(moveToBroker));
                 ReplicaSyncingMonitor.execute(
                     topicAdmin,
-                    ArgumentUtil.parseArgument(
+                    org.astraea.argument.Argument.parse(
                         new ReplicaSyncingMonitor.Argument(),
                         new String[] {
                           "--bootstrap.servers",
diff --git a/app/src/test/java/org/astraea/topic/ReplicaSyncingMonitorTest.java b/app/src/test/java/org/astraea/topic/ReplicaSyncingMonitorTest.java
index c238a757..8b9aee44 100644
--- a/app/src/test/java/org/astraea/topic/ReplicaSyncingMonitorTest.java
+++ b/app/src/test/java/org/astraea/topic/ReplicaSyncingMonitorTest.java
@@ -17,7 +17,6 @@ import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.apache.kafka.common.TopicPartition;
-import org.astraea.argument.ArgumentUtil;
 import org.astraea.utils.DataUnit;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -86,7 +85,7 @@ class ReplicaSyncingMonitorTest {
             () -> {
               ReplicaSyncingMonitor.execute(
                   mockTopicAdmin,
-                  ArgumentUtil.parseArgument(
+                  org.astraea.argument.Argument.parse(
                       new ReplicaSyncingMonitor.Argument(),
                       new String[] {
                         "--bootstrap.servers", "whatever:9092", "--interval", interval + "ms"
@@ -159,7 +158,7 @@ class ReplicaSyncingMonitorTest {
               try {
                 ReplicaSyncingMonitor.execute(
                     mockTopicAdmin,
-                    ArgumentUtil.parseArgument(
+                    org.astraea.argument.Argument.parse(
                         new ReplicaSyncingMonitor.Argument(),
                         new String[] {
                           "--bootstrap.servers",
@@ -211,7 +210,7 @@ class ReplicaSyncingMonitorTest {
           try {
             ReplicaSyncingMonitor.execute(
                 mockTopicAdmin,
-                ArgumentUtil.parseArgument(
+                org.astraea.argument.Argument.parse(
                     new ReplicaSyncingMonitor.Argument(),
                     new String[] {
                       "--bootstrap.servers",
@@ -282,7 +281,8 @@ class ReplicaSyncingMonitorTest {
 
     // act
     Consumer<String[]> execution =
-        (String[] args) -> ArgumentUtil.parseArgument(new ReplicaSyncingMonitor.Argument(), args);
+        (String[] args) ->
+            org.astraea.argument.Argument.parse(new ReplicaSyncingMonitor.Argument(), args);
 
     // assert
     correct.stream()
diff --git a/app/src/test/java/org/astraea/utils/DataSizeTest.java b/app/src/test/java/org/astraea/utils/DataSizeTest.java
index 5a21cfdf..e9df2fdb 100644
--- a/app/src/test/java/org/astraea/utils/DataSizeTest.java
+++ b/app/src/test/java/org/astraea/utils/DataSizeTest.java
@@ -148,7 +148,7 @@ class DataSizeTest {
 
   @Test
   void parseDataSize() {
-    var converter = new DataSize.Converter();
+    var converter = new DataSize.Field();
     assertEquals(DataUnit.Bit.of(100).toString(), converter.convert("100Bit").toString());
     assertEquals(DataUnit.Kb.of(100).toString(), converter.convert("100 Kb").toString());
     assertEquals(DataUnit.Mb.of(100).toString(), converter.convert("100 Mb").toString());