Javaソースとバイトコードの操作


Java仮想マシンプラットフォームコードのコンパイル、プロセスの実行:
1、ユーザー作成、または実行時動的コンパイル=>Javaソースコード
2、javac、またはツールを使用して動的に作成 => Javaバイトコード
3、Javaバイトコードは、仮想マシンによって実行される前に、コードの内容を変更してプログラムの動作を変更する.
一、Javaバイトコードフォーマット
ほとんどのJavaソースコードはコンパイル後に生成され、classファイルに保存されます.(もちろんバイトコードは、ネットワークを介してリモートサーバからダウンロードしたり、実行時に動的に生成したりすることもできます)
バイトコードは、正確には単一のJavaクラスまたは言い訳定義を含むバイトストリームであり、通常byte[]で示される.
バイトコードの一般的なフォーマット:
1、すべてのクラスと言い訳をフルネームで表し、「.」の代わりに「/」を使う.java.lang.String => java/lang/String
2、基本タイプは1文字で表します.B(byte)、C(char)、D(double)、F(float)、I(int)、J(long)、S(short)、Z(boolean)
3、配列タイプ、要素タイプの前に[]を付け、[]の個数は次元を表す.[[D:doubleタイプの2次元配列.
4、方法.形式は「(パラメータタイプ)戻り値タイプ」(戻り値void、Vで表す)である.int calculate(String str)=>(Ljava/lang/String)I.
注意:バイトコードは連続したバイトストリームで、各部分の意味が異なり、解析時には、異なるコンテンツのバイトデータ間の境界(データには一定長と不定長の区別がある)を識別する必要があります.具体的な詳細は、周志明の「Java仮想マシンを深く理解する」を参照してください.
二、動的コンパイルJavaソース
一般的なソースコードは、プログラマが事前に書き、コンパイラを介してバイトコードに変換して実行します.
ソースコードを動的にコンパイルすると、コンパイルと実行の2つのプロセスを同じにすることができ、実行時に完了し、プログラムに実行時にその動作を動的に修正する能力を増加させることができます.(バイトコードを修正するよりも、ソースコードを修正するほうが理解しやすい)
ソースコードを動的にコンパイルするシーンは一般的に簡単です.
Javaソースファイルの場合、実行時にAPIを使用してバイトコードをコンパイルし、クラスローダを介して仮想マシンにロードし、Java反射API呼び出しを使用します.
ソースコードは、ディスク、またはリモート・サーバから使用できます.
1、javacを使用するのは、最も直接的で最も簡単です(多くの場合、コマンドラインとして使用されますが、Java標準ライブラリが提供する最下位のオペレーティングシステム上のプロセスを作成する能力によって、実行時に動的に呼び出すことができます)
public class JavaCompiler{
   public void compile(Path src,Pathoutput)throws CompileException{
      ProcessBuilder pb = new ProcessBuilder("javac.exe", src.toString(), "-d", output.toString());
      try{
         pb.start();
      }catch(IOException e){
         throw new CompileException(e);
      }
   }
}
注:javacを使用するには、入力と出力がファイルのみであることが不足しています.また、外部プロセス方式を使用する性能も低いです.
JAvacツールは、プログラミングインタフェースプログラムの直接呼び出しも提供し、外部プロセスに比べて移植性とパフォーマンスが向上します.
public void compile(Path src, Path output) throws CompileException{
   String[] args = new String[] (src.toString(), "-d", output.toString());
   try{
      PrintWriter out = new PrintWriter(Paths.get("output.txt".toFile());
      com.sun.tools.javac.Main.compile(args, out);
   }catch(FileNotFoundException e){
      throw new CompileException(e);
   }
}

注意:com.sun.tools.*は、Oraclesのプライベート・インプリメンテーションであり、将来的には変更が発生し、メンテナンス上の問題が発生する可能性があります.
2、JavaコンパイラAPI
JavaSE 6からJavaコンパイラ関連APIがJSR 199として規範化され,コンパイラAPIを用いてコンパイラプロセスをより細かく制御できるようになった.
public class JavaCompilerAPICompiler{
   public void compile(Path src, Path outpu) throws IOException{
      JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//    Java           。
	//JavaFileManager          Java             
      try(StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null)){
	 //FileObject    API                ,        ,             ,               
	 //JavaFileObject   FileObject
         Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(src.toFile());
         Iterable<String> options = Arrays.asList("-d", output.toString());
         CompilationTask task = compiler.getTask(null , fileManager, null, options, null, compilationUnits);
         boolean result = task.call();
      }
   }
}

注意:コンパイラAPIはjavacツールに比べて、Javaソースコードの存在がファイル形式に限定されないことが重要です.
JavaFileObjectインタフェースの実装オブジェクトは、ソースコードのソースとして使用できます.JavaFileObjectインタフェースを実装する場合、既存のjavax.tools.SimpleJavaFileObjectクラスを継承するのが良い方法です.
例:文字列からJavaFileObjectインタフェースの実装オブジェクトを作成します.
public class StringSourceJavaFileObject extends SimpleJavaFileObject{
   private String content;
   public StringSourceJavaFileObject(String name, String content){
      super(URI.create("String:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);
      this.content = content;
   }
   public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
      return content;
   }
}
例:カッコ付き四則演算式の値を計算するJavaプログラムを作成する
(1、Stringを解析するのに苦労する;2、スクリプト言語でAPIをサポートする;3、JavaコンパイラAPIを使用して、式をJavaメソッドの内容とする)
public class Calculator extends ClassLoader{
   public double calculate(String expr) throws Exception{
      String className = "CalculatorMain";
      String methodName = "calculate";
      String source = "public class " + className + " {public static double " + methodName + "(){ return " + expr + ";}}";
      JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
      StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
      JavaFileObject sourceObject = new StringSourceJavaFileObject(className, source);
      Iterable<? extends JavaFileObject> fileObjects = Array.asList(sourceObject);
      Path output = File.createTempDirectory("calculator");
      Iterable<String> options = Arrays.asList("-d", output.toString());
      CompilationTask task = compiler.getTask(null, fileManager, null, options, null, fileObjects);
      boolean result = task.call();
      if(result){
         byte[]  classData = Files.readAllBytes(Paths.get(output.toString(), className + ".class"));
         Class<?> clazz = defineClass(className, classData, 0, classData.length);
         Method method = clazz.getMethod(methodName);
         Object value = method.invoke(null);
         return (Double) value;
      }else{
         throw new Exception("        ")
      }
   } 
}
動的に生成されたJavaソースコードの内容例:
public class CalculatorMain{
   public static double calculate(){
      return (3+2) * 5;
   }
}

注意:JavaコンパイルAPIはソースコードをコンパイルするために規範的なAPIを提供していますが、能力は限られており、より強力なのはEclipse JDTコンパイラ(Java development tools)で、インタフェースがより豊富になります.
三、バイトコードの強化
場合によっては、関連するソースコードが得られず、バイナリのバイトコードのみが得られる場合があります.バイトコードを処理するには、バイトコード強化技術が必要です.
軽量級のASMやAspectJなどが選べます.
四、注釈
注記(annotation)はJ 2 SE 5.0が導入したJavaに対する重要な修正の一つである.
1、注釈宣言に使用すること「@interface」
2、注記は汎用宣言を追加することはできません.また、extendsを使用して他のインタフェースから継承することはできません.すべての注記はjava.lang.annotation.Annotationから暗黙的に継承されます.
3、メタ注釈(java.lang.annotaion.Target)注釈が使える範囲を宣言し、
TYPE(タイプ宣言),ANNOTATION_TYPE(注釈タイプ宣言),PACKAGE(パッケージ宣言),CONTRUCTOR(構築方法),FIELD(ドメイン),METHOD(メソッド),PARAMETER(メソッドパラメータ),LOCAL_VARIABLE(メソッド中のローカル変数).
4、メタ注釈(java.lang.annotaion.Rentention)は、注釈の保存ポリシーを表し、デフォルトはCLASSである.
SOURCE(ソースファイルのみ存在)、CLASS(バイトコード中)、RUNTIME(バイトコード中、実行時に使用可能、APIを反射して注釈関連情報を取得可能)
注意:ローカル変数の注釈は、いずれの場合もバイトコードに保持されません.
5、メタ注釈(java.lang.annotation.Inherited)は、注釈タイプに対応する注釈宣言が継承可能であることを示す.
Java標準ライブラリには、いくつかの一般的な注釈があります.
a、java.lang.Override
b、java.lang.Deprecated
c、java.lang.SuppressWarnings
6、注釈タイプの作成
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Author{ 
   String name();
   String email();
   boolean enableEmailNotfication() default true;
}
//              ,  value       ,        。
@Target(ElementType.TYPE)
public @interface Employee{
   enum EMPLOYEE_TYPE {REGULAR, CONTRACT};
   EMPLOYEE_TYPE value();
}
@Target(ElementTYPE.TYPE)
public @interface WithArrayValue{
   String name();
   Class<?> [] appliedTo();
}

7、注釈タイプの使用
@Author(name="Alex", email = "[email protected]")
public class MyClass{
}
@Employee(EMPLOYEE_TYPE.REGULAR)
public class AnotherClass{
}
@WithArrayValue(name="Test",appliedTo = {String.class, Integer.class})
public class ArrayClass{
}
8、処理注記
処理には、コンパイル時、または実行時の2つの方法があります.
J 2 SE 5.0で使用されているaptツールとOracleプライベートMirror APIツールは、JavaSe 7で廃棄として宣言されています.
J 2 SE 6.0はPluggable Annotaion Processing APIを導入し,注釈処理機構を標準化した.
a、注釈を分析する.例えば、1つのクラスがどれだけ注釈をロードしたかを統計する.(コンパイル時)
b、注釈に基づいて、ソースコードの作成と修正(実行時)
c、反射処理を用いた注釈(実行時)の典型的なシーン:注釈、反射APIと動的エージェントを結合する.注釈は実行時の挙動を設定し、反射APIは注釈を解析し、動的に応用を担当する具体的な挙動をもたらす.
例:権限管理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Role{
   String[] value();
}
public interface EmployeeInfoManager{
   @Role("manager")
   public void updateSalary();
}
public class EmployeeInfoManagerFactory{
   private static class AccessInvocationHandler<T> implements InvocationHandler{
      private final T targetObject;
      public AccessInvocationHandler(T targetObject){
      this.targetObject = targetObject;
   }
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
      Role annotation = method.getAnnotation(Role.class);
      if(annotation != null){
         String[] roles = annotation.value();
         String currentRole = AccessManager.getCurrentUserRole();
         if(!Arrays.asList(roles).contains(currentRole)){
            throw new RuntimeException("          。")
         }
      }
      return method.invoke(targetObject, args);
   }

   public static EmployeeInfoManager getManager(){
      EmployeeInfoManager instance = new DefaultEmployeeInfoManager();
      return (EmployeeInfoManager)Proxy.newProxyInstance(instance.getClass().getClassLoader(), new Class<?>{EmployeeInfoManager.class},new AccessInvocationHandler<EmployeeInfoManager>(instance));
   }
}