Friday, 30 September 2011

Looking into the Magic of Dependency Injection Using Annotations - Part 2

This is the second in a short series of blogs I’m presenting that looks into implementing dependency injection using annotations. If you’re familiar with Spring or EJB3, you’ll know the sort of thing I’m talking about: you write you class; add a few annotations, for example @Autowired or @Ejb; deploy it to your web-server and then et volia it’s all glued together and works, as if by magic...

This blog isn’t going to write a fully formed DI factory using annotations, but it will give you a few hints in to the kinds of techniques used by the Guys at Spring and the EJB3 Team.

In order to write a DI factory like this, you need to complete a few specific tasks. These include figuring out which classes on your class-path are annotated, instantiating those classes and gluing them all together. Yesterday’s blog demonstrated how to find all the .class files that are on your class-path and held on your file system. Today’s blog takes this one step further and examines how to find classes that are stored in JAR files located on your file system

The scenario I’m using is the same as yesterday’s: given a package name, locate and display all the class names in that package. This means that the code below will be fairly of a similar from:

public class JarFileSample {

 
private static final String JAR_FILE_PACKAGE = "org.slf4j";

 
private static final Pattern pattern = Pattern.compile("^" + JAR_FILE_PACKAGE + ".*");

 
public static void main(String[] args) throws IOException, ClassNotFoundException {

   
System.out.println("Finding all classes in jarfile package...");
    JarFileSample instance =
new JarFileSample();

    List<Class<?>> result = instance.getClasses
(JAR_FILE_PACKAGE);
    listResults
(result);

    System.out.println
("-- END --");
 
}

 
private List<Class<?>> getClasses(String packageName) throws ClassNotFoundException, IOException {

   
String path = packageName.replace('.', '/'); // Convert the package name
   
List<File> directories = getPackageDirectories(path);
   
return walkJars(directories);
 
}

 
private List<File> getPackageDirectories(String path) throws IOException {

   
List<File> files = new ArrayList<File>();
    ClassLoader classLoader = getClassLoader
();
    Enumeration<URL> resources = classLoader.getResources
(path);
   
while (resources.hasMoreElements()) {
     
URL resource = resources.nextElement();
      File file = getNextFile
(resource);
      files.add
(file);
   
}
   
return files;
 
}

 
private File getNextFile(URL resource) throws UnsupportedEncodingException {
   
String fileNameDecoded = URLDecoder.decode(resource.getFile(), "UTF-8");
    fileNameDecoded = fileNameDecoded.substring
(fileNameDecoded.indexOf(":") + 1, fileNameDecoded.indexOf("!"));
   
return new File(fileNameDecoded);
 
}

 
private ClassLoader getClassLoader() {
   
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
   
assert classLoader != null;
   
return classLoader;
 
}

 
private List<Class<?>> walkJars(List<File> directories) throws IOException, ClassNotFoundException {

   
List<Class<?>> classes = new ArrayList<Class<?>>();
   
for (File directory : directories) {
     
classes.addAll(walkJar(directory));
   
}

   
return classes;
 
}

 
private List<Class<?>> walkJar(File directory) throws IOException, ClassNotFoundException {

   
List<Class<?>> classes = new ArrayList<Class<?>>();
    JarFile jarFile =
new JarFile(directory);

    Enumeration<JarEntry> jarEntries = jarFile.entries
();

   
while (jarEntries.hasMoreElements()) {
     
JarEntry jarEntry = jarEntries.nextElement();
      addClassFromJar
(jarEntry, classes);
   
}

   
return classes;
 
}

 
private void addClassFromJar(JarEntry jarEntry, List<Class<?>> classes) {
   
if (isMatchingClass(jarEntry)) {
     
String fileName = jarEntry.getName();
     
if (isValidClassName(fileName)) {
       
Class<?> clazz = createClass(fileName);
       
if (isNotNull(clazz)) {
         
classes.add(clazz);
       
}
      }
    }
  }

 
private boolean isMatchingClass(JarEntry jarEntry) {

   
boolean retVal = false;
   
if (!jarEntry.isDirectory()) {
     
String name = jarEntry.getName();
      Matcher matcher = pattern.matcher
(name);
      retVal = matcher.matches
();
   
}
   
return retVal;
 
}

 
private boolean isValidClassName(String fileName) {
   
return fileName.endsWith(".class") && !fileName.contains("$");
 
}

 
private Class<?> createClass(String fileName) {

   
try {
     
String className = getClassName(fileName);
     
return Class.forName(className);
   
} catch (Throwable e) {
     
e.printStackTrace();
     
return null;
   
}
  }

 
private String getClassName(final String fileName) {

   
String retVal = fileName.substring(0, fileName.length() - 6);
    retVal = retVal.replaceAll
("/", ".");

   
return retVal;
 
}

 
private boolean isNotNull(Object obj) {
   
return obj != null;
 
}

 
private static void listResults(List<Class<?>> clazzes) {

   
int i = 1;
   
for (Class<?> clazz : clazzes) {
     
System.out.println(i++ + ") Found: " + clazz.getName());
   
}
  }
}

Being a sample, I’ve hard coded the package name. This is then used by the ClassLoader to find all its associated URL resources. In saying all the URL resources, I’m taking into account that classes belonging to the same package can be split across several different JAR files. In the code below, for example, I use a start point package name oforg.slf4j, which by design spreads its classes over multiple JAR files.

In interrogating the ClassLoader, we’ll obtain an old fashioned Enumeration containing URL resources with names that look something like this:

file:/Users/Roger/.m2/repository/org/slf4j/slf4j-log4j12/1.6.1/slf4j-log4j12-1.6.1.jar!/org/slf4j

Obviously, we need access to the actual JAR file, so this name has to be fixed up to look like this:

/Users/Roger/.m2/repository/org/slf4j/slf4j-log4j12/1.6.1/slf4j-log4j12-1.6.1.jar

Note that I’m assuming that that the JAR files I’m parsing are on the same file system as my code. For our purposes this is not a bad assumption; it’s just something to be aware of.

Once we have list of JAR file File classes, the next job is to open them and take a look inside. The JDK provides you with a whole bunch of classes for this purpose all beginning with ‘Jar’. In this sample I’m using: JarFile and JarEntry as all I need are the fully qualified class names. If you look at the code you can see that I’ve not had to do anything as complicated as yesterday, when I used recursion to walk the directory structure. This is because the JarFile and JarEntry classes provide you with all the path information you need.

From the code, you can see that the JarFile provides you with an enumeration of JarEntry classes. Each JarEntry gives you enough information to determine whether or not it’s a class entry that matches the starting point package name. If it is, then all you do is create a Class class and add it to the output list.

There is a fly in this ointment, which if you change the starting point path name and run this code, you may spot. It occurs when a class in a JAR file depends on another class that’s stored in another JAR file, and that file is unavailable. When this happens you’ll get a NoClassDefFoundError like the one below that tells you that Log4J’s JmsAppender class depends upon a missing JMSException:

java.lang.NoClassDefFoundError: javax/jms/JMSException
 at java.lang.Class.forName0(Native Method)
 at java.lang.Class.forName(Class.java:169)
 at marin.io.JarFileSample.createClass(JarFileSample.java:169)

In our case, this isn’t a problem as we can’t instantiate classes in a DI factory when JARs are missing, so all we have to do is tell the developer to add the missing JAR...

Together with yesterday’s blog, we now have two parts of the puzzle in that we can find classes both stored in JAR files and on the file system. The next step is to figure out how to test for the appropriate annotations, which’ll be a subject for another day.

No comments: