使用ASM或Javassist改进字段获取和设置性能

时间:2010-06-11 12:37:03

标签: java assembly java-bytecode-asm

我想避免在我正在开发的开源项目中反思。在这里,我有类似以下的课程。

public class PurchaseOrder {

   @Property
   private Customer customer;

   @Property
   private String name;
}

我扫描@Property注释以确定我可以设置并从PurchaseOrder反射获取。有很多这样的类都使用java.lang.reflect.Field.get()java.lang.reflect.Field.set()

理想情况下,我想为每个属性生成如下的调用者。

public interface PropertyAccessor<S, V> {
   public void set(S source, V value);
   public V get(S source);
}

现在,当我扫描课程时,我可以像这样创建一个PurchaseOrder的静态内部类。

static class customer_Field implements PropertyAccessor<PurchaseOrder, Customer> {
   public void set(PurchaseOrder order, Customer customer) {
      order.customer = customer;
   }  
   public Customer get(PurchaseOrder order) {
      return order.customer;
   }
}

有了这些,我完全避免了反思的代价。我现在可以使用原生性能设置并从我的实例中获取。任何人都可以告诉我如何做到这一点。代码示例会很棒。我在网上搜索了一个很好的例子但是却找不到这样的东西。 ASM和Javasist的例子也很差。

这里的关键是我有一个可以传递的界面。所以我可以有各种实现,可能有一个默认使用Java Reflection,一个使用ASM,一个使用Javassist?

非常感谢任何帮助。

5 个答案:

答案 0 :(得分:10)

<强> ASM

使用ASMifierClassVisitor,您可以准确地看到需要编写哪些代码来生成内部类:

ASMifierClassVisitor.main(new String[] { PurchaseOrder.customer_Field.class
    .getName() });

其余的只是确定在生成器代码中需要参数化的位数。 PurchaseOrder$customer_Field的示例输出将成为文件inject/PurchaseOrder$customer_Field.class

