Skip to content
Anjunar/ DOCS

Commands by annotation

A command table built from annotated methods: find them, describe their parameters for help texts, and call them by name. The pattern behind routers, CLI tools and RPC endpoints.

Model

Methods that carry a name

A method annotation with a value. The class stays an ordinary class you can test without the command table.

java
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Command { String value(); }
scala
class Shell { @Command("greet") def greet(name: String): String = s"Hello $name" @Command("shout") def shout(text: String): String = text.toUpperCase def helper(): Unit = () }
Table

From methods to a map

methods includes inherited and overridden methods exactly once, and findAnnotation also sees annotations placed on an interface method.

scala
val shell = TypeResolver.resolve(classOf[Shell]) val commands = shell.methods.flatMap { method => Option(method.findAnnotation(classOf[Command])).map(command => command.value() -> method) }.toMap commands.toSeq.sortBy(_._1).foreach { case (name, method) => println(s"$name(${method.parameters.map(p => s"${p.name}: ${p.parameterType.name}").mkString(", ")})") }
Output
greet(name: String) shout(text: String)
Call

By name, with checked arguments

Check the argument count against parameters before invoking; the parameter types tell you how to convert text input.

scala
def run(target: Shell, name: String, args: String*): Any = val method = commands.getOrElse(name, sys.error(s"Unknown command $name")) require(method.parameters.length == args.length, s"$name takes ${method.parameters.length} arguments") method.invoke(target, args*) println(run(new Shell, "greet", "Ada")) // Hello Ada
Build the table once

Resolving and scanning happen once; afterwards every call is a map lookup and an invoke.