【问题标题】:Implementing custom ClassLoader to scan /WEB-INF/classes directory实现自定义 ClassLoader 以扫描 /WEB-INF/classes 目录
【发布时间】:2013-11-11 14:22:04
【问题描述】:

为了减少对外部库的依赖(主要是作为学习练习),我决定将 ServletContextListener 添加到我正在开发的教学网络应用程序中。它将通过扫描“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. 如果尚未加载,请尝试从我的“WEB-INF/classes”目录中拉入该类。
  4. 如果失败,委托给父类加载器。
  5. 如果仍然失败,请炸毁(投掷)。

我可以保证在扫描类文件后我正确地传递了类名 - 如果我用 Class.forName( className ) 替换 MetascanClassLoader.load( className ) 调用,这将非常有效,但如前所述,我没有不想敲打permgen。

似乎只有在尝试加载包含对只能与 Tomcat 打包的类的引用的类时才会崩溃。 Java SE 类完全没有问题。

如果您还碰巧注意到我正在做的任何特别阴险/令人反感的事情,除了导致它不起作用之外,请告诉我。

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

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

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

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

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

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

再次更新:感谢 Holger 在下面的精彩“slap-with-a-trout”时刻,我非常愚蠢地认为我可以推断出 Class 中私有变量的含义正在返回的对象。

我在ClassLoader 中扔了几个System.out.print()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();

结果:

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

哎呀!但后来它击中了我。请在下面查看我的答案。

【问题讨论】:

  • “(我使用 Eclipse 检查了内部字段以发现这一点。)” 这没有任何意义。在 OOP 中进行封装是有原因的。仅查看对象的私有字段并不能告诉您任何有关语义的信息。我敢打赌,如果你跟注,例如getDeclaredMethods(),在类实例上而不是用调试器窥视,事情看起来会完全不同。
  • 优秀的评论。偷看出现了,至于其他类,这些字段似乎已设置,我错误地认为这与问题有关。我会用我的新发现更新问题。
  • 据我所知,这些字段可能只是存储以前挖掘的值。

标签: java tomcat classloader


【解决方案1】:

我记得我今天读到了一些关于 java.lang.Class 平等的有趣内容,当我回去尝试调试这个东西时,我完全没有想到。 Jon Skeet mentioned in the answer to another question:

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

请注意,您的代码仅考虑完全匹配,但是 - 它不会提供(例如)instanceof 在查看值是否引用作为给定类的实例的对象时所做的那种“赋值兼容性” .为此,您需要查看 Class.isAssignableFrom。

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

有问题的代码行是

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

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

MyAnnotationOne.class 引用的类由自定义ClassLoader 的类加载器(在大多数情况下是它的父类)加载。但是,附加到clazzMyAnnotationOne 类由自定义ClassLoader 本身加载。结果,equals() 方法返回了false,我们又得到了null

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

【讨论】:

    猜你喜欢
    • 2011-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-26
    • 2013-04-23
    相关资源
    最近更新 更多