public static byte[] dump () throws Exception {

  ClassWriter cw = new ClassWriter(0);
  FieldVisitor fv;
  MethodVisitor mv;
  AnnotationVisitor av0;

  cw.visit(V1_6, ACC_SUPER, "inject/PurchaseOrder$customer_Field",
      "Ljava/lang/Object;"+
      "Linject/PropertyAccessor<Linject/PurchaseOrder;Linject/Customer;>;", 
      "java/lang/Object",
      new String[] { "inject/PropertyAccessor" });
//etc

(我使用“注入”作为包。)

您还必须使用ASM的访问者类创建合成访问者:

{
mv = cw.visitMethod(ACC_STATIC + ACC_SYNTHETIC, "access$0", 
          "(Linject/PurchaseOrder;Linject/Customer;)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitFieldInsn(PUTFIELD, "inject/PurchaseOrder",
          "customer", "Linject/Customer;");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_STATIC + ACC_SYNTHETIC, "access$1", 
          "(Linject/PurchaseOrder;)Linject/Customer;", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "inject/PurchaseOrder", "
          customer", "Linject/Customer;");
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}

有关如何注入方法的示例,请参阅this project


  

有了这些,我完全避免了反思的代价。

因为这一切都将在运行时完成:

  • 此解析和代码生成需要预付费用
  • 你需要以某种方式发现和反省这些生成的类型

答案 1 :(得分:4)

使用Javassist的示例,但它确实要求您的属性具有包级别保护而不是私有

public class AccessorGenerator {

    private final ClassPool pool;

    public PropertyGenerator() {
        pool = new ClassPool();
        pool.appendSystemPath();
    }

    public Map<String, PropertyAccessor> createAccessors(Class<?> klazz) throws Exception {
        Field[] fields = klazz.getDeclaredFields();

        Map<String, PropertyAccessor> temp = new HashMap<String, PropertyAccessor>();
        for (Field field : fields) {
            PropertyAccessor accessor = createAccessor(klazz, field);
            temp.put(field.getName(), accessor);
        }

        return Collections.unmodifiableMap(temp);
    }

    private PropertyAccessor createAccessor(Class<?> klazz, Field field) throws Exception {
        final String classTemplate = "%s_%s_accessor";
        final String getTemplate = "public Object get(Object source) { return ((%s)source).%s; }";
        final String setTemplate = "public void set(Object dest, Object value) { return ((%s)dest).%s = (%s) value; }";

        final String getMethod = String.format(getTemplate, 
                                               klazz.getName(),
                                               field.getName());
        final String setMethod = String.format(setTemplate, 
                                               klazz.getName(), 
                                               field.getName(), 
                                               field.getType().getName());

        final String className = String.format(classTemplate, klazz.getName(), field.getName());

        CtClass ctClass = pool.makeClass(className);
        ctClass.addMethod(CtNewMethod.make(getMethod, ctClass));
        ctClass.addMethod(CtNewMethod.make(setMethod, ctClass));
        ctClass.setInterfaces(new CtClass[] { pool.get(PropertyAccessor.class.getName()) });
        Class<?> generated = ctClass.toClass();
        return (PropertyAccessor) generated.newInstance();
    }

    public static void main(String[] args) throws Exception {
        AccessorGenerator generator = new AccessorGenerator();

        Map<String, PropertyAccessor> accessorsByName = generator.createAccessors(PurchaseOrder.class);

        PurchaseOrder purchaseOrder = new PurchaseOrder("foo", new Customer());

        accessorsByName.get("name").set(purchaseOrder, "bar");
        String name = (String) accessorsByName.get("name").get(purchaseOrder);
        System.out.println(name);
    }
}

答案 2 :(得分:1)

您也可以使用注释处理器,从而避免字节码操作的复杂性。 (见this article on javabeat

答案 3 :(得分:0)

我很惊讶反思是这么慢。如果你预热JVM,它不应该比直接访问慢5倍。 BTW微型基准测试可以提供误导的结果,因为如果没有真正的工作,简单的getter / setter可以轻松优化为零。

另一种避免反射和字节代码的方法是使用sun.misc.Unsafe类。它必须小心处理,并且不能移植到所有JVM,但它比反射快2-3倍。请参阅我的本质 - rmi项目中的示例。

另一个选择是生成代码并动态编译。您可以使用Compiler API或BeanShell等库。

注意:如果您有私有字段,则无法使用字节代码从另一个类访问它。这是JVM限制。内部类和嵌套类通过为您生成访问器方法来避免这种情况,例如使用私有方法在类中访问$ 100(您可能已经在调用堆栈中看到过这些)但是,这意味着您无法在不更改原始字段的情况下添加类来访问私有字段类。

答案 4 :(得分:-2)

  

目标是表现!

是的,这在很多情况下都是目标。但是你现在用PropertyAccessor做的事情你的表现会下降!每次要获取或设置属性时,都必须为customer_Field创建新实例。或者你必须保留你的实例。我不知道简单的getter或setter是什么问题。

public class PurchaseOrder {

   @Property
   private Customer customer;

   @Property
   private String name;

   pulic void setCustomer(Customer c)
   {
       this.customer = c;
   }

   public Customer getCustomer()
   {
       return customer;
   }

   // The same for name
}

这是表演!本机代码可能快14倍,但你真的需要那么快吗? Java很棒。为什么?因为它具有平台独立性。如果你要制作原生的东西,Java的力量就会消失。那么等待一分钟完成程序需要做的事情和等待50秒之间的区别是什么。 “我的速度提高了14倍?”您不仅需要获取和设置。您需要对所有数据执行某些操作。

我认为它不会更快,因为你只是在设置对象实例和基元。 Native Java是为:

创建的
  • 必须计算机器代码中比使用Java运行时环境更快的东西的方法(许多java.lang.Math方法,如sqrt()。他们可以用Java编程,但它会是慢)
  • Java无法独立完成的事情,例如:退出应用程序,创建套接字,写入/读取文件,调用其他进程等等......这不是纯粹的Java,它就是Native Machine代码。

所以我希望我说服你,你会用Java保留它。