3

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

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

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

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返回对象中私有变量的含义。

我在我的里面扔了几个System.out.print()s ClassLoadersynchronized当然 - 我第一次尝试没有同步非常混乱!)我在 return 语句之前卡住了以下行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

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

4

1 回答 1

0

我记得今天读到一些有趣的关于java.lang.Class平等的东西,当我回去尝试调试这个东西时,我完全忘记了。Jon Skeet 在回答另一个问题时提到

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

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

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

有问题的代码行是

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

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

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

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

于 2013-11-12T10:38:45.527 回答