When writing an annotation-based framework, you many times have the need to find all classes that use a specific annotation at deployment time to initialize your framework. For example, the EJB 3.0 deployer needs to know which classes are annotated with @Stateless, @Stateful, and @MessageDriven, so it can create a container for each of those classes. For JPA, it needs to find a given set of classes within an archive that are annotated with @Entity so that it can define its ORM mapping. This scanning for annotations can be done at runtime using various techniques and open source libraries. I want to discuss how to do this in my blog and point you to a small project I created at sourceforge to help out with this.
Finding archives to scan
The first thing you need to do is to find actual archives you want to scan for classes. Depending on your environment there are different ways to accomplish this.
Java classpath
The “java.class.path” system property, if set properly (FYI, maven doesn’t set it properly), can be a way to obtain a list of classpath/archives you can scan for annotated classes. Although it it only provides relative paths, you can easily turn these into URLs and/or InputStreams for iterating over.
Using your classloader
Using ClassLoader.getResource() and ClassLoader.getResources() is a way to obtain URLs to specific resources in your classpath. From these specific resources you can extract the base url of the classpath/archive the resource resides in by chopping off the base resource name. For example, one common technique is to have a marker file in each of your archives. For JPA, this marker file is META-INF/persistence.xml. You could then to ClassLoader.getResources() to find all classpaths/archives that contain that file:
ClassLoader cl = Thread.currentThread().getContextClassLoader(); Enumeration<URL> urls = cl.getResources("META-INF/persistence.xml");
Calculating the URL of the classpath/archive is just a matter of a little string manipulation of the url.
Web Applications
In web applications its very easy to obtain URLs that point to jars within WEB-INF/lib or to extract where the WEB-INF/classes path is by using the ServletContext class. The class has a nice method getResourcePaths() that returns URLs to what you want. For example:
List<URL> urls = new ArrayList<URL>(); Set libJars = servletContext.getResourcePaths("/WEB-INF/lib"); for (Object jar : libJars) { try { urls.add(servletContext.getResource((String) jar)); } catch (MalformedURLException e) { throw new RuntimeException(e); } }
ServletContext.getResourcePaths() returns a directory like list of all paths under the specified base path you provide as a parameter. Passing in “/WEB-INF/lib” will get you a listing of all .jar files within /WEB-INF/lib of your web application. Once you have this listing, you can do ServletContext.getResource() to obtain a URL pointing to each of the .jar files.
To find the /WEB-INF/classes is a little trickier.
URL classesPath = null; Set libJars = servletContext.getResourcePaths("/WEB-INF/classes"); for (Object jar : libJars) { try { URL url = servletContext.getResource((String) jar); String urlString = url.toString(); int index = urlString.lastIndexOf("/WEB-INF/classes/"); urlString = urlString.substring(0, index + "/WEB-INF/classes/".length()); classesPath = new URL(urlString); break; } catch (MalformedURLException e) { throw new RuntimeException(e); } }
You use the same ServletContext.getResourcePaths() and ServletContext.getResource() but you must do some string manipulation to the URL to get the base path. If you are deploying things within a web framework, you can write a ServletContextListener that obtains access to the servlet context.
Browsing archives
Once you have URLs pointing to directories or .jar files that make up your classpath (or the set of archives/paths you want to scan) you need to browse them. You can usually assume that URLs ending in “/” are some form of a directory while those not ending in “/” are .jar files. If you hack and step through URLClassLoader code, you’ll find that it makes the same assumption. Browsing jars is very easy. If you have a URL pointing to a .jar file all you need to do is open an InputStream to it and instantiate a JarInputStream. This class has methods that allow you to list and open files within the jar archive.
URLs that point to a directory poses an abstraction problem. Browsing a directory structure is protocol specific. If the URL protocol is “file:” you can just use java.io.File. If its “http:”, you need to use a WebDAV library. If its a different protocol, you need to find a library that allows you to browse that particular protocol.
Finding annotated classes
So, once you have access to your classpaths you can obtain a list of .class files within those classpaths. It generally is a very bad idea to load each and every one of those classes using your ClassLoader and use the Java reflection API to scan for annotations you are interested in.
- Use the Java reflection API only allows you to see annotations that are visible at runtime. If you remember your JDK 5.0 lessons, there are 3 types of annotation. Source, Class, and Runtime. Class and Runtime are compiled into your .class files, but only Runtime are visible at runtime.
- You generally do not use each and every class in the libraries you are scanning. If you load each class into your ClassLoader, you are filling up the JVM’s perm-gen space and wasting resources.
So, if you’re not going to load the class through your classloader, how do you scan for annotations? The answer is to use a bytecode processing library like Javassist or ASM. I am most familiar with Javassist since I wrote the annotation processing for it so let’s show examples using that library. ASM is perfectly good as well for this purpose.
With Javassist, you load up a ClassFile instance from the InputStreams you browse from your .jar or classpath. This object allows you to view the bytecode structure of your .class file without loading the class and find the string names of the annotations attached to each element of the class
DataInputStream dstream = new DataInputStream(new BufferedInputStream(bits)); ClassFile cf = new ClassFile(dstream); String className = cf.getName(); AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.visibleTag); AnnotationsAttribute invisible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.invisibleTag); for (javassist.bytecode.Annotation ann : visible.getAnnotations()) { System.out.println("@" + ann.getTypeName()); }
The visible and invisible attributes correspond to Runtime and Class visible annotations. From this information you can obtain annotations attached to the class. Javassist has a reflection-like api that allows you to iterate over methods and fields of the ClassFile. Getting annotation information for methods and fields is exactly the same as getting them from the class.
New Scannotation Framework
I’m writing this blog because I actually had to do a lot of this scanning for JBoss’s EJB container and recently the JAX-RS implementation I’m working on. Because I thought this code might be useful, I created a sourceforge project for it called Scannotation. The Scannotation framework encapsulates all the ideas and functionality I talked about in this blog. It centers around three classes: ClasspathUrlFinder, WarUrlFinder, and AnnotationDB.
ClasspathUrlFinder finds classpath URLs for you. It has methods to obtain them from the java.class.path System property. Other methods to find an archive URL by providing a classloader resource name as described earlier in this blog. WarUrlFinder encapsulates finding URLs for your WEB-INF/classes directory as well as the jars in WEB-INF/lib. Its pointless to repeat the javadocs for these classes so just follow the links above to find out more information.
The AnnotationDB class consumes URLs you find through ClasspathUrlFinder or WarUrlFinder. It scans them for .class files and uses Javassist to make two indexes:
- An index keyed on the fully qualified name of an annotation, with a set of classes that use that annotation
- An index of fully qualified class names with a set of annotations that that particular class uses
Here’s an example of scanning your Classpath:
URL[] urls = ClasspathUrlFinder.findClassPaths(); // scan java.class.path AnnotationDB db = new AnnotationDB(); db.scanArchives(urls);
Here’s another example of scanning all JPA archives:
URL[] urls = ClasspathUrlFinder.findResourceBases("META-INF/persistence.xml"); AnnotationDB db = new AnnotationDB(); db.scanArchives(urls); Set<String> entityClasses = db.getAnnotationIndex().get(javax.persistence.Entity.class.getName());
From this mini annotation database, your annotation frameworks can pick out which classes the care about more easily. Eventually I want to write ant task and maven plugin that will add an annotation index into a file within META-INF of your jars. That way you could precompile this index at build time and save some CPU cycles. If anybody is interested in doing this, let me know and I’ll give you SVN access to the project.
EDITED 3/31/2009:
You might want to check out the Reflections project. I think they have taken scannotations to the next level. I personally have not been able to maintain the scannotations project.
Jan 15, 2008 @ 19:17:56
wow, great idea with that, java couldnt make it any more simple. you could also use apt powered by sun. annotation support in language is uber bad, but the good news is that it will never change.
Jan 15, 2008 @ 19:29:43
Very interesting. Thanks for sharing this.
Jan 16, 2008 @ 06:13:01
Thanks for writing that up.
Jan 16, 2008 @ 12:40:56
Any plans to integrate this with JBossVFS?
I can have a crack at the caching part, but would like to make it more pluggable than just a precompiled index file.
e.g. pushing the information into our deployers attachments or scoped metadata. 😉
Jan 16, 2008 @ 13:00:02
????
Spring 2.5 includes a powerful class scanning framework based on ASM (but you can change the implementation if you want to). You can scan for annotations, methods, anything.
Not invented here?
Jan 16, 2008 @ 13:41:26
Good post. I do agree with Steve D. I can see using this for other things as well!
Jan 16, 2008 @ 15:25:25
@Ales, would be great to integrate with JBossVFS. They only problem with JBoss VFS outside of jboss is that it has its own URL handlers. Because URL handler pluggability sucks in JDK, this is an issue with things like Tomcat.
@Steven D: a) Why would I want to create a dependency on a bloated heavy Spring distribution just to get at some simple annotation processing? b) On NIH, this code was extracted from code written years ago when Spring still thought annotations were a bad thing.
Jan 18, 2008 @ 09:08:30
Cool Bill, good work. Straight and simple.
Jan 18, 2008 @ 09:11:27
One thing though: Maybe it would be cool to add override capabilities to the DB, in a way similar to what JBoss EJB3 does. So that you could query the DB for meta data regardless if it comes from source annotation or an external descriptor.
Jan 18, 2008 @ 14:58:32
The DB is just a hashmap so you can add whatever you want.
Jan 27, 2008 @ 15:07:15
@Bill >> They only problem with JBoss VFS outside of jboss is that it has its own URL handlers. Because URL handler pluggability sucks in JDK, this is an issue with things like Tomcat.
So even our org.jboss.virtual.protocol.vfsXYZ packages + Handlers don’t help here?
Or the VFS.init ‘java.protocol.handler.pkgs’ setting?
Feb 08, 2008 @ 20:44:02
Any way to just find all classes that have a certain annotation and have it return a list of those classes?
Feb 08, 2008 @ 20:58:32
Forget it, figured it out:
Set entities = db.getAnnotationIndex().get(MyAnnotation.class.getName());
Nice work Bill! Thanks.
Feb 14, 2014 @ 18:13:16
You just saved my life !!!
A beginners guide to best scanning vendors pci dss
Feb 13, 2008 @ 11:45:45
Apr 21, 2008 @ 14:10:21
Hello Bill,
very good job… if I were able to let it work.
I have to scan the “deploy” directory of JBoss for finding annotated classes at JBoss startup, but the following code doesn’t work:
String homeDir = System.getProperty(“jboss.server.home.dir”) + “\\deploy\\”;
File file = new File(homeDir);
URL url = file.toURI().toURL();
AnnotationDB db = new AnnotationDB();
db.scanArchives(url);
Set annotatedClasses =
db.getAnnotationIndex().get(MyAnnotation.class.getName());
“annotatedClasses” is null, even if there are annotated classes in existing “.ear” files.
Same result if url points to a specific ear file. “MyAnnotation”, of course, is placeholder.
Is your code able to scan inside jars but not ears/sars/wars…? Or, did I wrote something wrong?
Thanks in advance for your help.
Professional Software Development » Working with Java 5 Annotations
May 04, 2008 @ 21:09:28
May 05, 2008 @ 11:14:51
I have developed my own class that recursively scans a dir (in my case, JBoss deploy dir) getting into any zip/rar/ear/jar/war… and archives contained inside up to any depth level. It checks all existing classes using javassist to examine bytecode (thanks for your tips) and finds the annotations I need (when found, I load the class and register it in my Class Registry). About 150 lines of code.
Получение всех Java классов из ClassPath | Alno’s blog: C++, Java и Rails
Jul 31, 2008 @ 12:23:07
Получение всех Java классов из ClassPath at Помощь в программировании
Sep 05, 2008 @ 16:38:06
Sep 29, 2008 @ 02:42:13
If your way is base on physical location to look for .class file it will not work in weblogic.
Oct 30, 2008 @ 22:32:59
Hey I gave this a try with a project I am working on and it works really well. I did run into a problem where one of the jars in the classpath had a bad CRC so an IOException kept the annotation scanner from continuing. I had to work around it by filtering out the bad jar from the list of classpath urls provided to the scanner. It would be nice to add in some tolerance to these types of problems. I would imagine this would only happen with jars you aren’t interested in and if it was a problem the developer should be able to correct the IOException. I could also see a need for an include package names. I would be interesting in making those changes if you are willing.
Thanks,
Jas
Dec 01, 2008 @ 07:43:40
Hey Bill,
nice work, really nice!
I’ve got one restriction when running on a windows machine: the file path mustn’t be longer than 248 characters, otherwise the java.io.File can’t handle the file. Within FileIterator the dir.listFiles() works fine, but then if i ask one File .exists() or .canRead() I always get ‘false’. Thus when simply using one file handle without prechecking, I’m definitely running in an IOException and i have no chance of continuing the process just without that one specific file. This problem occurs for me when parsing the webapp-classes within WEB-INF/classes of our deployed app in jboss (this can be a really really long path).
Maybe a simple .canRead() within the create method of the FileIterator would be helpful. When you can’t read a file, why processing it? Alternatively you could simply call ‘return next()’ when the FileNotFoundException occurs within the FileIterator.next() instead of throwing a RuntimeException? Could be also a switch within the AnnotationDB: ‘skipBadFiles(boolean)’ ? Just some tolerance, as Jason suggested.
Best regards
Bastian
Dec 08, 2008 @ 17:48:33
Hello,
First of all, congratulations for the article and the framework. I’ve downloaded and tested it. It is working correctly.
I have just a doubt about it. I could get the annotated class name. How could I get the annotated methods’ names?
For example, Do I have a way to find method’s name for a given Method Annotation?
Thanks,
Marcio Lima
Jan 14, 2009 @ 22:55:11
Hello Bill,
Thanks a lot for this framework. I’ve used it with JBoss 4.2 and Glassfish v2 on Mac and Windows – it worked just perfect! But now i wanted to use it with JBoss 5 and i get an IOException (Unable to scan directory of protocol: vfszip). What do You recommend what i have to do to fix this?
Thanks,
Adrian
Mar 28, 2009 @ 15:47:20
Will it work in an OSGi container?
May 30, 2009 @ 00:29:00
EXACTLY what i needed dude! thanks a ton!
Regards
Vyas, Anirudh
Programming Thoughts @ Work
Sep 28, 2009 @ 02:25:37
I also tried the reflections project but it has a lot of problems that took more time to solve than I could spend. Yours worked first try.
Thank you!
Dec 14, 2009 @ 22:31:15
Bill, is anyone maintaining this project anymore? I’d like to see some of the patches in the bug tracker applied and released. If there isn’t anyone I would like to offer to help.
Thank you,
Jonathan
Feb 22, 2010 @ 16:31:08
Hey Bill,
thanks for the article.
You guys might also like to try my take on the classpath scanning issue. I recently released a small library focused on this issue on Sourceforge. Besides removing the up to now usually required dependency on Spring and the boiler plate code associated with it, it actually makes it fool-proof to scan for classes annotated with some annotation, implementing some interface or extending some superclass. All you have to do is write a simple query statement like
ClasspathScanner scanner = new ClasspathScanner();
Set classes = scanner.getClasses(new ClassQuery() {
protected void query() {
select().
from(“sample”).
returning(allAnnotatedWith(PreferencePage.class));
}
});
to retrieve all the classes annotated with the @PreferencePage annotation.
For more info on that and to give it a try, visit http://sourceforge.net/projects/extcos.
Thank you Bill and all those that went before me for the inspiration.
Matt
Dec 23, 2010 @ 13:40:03
Hi Bill, you mention that:
“The “java.class.path” system property, if set properly (FYI, maven doesn’t set it properly), can be a way to obtain a list of classpath/archives you can scan for annotated classes. ”
If I want to use scannotation inside a maven 2 plugin I am writing, how do I fix the problem of maven not setting java.class.path correctly? Should maven plugins invoke scannotation in a different way?
Sep 01, 2011 @ 06:47:40
OK, I give up. I tried the sample code you gave and it sort of works. It finds annotations in my class files, but the only annotations it finds is @net.jcip.annotations.ThreadSafe even though there are many other annotations. What’s up with that?
I tried looking at ASM, but the API is way too mind bending and it’s almost midnight for me |-)
Intégrer des annotations dans son projet « So@t blog
Mar 22, 2012 @ 18:57:59
How do annotation-configured frameworks (ie. Tomcat, Hibernate) scan all classes in the classpath for annotations? | Stackforum
Nov 10, 2013 @ 13:49:48
Aug 18, 2014 @ 01:22:13
I just released a fast and lightweight classpath scanner that reads the classfile binary header directly, for ultra-fast classpath scanning: https://github.com/lukehutch/fast-classpath-scanner
Dependency Injection With Annotations | Yearwood Answers
Dec 06, 2014 @ 23:58:20