Login | Register   
LinkedIn
Google+
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Eliminate Boilerplate Code with the PICA Technique for Java

Combining reflection, dynamic proxies, and annotations proves to be such a powerful technique that it deserves its own name.


advertisement

eflection, dynamic proxies, and annotations are far from new capabilities in Java, having been introduced in JDK 1.1, 1.3, and 1.5, respectively. Lately, though, I've witnessed a spectacular combination of these features in a number of different projects. The combination is quite innovative, so much so that I was surprised no one had coined a name for it. So, I decided to call it the Proxied Interfaces Configured with Annotations (PICA) technique, which this article describes.

Introduction to the PICA Technique

The gist of the PICA technique is:

  1. Create an interface and mark some combination of the interface itself, its methods, and its methods' parameters with annotations that serve to configure specific behaviors you want the methods to exhibit.
  2. Create an InvocationHandler that uses data from the annotations to direct the behaviors that should occur when the interface's methods are invoked.
  3. Using java.lang.reflect.Proxy.newProxyInstance(), create an instance of a dynamic proxy that implements the interface and uses the invocation handler.

As an example of the PICA technique in action, look at JewelCli, one of a number of Java command-line parsing libraries. Many of these libraries ask their users to configure the command-line switches that should be recognized and parse the command line via the typical imperative programming model—lots of verbose calls to methods on Java APIs. For example, here is a usage scenario for Commons CLI:



CommandLineParser parser = new PosixParser();
Options options = new Options();
options.addOption("a", "all", false, 
   "do not hide entries starting with .");
options.addOption("A", "almost-all", false, 
   "do not list implied . and .." );
options.addOption("b", "escape", false, 
   "print octal escapes for nongraphic characters");
options.addOption(OptionBuilder.withLongOpt( "block-size" )
    .withDescription( "use SIZE-byte blocks" )
    .hasArg()
    .withArgName("SIZE")
    .create());
options.addOption("B", "ignore-backups", false, 
   "do not list implied entries ending with ~");
options.addOption("c", false, 
   "with -lt: sort by, and show, ctime (time of last modification " + 
   "of file status information) with -l:show ctime and sort by name " + 
   "otherwise: sort by ctime" );
options.addOption("C", false, "list entries by columns");
String[] args = { "--block-size=10" };
try {
    CommandLine line = parser.parse(options, args);
    if (line.hasOption("block-size")) {
        System.out.println(line.getOptionValue("block-size"));
    }
}
catch (ParseException exp) {
    System.out.println("Unexpected exception:" + exp.getMessage());
}

JewelCli adopts a more declarative approach. First, the programmer creates a Java interface, a PICA, as shown below:

package uk.co.flamingpenguin.jewel.cli.examples;
import java.io.File;
import java.util.List;
import uk.co.flamingpenguin.jewel.cli.CommandLineInterface;
import uk.co.flamingpenguin.jewel.cli.Option;
import uk.co.flamingpenguin.jewel.cli.Unparsed;
@CommandLineInterface(application="rm")
public interface RmExample
{
   @Option(shortName="d", longName="directory", 
      description="unlink FILE, even if it is a " + 
      "non-empty directory (super-user only)")
   boolean isRemoveNonEmptyDirectory();
   @Option(shortName="f", description=
      "ignore nonexistent files, never prompt")
   boolean isForce();
   @Option(shortName="i", description="prompt before any removal")
   boolean isInteractive();
   @Option(shortName={"r", "R"}, description=
      "remove the contents of directories recursively")
   boolean isRecursive();
   @Option(shortName="v", description=
      "explain what is being done")
   boolean isVerbose();
   @Option(description=
      "display this help and exit")
   boolean isHelp();
   @Option(description=
      "output version information and exit")
   boolean isVersion();
   @Unparsed(name="FILE")
   List<File> getFiles();
}
Author's Note: The preceding code example was borrowed from the JewelCli site.

Notice how each annotation on RmExample contributes to the desired behavior. The @CommandLineInterface annotation provides an application name used to generate a help screen for the command-line parser. The @Option annotation marks methods that, when invoked, access parsed options as described by the shortName and longName attributes, and specifies a description of the option to be used in command-line help. Such options are to be converted into instances of the method's return type. Finally, @Unparsed marks a method that, when invoked, returns any non-option arguments parsed from a command line.

Next, the programmer supplies a CliFactory with the interface's class object and the arguments to parse:

RmExample parsed = CliFactory.parseArguments(
   RmExample.class, "-v", "-rf", "/tmp/*", "./tmp");

The parseArguments() method handles parsing the command-line switches and their arguments (if any), and arranges for them to be accessed via the returned instance of the given interface. Behind the scenes, CliFactory creates a dynamic proxy that implements RmExample and provides it with an InvocationHandler, which intercepts calls to the interface's methods and responds to them by returning the results of options that correspond to the methods.

The volume of client code required to parse the arguments and retrieve their type-converted values is very small indeed. CliFactory does the grunt work of processing a PICA and argument list behind the scenes. All the caller must do is provide a PICA type. Even a quick glance at the PICA class shows that it's easy to see all the supported command-line switches, their intended types, and how they contribute to a help screen, without having to wade through lots of clunky API calls.



Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap