- PR -

Jar ファイルからのクラス読み込み処理

投稿者投稿内容
ヒラ
常連さん
会議室デビュー日: 2006/11/30
投稿数: 20
投稿日時: 2006-12-10 10:55
こんにちは。
jarファイルの中のクラスファイルを読んでくる処理のところで、クラスがあるにも関わらずClassNotFoundの例外が出てしまいます。
どうやら、classLoaderのところで、うまく読み込めていないようなのですが、どうすればいいのかわかりません。
ちなみに、下のようなエラーが出ます。
Exception in thread "main" java.lang.ClassNotFoundException: A
at asm.Adapt.loadClass(Adapt.java:52)
at java.lang.ClassLoader.loadClass(Unknown Source)
at asm.Adapt.main(Adapt.java:139)
Caused by: java.io.IOException: Class not found
at org.objectweb.asm.ClassReader.readClass(Unknown Source)
at org.objectweb.asm.ClassReader.<init>(Unknown Source)
at asm.Adapt.loadClass(Adapt.java:46)
... 2 more

ソースコードは以下の通りです。ご教授よろしくお願いします。

public class Adapt extends ClassLoader {
private static final class TestClassLoader extends ClassLoader{
private final String className;
private final ClassLoader cl;

public TestClassLoader(ClassLoader cl, String className){
super();
this.cl = cl;
this.className = className;
}

public Class loadClass(String name) throws ClassNotFoundException
{
if (className.equals(name)) {
try{
byte[] bytecode = transformClass(className);
// returns the adapted class
return super.defineClass(className, bytecode, 0, bytecode.length);
}catch(IOException ex){
throw new ClassNotFoundException("Load error: " + ex.toString(), ex);
}
}
return cl.loadClass(name);
}

private byte[] transformClass(String className) throws IOException{
// gets an input stream to read the bytecode of the class
String resource = className.replace('.', '/') + ".class";
InputStream is = getResourceAsStream(resource);
byte[] b;

ClassWriter cw = new ClassWriter(true, true);
NotifierClassVisitor ncv = new NotifierClassVisitor(cw);
ClassReader cr = new ClassReader(is);
cr.accept(ncv, false);
return cw.toByteArray();

// stores the adapted class on disk
try {
FileOutputStream fos = new FileOutputStream(resource + ".adapted");
fos.write(b);
fos.close();
} catch (Exception e) {
}

return b;
}
}

public static void main(final String args[]) throws Exception {
JarFile jarfile = new JarFile(args[0]);

for(Enumeration en = jarfile.entries(); en.hasMoreElements();){
JarEntry entry = (JarEntry)en.nextElement();

if(entry.getName().endsWith(".class")){
String className = entry.getName().replaceAll("/", "\\\\.").replaceAll("\\\\.class", "");

// loads the application class (in args[0]) with an Adapt class loader
ClassLoader loader = new TestClassLoader(className.getClass().getClassLoader(), className);
Class c = loader.loadClass(className);

// calls the 'main' static method of this class with the
// application arguments (in args[1] ... args[n]) as parameter
Method m = c.getMethod("main", new Class[] { String[].class });
String[] applicationArgs = new String[args.length - 1];
System.arraycopy(args, 1, applicationArgs, 0, applicationArgs.length);
m.invoke(null, new Object[] { applicationArgs });
}
}
}
}
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2006-12-10 12:01
突っ込みどころが色々あるのですが、

ClassLoader loader = new TestClassLoader(className.getClass().getClassLoader(), className);

システムクラスローダで読み込まれたクラスのクラスローダを取得するときは、nullが返るようになっているので、Stringクラスのクラスローダを取得しても、戻り値はnullになります。

InputStream is = getResourceAsStream(resource);
は、親クラスローダに委譲していますが、コンストラクタでsuper()ってやっているので、リソースの解決はできない思います。nullが戻ってきていませんか?

実行環境がないので、「おかしいのでは?」というレベルの話ですが、コードを見る限り、単にデバッグしてないだけに思えます。ステップ実行くらいは最低限やってみましょう。

