精华内容
下载资源
问答
  • 下面这个谜题测试了你关于二进制兼容性(binary compatibility)的知识:当你改变了某个类所依赖的另外一个类时,第一个类的行为会发生什么改变呢?更特殊的是,假设你编译的是如下的2个类。第一个作为一个客户端,...

    下面这个谜题测试了你关于二进制兼容性(binary compatibility)的知识:当你改变了某个类所依赖的另外一个类时,第一个类的行为会发生什么改变呢?更特殊的是,假设你编译的是如下的2个类。第一个作为一个客户端,第二个作为一个库类,会怎么样呢:

    1. public class PrintWords {  
    2.     public static void main(String[] args) {  
    3.         System.out.println(Words.FIRST  + " " +   
    4.                            Words.SECOND + " " +  
    5.                            Words.THIRD);  
    6.     }  
    7. }  
    8.   
    9. public class Words {  
    10.     private Words() { };  // Uninstantiable  
    11.   
    12.     public static final String FIRST  = "the";  
    13.     public static final String SECOND = null;  
    14.     public static final String THIRD  = "set";  
    15. }   

    现在假设你像下面这样改变了那个库类并且重编译了这个类,但并不重编译客户端的程序:

    1. public class Words {  
    2.     private Words() { };  // Uninstantiable  
    3.   
    4.     public static final String FIRST  = "physics";  
    5.     public static final String SECOND = "chemistry";  
    6.     public static final String THIRD  = "biology";  
    7. }  

    此时,客户端的程序会打印出什么呢?

    简单地看看程序,你会觉得它应该打印 physics chemistry biology;毕竟Java是在运行期对类进行装载的,所以它总是会访问到最新版本的类。但是更深入一点的分析会得出不同的结论。对于常量域的引用会在编译期被转化为它们所表示的常量的值[JLS 13.1]。这样的域从技术上讲,被称作常量变量(constant variables),这可能在修辞上显得有点矛盾。一个常量变量的定义是:一个在编译期被常量表达式初始化的final的原始类型或String类型的变量[JLS 4.12.4]。在知道了这些知识之后,我们有理由认为客户端程序会将初始值Words.FIRST, Words.SECOND, Words.THIRD编译进class文件,然后无论Words类是否被改变,客户端都会打印the null set。

    这种分析可能是有道理的,但是却是不对的。如果你运行了程序,你会发现它打印的是the chemistry set。这看起来确实太奇怪的了。它为什么会做出这种事情呢?答案可以在编译期常量表达式(compile-time constant expression)[JLS 15.28]的精确定义中找到。它的定义太长了,就不在这里写出来了,但是理解这个程序的行为的关键是null不是一个编译期常量表达式

    由于常量域将会编译进客户端,API的设计者在设计一个常量域之前应该深思熟虑。如果一个域表示的是一个真实的常量,例如π或者一周之内的天数,那么将这个域设为常量域没有任何坏处。但是如果你想让客户端程序感知并适应这个域的变化,那么就不能让这个域成为一个常量。有一个简单的方法可以做到这一点:如果你使用了一个非常量的表达式去初始化一个域,甚至是一个final域,那么这个域就不是一个常量。你可以通过将一个常量表达式传给一个方法使得它变成一个非常量,该方法将直接返回其输入参数。

    如果我们使用这种方法来修改Word类,在Words类被重新修改和编译之后,PrintWords类将打印出physics chemistry biology:

    1. public class Words {  
    2. private Words() {};  // Uninstantiable  
    3.   
    4.     public static final String FIRST   = ident("the");  
    5.     public static final String SECOND  = ident(null);  
    6.     public static final String THIRD   = ident("set");  
    7.   
    8.     private static String ident(String s) {  
    9.         return s;  
    10.     }  
    11. }   

    在5.0版本中引入的枚举常量(enum constants),虽然有这样一个名字,但是它们并不是常量变量。你可以在枚举类型中加入枚举常量,对它们重新排序,甚至可以移除没有用的枚举常量,而且并不需要重新编译客户端。

    总之,常量变量将会被编译进那些引用它们的类中。一个常量变量就是任何被常量表达式初始化的原始类型或字符串变量。令人惊讶的是,null不是一个常量表达式。

    对于语言设计者来说,在一个动态链接的语言中,将常量表达式编译进客户端可能并不是一个好主意。这让很多程序员大吃一惊,并且很容易产生一些难以查出的缺陷:当缺陷被侦测出来的时候,那些定义常量的源代码可能已经不存在了。另外一方面,将常量表达式编译进客户端使得我们可以使用if语句来模拟条件编译(conditional compilation)[JLS 14.21]。为了正当目的可以不择手段的做法是需要每个人自己来判断的。 

    展开全文
  • Java运行时修改字节码技术 Java运行时动态修改字节码技术,常用的有javassist asm来实现。不过最近在分析openrasp-java这块时,程序使用的javassist来动态插桩关键,达到监控某些程序的行为,OpenRasp使用这技术...

    Java运行时修改字节码技术

    Java运行时动态修改字节码技术,常用的有javassist asm来实现。不过最近在分析openrasp-java这块时,程序使用的javassist来动态插桩关键类,达到监控某些程序的行为,OpenRasp使用这个技术来实现了监控程序的行为。为了分析OpenRasp和理解其使用的技术原理,先做一个java动态修改指令基础知识的补充。

    第一个程序

    有如下程序

    package com.company;
    
    import java.net.URL;
    import  java.io.File;
    import java.net.URLDecoder;
    import java.util.Set;
    
    public class Test1 {
        private String aa="heh";
    
        public Test1(){}
    
        @Override
        public String toString() {
            return "Test1{" +
                    "aa='" + aa + '\'' +
                    '}';
        }
    }
    

    正常情况下调用toString()方法,会得到如下

    InsertCode:Test1{aa='heh'}
    

    如果想要在toString()方法前插入某一个方法块,输出如下内容

    方法调用前 ----->>aa
    方法调用前 ----->>aaa 
    方法调用后 ---->> bbbb
    InsertCode:Test1{aa='heh'}
    

    可以借用javassist工具类操作对应的字节码。

    动态修改字节码–常用javassist类(这里是根据写的样例记录的,不是针对所有情况)

    要想在动态修改程序行为,则需要使用javassist内的三个主要类

    ClassPool --> 是CtClass的一个容器 要想获得一个类对象,必须
    CtClass  -->和java的Class类似
    CtMethod -->和java的Method类似
    
    • ClassPool --> 是CtClass的一个容器 要想获得一个类对象,必须通过这个对象获取
    CtClass ctClzz =classPool.get("完整的类名,例如com.test.demn.A");
    如果当前的classPool内没有这个类,则会报javassist.NotFoundException: com.company.Test1 后续会提到
    #参考
    http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/ClassPool.html
    
    • CtClass -->和java的Class类似代表了一个类对象,从ClassPool内获取到
    参考
    http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/CtClass.html
    
    • CtMethod -->和java的Method类似代表的一个方法对象,继承自CtBeHavior
    参考
    http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/CtMethod.html
    

    在知道了这几个类之后,接着就是动手尝试修改com.company.Test1toString()方法。

    插入代码到toString()方法

    编写TestInsetOPs类

    package com.company;
    
    import javassist.*;
    import org.junit.Test;
    
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.LinkedList;
    
    //测试插入程序
    public class TestInsertOps {
    
    
    
        private static ClassPool insertCode() {
    
            try {
    
                //todo 两个方式回去ClassPool 1)ClassPool.getDefault(); 内部会自动调用appendSystemPath方法 2)可以直接new,不过要手动 appendSystemPath
                ClassPool pool = new ClassPool();// ClassPool.getDefault();
                pool.appendSystemPath();//如果添加到系统环境中内程序可以执行,否则会javassist.NotFoundException: com.company.Test1
                CtClass clazz = pool.get("com.company.Test1");
                CtMethod method = clazz.getDeclaredMethod("toString");
                method.insertBefore("{ System.out.println(\"方法调用前 ----->>aaa \"); }");
                method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }");
    
                return pool;
            } catch (NotFoundException e) {
    // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (CannotCompileException e) {
    // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static void testInsertCode() throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
    
    
            ClassPool classPool = insertCode();
            if (classPool == null) {
                System.out.println("程序添加失败 --> classPool is NullPointer");
                return;
            }
    
    
    
            // ClassPool pool =  new ClassPool()
            Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance();
    
            System.out.println("InsertCode:" + test1.toString());
    
        }
    }
    
    

    首先,我们需要获取一个ClassPool对象,通常有两个方法获取

    • 第一种 ClassPool.getDefault()
      这个方法的好处是调用getDefault()方法,其内部封装单例了方法,如下
     public static synchronized ClassPool getDefault() {
            if (defaultPool == null) {
                defaultPool = new ClassPool((ClassPool)null);
                defaultPool.appendSystemPath();
            }
    
            return defaultPool;
        }
    

    可以看到其内部调用了new ClassPool()方法,因此,第二种方法就是

    • 第二种 new ClassPool(),构造函数如下
       public ClassPool() {
            this((ClassPool)null);
        }
    
        -->
          public ClassPool(ClassPool parent) {
            this.childFirstLookup = false;
            this.cflow = null;
            this.classes = new Hashtable(191);
            this.source = new ClassPoolTail();
            this.parent = parent;
            if (parent == null) {
                CtClass[] pt = CtClass.primitiveTypes;
    
                for(int i = 0; i < pt.length; ++i) {
                    this.classes.put(pt[i].getName(), pt[i]);
                }
            }
    
            this.cflow = null;
            this.compressCount = 0;
            this.clearImportedPackages();
        }
    

    两个不同的点在于,getDefault自动调用了

    defaultPool.appendSystemPath();
    

    如果这个方法不调用,则就是报错误javassist.NotFoundException:。如果使用的是第二种方法获取ClassPool对象,需要调用这个方法。

    其次调用ClassPool.get(String className)获取一个CtClass对象,

    CtClass clazz = pool.get("com.company.Test1");
    

    接着获取一个CtMethod,可以通过getMethods()和getDeclareMethod()来获取,和java的反射类似使用,只是Method-->CtMethod

    这里获取toString方法,使用getDeclareMethod方法并判断是否是预期的方法

       CtMethod method = clazz.getDeclaredMethod("toString");
    

    在得到了方法体之后,调用insertBefore(String method)在方法入口处插入第一段方法

    
    

    接着调用insertAfter(String method)在return之前添加程序

     method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }");
    

    insertAfter和insertBefore的 method参数是一个字符串,并且是一个完整的方法调用,这个在openrasp中有体现

    最后调用toString方法

    Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance();
    System.out.println("InsertCode:" + test1.toString());
    

    输入结果为

    方法调用前 ----->>aaa 
    方法调用后 ---->> bbbb
    InsertCode:Test1{aa='heh'}
    

    验证程序写入的位置

    为了验证程序添加到正确的位置,这里修改了TestInsertOps的程序,在InsertCode内添加了如下内容

    package com.company;
    
    import javassist.*;
    import org.junit.Test;
    
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.LinkedList;
    
    //测试插入程序
    public class TestInsertOps {
    
    
        private static void save(byte[] data,String filename) throws IOException {
            String path = System.getProperty("user.dir");
            File cache = new File(path+File.separator+"cache");
            if(!cache.isDirectory()){
                cache.mkdirs();
            }
            path = cache+File.separator+filename;
            File file = new File(path);
            if(file.exists()){
                file.delete();
            }
    
            FileOutputStream fileOutputStream  = new FileOutputStream(file);
            fileOutputStream.write(data);
            fileOutputStream.flush();
            fileOutputStream.close();
            System.out.println("保存成功 "+file.getAbsolutePath());
    
    
        }
    
        private static ClassPool insertCode() {
    
            try {
    
                //todo 两个方式回去ClassPool 1)ClassPool.getDefault(); 内部会自动调用appendSystemPath方法 2)可以直接new,不过要手动 appendSystemPath
    
                ClassPool.getDefault();
                ClassPool pool = new ClassPool();// ClassPool.getDefault();
                pool.appendSystemPath();//如果添加到系统环境中内程序可以执行,否则会javassist.NotFoundException: com.company.Test1
                CtClass clazz = pool.get("com.company.Test1");
                //
                byte[] origin_clzz=clazz.toBytecode();
                save(origin_clzz,"origin_clzz.class");
                if (clazz.isFrozen())
                {
                    System.out.println("Frozen ...");
                    clazz.defrost();
    
                }
                CtMethod method = clazz.getDeclaredMethod("toString");
                method.insertBefore("{ System.out.println(\"方法调用前 ----->>aaa \"); }");
                method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }");
    //            clazz.writeFile();
    
                byte[] mode_clzz=clazz.toBytecode();
                save(mode_clzz,"mode_clzz.class");
    
    
                return pool;
            } catch (NotFoundException e) {
    // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (CannotCompileException e) {
    // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static void testInsertCode() throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
    
    
            ClassPool classPool = insertCode();
            if (classPool == null) {
                System.out.println("程序添加失败 --> classPool is NullPointer");
                return;
            }
    
    
    
            // ClassPool pool =  new ClassPool()
            Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance();
    
            System.out.println("InsertCode:" + test1.toString());
    
        }
    }
    

    程序运行后可以得到两个类origin_clzz.class和mode_clzz.class。使用Idea的反编译功能,可以看到被修改的类Test1前后差别

    Before :

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package com.company;
    
    import java.io.File;
    import java.net.URL;
    import java.net.URLDecoder;
    import java.util.Set;
    
    public class Test1 {
        private String aa = "heh";
    
        public Test1() {
        }
    
        public String getLocalPath() {
            URL url = this.getClass().getProtectionDomain().getCodeSource().getLocation();
            String decode = URLDecoder.decode(url.getFile());
            System.out.println("Before " + url.getFile() + "decodeUrl " + decode + " replace " + decode.replace("+", "%2B") + "parent " + (new File(decode)).getParent());
            return decode;
        }
    
        public void testParam(Set<String> result) {
            String[] objec = new String[]{"AAA", "BBB", "CCC", "DDD"};
            String[] var3 = objec;
            int var4 = objec.length;
    
            for(int var5 = 0; var5 < var4; ++var5) {
                String a = var3[var5];
                result.add(a);
            }
    
        }
    
        public String toString() {
            return "Test1{aa='" + this.aa + '\'' + '}';
        }
    }
    

    After:

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package com.company;
    
    import java.io.File;
    import java.net.URL;
    import java.net.URLDecoder;
    import java.util.Set;
    
    public class Test1 {
        private String aa = "heh";
    
        public Test1() {
        }
    
        public String getLocalPath() {
            URL url = this.getClass().getProtectionDomain().getCodeSource().getLocation();
            String decode = URLDecoder.decode(url.getFile());
            System.out.println("Before " + url.getFile() + "decodeUrl " + decode + " replace " + decode.replace("+", "%2B") + "parent " + (new File(decode)).getParent());
            return decode;
        }
    
        public void testParam(Set<String> result) {
            String[] objec = new String[]{"AAA", "BBB", "CCC", "DDD"};
            String[] var3 = objec;
            int var4 = objec.length;
    
            for(int var5 = 0; var5 < var4; ++var5) {
                String a = var3[var5];
                result.add(a);
            }
    
        }
    
        public String toString() {
            System.out.println("方法调用前 ----->>aaa ");
            String var2 = "Test1{aa='" + this.aa + '\'' + '}';
            System.out.println("方法调用后 ---->> bbbb");
            return var2;
        }
    }
    

    完。

    总结

    java可以在运行时修改程序,通过javaasist工具类来实现,通过ClassPool,CtClass,CtMethod来实现。

    展开全文
  • 深夜更新 Jenkins 插件的时候,遇到了一个问题:插件下载速度太慢了,并且有大概率失败。 因此我研究了一下 Jenkins 的插件升级机制,研究是否可以使用镜像站点加速。 初步研究 Jenkins 的 升级信息(包含本体和插件...

    遇到问题

    起因

    深夜更新 Jenkins 插件的时候,遇到了一个问题:插件下载速度太慢了,并且有大概率失败。

    因此我研究了一下 Jenkins 的插件升级机制,研究是否可以使用镜像站点加速。

    初步研究

    Jenkins 的 升级信息(包含本体和插件)来自于 http://mirrors.jenkins-ci.org/updates/update-center.json 这个 URL。

    这个 JSON 文件描述了所有可用插件的 版本信息下载地址 ,并且 Jenkins 在 插件中心高级 选项卡里提供了修改这个地址的功能。

    我找到了 jenkins-zh 社区提供的 升级信息 镜像地址 https://updates.jenkins-zh.cn/update-center.json,他们所提供的 JSON 文件内部,将所有 下载地址 替换为等价的 清华 TUNA 镜像站 的地址。

    遭遇困境

    但是当我把镜像地址填入 Jenkins,点击 更新 时,界面突然弹出来巨长一串超出屏幕的报错,提示 签名验证失败

    为什么

    Jenkins 在这个 JSON 文件内做了签名认证。

    也就是说,Jenkins 允许你从另外的 URL 获取到 update-center.json 这个文件,但是 JSON 文件内部描述的 下载地址 等信息是 无法修改 的,否则会破坏签名认证。

    这也就意味着,插件下载的过程仍然无法使用镜像站点加速。

    怎么办

    两个方法:

    1. 重新签名 update-center.json 并替换 Jenkins 内的 CA 文件,让修改后的 update-center.json 变得合法
    2. 修改代码重新编译 Jenkins,关掉这个签名认证 (既然允许自定义 升级信息 地址,又何必非要做 签名认证 呢?)

    方法 1 的问题

    如果有一天我不使用这个镜像站了,我还得想办法恢复 CA 文件。

    并且每次 update-center.json 有变动,我都得重新签名。尽管可以制作或者寻找现有的自动化工具,但是终究需要额外部署一套工具。

    方法 2 的问题

    每次 Jenkins 版本更新都需要重新编译 Jenkins,着实麻烦。

    更好的办法

    我想到了一个更好的办法,使用 Java Agent 机制,在运行时覆盖掉 Jenkins 签名验证方法,让它跳过实际的签名验证,永远返回正确。

    这样即便是 Jenkins 版本更新,只要 升级信息 签名验证代码流程不变,这个方法就可以一直有效。

    实施流程

    什么是 Java Agent

    Java Agent 通过在 Java 命令上添加参数 -javaagent:XXXXXX.jar 来启动,它允许你在主类之外,额外加载一个 Jar 包,并提供机制,允许你在类加载时,修改类的字节码。

    通过 Java Agent 机制,可以在不修改源代码的情况下,在运行时修改 Java 程序。

    定位 Jenkins 代码

    简要查阅了 Jenkins 的源代码,定位到了 Jenkins 中,负责校验升级信息签名的方法来自 hudson.model.UpdateSite 类的 verifySignature 方法

        /**
         * Verifies the signature in the update center data file.
         */
        private FormValidation verifySignature(JSONObject o) throws IOException {
            return getJsonSignatureValidator().verifySignature(o);
        }
    

    参见 https://github.com/jenkinsci/jenkins/blob/6d6c2793e41539c214241cc49df6515ec0395ff4/core/src/main/java/hudson/model/UpdateSite.java#L267

    我们的目标是,将这个方法的代码体,修改成如下内容

    return FormValidation.ok();
    

    这样,修改后的方法,永远会返回校验正常。

    编写 Java Agent

    知道了要修改什么,就可以开始编写 Java Agent 了。

    我们选用 javassist 这个库,这是一个 IBM 推出的库,可以便捷地修改 Java 类字节码。

    创建 Maven 工程

    首先我们创建一个 Maven 工程,和常见的工程类似,但是 Pom 文件的设置有些许区别:

    <!-- 略去了不重要的内容 -->
        <dependencies>
            <!-- 引入 javassist 包 -->
            <dependency>
                <groupId>org.javassist</groupId>
                <artifactId>javassist</artifactId>
                <version>3.27.0-GA</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <configuration>
                        <!-- Java Agent 的 MANIFEST 里面不是 Main-Class-->
                        <!-- 我们使用 premain 模式,因此要写 Premain-Class -->
                        <archive>
                            <manifestEntries>
                                <Premain-Class>net.landzero.jenkins.tune.Agent</Premain-Class>
                            </manifestEntries>
                        </archive>
                    </configuration>
                </plugin>
                <!-- 使用 shade 插件,把 javassist 直接包含在成品 Jar 包内部 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-shade-plugin</artifactId>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>shade</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <filters>
                            <!-- 过滤掉 javassist 自己的 META-INF 文件(可能多此一举了) -->
                            <filter>
                                <artifact>org.javassist:*</artifact>
                                <excludes>
                                    <exclude>META-INF/license/**</exclude>
                                    <exclude>META-INF/*</exclude>
                                    <exclude>META-INF/maven/**</exclude>
                                    <exclude>LICENSE</exclude>
                                    <exclude>NOTICE</exclude>
                                    <exclude>/*.txt</exclude>
                                    <exclude>build.properties</exclude>
                                </excludes>
                            </filter>
                        </filters>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    <!-- 略去了不重要的内容 -->
    

    编写 Agent 主类

    package net.landzero.jenkins.tune;
    
    // 此处略却一大堆 import
    
    public class Agent {
    
        // Java Agent 的启动入口不是 main,而是 premain,并且有特定的方法签名
        public static void premain(String agentArgs, Instrumentation inst) {
            // 注册一个类转换器
            inst.addTransformer(new UpdateSiteTransformer());
        }
    
        private static class UpdateSiteTransformer implements ClassFileTransformer {
    
            private static final String CLASS_NAME = "hudson.model.UpdateSite";
    
            private static final String CLASS_NAME_INTERNAL = CLASS_NAME.replace('.', '/');
    
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                if (className == null) {
                    return null;
                }
                // 如果不是 hudson.model.UpdateSite 就忽略掉,不做修改
                // 这个方法传入的 className 是使用 “/” 分割的类名,而不是标准的 "." 分割的类名
                if (!CLASS_NAME_INTERNAL.equals(className)) {
                    return null;
                }
                try {
                    // 加载 javassist 类池
                    ClassPool cp = ClassPool.getDefault();
                    // 因为 Jenkins 是 WAR 包,还需要把 loader 补充进去,不然下面 javassist 解析字节码会报告找不到 import 的类
                    cp.appendClassPath(new LoaderClassPath(loader));
                    // 把字节码载入进去,生成 CtClass
                    CtClass cc = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
                    // 定位到方法 verifySignature
                    CtMethod cm = cc.getDeclaredMethod("verifySignature");
                    // 修改方法代码体
                    cm.setBody("{ return hudson.util.FormValidation.ok(); }");
                    // 返回重新编译的字节码
                    return cc.toBytecode();
                } catch (Exception e) {
                    return null;
                }
            }
    
        }
    }
    

    打包

    直接 mvn clean package 打包,得到 jenkins-tune-1.0-SNAPSHOT.jar 文件,这里面包含了我们写的 Agent 类和依赖项 javassist

    部署 Java Agent

    jenkins-tune-1.0.0-SNAPSHOT.jar 复制到服务器上,假设复制到 /usr/local/lib/jenkins-tune.jar 这个位置。

    编辑 /etc/init.d/jenkins ,寻找 JAVA_CMD=... 那一行,添加一句 -javaagent:/usr/local/lib/jenkins-tune.jar

    (如果是 yum 包安装的 Jenkins,也可以修改 /etc/default/jenkins 中的 JAVA_ARGS 字段)

    (如果使用的是 WAR 包手动部署,则需要修改 Tomcat 的启动脚本)

    systemctl restart jenkins 重新启动 Jenkins 就大功告成了。

    结果

    我重新填入了 jenkins-zh 提供的镜像地址,这次就再也没有提示签名错误了,也可以正常地从 清华 TUNA 镜像站 拉取插件更新,速度自然是飞快。

    代码地址

    https://github.com/guoyk93/jenkins-tune

    如果你不想自己编译 Jar 包,可以在 Release 页面找到预先编译好的 Jar 包

    展开全文
  • 最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的...那就着手从Java如何根据字符串模板在运行时动态生成对象。Java是一门静态语言,通常,我们需要...

    最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的逻辑。如果手写不仅每次都要修改代码,还要每次测试发版,而且无法灵活根据用户定义的规则动态处理逻辑。所以想到将公共逻辑写到父类实现,将特定逻辑根据字符串动态生成子类处理。这就可以一劳永逸解决这个问题。

    那就着手从Java如何根据字符串模板在运行时动态生成对象。

    Java是一门静态语言,通常,我们需要的class在编译的时候就已经生成了,为什么有时候我们还想在运行时动态生成class呢?

    经过一番网上资料查找,由繁到简的方式总结如下:

    一、利用JDK自带工具类实现

    现在问题来了,动态生成字节码,难度有多大?

    如果我们要自己直接输出二进制格式的字节码,在完成这个任务前,必须先认真阅读JVM规范第4章,详细了解class文件结构。估计读完规范后,两个月过去了。

    所以,第一种方法,自己动手,从零开始创建字节码,理论上可行,实际上很难。

    第二种方法,使用已有的一些能操作字节码的库,帮助我们创建class。

    目前,能够操作字节码的开源库主要有CGLib和Javassist两种,它们都提供了比较高级的API来操作字节码,最后输出为class文件。

    比如CGLib,典型的用法如下:

    f2940af480e1b4b65ede0eee02de2ab9.png

    Enhancer e = new Enhancer();

    e.setSuperclass(...);

    e.setStrategy(new DefaultGeneratorStrategy() {

    protected ClassGenerator transform(ClassGenerator cg) {

    return new TransformingGenerator(cg,

    new AddPropertyTransformer(new String[]{ "foo" },

    new Class[] { Integer.TYPE }));

    }});

    Object obj = e.create();

    f2940af480e1b4b65ede0eee02de2ab9.png

    比自己生成class要简单,但是,要学会它的API还是得花大量的时间,并且,上面的代码很难看懂对不对?

    有木有更简单的方法?

    有!

    Java的编译器是javac,但是,在很早很早的时候,Java的编译器就已经用纯Java重写了,自己能编译自己,行业黑话叫“自举”。从Java 1.6开始,编译器接口正式放到JDK的公开API中,于是,我们不需要创建新的进程来调用javac,而是直接使用编译器API来编译源码。

    使用起来也很简单:

    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    int compilationResult = compiler.run(null, null, null, ‘/path/Test.java‘);

    这么写编译是没啥问题,问题是我们在内存中创建了Java代码后,必须先写到文件,再编译,最后还要手动读取class文件内容并用一个ClassLoader加载。

    有木有更简单的方法?

    有!

    其实Java编译器根本不关心源码的内容是从哪来的,你给它一个String当作源码,它就可以输出byte[]作为class的内容。

    所以,我们需要参考Java Compiler API的文档,让Compiler直接在内存中完成编译,输出的class内容就是byte[]。

    f2940af480e1b4b65ede0eee02de2ab9.png

    Map results;

    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);

    try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {

    JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);

    CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));

    if (task.call()) {

    results = manager.getClassBytes();

    }

    }

    f2940af480e1b4b65ede0eee02de2ab9.png

    上述代码的几个关键在于:

    用MemoryJavaFileManager替换JDK默认的StandardJavaFileManager,以便在编译器请求源码内容时,不是从文件读取,而是直接返回String;

    用MemoryOutputJavaFileObject替换JDK默认的SimpleJavaFileObject,以便在接收到编译器生成的byte[]内容时,不写入class文件,而是直接保存在内存中。

    最后,编译的结果放在Map中,Key是类名,对应的byte[]是class的二进制内容。

    为什么编译后不是一个byte[]呢?

    因为一个.java的源文件编译后可能有多个.class文件!只要包含了静态类、匿名类等,编译出的class肯定多于一个。

    如何加载编译后的class呢?

    加载class相对而言就容易多了,我们只需要创建一个ClassLoader,覆写findClass()方法:

    f2940af480e1b4b65ede0eee02de2ab9.png

    class MemoryClassLoader extends URLClassLoader {

    Map classBytes = new HashMap();

    public MemoryClassLoader(Map classBytes) {

    super(new URL[0], MemoryClassLoader.class.getClassLoader());

    this.classBytes.putAll(classBytes);

    }

    @Override

    protected Class> findClass(String name) throws ClassNotFoundException {

    byte[] buf = classBytes.get(name);

    if (buf == null) {

    return super.findClass(name);

    }

    classBytes.remove(name);

    return defineClass(name, buf, 0, buf.length);

    }

    }

    f2940af480e1b4b65ede0eee02de2ab9.png

    总结以上,那么我们来编写一个Java脚本引擎吧:

    二、利用三方Jar包实现

    利用三方包com.itranswarp.compiler来实现:

    1. 引入Maven依赖包:

    2. 编写工具类

    f2940af480e1b4b65ede0eee02de2ab9.png

    public class StringCompiler {

    public static Object run(String source, String...args) throws Exception {

    // 声明类名

    String className = "Main";

    String packageName = "top.fomeiherz";

    // 声明包名:package top.fomeiherz;

    String prefix = String.format("package %s;", packageName);

    // 全类名:top.fomeiherz.Main

    String fullName = String.format("%s.%s", packageName, className);

    // 编译器

    JavaStringCompiler compiler = new JavaStringCompiler();

    // 编译:compiler.compile("Main.java", source)

    Map results = compiler.compile(className + ".java", prefix + source);

    // 加载内存中byte到Class>对象

    Class> clazz = compiler.loadClass(fullName, results);

    // 创建实例

    Object instance = clazz.newInstance();

    Method mainMethod = clazz.getMethod("main", String[].class);

    // String[]数组时必须使用Object[]封装

    // 否则会报错:java.lang.IllegalArgumentException: wrong number of arguments

    return mainMethod.invoke(instance, new Object[]{args});

    }

    }

    f2940af480e1b4b65ede0eee02de2ab9.png

    3. 测试执行

    f2940af480e1b4b65ede0eee02de2ab9.png

    public class StringCompilerTest {

    public static void main(String[] args) throws Exception {

    // 传入String类型的代码

    String source = "import java.util.Arrays;public class Main" +

    "{" +

    "public static void main(String[] args) {" +

    "System.out.println(Arrays.toString(args));" +

    "}" +

    "}";

    StringCompiler.run(source, "1", "2");

    }

    }

    f2940af480e1b4b65ede0eee02de2ab9.png

    三、利用Groovy脚本实现

    以上两种方式尝试过,后来发现Groovy原生就支持脚本动态生成对象。

    1. 引入Groovy maven依赖

    org.codehaus.groovy

    groovy-all

    2.4.13

    2. 直接上测试代码

    f2940af480e1b4b65ede0eee02de2ab9.png

    @Test

    public void testGroovyClasses() throws Exception {

    //groovy提供了一种将字符串文本代码直接转换成Java Class对象的功能

    GroovyClassLoader groovyClassLoader = new GroovyClassLoader();

    //里面的文本是Java代码,但是我们可以看到这是一个字符串我们可以直接生成对应的Class>对象,而不需要我们写一个.java文件

    Class> clazz = groovyClassLoader.parseClass("package com.xxl.job.core.glue;\n" +

    "\n" +

    "public class Main {\n" +

    "\n" +

    " public int age = 22;\n" +

    " \n" +

    " public void sayHello() {\n" +

    " System.out.println(\"年龄是:\" + age);\n" +

    " }\n" +

    "}\n");

    Object obj = clazz.newInstance();

    Method method = clazz.getDeclaredMethod("sayHello");

    method.invoke(obj);

    Object val = method.getDefaultValue();

    System.out.println(val);

    }

    展开全文
  • 因为在有些时候,我们还真得在运行时一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?以User为例:public classUser {privateString id;privateString name;publicS.....
  • 最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的...那就着手从Java如何根据字符串模板在运行时动态生成对象。Java是一门静态语言,通常,我们需要...
  • 您可以使用命令行在系统属性上定义-DpropertyName=propertyValue所以你可以写java -jar selenium-rc.jar -Dhttp.proxyHost=YourProxyHost -Dhttp.proxyPort=YourProxyPort编辑:您可以编写一个作为应用程序启动器的...
  • 因为在有些时候,我们还真得在运行时一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?以User为例:public class User {private String id;private String name;publ.....
  • Java运行时动态生成class的方法

    千次阅读 2017-05-11 09:21:56
    Java运行时动态生成class的方法Java是一门静态语言,通常,我们需要的class在编译的时候就已经生成了,为什么有时候我们还想在运行时动态生成class呢?因为在有些时候,我们还真得在运行时为一个类动态创建子类。...
  • 一个类是base.Base,存在./path1/base/路径下,第二个类是sub.Sub,存放在./path2/sub/路径下,Sub继承了Base。源代码如下: <code class="language-java">package base; public ...
  • 因为在有些时候,我们还真得在运行时一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?以User为例:public class User {private String id;private String name;publ...
  • 因为在有些时候,我们还真得在运行时一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢? 以User为例: public class User { private String id; priv
  • 我对类加载的理解是一个类在第一次需要被加载(以非常简单的方式放置).使用-verbose:class和Iterators类的修改版本运行以下示例,该类在调用其clinit打印消息我观察到了一些我无法解释的内容:public class ...
  • Java运行于手机的一个联系人管理小程序,RMS记录读取和修改程序,  Display display = null; // 设备的显示器  List list = null;  TextField nameField; // 姓名文本域  TextField honeField; //电话号码...
  • 今天写了一个工具,导入Excel文件,根据文件批量生成SQL,但是运行的时候,一直报错,报错如下: java.lang.OutOfMemoryError: Java heap space 调整idea中setting中的compiler的Build process heap size(Mbytes)...
  • java 运行时类型识别(RTTI) - 2 - 反射

    千次阅读 2012-07-30 12:55:34
    本文将叙述如何运行时查看信息,其中包括变量,方法,以及通过反射修改变量,执行方法等 包括如何反射匿名内部及如何执行其方法,但是笔者强烈不建议这么做,这里只是演示反射而已 下面是一个测试...
  • 因为在有些时候,我们还真得在运行时一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?以User为例:public class User {private String id;private String name;publ.....
  • java 运行jar包 找不到jdk 中的

    千次阅读 2017-10-10 18:01:02
    今天打了jar 包,总是会报错,找不到 jdk 中的 java.lang.NoClassDefFoundError: javax/xml/ws/Service 这Service 明明是 jdk 中 rt.jar 中的 ,怎么会找不到呢? 开始不停地修改环境变量中···· ...
  • 最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的...那就着手从Java如何根据字符串模板在运行时动态生成对象。Java是一门静态语言,通常,我们需要...
  • 本人想修改java.lang.String,然后导出成jar,代替原来虚拟机的String使用,但是在添加一个布尔数组后,导出成jar并使用出现下面的错误: Error occurred during initialization of VM java.lang....
  • 前言:首先我们来思考一个问题,我们进程在项目开发中,仅仅修改了几个类的代码,之后将这些类的class文件拷贝到web容器对应位置中,如果不重启服务能否保住我们修改的代码也被加载?同是也考虑一下,是否是在web...
  • Byte Buddy是一个代码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。 除了的代码生成实用程序外,Byte Buddy还允许创建任意,并且不限于实现用于创建运行时代理的接口。 此外,...
  • 因为java classloader没有任何一种机制来卸下一系列存在的,也不能用的新版本来替换老版本,为了在一个运行的虚拟机中更新相关的,classloader必须被替换掉。当它被替换,它所装载的所有以及衍生的子...
  • 一个类可以在声明时使用一些修饰符,这些修饰符会影响其运行时行为: 访问修饰符:public,protected和private要求重写(override):abstract限定为一个实例:static禁止修改:final强制精确浮点
  • Java探针可以在Java应用运行时毫无感知的切入应用代码,是种用于监听代码行为或改变代码行为的工具。分布式调用链路追踪的实现无非两种方式,代码侵入式和非代码侵入式,基于Java探针实现的属于非代码侵入式。运行...
  • 本例子展示了如下功能:1、删除方法中的 打印日志代码。2、删除指定的 方法 和 成员变量。3、检测方法中 是否有 new Thread 代码。...7、在现有 中生成一个方法 ,并在运行时验证。环境:gradle...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,830
精华内容 732
关键字:

java运行时修改一个类

java 订阅