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.