3

外部ライブラリへの依存を減らすために (そして主に学習課題として)、開発中の教育用 Web アプリケーションに ServletContextListener を追加することにしました。「WEB-INF/classes」ディレクトリをスキャンして、特定の注釈を持つクラス名 (文字列として格納) のレジストリを構築します。

この一環として、最初に使用した WebappClassLoader を悪用してコンテキストがロードされたときに permgen がいっぱいになるのを防ぐために、頻繁に破棄できるカスタム ClassLoader を作成しました。

残念ながら、java.lang.NoClassDefFoundError: javax/websocket/server/ServerEndpointConfig$ConfiguratorServerEndpointConfigurators の 1 つをロードしようとすると、厄介な例外が発生します。

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 回ロードしようとしないようにします。
  2. findLoadedclass(); を使用して、このクラスが既に読み込まれているかどうかを確認します。
  3. まだロードされていない場合は、「WEB-INF/classes」ディレクトリからクラスを取得してみてください。
  4. これが失敗した場合は、親クラスローダーに委譲します。
  5. それでもだめなら爆破(投げる)。

クラス ファイルをスキャンした後、クラス名を正しく渡していることを保証できます。これは、MetascanClassLoader.load( className ) 呼び出しを Class.forName( className ) に置き換えると完全に機能しますが、前述のように、そうしたくありません。パーマを叩きます。

Tomcat でのみパッケージ化されているクラスへの参照を含むクラスをロードしようとしたときにのみフォールオーバーするようです。Java SE クラスではまったく問題ありません。

機能しない原因以外に、私が行っている特に陰湿な/不快なことに気付いた場合はお知らせください.

UPDATE : スーパークラスのデフォルト コンストラクターを使用すると、親クラスローダーがシステム クラスローダーに設定されるようです。これにより、Tomcat で見つかった欠落クラスが説明されます。

コンストラクターの最初の行として次の行を追加しました。

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

残念ながら、ClassLoader によって返されるすべての Class オブジェクトが空になり、クラス名以外の情報がないため、まだ問題が発生しています。(これを発見するために、Eclipse を使用して内部フィールドを検査しました。)

詳細情報: Java SE クラスは、オブジェクト インスペクションにより、引き続き正しくロードされています。WEB-INF\classes にあるクラスの 1 つを調べると、loadClass() が呼び出された後の任意の時点でクラス オブジェクトを調べようとすると、このような動作になります (パッケージ名を非表示にして不要なプロジェクト情報を共有しないようにします)。 日食検査.

また、loadClass( name, resolve ) の解決を true にハードコーディングすることによって resolveClass() が呼び出されるようにしようとしましたが、これは違いはありません。

再度更新Class: 以下の Holger の優れた「マスを平手打ち」の瞬間のおかげで、返されるオブジェクトのプライベート変数の意味を推測できると考えるのは非常に愚かでした。

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 が別の質問への回答で言及しました:

はい、そのコードは有効です - 2 つのクラスが同じクラスローダーによってロードされた場合。完全修飾名に基づいて、異なるクラスローダによって (おそらく異なる場所から) ロードされた場合でも、2 つのクラスを同等に扱いたい場合は、代わりに完全修飾名を比較してください。

ただし、コードは完全一致のみを考慮することに注意してください。値が特定のクラスのインスタンスであるオブジェクトを参照しているかどうかを確認するときに instanceof が行うような「割り当ての互換性」は提供されません。そのためには、Class.isAssignableFrom を見たいと思うでしょう。

asjava.lang.Classは をオーバーライドせずjava.lang.Object.equals()、各Classオブジェクトはその への参照を保持しますClassLoader。たとえ 2 つのClasses が同じクラス、データ、および名前を持っていたとしても、それらが 2 つの異なる から来た場合は等しくなりませんClassLoaders。それで、それはここにどのように適用されますか?

問題のあるコード行は

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

上記の質問の最終更新で を呼び出すと、上記の if ステートメントでリテラルclazz.getAnnotations()によって参照されるクラスの完全修飾クラス名と等しい完全修飾クラス名を持つ注釈がリストされます。MyAnnotationOne.classただし、注釈クラスが同じかどうかを確認するためにclazz.getAnnotation()内部equals()呼び出しを使用していると思います。

によって参照されるクラスMyAnnotationOne.classは、 customClassLoaderの classLoader (ほとんどの場合、その親になります) によってロードされます。ただし、MyAnnotationOneアタッチされているクラスはclazz、カスタムClassLoader自体によって読み込まれます。その結果、equals()メソッドはfalseを返し、戻りますnull

これで私を正しい方向に押し戻し、最終的に答えにたどり着いてくれた Holger に感謝します。

于 2013-11-12T10:38:45.527 に答える