This blog post is about just one of the features of Oracle Coherence that you probably never knew about, that is, decompiling byte code. Last week I had to write some code that required me to find all of the classes annotated with a particular annotation, the reason for this is not important for this post, just the fact that I needed to do it. Well, I bet you never knew that Oracle Coherence can do it out of the box.
Now, I know there are third-party open source libraries that can do what I want, Javassist was one that I looked at, but my problem was that the project I am working on includes a client API and we have to keep the number of third-party dependencies for this to a bear minimum (Coherence and a couple of others) as we cannot dictate to clients what third-party libraries they use. So, even though something like Javassist would have allowed me to do what I want, I need this code in our client API so I didn’t really want to go down the third-party jar route unless I had to.
If any of you are like me you will have seen the packages and classes inside the Coherence jar file displayed in your IDE. I use IntelliJ which nicely lists them in the Project tree on the left on my screen, so like me you might have wondered what some of them do. So I have always wondered about the com.tangosol.dev package with its assembler, compiler and disasembler sub-packages. After having the history of Coherence explained to me by someone at Oracle a while ago I think I know what these are for but have never looked at them in any detail; they are not part of the public documented API either so they are a little opaque.
Going back to my original requirement, I have to find all the classes that have a specific annotation. This falls into two parts; first you need to be able to scan all of the classes on the classpath, either from jar files or directories and second, when you have all of the class files, you need to be able to find the annotated ones.
The first part is simple, and thanks to Google, I found there are a few ways to do it. As I do not want to scan all the third-party jar files or classes on my classpath I chose to go down the route of having a marker file in the META-INF package of my code. It is easy to find the URL of this file from the ClassLoader then go up a level to get the root directory name or jar file name. From there you can scan down the directories or jar file entries to a list get all of the class file names. I went a step further and made this marker file a properties file that has a property listing package names to be searched. This makes the scanner a bit more efficient as it does not need to look inside all the packages in a jar or directory.
Now I have a list of all of URLs for the class files in the packages to be searched I need to find which of those classes are annotated with a specified annotation. The really bad way to do this is call Class.forname()
for each one, as this is slow and will load each class into memory and do any static initialisation. The recommended way is to read the byte-code, hence my earlier look at Javassist and the purpose of this post.
Having played a bit with Javassist I saw that its API reads byte-code by taking in a DataInputStream from the relevant class file. This jogged my memory about Coherence and its com.tangosol.dev.disassembler package, and sure enough it too has a class called ClassFile that has a constructor taking a DataInputStream, so now my inquisitive nature was pricked and I had to dig further. It didn’t take much work to get the code to read the required class files, now I just needed to work out how to get the annotation information from the ClassFile instance; unfortunately nothing looked very obvious from the method names on the ClassFile class. So after more digging I finally had to resort to hand coding something to do the very small missing bit of getting the annotation information from the list of Attribute instances in the ClassFile. This was not too bad, but I was a bit disapointed that Coherence was so close but couldn’t do the whole job.
A day or so later I was poking around in the com.tangosol.dev.assembler package and found that it contains a number of classes to do with annotations and it also contains a ClassFile class with a constructor taking a DataInputStream. Hmm… maybe this is what I was missing earlier. After half an hour or so of experimenting I had working code that now used nothing but the functionality provided by Coherence.
Here is the code for my scanner.
import com.tangosol.dev.assembler.Annotation; import com.tangosol.dev.assembler.ClassConstant; import com.tangosol.dev.assembler.ClassFile; import com.tangosol.dev.assembler.RuntimeVisibleAnnotationsAttribute; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.JarURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; public class AnnotatedClassFinder { public List<String> findAnnotatedClassNames(String resourceName, String annotationClassName) throws Exception { Enumeration<URL> urlEnumeration = Thread.currentThread().getContextClassLoader().getResources(resourceName); return findAnnotatedClassNames(urlEnumeration, resourceName, annotationClassName); } public List<String> findAnnotatedClassNames(Enumeration<URL> urlEnumeration, String resourceName, String annotationClassName) throws Exception { List<String> annotatedClassNames = new ArrayList<String>(); while (urlEnumeration.hasMoreElements()) { URL url = urlEnumeration.nextElement(); List<String> packages = getPackageNames(url); if (packages.size() > 0) { String urlName = url.toExternalForm(); String base = urlName.substring(0, urlName.length() - resourceName.length()); if (base.contains(".jar!/")) { checkJarFile(annotationClassName, url, packages, annotatedClassNames); } else { checkDirectory(annotationClassName, new URL(base), packages, annotatedClassNames); } } } return annotatedClassNames; } public void checkDirectory(String annotationClassName, URL url, List<String> packages, List<String> annotatedClassNames) throws Exception { File root = new File(url.toURI()); for (String packageName : packages) { File file = new File(root, packageName.replace(".", File.separator)); if (file.exists() && file.isDirectory()) { for (File child : file.listFiles()) { if (child.getName().endsWith(".class")) { String className = getAnnotatedClassName(annotationClassName, new FileInputStream(child)); if (className != null) { annotatedClassNames.add(className); } } } } } } public void checkJarFile(String annotationClassName, URL url, List<String> packages, List<String> annotatedClassNames) throws IOException { JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); JarFile jarFile = jarURLConnection.getJarFile(); Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); if (shouldIncludeEntry(entry, packages)) { String className = getAnnotatedClassName(annotationClassName, jarFile.getInputStream(entry)); if (className != null) { annotatedClassNames.add(className); } } } } public boolean shouldIncludeEntry(JarEntry entry, List<String> packages) { if (entry.isDirectory()) { return false; } String entryName = entry.getName(); if (!entryName.endsWith(".class")) { return false; } for (String packageName : packages) { if (entryName.startsWith(packageName)) { return true; } } return false; } public List<String> getPackageNames(URL propertiesFileURL) throws IOException { System.err.println("Checking " + propertiesFileURL); Properties properties = new Properties(); properties.load(propertiesFileURL.openStream()); String packagesProperty = properties.getProperty("packages"); if (packagesProperty != null && packagesProperty.length() > 0) { return Arrays.asList(packagesProperty.split(",")); } return Collections.emptyList(); } public String getAnnotatedClassName(String annotationClassName, InputStream inputStream) throws IOException { String requiredAnnotation = "L" + annotationClassName + ";"; DataInputStream dataInputStream = new DataInputStream(inputStream); try { ClassFile classFile = new ClassFile(dataInputStream); RuntimeVisibleAnnotationsAttribute attribute = (RuntimeVisibleAnnotationsAttribute) classFile.getAttribute("RuntimeVisibleAnnotations"); if (attribute != null) { Iterator iterator = attribute.getAnnotations(); while (iterator.hasNext()) { Annotation annotation = (Annotation) iterator.next(); ClassConstant annotationSignature = new ClassConstant(annotation.getAnnotationType()); if (requiredAnnotation.equals(annotationSignature.getJavaName())) { return classFile.getClassConstant().getJavaName(); } } } } finally { dataInputStream.close(); } return null; } }
To use the class you just do this…
String propertiesFileName = "META-INF/gridman.properties"; String annotationName = "java.lang.Deprecated"; AnnotatedClassFinder finder = new AnnotatedClassFinder(); List<String> annotatedClasses = finder.findAnnotatedClassNames(propertiesFileName, annotationName);
The code above will find all the classes in directories or jar files on the classpath that contain a META-INF/gridman.properties file that are annotated with the @Deprecated annotation.
That’s all there is to it, easy decompilation of class files and no other dependencies except for Coherence. After six or so years Coherence still manages to spring some suprises.
Very cool, but I’d probably use ASM instead, which has been embedded in coherence.jar since 3.7.1 release ;-)
The main reason we added ASM was to implement partial classes for Coherence REST, but you can use it for annotation scanning and anything else as well (I just used it on a project to generate POF serialization code based on annotations)
Cheers,
Aleks