I've attached the updated ResolverUtil from the Stripes 1.5.x branch in
SVN. I also included an ActionBean that you can use after your app has
loaded to generate and save the cache file. You may have to tweak the
ActionBean a little because I just modified the one I use and didn't try
to compile it. The cache file needs to be saved in /WEB-INF/classes/ for
the ResolverUtil to find it.
Just don't forget that you have to manually remove the cache file before
Stripes will auto-discover any new classes.
Let me know if it works for you!
Aaron
nclemeur wrote:
Aaron Porter-3 wrote:
I made some changes to the ResolverUtil so that it could create a cache
file and I set it up to read from the cache file if it exists instead of
scanning packages. That cut load time in about half for me but it still
wasn't enough. I'm considering committing the changes to Stripes but it
worries me that it could become a problem if anyone forgets to replace
the cache file after adding new classes that would normally be
auto-discovered.
That sounds very interesting! Would you mind sharing your code? Maybe it
could be part of Stripes but disabled by default? Anyway, even if it is not
part of Stripes I would be interested by your code so that I can use it...
Did you simply seriliazed the "matches" field in the ResolverUtil class?
Thanks a lot
Nicolas
/* Copyright 2005-2006 Tim Fennell
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sourceforge.stripes.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
/**
* <p>
* ResolverUtil is used to locate classes that are available in the/a class path and meet arbitrary
* conditions. The two most common conditions are that a class implements/extends another class, or
* that is it annotated with a specific annotation. However, through the use of the {...@link Test}
* class it is possible to search using arbitrary conditions.
* </p>
*
* <p>
* A ClassLoader is used to locate all locations (directories and jar files) in the class path that
* contain classes within certain packages, and then to load those classes and check them. By
* default the ClassLoader returned by {...@code Thread.currentThread().getContextClassLoader()} is
* used, but this can be overridden by calling {...@link #setClassLoader(ClassLoader)} prior to
* invoking any of the {...@code find()} methods.
* </p>
*
* <p>
* General searches are initiated by calling the
* {...@link #find(net.sourceforge.stripes.util.ResolverUtil.Test, String)} ()} method and supplying a
* package name and a Test instance. This will cause the named package <b>and all sub-packages</b>
* to be scanned for classes that meet the test. There are also utility methods for the common use
* cases of scanning multiple packages for extensions of particular classes, or classes annotated
* with a specific annotation.
* </p>
*
* <p>
* The standard usage pattern for the ResolverUtil class is as follows:
* </p>
*
*<pre>
* esolverUtil<ActionBean> resolver = new ResolverUtil<ActionBean>();
* esolver.findImplementation(ActionBean.class, pkg1, pkg2);
* esolver.find(new CustomTest(), pkg1);
* esolver.find(new CustomTest(), pkg2);
* ollection<ActionBean> beans = resolver.getClasses();
*</pre>
*
* @author Tim Fennell
*/
public class ResolverUtil<T> {
/** An instance of Log to use for logging in this class. */
private static final Log log = Log.getInstance(ResolverUtil.class);
/**
* A simple interface that specifies how to test classes to determine if they are to be included
* in the results produced by the ResolverUtil.
*/
public static interface Test {
/**
* Will be called repeatedly with candidate classes. Must return True if a class is to be
* included in the results, false otherwise.
*/
boolean matches(Class<?> type);
}
/**
* A Test that checks to see if each class is assignable to the provided class. Note that this
* test will match the parent type itself if it is presented for matching.
*/
public static class IsA implements Test {
private Class<?> parent;
/** Constructs an IsA test using the supplied Class as the parent class/interface. */
public IsA(Class<?> parentType) {
this.parent = parentType;
}
/** Returns true if type is assignable to the parent type supplied in the constructor. */
@SuppressWarnings("unchecked")
public boolean matches(Class type) {
return type != null && parent.isAssignableFrom(type);
}
@Override
public String toString() {
return "is assignable to " + parent.getSimpleName();
}
}
/**
* A Test that checks to see if each class is annotated with a specific annotation. If it is,
* then the test returns true, otherwise false.
*/
public static class AnnotatedWith implements Test {
private Class<? extends Annotation> annotation;
/** Constructs an AnnotatedWith test for the specified annotation type. */
public AnnotatedWith(Class<? extends Annotation> annotation) {
this.annotation = annotation;
}
/** Returns true if the type is annotated with the class provided to the constructor. */
@SuppressWarnings("unchecked")
public boolean matches(Class type) {
return type != null && type.isAnnotationPresent(annotation);
}
@Override
public String toString() {
return "annotated with @" + annotation.getSimpleName();
}
}
/** The set of matches being accumulated. */
private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();
// Have to do it this way because we don't know T for static
@SuppressWarnings("unchecked")
private static Map<String, Set> cache;
/**
* The ClassLoader to use when looking for classes. If null then the ClassLoader returned by
* Thread.currentThread().getContextClassLoader() will be used.
*/
private ClassLoader classloader;
/**
* Provides access to the classes discovered so far. If no calls have been made to any of the
* {...@code find()} methods, this set will be empty.
*
* @return the set of classes that have been discovered.
*/
public Set<Class<? extends T>> getClasses() {
return matches;
}
@SuppressWarnings("unchecked")
private Map<String, Set> getCache() {
if (cache == null)
cache = importCache();
return cache;
}
@SuppressWarnings("unchecked")
private Map<String, Set> importCache() {
Map<String, Set> cache = new HashMap<String, Set>();
InputStream file = Thread.currentThread().getContextClassLoader().getResourceAsStream(
"StripesResolverCache.conf");
if (file != null) {
log.info("Loading cache...");
BufferedReader in = new BufferedReader(new InputStreamReader(file));
String line;
String key = null;
Set set = null;
try {
while ((line = in.readLine()) != null) {
if (line.matches("^[A-Z][|].*")) {
cache.put(key = line, set = new HashSet());
log.info("Cache key: ", key);
}
else if (set != null && line.matches("^\\s.*")) {
String className = line.trim();
try {
log.info(" Cache class: ", className);
getClass().getClassLoader().loadClass(className);
set.add(Class.forName(className));
}
catch (ClassNotFoundException e) {
log.error("Couldn't find class previously cached: ", className,
". Clearing cache for ", key);
cache.remove(key);
break;
}
catch (Throwable t) {
log.error("Caught exception while trying to load class ", className,
". Clearing cache for ", key);
cache.remove(key);
break;
}
}
}
}
catch (IOException e) {
log.error(e);
cache.clear();
}
}
return cache;
}
@SuppressWarnings("unchecked")
public static String exportCache() {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Set> entry : cache.entrySet()) {
sb.append(entry.getKey()).append('\n');
for (Class clazz : (Set<Class>) entry.getValue()) {
sb.append('\t').append(clazz.getName()).append('\n');
}
}
return sb.toString();
}
/**
* Returns the classloader that will be used for scanning for classes. If no explicit
* ClassLoader has been set by the calling, the context class loader will be used.
*
* @return the ClassLoader that will be used to scan for classes
*/
public ClassLoader getClassLoader() {
return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
}
/**
* Sets an explicit ClassLoader that should be used when scanning for classes. If none is set
* then the context classloader will be used.
*
* @param classloader a ClassLoader to use when scanning for classes
*/
public void setClassLoader(ClassLoader classloader) {
this.classloader = classloader;
}
private String generateCacheKey(String type, Class<?> clazz, String... packageNames) {
return StringUtil.combineParts(type, '|', clazz.getName(), '|', packageNames);
}
/**
* Attempts to discover classes that are assignable to the type provided. In the case that an
* interface is provided this method will collect implementations. In the case of a
* non-interface class, subclasses will be collected. Accumulated classes can be accessed by
* calling {...@link #getClasses()}.
*
* @param parent the class of interface to find subclasses or implementations of
* @param packageNames one or more package names to scan (including subpackages) for classes
*/
@SuppressWarnings("unchecked")
public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) {
String cacheKey = generateCacheKey("I", parent, packageNames);
Set<Class<? extends T>> cacheHits = (Set<Class<? extends T>>) getCache().get(cacheKey);
if (cacheHits != null) {
matches.addAll(cacheHits);
}
else if (packageNames != null) {
Test test = new IsA(parent);
for (String pkg : packageNames) {
find(test, pkg);
}
cache.put(cacheKey, matches);
}
return this;
}
/**
* Attempts to discover classes that are annotated with the annotation. Accumulated classes can
* be accessed by calling {...@link #getClasses()}.
*
* @param annotation the annotation that should be present on matching classes
* @param packageNames one or more package names to scan (including subpackages) for classes
*/
@SuppressWarnings("unchecked")
public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation,
String... packageNames) {
String cacheKey = generateCacheKey("A", annotation, packageNames);
Set<Class<? extends T>> cacheHits = (Set<Class<? extends T>>) getCache().get(cacheKey);
if (cacheHits != null) {
matches.addAll(cacheHits);
}
else if (packageNames != null) {
Test test = new AnnotatedWith(annotation);
for (String pkg : packageNames) {
find(test, pkg);
}
cache.put(cacheKey, matches);
}
return this;
}
/**
* Scans for classes starting at the package provided and descending into subpackages. Each
* class is offered up to the Test as it is discovered, and if the Test returns true the class
* is retained. Accumulated classes can be fetched by calling {...@link #getClasses()}.
*
* @param test an instance of {...@link Test} that will be used to filter classes
* @param packageName the name of the package from which to start scanning for classes, e.g.
* {...@code net.sourceforge.stripes}
*/
public ResolverUtil<T> find(Test test, String packageName) {
packageName = packageName.replace('.', '/');
ClassLoader loader = getClassLoader();
Enumeration<URL> urls;
try {
urls = loader.getResources(packageName);
}
catch (IOException ioe) {
log.warn("Could not read package: " + packageName, ioe);
return this;
}
while (urls.hasMoreElements()) {
String urlPath = urls.nextElement().getFile();
urlPath = StringUtil.urlDecode(urlPath);
// If it's a file in a directory, trim the stupid file: spec
if (urlPath.startsWith("file:")) {
urlPath = urlPath.substring(5);
}
// Else it's in a JAR, grab the path to the jar
if (urlPath.indexOf('!') > 0) {
urlPath = urlPath.substring(0, urlPath.indexOf('!'));
}
log.info("Scanning for classes in [", urlPath, "] matching criteria: ", test);
File file = new File(urlPath);
if (file.isDirectory()) {
loadImplementationsInDirectory(test, packageName, file);
}
else {
loadImplementationsInJar(test, packageName, file);
}
}
return this;
}
/**
* Finds matches in a physical directory on a filesystem. Examines all files within a directory
* - if the File object is not a directory, and ends with <i>.class</i> the file is loaded and
* tested to see if it is acceptable according to the Test. Operates recursively to find classes
* within a folder structure matching the package structure.
*
* @param test a Test used to filter the classes that are discovered
* @param parent the package name up to this directory in the package hierarchy. E.g. if
* /classes is in the classpath and we wish to examine files in /classes/org/apache
* then the values of <i>parent</i> would be <i>org/apache</i>
* @param location a File object representing a directory
*/
private void loadImplementationsInDirectory(Test test, String parent, File location) {
File[] files = location.listFiles();
StringBuilder builder = null;
// File.listFiles() can return null when an IO error occurs!
if (files == null) {
log.warn("Could not list directory " + location.getAbsolutePath()
+ " when looking for classes matching: " + test);
return;
}
for (File file : files) {
builder = new StringBuilder(100);
builder.append(parent).append("/").append(file.getName());
String packageOrClass = (parent == null ? file.getName() : builder.toString());
if (file.isDirectory()) {
loadImplementationsInDirectory(test, packageOrClass, file);
}
else if (file.getName().endsWith(".class")) {
addIfMatching(test, packageOrClass);
}
}
}
/**
* Finds matching classes within a jar files that contains a folder structure matching the
* package structure. If the File is not a JarFile or does not exist a warning will be logged,
* but no error will be raised.
*
* @param test a Test used to filter the classes that are discovered
* @param parent the parent package under which classes must be in order to be considered
* @param jarfile the jar file to be examined for classes
*/
private void loadImplementationsInJar(Test test, String parent, File jarfile) {
try {
JarEntry entry;
JarInputStream jarStream = new JarInputStream(new FileInputStream(jarfile));
while ((entry = jarStream.getNextJarEntry()) != null) {
String name = entry.getName();
if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
addIfMatching(test, name);
}
}
}
catch (IOException ioe) {
log.error("Could not search jar file '", jarfile, "' for classes matching criteria: ",
test, "due to an IOException: ", ioe.getMessage());
}
}
/**
* Add the class designated by the fully qualified class name provided to the set of resolved
* classes if and only if it is approved by the Test supplied.
*
* @param test the test used to determine if the class matches
* @param fqn the fully qualified name of a class
*/
@SuppressWarnings("unchecked")
protected void addIfMatching(Test test, String fqn) {
try {
String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
ClassLoader loader = getClassLoader();
log.trace("Checking to see if class ", externalName, " matches criteria [", test, "]");
Class type = loader.loadClass(externalName);
if (test.matches(type)) {
matches.add((Class<T>) type);
}
}
catch (Throwable t) {
log.warn("Could not examine class '", fqn, "'", " due to a ", t.getClass().getName(),
" with message: ", t.getMessage());
}
}
}import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.ActionBeanContext;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.action.StreamingResolution;
import net.sourceforge.stripes.action.UrlBinding;
import net.sourceforge.stripes.util.ResolverUtil;
@UrlBinding("/admin/resolvercache")
public class GenerateResolverCache implements ActionBean {
private ActionBeanContext context;
@Override
public ActionBeanContext getContext() {
return context;
}
@Override
public void setContext(ActionBeanContext context) {
this.context = context;
}
public Resolution export() {
getContext().getResponse().addHeader("Content-Disposition", "attachment;filename=\"StripesResolverCache.conf\"");
return new StreamingResolution("text/plain", ResolverUtil.exportCache());
}
}
------------------------------------------------------------------------------
Come build with us! The BlackBerry(R) Developer Conference in SF, CA
is the only developer event you need to attend this year. Jumpstart your
developing skills, take BlackBerry mobile applications to market and stay
ahead of the curve. Join us from November 9 - 12, 2009. Register now!
http://p.sf.net/sfu/devconference
_______________________________________________
Stripes-users mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/stripes-users