Javaagentによるバイトコードの変更によるaopの実現

12359 ワード

Javaagentによるバイトコードの変更によるaopの実現
起因:seata ATモードでグローバルトランザクションのコミット後にイベントをトリガーし、undo_を解析したいlogは、非同期書き込みesなどのいくつかの操作を実行します.seataはコードの切り込み点を提供していないため、プロジェクトにフレームコードと同じパッケージを構築するしかなく、フレームワークのクラスソースコードをコピーしてパッケージの下に置き、その中のソースコードを書き換え、classpath優先ロード原則は元のクラスを上書きする目的を達成する.このやり方では「優雅さ」が足りない.では、どのようにして無声でソースコードを強化することができますか?spring aopを思い浮かべますが、ソースコードはnewで出てきてspring工場の管理を受けていないので、この道は通じません.またClassLoaderを利用して、私たちが複写したコードをカスタマイズしてロードしたいと思っていますが、フレームワークは私たちがカスタマイズしたClassLoaderを使うことができません.この道も通じません.では、このclassをjvmにロードしてバイトコードに解析する過程で、こっそり元のバイトコードに置き換え、「盗天換日」計画が形成されるしかない.Java 5が提供するagentプロパティを使用して、この操作を完了します.この記事:Java 5プロパティInstrumentationの実践では、どのように使用するかを紹介しています.では始めましょう!
  • 準備作業、定義Adviceインタフェース、pom導入javassist依存
     public interface Advice {
     	void before(Object[] params);
     	void after(Object[] params);
     }
     
     // pom.xml     
     
         org.javassist
         javassist
         3.27.0-GA
     
    
  • アプリケーションにpremainメソッドを追加し、agentにエントリを提供します.クラス名は勝手で、mainメソッドに似ていて、どのクラスに置くかは重要ではありません.
     public class Premain {
         public static void premain(String agentArgs, Instrumentation inst) {
         	 //  Transformer        
             inst.addTransformer(new Transformer());
         }
     }
    
  • Transformerクラスを追加し、transformメソッド
     public class Transformer implements ClassFileTransformer {
     				
     	 //        
         private volatile Map map = null;
         
         /**
          *   :
          * loader -           ;        ,   null
          * className -                The Java Virtual Machine Specification         。  ,"java/util/List"。
          * classBeingRedefined -              ,           ;      ,   null
          * protectionDomain -              
          * classfileBuffer -              (    )
          *   :
          *              (     ),       ,    null。
          *   :
          * IllegalClassFormatException -                  
          */
         public byte[] transform(ClassLoader l, String className, Class> c, ProtectionDomain pd, byte[] b) {
             if (map == null) {
                 //     ,       
                 scanProps();
             }
             //          aop
             if (map.containsKey(className)) {
             	//         
                 Pack p = map.get(className);
                 //          ,    aop    
                 return TransformUtil.getNewByteCode(p.targetClassName, p.targetMethodName);
             }
             //   null          ,    
             return null;
         }
     
         /**
          *          advice.properties    ,         
          *     :
          * io.seata.rm.datasource.undo.AbstractUndoLogManager.batchDeleteUndoLog=com.a.b.c.AbstractUndoLogManagerAdvice
     	  * key      ,value    Advice     
          */
         private synchronized void scanProps() {
             if (map != null) {
             	// double check
                 return;
             }
             Map m = new HashMap<>();
             Enumeration urls = null;
             try {
             	 //          advice.properties     
                 urls = Thread.currentThread().getContextClassLoader().getResources("advice.properties");
             } catch (IOException e) {
                 e.printStackTrace();
             }
             if (urls != null) {
                 while (urls.hasMoreElements()) {
                     InputStream inStream = null;
                     Properties property = new Properties();
                     try {
                         inStream = urls.nextElement().openStream();
                         property.load(inStream);
                         property.stringPropertyNames().forEach(key -> {
                         	 //    key     
                             String targetClassName = key.substring(0, key.lastIndexOf("."));
                             //    key      
                             String targetMethodName = key.substring(key.lastIndexOf(".") + 1);
                             //    advice     
                             String adviceClassName = (String) property.get(key);
                             //    key  io/seata/rm.datasource/undo/AbstractUndoLogManager   
                             String k = targetClassName.replaceAll("\\.", "/");
                             Pack p = m.get(k);
                             //         
                             if (p == null) {
                                 p = new Pack(targetClassName, targetMethodName, adviceClassName);
                                 m.put(k, p);
                             } else {
                                 p.add(targetMethodName, adviceClassName);
                             }
                         });
                     } catch (Exception e) {
                         e.printStackTrace();
                     } finally {
                         if (inStream != null) {
                             try {
                                 inStream.close();
                             } catch (IOException e) {
                                 e.printStackTrace();
                             }
                         }
                     }
                 }
             }
             // map   volatile   ,      
             map = m;
        }
    
     	static class Pack {
         	String targetClassName;
         	//                 advice,     
         	Map targetMethodName = new HashMap<>();
    
            public Pack(String targetClassName, String targetMethodName, String adviceClassName) {
                 this.targetClassName = targetClassName;
                 this.targetMethodName.put(targetMethodName, adviceClassName);
            }
    
            public void add(String targetMethodName, String adviceClassName) {
                 this.targetMethodName.put(targetMethodName, adviceClassName);
            }
    	}
     }
    
  • を規定に従って書き換える.
  • 現在、どのクラス、どの方法、どのAdviceをエージェントとして使用する必要があるかがわかりました.バイトコードを変更してエージェントをTransformUtilでサポートする必要があります.javassistの使用方法は、このドキュメントを参照してください.http://www.javassist.org/tutorial/tutorial.html
     public class TransformUtil {
         /**
          *            
          *       、static  。
          *        、    、    
          *
          * @param targetClassName       Examples: java.lang.String
          * @param method_advice 
          * @return         
          */
         public static byte[] getNewByteCode(String targetClassName, Map method_advice) {
             try {
                 ClassPool pool = ClassPool.getDefault();
                 CtClass ctClass = pool.get(targetClassName);
     
                 //              
                 for (Map.Entry entry : method_advice.entrySet()) {
                     String targetMethodName = entry.getKey();
                     String adviceClassName = entry.getValue();
                     CtMethod originalMethod = ctClass.getDeclaredMethod(targetMethodName);
                     CtClass originalReturnType = originalMethod.getReturnType();
                     // advice name
                     String adviceFieldName = targetMethodName + "$advice";
                     //            
                     String newMethodName = "original$" + targetMethodName;
     
                     //   Advice       
                     String adviceField = "private" +
                     		//          static 
                             ((originalMethod.getMethodInfo().getAccessFlags() & AccessFlag.STATIC) == AccessFlag.STATIC ? " static " : " ") 
                             //     Advice       
                             + "final com.aop.javassist.Advice "
                             + adviceFieldName + " = new " + adviceClassName + "(); ";
                     ctClass.addField(CtField.make(adviceField, ctClass));
     
                     // ===================       ===================
                     //          
                     StringBuilder methodBody = new StringBuilder();
                     methodBody.append("{ ");
                     methodBody.append("Object[] params = $args; ");
                     //   before
                     methodBody.append("try { ");
                     methodBody.append(adviceFieldName)
                     		   .append(".before(params); ");
                     methodBody.append("} catch (Exception e) { ");
                     methodBody.append("e.printStackTrace(); ");
                     methodBody.append("} ");
                     //             ,      
                     if (!CtClass.voidType.equals(originalReturnType)) {
                         methodBody.append(originalReturnType.getName())
                         		   .append(" result = ")
                         		   .append(newMethodName)
                         		   .append("($$); ");
                     } else {
                         methodBody.append(newMethodName)
                         		   .append("($$); ");
                     }
                     //   after
                     methodBody.append("try { ");
                     methodBody.append(adviceFieldName)
                     				    .append(".after(params); ");
                     methodBody.append("} catch (Exception e) { ");
                     methodBody.append("e.printStackTrace(); ");
                     methodBody.append("} ");
                     //                   
                     if (!CtClass.voidType.equals(originalReturnType)) {
                         methodBody.append("return result; ");
                     }
                     methodBody.append("} ");
                     // ==================       =================
                     //        
                     originalMethod.setName(newMethodName);
                     //              ,                
                     CtMethod copyMethod = CtNewMethod.copy(originalMethod, targetMethodName, ctClass, null);
                     copyMethod.setBody(methodBody.toString());
                     //             
                     ctClass.addMethod(copyMethod);
                 }
                 byte[] b = ctClass.toBytecode();
                 //      CtClass  
                 ctClass.detach();
                 return b;
             } catch (Exception e) {
                 e.printStackTrace();
             }
             return null;
         }
     }
    
    ここで強化する構想は、元のclassメソッドを新しい名前に変更し、元のメソッド宣言と同じメソッドを追加し、新しいメソッドでAdviceのbefore、元のメソッド、Adviceのafterを順次呼び出し、元のメソッドに戻り値があればafterを呼び出した後、結果を返すことです.Stringクラスのsplitメソッドにaopを作成すると、Stringクラスはこのように変更されます:
     private final a.b.c.Advice split$advice = new a.b.c.AdviceImpl();
     
     public String[] split(String regex) {
     	Object[] params = new Object[]{regex}; 
     	try { 
     		split$advice.before(params);
     	} catch (Exception e) { 
     		e.printStackTrace(); 
     	} 
     	String[] result =  original$split(regex);
     	try { 
     		split$advice.after(params);
     	} catch (Exception e) { 
     		e.printStackTrace(); 
     	}
     	return result;
     }
     
     /**
     *     split   ,      
     */
     public String[] original$split(String regex) {
     	return split(regex, 0);
     }
    
  • String類のsplit法をテストしたことがないことを許してください.ここでは栗を1つ挙げるだけです.この変換器は現在,構造方法,リロード方法,抽象方法の変換をサポートしておらず,検証もしていない点も不足しているのではないでしょうか.
  • 以降、以上の「ワンタッチ操作」を有効にする工程にadviceを加える.propertiesプロファイルは、
            .        =AdviceImpl     
          Test    operation        :
     	a.b.c.d.Test.operation=d.c.b.a.AdviceImpl
    
    プロジェクトにAdvice実装クラス:
     public class AdviceImpl implements Advice {
         @Override
         public void before(Object[] params) {
             System.out.println("before");
         }
     
         @Override
         public void after(Object[] params) {
             System.out.println("after");
         }
     }
    
    プロジェクトのMANIFESTを組み込むように構成されている.MFは以下の2つを配置する必要がある:
     Manifest-Version: 1.0
     Premain-Class: com.aop.premain.Premain
    
    mvnでパッケージ化する場合、pomにプラグインを追加してパッケージ化後に自動的に上述の配置に加えることができる:
     	
     		maven-jar-plugin
     		3.0.2
     		
     			
     				
     					
     					com.aop.premain.Premain
     				
     			
     		
     	
    
    パッケージ化工事は工事の起動パラメータに追加する:
     -javaagent:jar  /test-aop.jar
    
  • このように,工事開始後に用いられるTestクラスの例は修正されている.
    ps:-javaagentが構成するjarパッケージは、premainメソッドを外部に含むjarである、MANIFESTを外部jarに組み込むことができる.MF構成.エンジニアリングが開始されると、このjarでpremainメソッドが検索され、後続の論理が実行され、得られたバイトコードが現在のjvmにロードされます.ここでは便宜上,本プロジェクト自体を外部のjar,自己agent自身とする.