既然您已经了解了如何使用Javassist和BCEL框架进行类工作(请参阅本系列以前的文章的清单 ),我将向您展示一个实用的类工作应用程序。 该应用程序正在运行时生成并立即加载到JVM中的类取代了对反射的使用。 在将其组合在一起的过程中,我将参考该系列的前两篇文章以及Javassist和BCEL的内容,因此对于原来的较长系列而言,它是一个很好的总结。文章。
对绩效的思考
在第二部分中 ,我展示了对于字段访问和方法调用,反射比直接代码要慢许多倍。 对于许多应用程序而言,这种迟缓不是问题,但是在某些情况下,性能至关重要。 在这些情况下,反射可能代表真正的瓶颈。 但是,用静态编译的代码替换反射可能会很混乱,并且在某些情况下(如在框架中,反射时访问的类或项目是在运行时提供的,而不是作为同一构建过程的一部分提供的)即使不进行重组也无法实现。整个应用程序。
类工作为您提供了一种将静态编译代码的性能与反射灵活性相结合的替代方法。 此处的基本方法是在运行时构造一个自定义类,该类将包装对目标类的访问(以前是通过反射实现的),使用的方式可以由通用代码使用。 将自定义类加载到JVM之后,然后设置为全速运行。
做好准备
清单1给出了该应用程序的起点。 在这里,我定义了一个简单的bean类HolderBean
和一个访问类ReflectAccess
。 访问类采用单个命令行参数,该参数必须是一个int
值的Bean类属性( value1
或value2
)之一的名称。 它增加命名属性的值,然后在退出之前打印出两个属性值。
清单1.反映一个bean
public class HolderBean
{
private int m_value1;
private int m_value2;
public int getValue1() {
return m_value1;
}
public void setValue1(int value) {
m_value1 = value;
}
public int getValue2() {
return m_value2;
}
public void setValue2(int value) {
m_value2 = value;
}
}
public class ReflectAccess
{
public void run(String[] args) throws Exception {
if (args.length == 1 && args[0].length() > 0) {
// create property name
char lead = args[0].charAt(0);
String pname = Character.toUpperCase(lead) +
args[0].substring(1);
// look up the get and set methods
Method gmeth = HolderBean.class.getDeclaredMethod
("get" + pname, new Class[0]);
Method smeth = HolderBean.class.getDeclaredMethod
("set" + pname, new Class[] { int.class });
// increment value using reflection
HolderBean bean = new HolderBean();
Object start = gmeth.invoke(bean, null);
int incr = ((Integer)start).intValue() + 1;
smeth.invoke(bean, new Object[] {new Integer(incr)});
// print the ending values
System.out.println("Result values " +
bean.getValue1() + ", " + bean.getValue2());
} else {
System.out.println("Usage: ReflectAccess value1|value2");
}
}
}
这是ReflectAccess
的两个示例运行,以说明结果:
[dennis]$ java -cp . ReflectAccess value1
Result values 1, 0
[dennis]$ java -cp . ReflectAccess value2
Result values 0, 1
建立胶水课
现在,我已经演示了代码的反射版本,我将向您展示如何用生成的类替换反射。 使这种替换正常工作涉及一个微妙的问题,可以追溯到本系列第1部分中有关类加载的讨论。 问题是我要在运行时生成一个类,该类要从访问类的静态编译代码访问,但是由于生成的类对于编译器不存在,因此无法直接引用它。
那么如何将静态编译的代码链接到生成的类呢? 基本解决方案是定义可由静态编译的代码访问的基类或接口,然后扩展该基类或在生成的类中实现该接口。 静态编译的代码然后可以直接调用方法,即使方法要到运行时才真正实现。
在清单2中,我定义了一个接口IAccess
,旨在为生成的代码提供此链接。 该界面包括三种方法。 第一种方法只是设置要访问的目标对象。 另外两个方法是用于访问int
属性值的get和set方法的代理。
清单2.胶水类的接口
public interface IAccess
{
public void setTarget(Object target);
public int getValue();
public void setValue(int value);
}
目的是生成的IAccess
接口实现将提供代码以调用目标类的相应get和set方法。 清单3显示了如何实现此接口的示例,假设我想访问清单1 HolderBean
类的value1
属性:
清单3. Glue类示例实现
public class AccessValue1 implements IAccess
{
private HolderBean m_target;
public void setTarget(Object target) {
m_target = (HolderBean)target;
}
public int getValue() {
return m_target.getValue1();
}
public void setValue(int value) {
m_target.setValue1(value);
}
}
清单2接口被设计为与特定类型的对象的特定属性一起使用。 该接口使实现代码保持简单-在使用字节码时始终是一个优点-但意味着实现类非常具体。 我要通过此接口访问的每种对象和属性类型都需要有一个单独的实现类,这限制了将此方法用作反射的常规替代方法。 只要仅在反射性能确实是瓶颈的情况下有选择地应用该技术,此限制就不会成为问题。
用Javassist生成
使用Javassist为清单2的 IAccess
接口生成实现类很容易-我只需要创建一个实现该接口的新类,为目标对象引用添加一个成员变量,然后通过添加一个无参数的构造函数和简单的实现方法。 清单4显示了完成这些步骤的Javassist代码,结构为一个方法调用,该方法调用接收目标类和获取/设置方法信息,并返回所构造类的二进制表示形式:
清单4. Javassist胶水类构造
/** Parameter types for call with no parameters. */
private static final CtClass[] NO_ARGS = {};
/** Parameter types for call with single int value. */
private static final CtClass[] INT_ARGS = { CtClass.intType };
protected byte[] createAccess(Class tclas, Method gmeth,
Method smeth, String cname) throws Exception {
// build generator for the new class
String tname = tclas.getName();
ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass(cname);
clas.addInterface(pool.get("IAccess"));
CtClass target = pool.get(tname);
// add target object field to class
CtField field = new CtField(target, "m_target", clas);
clas.addField(field);
// add public default constructor method to class
CtConstructor cons = new CtConstructor(NO_ARGS, clas);
cons.setBody(";");
clas.addConstructor(cons);
// add public setTarget method
CtMethod meth = new CtMethod(CtClass.voidType, "setTarget",
new CtClass[] { pool.get("java.lang.Object") }, clas);
meth.setBody("m_target = (" + tclas.getName() + ")$1;");
clas.addMethod(meth);
// add public getValue method
meth = new CtMethod(CtClass.intType, "getValue", NO_ARGS, clas);
meth.setBody("return m_target." + gmeth.getName() + "();");
clas.addMethod(meth);
// add public setValue method
meth = new CtMethod(CtClass.voidType, "setValue", INT_ARGS, clas);
meth.setBody("m_target." + smeth.getName() + "($1);");
clas.addMethod(meth);
// return binary representation of completed class
return clas.toBytecode();
}
我不打算通过这个代码的任何细节运行,因为,如果你一直在关注这个系列中,大部分的操作都已经很熟悉(如果您还没有该系列下,检查了部分5现在使用Javassist的概述)。
用BCEL生成
使用BCEL为清单2的 IAccess
生成实现类并不像使用Javassist那样容易,但是仍然不是很复杂。 清单5给出了用于此目的的代码。 该代码使用与清单4 Javassist代码相同的操作序列,但是由于需要为BCEL拼出每个字节码指令,因此运行时间更长。 与Javassist版本一样,我将跳过实现的详细信息(如果不熟悉任何内容,请参阅第7部分以获取BCEL的概述)。
清单5. BCEL胶水类构造
/** Parameter types for call with single int value. */
private static final Type[] INT_ARGS = { Type.INT };
/** Utility method for adding constructed method to class. */
private static void addMethod(MethodGen mgen, ClassGen cgen) {
mgen.setMaxStack();
mgen.setMaxLocals();
InstructionList ilist = mgen.getInstructionList();
Method method = mgen.getMethod();
ilist.dispose();
cgen.addMethod(method);
}
protected byte[] createAccess(Class tclas,
java.lang.reflect.Method gmeth, java.lang.reflect.Method smeth,
String cname) {
// build generators for the new class
String tname = tclas.getName();
ClassGen cgen = new ClassGen(cname, "java.lang.Object",
cname + ".java", Constants.ACC_PUBLIC,
new String[] { "IAccess" });
InstructionFactory ifact = new InstructionFactory(cgen);
ConstantPoolGen pgen = cgen.getConstantPool();
//. add target object field to class
FieldGen fgen = new FieldGen(Constants.ACC_PRIVATE,
new ObjectType(tname), "m_target", pgen);
cgen.addField(fgen.getField());
int findex = pgen.addFieldref(cname, "m_target",
Utility.getSignature(tname));
// create instruction list for default constructor
InstructionList ilist = new InstructionList();
ilist.append(InstructionConstants.ALOAD_0);
ilist.append(ifact.createInvoke("java.lang.Object", "<init>",
Type.VOID, Type.NO_ARGS, Constants.INVOKESPECIAL));
ilist.append(InstructionFactory.createReturn(Type.VOID));
// add public default constructor method to class
MethodGen mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID,
Type.NO_ARGS, null, "<init>", cname, ilist, pgen);
addMethod(mgen, cgen);
// create instruction list for setTarget method
ilist = new InstructionList();
ilist.append(InstructionConstants.ALOAD_0);
ilist.append(InstructionConstants.ALOAD_1);
ilist.append(new CHECKCAST(pgen.addClass(tname)));
ilist.append(new PUTFIELD(findex));
ilist.append(InstructionConstants.RETURN);
// add public setTarget method
mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID,
new Type[] { Type.OBJECT }, null, "setTarget", cname,
ilist, pgen);
addMethod(mgen, cgen);
// create instruction list for getValue method
ilist = new InstructionList();
ilist.append(InstructionConstants.ALOAD_0);
ilist.append(new GETFIELD(findex));
ilist.append(ifact.createInvoke(tname, gmeth.getName(),
Type.INT, Type.NO_ARGS, Constants.INVOKEVIRTUAL));
ilist.append(InstructionConstants.IRETURN);
// add public getValue method
mgen = new MethodGen(Constants.ACC_PUBLIC, Type.INT,
Type.NO_ARGS, null, "getValue", cname, ilist, pgen);
addMethod(mgen, cgen);
// create instruction list for setValue method
ilist = new InstructionList();
ilist.append(InstructionConstants.ALOAD_0);
ilist.append(new GETFIELD(findex));
ilist.append(InstructionConstants.ILOAD_1);
ilist.append(ifact.createInvoke(tname, smeth.getName(),
Type.VOID, INT_ARGS, Constants.INVOKEVIRTUAL));
ilist.append(InstructionConstants.RETURN);
// add public setValue method
mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID,
INT_ARGS, null, "setValue", cname, ilist, pgen);
addMethod(mgen, cgen);
// return bytecode of completed class
return cgen.getJavaClass().getBytes();
}
性能检查
现在,我已经获得了用于方法构造的Javassist版和BCEL版的代码,现在可以尝试一下它们,看看它们的工作效果如何。 我在运行时生成代码的最初原因是用更快的速度替换反射,因此最好进行性能比较以了解我的成功程度。 为了使它有趣,我还将看看用每个框架构造胶水类所需的时间。
清单6显示了我将用来检查性能的测试代码的主要部分。 runReflection()
方法运行测试的反射部分, runAccess()
运行直接访问部分, run()
控制整个过程(包括打印计时结果)。 runReflection()
和runAccess()
将要执行的循环数作为参数,而循环数又从命令行传递(使用此清单中未显示的代码,但包含在下载中)。 DirectLoader
类(清单6的末尾)仅提供了一种加载生成的类的简便方法。
清单6.性能测试代码
/** Run timed loop using reflection for access to value. */
private int runReflection(int num, Method gmeth, Method smeth,
Object obj) {
int value = 0;
try {
Object[] gargs = new Object[0];
Object[] sargs = new Object[1];
for (int i = 0; i < num; i++) {
// messy usage of Integer values required in loop
Object result = gmeth.invoke(obj, gargs);
value = ((Integer)result).intValue() + 1;
sargs[0] = new Integer(value);
smeth.invoke(obj, sargs);
}
} catch (Exception ex) {
ex.printStackTrace(System.err);
System.exit(1);
}
return value;
}
/** Run timed loop using generated class for access to value. */
private int runAccess(int num, IAccess access, Object obj) {
access.setTarget(obj);
int value = 0;
for (int i = 0; i < num; i++) {
value = access.getValue() + 1;
access.setValue(value);
}
return value;
}
public void run(String name, int count) throws Exception {
// get instance and access methods
HolderBean bean = new HolderBean();
String pname = name;
char lead = pname.charAt(0);
pname = Character.toUpperCase(lead) + pname.substring(1);
Method gmeth = null;
Method smeth = null;
try {
gmeth = HolderBean.class.getDeclaredMethod("get" + pname,
new Class[0]);
smeth = HolderBean.class.getDeclaredMethod("set" + pname,
new Class[] { int.class });
} catch (Exception ex) {
System.err.println("No methods found for property " + pname);
ex.printStackTrace(System.err);
return;
}
// create the access class as a byte array
long base = System.currentTimeMillis();
String cname = "IAccess$impl_HolderBean_" + gmeth.getName() +
"_" + smeth.getName();
byte[] bytes = createAccess(HolderBean.class, gmeth, smeth, cname);
// load and construct an instance of the class
Class clas = s_classLoader.load(cname, bytes);
IAccess access = null;
try {
access = (IAccess)clas.newInstance();
} catch (IllegalAccessException ex) {
ex.printStackTrace(System.err);
System.exit(1);
} catch (InstantiationException ex) {
ex.printStackTrace(System.err);
System.exit(1);
}
System.out.println("Generate and load time of " +
(System.currentTimeMillis()-base) + " ms.");
// run the timing comparison
long start = System.currentTimeMillis();
int result = runReflection(count, gmeth, smeth, bean);
long time = System.currentTimeMillis() - start;
System.out.println("Reflection took " + time +
" ms. with result " + result + " (" + bean.getValue1() +
", " + bean.getValue2() + ")");
bean.setValue1(0);
bean.setValue2(0);
start = System.currentTimeMillis();
result = runAccess(count, access, bean);
time = System.currentTimeMillis() - start;
System.out.println("Generated took " + time +
" ms. with result " + result + " (" + bean.getValue1() +
", " + bean.getValue2() + ")");
}
/** Simple-minded loader for constructed classes. */
protected static class DirectLoader extends SecureClassLoader
{
protected DirectLoader() {
super(TimeCalls.class.getClassLoader());
}
protected Class load(String name, byte[] data) {
return super.defineClass(name, data, 0, data.length);
}
}
为了进行简单的计时测试,我两次调用run()
方法,一次对清单1 HolderBean
类中的每个属性。 运行两次测试通过对于合理公平的测试很重要-代码的第一次通过将加载所有必需的类,这给Javassist和BCEL类生成过程增加了很多开销。 但是,第二遍不需要此开销,因此可以更好地估计在实际系统中使用类生成时需要多长时间。 这是执行测试时生成的输出的示例:
[dennis]$$ java -cp .:bcel.jar BCELCalls 2000
Generate and load time of 409 ms.
Reflection took 61 ms. with result 2000 (2000, 0)
Generated took 2 ms. with result 2000 (2000, 0)
Generate and load time of 1 ms.
Reflection took 13 ms. with result 2000 (0, 2000)
Generated took 2 ms. with result 2000 (0, 2000)
图1显示了在2K到512K的循环计数范围内调用时此计时测试的结果(该测试在运行Mandrake Linux 9.1的Athlon 2200+ XP系统上运行,使用Sun的1.4.2 JVM)。 在这里,我在每次测试运行中都包含了第二个属性的反射时间和生成的代码时间(因此,使用Javassist代码生成的时间是第一次,然后使用BCEL代码生成的时间是相同的)。 无论使用Javassist还是BCEL生成粘合类,执行时间都差不多,这是我希望看到的-但是进行确认总是很好的!
图1.反射与生成的代码速度(时间以毫秒为单位)
![反射与生成的代码速度](https://i-blog.csdnimg.cn/blog_migrate/f369e96f977a7d560e6e7140852e130a.png)
从图1可以看到,在每种情况下,生成的代码的执行速度都比反射执行的速度快得多。 生成的代码的速度优势随着循环数量的增加而增加,从2K循环大约以5:1开始,而到512K循环则以大约24:1开始。 对于Javassist,构造和加载第一个胶水类大约需要320毫秒(ms),而对于BCEL来说,则要花费370 ms,而对于Javassist,构造第二胶水类仅花费大约4 ms;对于BCEL,则要花费2 ms(尽管时钟分辨率只有1 ms ,因此这些时间非常艰难)。 如果将这些时间结合起来,您会发现,即使是在2K循环的情况下,生成类也将比使用反射提供总体上更好的性能(总执行时间约为4毫秒至6毫秒,而反射约为14毫秒)。
实际上,这种情况似乎更偏向于生成的代码,而不是该图表似乎表明的那样。 当我尝试低至25个循环时,反射代码仍然需要6到7毫秒才能执行,而生成的代码注册起来却太快了。 相对于较少的循环计数,反射所花费的时间似乎反映了在达到阈值时JVM中正在进行的一些优化。 如果我将循环计数降低到大约20以下,则反射代码也变得太快而无法注册。
加快您的前进速度
现在,您已经看到了运行时类工作可以为您的应用程序提供的性能。 下一次遇到棘手的性能优化问题时,请记住这一点-它可能只是可以省去大量重新设计工作的魔术子弹。 但是,课堂工作不仅对性能有好处。 这也是一种独特的灵活方法,可根据运行时要求定制您的应用程序。 即使您从来没有理由在代码中使用它,我也认为这是Java的功能之一,它使编程变得越来越有趣。
冒险进入类工作的实际应用程序,总结了有关Java编程动力学的系列文章。 但是请不要失望-当我介绍一些围绕Java字节码操作构建的工具时,您将很快有机会在developerWorks自助餐中试用其他一些类工作应用程序。 首先是有关Goose Goose的一对测试工具的文章。
翻译自: https://www.ibm.com/developerworks/java/library/j-dyn0610/index.html