Java中Javassist的使用

  • 内容
  • 评论
  • 相关

先来说一下为什么会有本篇文章。

有次工作需要,测试同学说希望做一个自动化的东西来直接测试和民生银行对接的接口。这个接口需要传递加密的密码,所以希望我提供一个能给密码加密的工具,通过这个工具获得加密后的密码,然后使用这个密码和民生交互。我说,好啊。

查看Android本地代码,发现加密的代码在民生的SDK中(不用看也能猜到),所以我们需要把SDK中的部分代码拿出来在其他机器上用。经过一番查找,找到了代码位置,是通过一个jar中的java方法调用的so中的C++方法。所以基本思路就是我们调用这个java方法就可以了。看一下这个方法所在的类:

package com.cfca.mobile.sipcryptor;

public final class SipCryptorJni {
    private static String n = "SipCryptor";

    public SipCryptorJni() {
    }

    public static native JniResult c8cb935752cff430df880add9a44f264e668efd83768e3c10bad3f8a038246ab();

    public static native JniResult _4bcfb86862e83c57e0d8f2b9b0c1a4e3630b01c02b747f520c64b3843b4840b/* $FF was: 04bcfb86862e83c57e0d8f2b9b0c1a4e3630b01c02b747f520c64b3843b4840b*/(long var0);

    public static native JniResult _9fcf1eeb6b7310cf362c1cc0ecea07044e508995b78be25f588346ff51d8478/* $FF was: 49fcf1eeb6b7310cf362c1cc0ecea07044e508995b78be25f588346ff51d8478*/(long var0, int var2, String var3, String var4);

    public static native JniResult a78a16b72f4c99f92d21680b4b8ffe68f551788be76912a1814bbdb8363de8d7(long var0, String var2);

    public static native JniResult _da92f438d10ff6c622bf0825545f423bdd112a14185570f429e32403dc40c64/* $FF was: 9da92f438d10ff6c622bf0825545f423bdd112a14185570f429e32403dc40c64*/(long var0, String var2);

    public static native JniResult _aa694dadbe87a52e3b453fe1158ee9a466283bde74977faca8208cbb731e5fd/* $FF was: 4aa694dadbe87a52e3b453fe1158ee9a466283bde74977faca8208cbb731e5fd*/(long var0, String var2);

    public static native JniResult _8b465754bf83b5d530dbe3a466f298b7468e548d1b2d4ae48068eb356703b38/* $FF was: 58b465754bf83b5d530dbe3a466f298b7468e548d1b2d4ae48068eb356703b38*/(long var0);

    public static native JniResult _041379f4a47cd05a88bed2eb1e388986e9b89c245505b43a2f2cf91d2e81eef/* $FF was: 6041379f4a47cd05a88bed2eb1e388986e9b89c245505b43a2f2cf91d2e81eef*/(long var0);

    public static native JniResult _f89dffc9bb42e8b5ae1716ec2cbcc3f3fbc041ea1c901ece28de8cd229d3cec/* $FF was: 7f89dffc9bb42e8b5ae1716ec2cbcc3f3fbc041ea1c901ece28de8cd229d3cec*/(long var0);

    public static native JniResult _1680c4c455b2450e411e6ed04275c091f69ba3c2f6e828e994eaf23b1a8e31c/* $FF was: 61680c4c455b2450e411e6ed04275c091f69ba3c2f6e828e994eaf23b1a8e31c*/(long var0, String var2);

    public static native JniResult _789690cffd6a2e820c2eb764a39f4e980ca52b5eeb306afcd661d91f141d507/* $FF was: 9789690cffd6a2e820c2eb764a39f4e980ca52b5eeb306afcd661d91f141d507*/(long var0, long var2);

    public static native JniResult _49fd00c060bd7ff862991b2468a94dcab6571642d45a546084bf4d0f1960ea1/* $FF was: 049fd00c060bd7ff862991b2468a94dcab6571642d45a546084bf4d0f1960ea1*/(long var0, int var2);

    static {
        System.loadLibrary("cfcaMLog");
        System.loadLibrary("smkernel");
        System.loadLibrary(n);
    }
}

看一个调用它的地方:

public final String f() throws CodeException {
    JniResult var1;
    if ((var1 = SipCryptorJni.7f89dffc9bb42e8b5ae1716ec2cbcc3f3fbc041ea1c901ece28de8cd229d3cec(this.handle)).getErrorCode() == 0) {
        return var1.getStringResult();
    } else {
        throw new CodeException("" + var1.getErrorCode(), "SipCryptor getEncryptedValue");
    }
}

