本文首先介绍了和 ASM 使用相关的四种文件,以及它们之间的相互转化。然后结合 web services 开发实例,介绍了使用 wsgen 开发时遇到的实际问题,如何写一个 ASM 的适配器去修改现有 class,最终如何产生 web services 文件。
ASM 是一个多用途的 Java 字节码操控和分析框架。它能以二进制的形式直接修改现有的类或动态生成的类。 ASM 提供了常用的转换和分析算法,是一种允许用户方便的组装定制化的复杂转换和代码分析工具。 ASM 也提供和其它的字节码工具类似的功能,但 ASM 的重点是使用的简单和高性能。由于 ASM 在设计实现的时候尽可能 的小的内存占用和提供更高的性能;因此在动态系统中使用 ASM 是很有优势的。ASM 作为一种轻量级、高性能的 Java 字节码操控和分析框架,设计了一种更有效的方法、提供更好的性能和内存占用。今天, ASM 在许多领域都有应用,并已成为事实上的字节码处理框架标准。
先写一个不带 web services 标注的 SayHelloImpl 类。
清单 1. 不带 web services 的 SayHelloImpl 类
public class SayHelloImpl {
public String sayHello(String s) {
return “Hello: ” + s ;
}
}
|
再用 wsgen 来创建 web services,wsgen 的使用如下:
清单 2. wsgen 实例
wsgen.exe – cp bin – s output – d output – r output – wsdl ibm.was.asm.SayHelloImpl |
我们将会遇到如下错误:
清单 3. 根据不带 web services 标注的类创建 web services 时遇到的错误信息
注释处理过程中遇到问题;
......
com.sun.tools.internal.ws.processor.modeler.ModelerException: [failed to localiz
e] A web service endpoint could not be found()
at com.sun.tools.internal.ws.processor.modeler.annotation.WebServiceAP.o
nError(WebServiceAP.java:215)
at com.sun.tools.internal.ws.processor.modeler.annotation.WebServiceAP.b
uildModel(WebServiceAP.java:322)
at com.sun.tools.internal.ws.processor.modeler.annotation.WebServiceAP.p
rocess(WebServiceAP.java:256)
at com.sun.mirror.apt.AnnotationProcessors$CompositeAnnotationProcessor.
process(AnnotationProcessors.java:60)
|
注意:我们通常是在 class 前使用 @WebService, 在方法前使用 @WebMethod 来将一个类标注为 web services 的。WAS 6.1 的 web services 功能部件包和 WAS 7 都支持基于 JAX-WS 标准 的 web services 的开发, 在 WAS 安装目录的 bin 目录下提供了命令行工具 wsgen,wsgen 支持自底向上的开发方式。 下面是 wsgen 的用法:
清单 4. wsgen 命令格式
注 wsgen [options] [SEI] |
主要选项:
- -d 指定生成的 class 文件位置;
- -s 指定生成的 Java 源文件位置;
- -r 指定生成的 resources 文件位置,如 wsdl,xsd;
- -cp 指定服务实现类所在的位置;
- -wsdl,-servicename,-portname 三个参数指定生成的 wsdl 文件中的 service 和 port 的名称。
SEI(Service Endpoint Interface) 是一个 endpoint implementation class,不能是一个 interface。所以首先要开发一个 endpoint 的实现类,如本例中的 SayHelloImpl 。用 @WebService 声明 web services ,然后将它编译,才能提供给 wsgen 来创建 web services 。
下面介绍本文要用到的四个概念(Java Source,Java Class,ASM Code,ASM Source)
- Java Source: 即我们通常编写的 Java 源文件;
- Java Class: Java 源文件编译后的字节码文件;
- ASM Source : 类似于对 class 反编译后的源文件,也就是 "textual byte code",但比原始的 Java Source 可读性要差, 参见清单 5。
- ASM Code: 指读写 class 文件的 ASM 程序代码。需要用到 ASM 提供的 API, 参见清单 6。
清单 5. SayHelloImpl 类对应的 ASM Source
// class version 50.0 (50)
// access flags 33
public class com/ibm/was/asm/SayHelloImpl {
// access flags 1
public <init>()V
ALOAD 0
INVOKESPECIAL java/lang/Object. <init> ()V
RETURN
MAXSTACK = 1
MAXLOCALS = 1
// access flags 1
public sayHello(Ljava/lang/String;)Ljava/lang/String;
NEW java/lang/StringBuilder
DUP
LDC "Hello:"
INVOKESPECIAL java/lang/StringBuilder. <init> (Ljava/lang/String;)V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)
Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
MAXSTACK = 3
MAXLOCALS = 2
}
|
清单 6. 生成 SayHelloImpl.class 的 ASM Code
package asm.com.ibm.was.asm;
import java.util.*;
import org.objectweb.asm.*;
import org.objectweb.asm.attrs.*;
public class SayHelloImplDump implements Opcodes {
public static byte[] dump () throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "com/ibm/was/asm/SayHelloImpl",
null, "java/lang/Object", null);
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "sayHello",
"(Ljava/lang/String;)Ljava/lang/String;", null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitLdcInsn("Hello:");
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder",
"<init>", "(Ljava/lang/String;)V");
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
"append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
"toString", "()Ljava/lang/String;");
mv.visitInsn(ARETURN);
mv.visitMaxs(3, 2);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
|
图 1 描述了 Java Source 文件、Java Class 文件、ASM Code 文件和 ASM Source 文件之间的转换关系。
图 1. 四种文件的转换图
如图 -1 所示,Java Source 文件通过 Javac 可以转换为 Java Class 文件。而相应的 Java Class 文件通过 Java 反编译器工具可转换为 Java Source 文件;
通 过 ASM Code 去创建和修改 Java Class 需要对 ASM API 比较熟悉才行,一个常见的问题时,怎么用 ASM Code 生成一个我们希望的 class 文件, 也就是说, 给定了 Java Class, 怎么得到其对应的 ASM Code 呢? 所幸的是, ASM 框架为我们提供了 ASMifierClassVisitor 工具来产生 Java Class 对应的 ASM Code。代码如下:
清单 7. 给定 class 文件,通过 ASMifierClassVisitor 获得其对应的 ASM Code
public void showClassASMProcess() {
ClassReader cr;
final String n = SayHelloImpl.class.getName();
try{
cr = new ClassReader(n);
cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)),
new Attribute[0],
ClassReader.SKIP_DEBUG);
} catch (Exception e) {
e.printStackTrace();
}
}
|
另外一个重要的转换就是, 给定了 Java Class 文件, 如何查看它的 ASM Source ? 这点对于我们验证 ASM Code 生成的 class 是否正确很有用。ASM 提供了另外一个工具 TraceClassVisitor, 来获得一个 Java Class 对应的 ASM Source。代码如下:
清单 8. 利用 TraceClassVisitor 查看 class 文件的 ASM Source
//file 是 class 文件的全路径名
public void showClassSource(String file) {
FileInputStream is ;
ClassReader cr;
try {
is = new FileInputStream(file);
cr = new ClassReader(is);
TraceClassVisitor trace = new TraceClassVisitor(new PrintWriter(System.out));
cr.accept(trace, ClassReader.SKIP_DEBUG);
is.close() ;
} catch (Exception e) {
e.printStackTrace() ;
}
}
{
e.printStackTrace();
}
}
|
ASM 类注入是指修改一个现有的 class 文件,在其中加入自己的代码。按通常思维,我们需要利用 ASM 提供的 reader 类去读取所要修改的类文件, 找到要修改的地方,如方法名或属性名,然后在该处插入自己的代码或修改现有的代码,这种思路将使问题复杂化。 ASM 为我们提供了访问类文件的 Visitor 模式,来遍历一个 class 文件,Visitor 是一个实现 ClassVisitor 接口的类。 要注入一个 class 文件,先要找到一个合适的 Visitor 做向导, ASM 提供了好几种 Visitor, 最常用的是 ClassWriter。同时我们需要提供一个适配器类 ClassAdapter, Visitor 会带着这个 Adapter 一起去遍历, 然后在遍历过程中回调 Adapter 提供的方法。我们在 Adapter 的这些方法中就可以实现我们的修改和定制逻辑。当然,如果你不想做任何修改,那 Visitor 遍历完后将得到一个和被遍历的 class 完全一样的拷贝。
清单 9. 类注入的完整过程
public void addAnnotationToExistingClass() {
FileInputStream is ;
ClassReader cr;
try {
String classfile = "E:\\SayHelloImpl.class" ; // 待遍历的类
showClassSource(classfile) ; // 打印该类的 ASM source
is = new FileInputStream(classfile);
cr = new ClassReader(is);
// 此处我们使用 ClassWriter 做 Visitor
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 给 Visitor 提供一个 Adapter
AddAnnotationAdapter adapter = new AddAnnotationAdapter(cw);
cr.accept(adapter, ClassReader.SKIP_DEBUG);
// 遍历完后,生成的 class 保存在字节数组中
byte[] b = cw.toByteArray();
try {
// 将字节数组输出到文件中
// 为了不至于覆盖掉原有的 SayHelloImpl.class 文件,我们将结果输出到 _SayHelloImpl.class
FileOutputStream fout = new FileOutputStream("E:\\_SayHelloImpl.class");
fout.write(b) ;
fout.flush();
fout.close() ;
} catch (Exception e) {
e.printStackTrace() ;
}
// 验证 _SayHelloImpl.class 是否包含我们注入的代码
showClassSource("E:\\_SayHelloImpl.class") ;
} catch (Exception e) {
e.printStackTrace();
}
}
|
我们已经知道了如何往 class 中注入自己的代码,那对于一个已有的 Java bean,怎么往里注入 @WebService 和 @WebMethod 呢?根据上面我们讲的转换关系,我们可以先写一个带 annotation 的 Java Source,编译得到 Java Class, 再得到其 ASM Code。下面是利用 ASM 给一个不带任何 Annotation 的 class 添加 @WebService 和 @WebMethod 的步骤。
- 首先在 SayHelloImpl.java 中添加 @WebService 和 @WebMethod:
清单 10. 带 web services 标注的 SayHelloImpl 类@WebService public class SayHelloImpl { @WebMethod public String sayHello(String s) { return "Hello: " + s ; } } - 通过 ASMifierClassVisitor 工具获得 SayHelloImpl class 的 ASM Code,见清单 6。
- 写一个 ClassAdapter 类, ClassAdapter 其实也是一个 Visitor。根据 ASM Code 的提示, 我们就可以知道如何利用 ASM API 去获得我们希望的 class 文件。下面是 Adapter 的示例代码。
清单 11. 添加 Annotation 的 Adapter 类public class AddAnnotationAdapter extends ClassAdapter implements Opcodes{ //private String annotationDesc; private boolean isAnnotationPresent;
public AddAnnotationAdapter(ClassVisitor cv) { super(cv); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(V1_6, ACC_PUBLIC + ACC_SUPER, name, signature, superName, interfaces); AnnotationVisitor av0; av0 = cv.visitAnnotation("Ljavax/jws/WebService;", true); av0.visitEnd(); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return cv.visitAnnotation(desc, visible); } @Override public void visitInnerClass(String name, String outerName, String innerName, int access) { cv.visitInnerClass(name, outerName, innerName, access); } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { return cv.visitField(access, name, desc, signature, value); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if (!name.equals("<init>")) { AnnotationVisitor av0; av0 = mv.visitAnnotation("Ljavax/jws/WebMethod;", true); av0.visitEnd(); } return mv; } @Override public void visitEnd() { cv.visitEnd(); } }
最后,我们采用上面讲的类注入办法,即可得到一个带 @WebService 和 @WebMethod 标注的 class 文件。再通过 wsgen 工具即可成功创建 web services,该命令将会在 src 目录下生成 web services 描述文件:SayHelloImplService.wsdl,SayHelloImplService_schema1.xsd,同时生成了 2 个 JAX-WS 文件,分别为:SayHello.java,SayHelloResponse.java。
当然,在实际应用中,我们可能需要将一个 class 的某些特定方法发布为 web services,这就需要我们对 Adapter 做进一步的改造,在 visitMethod() 方法中根据方法名称等参数做进一步的处理。
本文简要介绍了 ASM 字节码工具及其在 web services 开发中的应用。web services 的使用已经越来越广泛, 但在改造遗留系统过程中会遇到不少问题,本文针对其中一个主要问题给出了解决办法,该方法无须对现有系统做大量的改动, 即可将现存的 Java bean 转化成 web services 。本文对那些正考虑迁移到 JAX-WS 编程模型上的项目有一定的参考价值。
学习
- 获得 ASM 的更多资料。
- 访问 WebSphere Application Server V7 信息中心
- 利用 WAS 6.1 web services 功能部件包开发 JAX-WS 2.0 web services
获得产品和技术
- 最受欢迎的 WebSphere 试用软件下载:下载关键 WebSphere 产品的免费试用版。
- IBM developerWorks 软件下载资源中心:IBM deveperWorks 最新的软件下载。
- IBM developerWorks 工具包:下载关键 WebSphere 最新的产品工具包。
讨论
-
IBM developerWorks 社区 BPM 群组:为开发人员设立的 BPM 群组,了解业务流程管理解决方案的最新技术资源。
- 加入 developerWorks 中文社区,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。
- 加入 IBM 软件下载与技术交流群组,参与在线交流。