为了减少对外部库的依赖(并且主要作为学习练习),我决定将 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 实现的影响。
这是我正在尝试做的一般流程:
- 为我尝试加载的类名获取一个锁,以确保其他线程(如果我尝试在将来的某个时间点并行执行此 ClassLoader)不要尝试加载同一个类两次。
- 检查我是否已经使用 findLoadedclass(); 加载了这个类;
- 如果尚未加载,请尝试从我的“WEB-INF/classes”目录中拉入该类。
- 如果失败,委托给父类加载器。
- 如果这仍然失败,请炸毁(投掷)。
我可以保证在扫描类文件后我正确地传递了类名 - 如果我用 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 ClassLoader
(synchronized
当然 - 我第一次尝试没有同步非常混乱!)我在 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
哎呀!但后来它击中了我。在下面检查我的答案。