什么鬼东西!!!方法是以数字为开头的!!!我们知道,Java中(其他应该也是)方法是不能以数字为开头的,所以说,我们直接这么调用肯定是不行的!那SDK怎么就可以?我猜测应该是做了手脚,他们直接操作了字节码,这样来防止其他人直接使用。这一招很优秀啊,这样java没法直接调用,而且c++中由于对应的方法会加上java包名导致方法不会由数字开头,可以正常使用,高啊,实在是高。

怎么办

可以直接使用反射,这样方法名是一个字符串,可以直接调用了。或者自己创建一个类似上面的代码的使用方式。即:

SipCryptorJni.7f89dffc9bb42e8b5ae1716ec2cbcc3f3fbc041ea1c901ece28de8cd229d3cec()

为什么不直接使用它的代码?

因为它里面的方法牵扯到其他本多的类,仅仅把这一个类摘出来是不太好使用的。

当然了,由于某种特殊的原因我们不能用反射。

要想这样写,常规的方式肯定不行,我们需要一个能直接操作class的工具,这时候我就找到了Javassist这样的一个工具。

好了,以上只是背景,接下来和上面没什么关系了。

Javassist是什么

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

所以说,Javassist于ASM一样,都是一个可以操作字节码的工具。那么它和ASM比有什么优缺点呢?

  • Javassist使用简单,不需要像ASM那样知道字节码的细节。
  • Javassist的效率要低于ASM,但是要高于反射。

在性能上Javassist高于反射,但低于ASM,因为Javassist增加了一层抽象。在实现成本上Javassist和反射都很低,而ASM由于直接操作字节码,相比Javassist源码级别的api实现成本高很多。几个方法有自己的应用场景,比如Kryo使用的是ASM,追求性能的最大化。而NBeanCopyUtil采用的是Javassist,在对象拷贝的性能上也已经明显高于其他的库,并保持高易用性。实际项目中推荐先用Javassist实现原型,若在性能测试中发现Javassist成为了性能瓶颈,再考虑使用其他字节码操作方法做优化。

Javassist结构于功能

Javassist中最为重要的是ClassPool,CtClass ,CtMethods 以及 CtFields这几个类。

  • ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
  • CtClass:表示一个类,这些CtClass对象可以从ClassPool获得。
  • CtMethods:表示类中的方法。
  • CtFields :表示类中的字段。

使用示例

package javassist;

import javassist.bytecode.AccessFlag;

public class JavassistTest {

    public static void main(String[] args) {
        makeClass();
        modifyClass();
    }

    private static void modifyClass() {
        ClassPool pool = ClassPool.getDefault();
        try {
            CtClass cc = pool.get("javassist.WillModifyClass");
            CtMethod m = cc.getDeclaredMethod("add");
            m.insertBefore("{ System.out.println($1); System.out.println($2); }");
            m.insertAfter("{ System.out.println(x); System.out.println(y); }");
            m.setName("addXY");
            cc.writeFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void makeClass() {
        ClassPool pool = ClassPool.getDefault();
        CtClass ct = pool.makeClass("javassist.MakeClass");
        ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Runnable")});
        try {
            CtField field = new CtField(CtClass.intType, "id", ct);
            field.setModifiers(AccessFlag.PUBLIC);
            ct.addField(field);
            CtConstructor constructor = CtNewConstructor.make("public MakeClass(int id){this.id=id;}", ct);
            ct.addConstructor(constructor);
            CtMethod say = CtNewMethod.make("public void say(String hello){ System.out.println(hello);}", ct);
            ct.addMethod(say);
            ct.writeFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package javassist;

public class WillModifyClass {

    int x, y;
    void add(int dx, int dy) { x += dx; y += dy; }
}

以上就是创建和修改类的基本代码。它的运行后可以修改和生成如下内容:

package javassist;

public class WillModifyClass {
    int x;
    int y;

    public WillModifyClass() {
    }

    void addXY(int dx, int dy) {
        System.out.println(dx);
        System.out.println(dy);
        this.x += dx;
        this.y += dy;
        Object var4 = null;
        System.out.println(this.x);
        System.out.println(this.y);
    }
}
package javassist;

public class MakeClass implements Runnable {
    public int id;

    public MakeClass(int var1) {
        this.id = var1;
    }

    public void say(String var1) {
        System.out.println(var1);
    }
}

可以看到,Javassist的基本使用还是特别的简单的,无需所太多的解释(当然深入的话还是需要各位去探索)。同时我们可以看到,创建和修改的代码,并非特别完美,例如修改的WillModifyClass类中Object var4 = null;不知道为什么会有这句,也有可能是我的工具有问题;创建的MakeClass实现了Runnable,工具却不能校验真正有没有实现。

如何解决最上面的问题

不用多说了吧,用Javassist直接修改原来的class加一个方法来代理目标方法就可以了。

评论

0条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注