实现自定义ClassLoader以扫描/ WEB-INF / classes目录

时间:2013-11-11 14:22:04

标签: java tomcat classloader

为了减少对外部库的依赖(主要是作为学习练习),我决定将ServletContextListener添加到我正在开发的教学webapp中。它将通过扫描“WEB-INF / classes”目录来构建一个具有特定注释的类名注册表(存储为字符串)。

作为其中的一部分,我编写了一个自定义的ClassLoader,我可以经常抛弃它,以防止在加载上下文时通过滥用我开始使用的WebappClassLoader来填充permgen。

不幸的是,当我尝试加载一个ServerEndpointConfigurators时,我遇到了一个讨厌的java.lang.NoClassDefFoundError: javax/websocket/server/ServerEndpointConfig$Configurator异常。

public class MetascanClassLoader extends ClassLoader
{
    private final String myBaseDir;

    public MetascanClassLoader( final String baseDir )
    {
        if( !baseDir.endsWith( File.separator ) )
        {
            myBaseDir = baseDir + File.separator;
        }
        else
        {
            myBaseDir = baseDir;
        }
    }

    @Override
    protected Class<?> loadClass( final String name, final boolean resolve )
    throws ClassNotFoundException
    {
        synchronized( getClassLoadingLock( name ) )
        {
            Class<?> clazz = findLoadedClass( name );
            if (clazz == null)
            {
                try
                {
                    final byte[] classBytes =
                        getClassBytesByName( name );
                    clazz = defineClass(
                        name, classBytes, 0, classBytes.length );
                }
                catch (final ClassNotFoundException e)
                {
                    if ( getParent() != null )
                    {
                        clazz = getParent().loadClass( name );
                    }
                    else
                    {
                        throw new ClassNotFoundException(
                            "Could not load class from MetascanClassloader's " +
                                "parent classloader",
                            e );
                    }
                }
            }
            if( resolve )
            {
                resolveClass( clazz );
            }
            return clazz;
        }
    }

    private byte[] getClassBytesByName( final String name )
    throws ClassNotFoundException
    {
        final String pathToClass =
            myBaseDir + name.replace(
                '.', File.separatorChar ) + ".class";
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try( final InputStream stream = new FileInputStream( pathToClass ) )
        {
            int b;
            while( ( b = stream.read() ) != -1 )
            {
               baos.write( b );
            }
        }
        catch( final FileNotFoundException e )
        {
            throw new ClassNotFoundException(
                "Could not load class in MetascanClassloader.", e );
        }
        catch( final IOException e )
        {
            throw new RuntimeException( e );
        }
        return baos.toByteArray();
    }
}

部分代码受默认ClassLoader实现的影响。

以下是我正在尝试做的一般流程:

  1. 获取我正在尝试加载的类名的锁定,以确保其他线程(如果我在将来的某个时候尝试在并行执行中命中此ClassLoader)不要尝试加载同一个类两次。
  2. 使用findLoadedclass();
  3. 检查我是否已加载此类
  4. 如果尚未加载,请尝试从我的“WEB-INF / classes”目录中提取该类。
  5. 如果失败,请委托父类加载器。
  6. 如果仍然失败,请炸掉(扔)。
  7. 我可以保证在扫描类文件后正确传递类名 - 如果我用Class.forName(className)替换MetascanClassLoader.load(className)调用,这非常有效,但如前所述,我不知道我想锤击permgen。

    在尝试加载包含对只能与Tomcat打包的类的引用的类时,它才会出现问题。它完全没有Java SE类的问题。

    如果你碰巧发现任何特别阴险/令人反感的事情,请告诉我,除了让它不起作用之外我还要做些什么。

    UPDATE :似乎使用超类的默认构造函数将父类加载器设置为系统类加载器,这将解释在Tomcat中找到的缺失类。

    我在构造函数的第一行添加了以下行:

    super( Thread.currentThread().getContextClassLoader() );
    

    不幸的是,我仍然遇到问题,因为我的ClassLoader返回的所有Class对象现在都是空的,除了类名之外没有任何信息。 (我使用Eclipse检查内部字段以发现它。)

    更多信息:通过对象检查,Java SE类仍然正确加载。当我检查生活在WEB-INF \ classes中的一个类时,这是我在调用loadClass()后在任何时候检查类对象时得到的行为(我隐藏了包名称以防止共享不必要的项目信息)。 Eclipse inspection

    我还尝试确保通过将loadClass(name,resolve)的硬编码解析为true来调用resolveClass(),但这没有区别。

    再次更新:感谢Holger在下面出色的“啪嗒啪嗒啪嗒啪嗒啪”的一刻,我觉得我可以推断出{{1中的私有变量的含义 - 我是非常愚蠢的正在返回的对象。

    我在我的ClassSystem.out.print()当中扔了几个ClassLoader s - 我第一次尝试没有同步时非常凌乱!)我在之前的那一行卡住了以下一行返回synchronized

    的陈述
    loadClass()

    这给了我期待的结果,打印出了以下内容:

    System.out.print( clazz.getName() + " annotations:" );
    for( final Annotation a : clazz.getAnnotations() )
    {
        System.out.print( " " + a.annotationType().getName() + ";" );
    }
    System.out.println();
    

    但是等一下 - 它有效吗?

    org.fun.MyClass annotations: org.fun.MyAnnotationOne; org.fun.MyAnnotationsTwo;
    

    结果:

    System.out.print( clazz.getName() + " annotations:" );
    for( final Annotation a : clazz.getAnnotations() )
    {
        System.out.print( " " + a.annotationType().getName() + ";" );
    }
    System.out.print( "HAS_ANNOTATION:" );
    if( clazz.getAnnotation( MyAnnotationOne.class ) != null )
    {
        System.out.print( "true" );
    }
    else
    {
        System.out.print( "false" );
    }
    System.out.println();
    

    糟糕!但后来它袭击了我。检查下面的答案。

1 个答案:

答案 0 :(得分:0)

我记得今天阅读有关java.lang.Class平等的一些有趣内容,当我回去试图调试这个东西时完全跳过了我的脑海。 Jon Skeet mentioned in the answer to another question

  

是的,该代码有效 - 如果这两个类已由同一个类加载器加载。如果你希望这两个类被视为相等,即使它们已被不同的类加载器加载,可能来自不同的位置,基于完全限定的名称,那么只需比较完全限定的名称。

     

请注意,您的代码只考虑完全匹配,但是 - 它不会提供(当然)instanceof在查看值是否指向作为给定类的实例的对象时执行的“赋值兼容性”类型。为此,您需要查看Class.isAssignableFrom。

由于java.lang.Class未覆盖java.lang.Object.equals(),并且每个Class对象都会保留对其ClassLoader的引用,即使两个Class具有相同的类,数据和名称,如果它们来自两个不同的ClassLoaders则不相等。那么这在哪里适用?

有问题的代码行是

if( clazz.getAnnotation( MyAnnotationOne.class ) != null )

如上所述,在问题的最后更新中调用clazz.getAnnotations()将列出一个带有完全限定类名的注释,该类名等于{{1}引用的类的完全限定类名。上面的if语句中的文字。但是,我猜测MyAnnotationOne.class使用一些内部clazz.getAnnotation()调用来检查注释类是否相同。

equals()引用的类由自定义MyAnnotationOne.class的classLoader加载(在大多数情况下,它将是其父级)。但是,附加到ClassLoader的{​​{1}}类由自定义MyAnnotationOne本身加载。因此,clazz方法返回ClassLoader,我们返回equals()

非常感谢Holger将我推向了正确的方向并最终让我得到答案。