あと、この手のややこしいコードを貼り付けるときはBBコードを使いましょう。読むのが大変です。
あしゅ
ぬし
会議室デビュー日: 2005/08/05
投稿数: 613
投稿日時: 2006-12-10 12:15
引用:

ヒラさんの書き込み (2006-12-10 10:55) より:
private byte[] transformClass(String className) throws IOException{
// gets an input stream to read the bytecode of the class
String resource = className.replace('.', '/') + ".class";
InputStream is = getResourceAsStream(resource);
byte[] b;

ClassWriter cw = new ClassWriter(true, true);
NotifierClassVisitor ncv = new NotifierClassVisitor(cw);
ClassReader cr = new ClassReader(is);
cr.accept(ncv, false);
return cw.toByteArray();

// stores the adapted class on disk
try {
FileOutputStream fos = new FileOutputStream(resource + ".adapted");
fos.write(b);
fos.close();
} catch (Exception e) {
}

return b;
}



ClassLoaderを直接拡張しているので、親クラスローダのクラスパスにjarが
入っていない限り、getResourceAsStream(resource)で取得するリソースも
自分で解決しないといけません。

他にも怪しいところはありますが。

#同じインデントレベルでreturn cw.toByteArray()した後も
#コードが続いてるのはなぜでしょう?コンパイルエラーでは?

引用:

public class Adapt extends ClassLoader {
private static final class TestClassLoader extends ClassLoader{



なぜ二つのクラスがClassLoaderを拡張しているのか意味不明ですが、
java.net.URLClassLoaderを使えば自力でjarを扱わずに済むはずです。

ClassLoader#loadClass(String)しかオーバーライドせず、
実働メソッドのloadClass(String, boolean)やfindClass(String)を
オーバーライドしないのは非常に危なっかしい気がします。
ヒラ
常連さん
会議室デビュー日: 2006/11/30
投稿数: 20
投稿日時: 2006-12-11 02:07
>かつのりさん
InputStreamのところは、ご指摘どおり、nullが返ってきてしまいます。
それは、super()がいけないということですか?
リソースの解決にならないということですが、どうしたらいいのか、わかりません。

>あしゅさん
リソースを自分で解決するというのは、どういうことなのでしょうか。
申し訳ありませんが、もう少し具体的にお話いただけると幸いです。
return cw.toByteArray() のところは、正しくは、b = cw.toByteArray()でした。すみません。

引き続き、もう一度チェックしながら作り直していますが、やはりClassReaderの部分でエラーが出続けています。
それは、かつのりさんやあしゅさんのおっしゃったとおり、リソースの問題なのですが、どう変更すべきなのかがいまいちわかりません。
引き続きのご教授をどうかよろしくおねがいします。
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2006-12-11 10:26
java.lang.ClassLoaderを継承してsuper()とやると、
親クラスローダがシステムクラスローダになります。
システムクラスローダがリソースを取得する対象は、一般的に言われる実行時クラスパスです。

で、オーバーライドされていないgetResource系のメソッドは、親クラスローダに委譲します。
親クラスローダで解決出来ない場合に実行時クラスパスを参照して駄目ならnullを返します。

一般的にクラスローダを自作するときには、
1.リソースの取得系のメソッドは、リソースの取得方法を解決する
2.クラスの読み込み系のメソッドは、委譲方法をカスタムする
というように実装します。

ヒラさんが作ったクラスローダは、名前からバイト列を取得する方法を知らないので、
リソースの解決が出来ないというわけです。
ヒラ
常連さん
会議室デビュー日: 2006/11/30
投稿数: 20
投稿日時: 2006-12-12 05:03
下記のようにプログラムを大幅に変更してみました。
が、これだとエラーが出ます。
三行目でprivate final ByteArrayOutpuStream baos;
という風に渡してみたのですが、mainのClassRoader = new Adapt(baos, className);で引っかかってしまいます。
当たり前だというご指摘を頂きそうですが・・・javaを始めてからまだ日が浅いのでご勘弁を。
方向性的にはよくなっているでしょうか?

コード:
public class Adapt extends ClassLoader {
 private final String className;
 private final ByteArrayOutputStream baos;
	
 public Adapt(ByteArrayOutputStream baos, String className){
  this.baos = baos;
  this.className = className;
		
  System.out.println("this.baos: " + this.baos);
  System.out.println("this.className: " + this.className);
 }

 protected synchronized Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException
 {
  byte[] b = null;
  byte[] bytes = baos.toByteArray();
			
  try{
   ClassReader cr = new ClassReader(new ByteArrayInputStream(bytes));
  ClassWriter cw = new ClassWriter(true, true);
  NotifierClassVisitor ncv = new NotifierClassVisitor(cw);
  cr.accept(ncv, false);
  b = cw.toByteArray();
  }catch(Exception e){
  throw new ClassNotFoundException(name, e);
  }	
				
  // stores the adapted class on disk
  ...
	
  // returns the adapted class
  return defineClass(name, b, 0, b.length);
 }
	

  public static void main(final String args[]) throws Exception {
   File file = new File(args[0]);
   JarFile jarfile = new JarFile(file);
   JarInputStream jis = new JarInputStream(new BufferedInputStream(new FileInputStream(file)));
			
   JarEntry entry= null;
				
   while((entry = (JarEntry) jis.getNextEntry()) != null){ 
    String name = entry.getName();
    String className = name.replace("/", "\\.").replaceAll("\\.class", "");
				
    if(entry.getName().endsWith(".class")){
     int len;
     byte buffer[] = new byte[1024];
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
					
     for(;;){
      len = jis.read(buffer);
      if(len < 0){
       break;
      }
      baos.write(buffer, 0, len);
     }
					
     byte[] bytes = baos.toByteArray();

     System.out.println("loading " + name + " ... ");
     System.out.println("bytes = " + bytes);
    }
				 
    ClassLoader loader = new Adapt(baos, className);
    Class c = loader.loadClass(className);
   }
  }
 }

だっちょ
大ベテラン
会議室デビュー日: 2006/12/05
投稿数: 115
投稿日時: 2006-12-12 09:39
ちょっとコンパイルしてみましたが、
1) コンパイルエラーになりました。(loadClassのところはコメントで除いておいて)
main関数の局所変数baosのスコープが変で、
ByteArrayOutputStream baos = new ByteArrayOutputStream();

if(entry.getName().endsWith(".class")){
の前にしないとだめです。

2) Jarエントリはすべてクラスファイルだと思っているようですが、
 ディレクトリエントリをクラスのように扱おうとしてloadClassで例外になります。

while((entry = (JarEntry) jis.getNextEntry()) != null){
String name = entry.getName();
の後に
System.out.println("name=" + name);
をいれて動かして見れば、まずディレクトリ名がでてくることがわかると思います。
したがってクラスを処理するのなら、
if (!name.endsWith(".class")) continue;
を入れておけばよいと思います。

3) classNameが間違っています。
 これも
String className = name.replace("/", "\\.").replaceAll("\\.class", "");
 のあとに
System.out.println("className=" + className);
 をいれて動かしてみればすぐわかりますが、a/test.classがa\.testとなります。
String className = name.replace("\\", ".").replaceAll("\\.class", "");
とするべきでしょう。


ヒラ
常連さん
会議室デビュー日: 2006/11/30
投稿数: 20
投稿日時: 2006-12-12 11:10
>だっちょさん
さっそくご指摘いただいた部分を直してみました。
ご指摘いただいて初めてはっと気づいて、恥ずかしく思います。
さて、その後loadClassの部分も含めコンパイルしてみたのですが、
今度は
return defineClass(name, b, 0, b.lengh);
の部分でエラーが出てしまいました。
これは、リターンしているものが違うのでしょうか?

スキルアップ/キャリアアップ(JOB@IT)