Java字节码工具ASM在Web Service开发中的应用

Posted on

Java字节码工具ASM在Web Service开发中的应用

在基于 JAX-WS 标准的 web services 的开发中,不少实际场景都是希望采用自底向上的开发方式, 即基于已有的 Java bean 来创建 web services 。WebSphere Application Server ( 以下简称 WAS ) 提供了命令行的工具 wsgen 和相对应的 Ant task 来支持这种开发过程,而且这两个工具比较适合大型项目的自动化构建。 这两个工具的使用前提是 Java bean 中事先添加有 web services 的 Annotation 标注,而在现有的业务系统中,class 文件一般是不带 Annotation 的,这就需要开发人员去修改现有的代码,以手工方式添加 Annotation,但这样带来的工作量太大且容易出错。本文介绍了一种解决途径,可以不用修改源代码,而是利用字节码工具 ASM 直接修改 class 文件, 在字节码文件中自动注入 Annotation ,然后再利用 wsgen 工具就可以很方便地生成 web services 应用。 本文同时也总结了使用 ASM 的一些实践。

本文首先介绍了和 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 类1

public

class

SayHelloImpl {

2

public

String sayHello(String s) { 3

return

“Hello: ” + s ;

4

} 5

}

再用 wsgen 来创建 web services,wsgen 的使用如下: 清单 2. wsgen 实例1

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 Source01

// class version 50.0 (50)

02

// access flags 33 03

public

class

com/ibm/was/asm/SayHelloImpl {

04

05

// access flags 1

06

public

()V 07

ALOAD

0

08

INVOKESPECIAL java/lang/Object. ()V 09

RETURN

10

MAXSTACK =

1 11

MAXLOCALS =

1

12

13

// access flags 1

14

public

sayHello(Ljava/lang/String;)Ljava/lang/String; 15

NEW java/lang/StringBuilder

16

DUP 17

LDC

"Hello:"

18

INVOKESPECIAL java/lang/StringBuilder. (Ljava/lang/String;)V 19

ALOAD

1

20

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) 21

Ljava/lang/StringBuilder;

22

INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 23

ARETURN

24

MAXSTACK =

3 25

MAXLOCALS =

2

26

}清单 6. 生成 SayHelloImpl.class 的 ASM Codeview sourceprint?

01

package

asm.com.ibm.was.asm;

02

import

java.util./*; 03

import

org.objectweb.asm./*;

04

import

org.objectweb.asm.attrs./*; 05

public

class

SayHelloImplDump

implements

Opcodes {

06

public

static

byte

[] dump ()

throws

Exception { 07

ClassWriter cw =

new

ClassWriter(

0

);

08

FieldVisitor fv; 09

MethodVisitor mv;

10

AnnotationVisitor av0; 11

12

cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER,

"com/ibm/was/asm/SayHelloImpl"

, 13

null

,

"java/lang/Object"

,

null

);

14

15

{

16

mv = cw.visitMethod(ACC_PUBLIC,

""

,

"()V"

,

null

,

null

); 17

mv.visitCode();

18

mv.visitVarInsn(ALOAD,

0

); 19

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/Object"

,

""

,

"()V"

);

20

mv.visitInsn(RETURN); 21

mv.visitMaxs(

1

,

1

);

22

mv.visitEnd(); 23

}

24

{ 25

mv = cw.visitMethod(ACC_PUBLIC,

"sayHello"

,

26

"(Ljava/lang/String;)Ljava/lang/String;"

,

null

,

null

); 27

mv.visitCode();

28

mv.visitTypeInsn(NEW,

"java/lang/StringBuilder"

); 29

mv.visitInsn(DUP);

30

mv.visitLdcInsn(

"Hello:"

); 31

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/StringBuilder"

,

32

""

,

"(Ljava/lang/String;)V"

); 33

mv.visitVarInsn(ALOAD,

1

);

34

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 35

"append"

,

"(Ljava/lang/String;)Ljava/lang/StringBuilder;"

);

36

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 37

"toString"

,

"()Ljava/lang/String;"

);

38

mv.visitInsn(ARETURN); 39

mv.visitMaxs(

3

,

2

);

40

mv.visitEnd(); 41

}

42

cw.visitEnd(); 43

return

cw.toByteArray();

44

} 45

}

图 1 描述了 Java Source 文件、Java Class 文件、ASM Code 文件和 ASM Source 文件之间的转换关系。 图 1. 四种文件的转换图 图 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 Code01

public

void

showClassASMProcess() {

02

ClassReader cr; 03

final

String n = SayHelloImpl.

class

.getName();

04

05

try

{

06

cr =

new

ClassReader(n);
07

cr.accept(

new

ASMifierClassVisitor(

new

PrintWriter(System.out)),

08

new

Attribute[

0

], 09

ClassReader.SKIP_DEBUG);

10

11

}

catch

(Exception e) {

12

e.printStackTrace(); 13

}

14

}

另外一个重要的转换就是, 给定了 Java Class 文件, 如何查看它的 ASM Source ? 这点对于我们验证 ASM Code 生成的 class 是否正确很有用。ASM 提供了另外一个工具 TraceClassVisitor, 来获得一个 Java Class 对应的 ASM Source。代码如下: 清单 8. 利用 TraceClassVisitor 查看 class 文件的 ASM Source01

//file 是 class 文件的全路径名

02

public

void

showClassSource(String file) { 03

FileInputStream is ;

04

ClassReader cr; 05

06

try

{ 07

is =

new

FileInputStream(file);

08

cr =

new

ClassReader(is); 09

TraceClassVisitor trace =

new

TraceClassVisitor(

new

PrintWriter(System.out));

10

cr.accept(trace, ClassReader.SKIP_DEBUG); 11

12

is.close() ; 13

}

catch

(Exception e) {

14

e.printStackTrace() ; 15

}

16

} 17

{

18

e.printStackTrace(); 19

}

20

}

ASM 类注入

ASM 类注入是指修改一个现有的 class 文件,在其中加入自己的代码。按通常思维,我们需要利用 ASM 提供的 reader 类去读取所要修改的类文件, 找到要修改的地方,如方法名或属性名,然后在该处插入自己的代码或修改现有的代码,这种思路将使问题复杂化。 ASM 为我们提供了访问类文件的 Visitor 模式,来遍历一个 class 文件,Visitor 是一个实现 ClassVisitor 接口的类。 要注入一个 class 文件,先要找到一个合适的 Visitor 做向导, ASM 提供了好几种 Visitor, 最常用的是 ClassWriter。同时我们需要提供一个适配器类 ClassAdapter, Visitor 会带着这个 Adapter 一起去遍历, 然后在遍历过程中回调 Adapter 提供的方法。我们在 Adapter 的这些方法中就可以实现我们的修改和定制逻辑。当然,如果你不想做任何修改,那 Visitor 遍历完后将得到一个和被遍历的 class 完全一样的拷贝。 清单 9. 类注入的完整过程01

public

void

addAnnotationToExistingClass() {

02

FileInputStream is ; 03

ClassReader cr;

04

try

{
05

String classfile =

"E:\SayHelloImpl.class"

;

// 待遍历的类

06

showClassSource(classfile) ;

// 打印该类的 ASM source 07

08

is =

new

FileInputStream(classfile);
09

cr =

new

ClassReader(is);

10

// 此处我们使用 ClassWriter 做 Visitor 11

ClassWriter cw =

new

ClassWriter(ClassWriter.COMPUTE_MAXS);

12

// 给 Visitor 提供一个 Adapter 13

AddAnnotationAdapter adapter =

new

AddAnnotationAdapter(cw);

14

cr.accept(adapter, ClassReader.SKIP_DEBUG);
15

16

// 遍历完后,生成的 class 保存在字节数组中 17

byte

[] b = cw.toByteArray();

18

19

try

{

20

// 将字节数组输出到文件中 21

// 为了不至于覆盖掉原有的 SayHelloImpl.class 文件,我们将结果输出到 _SayHelloImpl.class

22

FileOutputStream fout =

new

FileOutputStream(

"E:\_SayHelloImpl.class"

); 23

fout.write(b) ;

24

fout.flush(); 25

fout.close() ;

26

}

catch

(Exception e) { 27

e.printStackTrace() ;

28

} 29

30

// 验证 _SayHelloImpl.class 是否包含我们注入的代码 31

showClassSource(

"E:\_SayHelloImpl.class"

) ;

32

}

catch

(Exception e) { 33

e.printStackTrace();

34

} 35

}

web services 标记注入过程

我们已经知道了如何往 class 中注入自己的代码,那对于一个已有的 Java bean,怎么往里注入 @WebService 和 @WebMethod 呢?根据上面我们讲的转换关系,我们可以先写一个带 annotation 的 Java Source,编译得到 Java Class, 再得到其 ASM Code。下面是利用 ASM 给一个不带任何 Annotation 的 class 添加 @WebService 和 @WebMethod 的步骤。

  1. 首先在 SayHelloImpl.java 中添加 @WebService 和 @WebMethod: 清单 10. 带 web services 标注的 SayHelloImpl 类1

@WebService

2

public

class

SayHelloImpl { 3

@WebMethod

4

public

String sayHello(String s) { 5

return

"Hello: "

  • s ;

6

} 7

}

  1. 通过 ASMifierClassVisitor 工具获得 SayHelloImpl class 的 ASM Code,见清单 6。
  2. 写一个 ClassAdapter 类, ClassAdapter 其实也是一个 Visitor。根据 ASM Code 的提示, 我们就可以知道如何利用 ASM API 去获得我们希望的 class 文件。下面是 Adapter 的示例代码。 清单 11. 添加 Annotation 的 Adapter 类01

public

class

AddAnnotationAdapter

extends

ClassAdapter

implements

Opcodes{

02

//private String annotationDesc; 03

private

boolean

isAnnotationPresent;

04

05

public

AddAnnotationAdapter(ClassVisitor cv) {

06

super

(cv); 07

}

08

09

@Override

10

public

void

visit(

int

version,

int

access, String name, String signature, 11

String superName, String[] interfaces) {

12

13

cv.visit(V1_6, ACC_PUBLIC + ACC_SUPER, name, signature, superName,

14

interfaces); 15

AnnotationVisitor av0;

16

av0 = cv.visitAnnotation(

"Ljavax/jws/WebService;"

,

true

); 17

av0.visitEnd();

18

} 19

20

@Override 21

public

AnnotationVisitor visitAnnotation(String desc,

boolean

visible) {

22

return

cv.visitAnnotation(desc, visible); 23

}

24

25

@Override

26

public

void

visitInnerClass(String name, String outerName, 27

String innerName,

int

access) {

28

cv.visitInnerClass(name, outerName, innerName, access); 29

}

30

31

@Override

32

public

FieldVisitor visitField(

int

access, String name, String desc, 33

String signature, Object value) {

34

return

cv.visitField(access, name, desc, signature, value); 35

}

36

37

@Override

38

public

MethodVisitor visitMethod(

int

access, String name, String desc, 39

String signature, String[] exceptions) {

40

MethodVisitor mv = cv.visitMethod(access, name, desc, signature, 41

exceptions);

42

if

(!name.equals(

""

)) { 43

AnnotationVisitor av0;

44

av0 = mv.visitAnnotation(

"Ljavax/jws/WebMethod;"

,

true

); 45

av0.visitEnd();

46

} 47

return

mv;

48

} 49

50

@Override 51

public

void

visitEnd() {

52

cv.visitEnd(); 53

}

54

55

}

最后,我们采用上面讲的类注入办法,即可得到一个带 @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 编程模型上的项目有一定的参考价值。 来源: [http://www.oschina.net/question/129540_28430](http://www.oschina.net/question/129540_28430)

在基于 JAX-WS 标准的 web services 的开发中,不少实际场景都是希望采用自底向上的开发方式, 即基于已有的 Java bean 来创建 web services 。WebSphere Application Server ( 以下简称 WAS ) 提供了命令行的工具 wsgen 和相对应的 Ant task 来支持这种开发过程,而且这两个工具比较适合大型项目的自动化构建。 这两个工具的使用前提是 Java bean 中事先添加有 web services 的 Annotation 标注,而在现有的业务系统中,class 文件一般是不带 Annotation 的,这就需要开发人员去修改现有的代码,以手工方式添加 Annotation,但这样带来的工作量太大且容易出错。本文介绍了一种解决途径,可以不用修改源代码,而是利用字节码工具 ASM 直接修改 class 文件, 在字节码文件中自动注入 Annotation ,然后再利用 wsgen 工具就可以很方便地生成 web services 应用。 本文同时也总结了使用 ASM 的一些实践。

本文首先介绍了和 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 类1

public

class

SayHelloImpl {

2

public

String sayHello(String s) { 3

return

“Hello: ” + s ;

4

} 5

}

再用 wsgen 来创建 web services,wsgen 的使用如下: 清单 2. wsgen 实例1

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 Source01

// class version 50.0 (50)

02

// access flags 33 03

public

class

com/ibm/was/asm/SayHelloImpl {

04

05

// access flags 1

06

public

()V 07

ALOAD

0

08

INVOKESPECIAL java/lang/Object. ()V 09

RETURN

10

MAXSTACK =

1 11

MAXLOCALS =

1

12

13

// access flags 1

14

public

sayHello(Ljava/lang/String;)Ljava/lang/String; 15

NEW java/lang/StringBuilder

16

DUP 17

LDC

"Hello:"

18

INVOKESPECIAL java/lang/StringBuilder. (Ljava/lang/String;)V 19

ALOAD

1

20

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) 21

Ljava/lang/StringBuilder;

22

INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 23

ARETURN

24

MAXSTACK =

3 25

MAXLOCALS =

2

26

}清单 6. 生成 SayHelloImpl.class 的 ASM Code01

package

asm.com.ibm.was.asm;

02

import

java.util./*; 03

import

org.objectweb.asm./*;

04

import

org.objectweb.asm.attrs./*; 05

public

class

SayHelloImplDump

implements

Opcodes {

06

public

static

byte

[] dump ()

throws

Exception { 07

ClassWriter cw =

new

ClassWriter(

0

);

08

FieldVisitor fv; 09

MethodVisitor mv;

10

AnnotationVisitor av0; 11

12

cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER,

"com/ibm/was/asm/SayHelloImpl"

, 13

null

,

"java/lang/Object"

,

null

);

14

15

{

16

mv = cw.visitMethod(ACC_PUBLIC,

""

,

"()V"

,

null

,

null

); 17

mv.visitCode();

18

mv.visitVarInsn(ALOAD,

0

); 19

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/Object"

,

""

,

"()V"

);

20

mv.visitInsn(RETURN); 21

mv.visitMaxs(

1

,

1

);

22

mv.visitEnd(); 23

}

24

{ 25

mv = cw.visitMethod(ACC_PUBLIC,

"sayHello"

,

26

"(Ljava/lang/String;)Ljava/lang/String;"

,

null

,

null

); 27

mv.visitCode();

28

mv.visitTypeInsn(NEW,

"java/lang/StringBuilder"

); 29

mv.visitInsn(DUP);

30

mv.visitLdcInsn(

"Hello:"

); 31

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/StringBuilder"

,

32

""

,

"(Ljava/lang/String;)V"

); 33

mv.visitVarInsn(ALOAD,

1

);

34

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 35

"append"

,

"(Ljava/lang/String;)Ljava/lang/StringBuilder;"

);

36

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 37

"toString"

,

"()Ljava/lang/String;"

);

38

mv.visitInsn(ARETURN); 39

mv.visitMaxs(

3

,

2

);

40

mv.visitEnd(); 41

}

42

cw.visitEnd(); 43

return

cw.toByteArray();

44

} 45

}

图 1 描述了 Java Source 文件、Java Class 文件、ASM Code 文件和 ASM Source 文件之间的转换关系。 图 1. 四种文件的转换图 图 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 Codeview sourceprint?

01

public

void

showClassASMProcess() {

02

ClassReader cr; 03

final

String n = SayHelloImpl.

class

.getName();

04

05

try

{

06

cr =

new

ClassReader(n);
07

cr.accept(

new

ASMifierClassVisitor(

new

PrintWriter(System.out)),

08

new

Attribute[

0

], 09

ClassReader.SKIP_DEBUG);

10

11

}

catch

(Exception e) {

12

e.printStackTrace(); 13

}

14

}

另外一个重要的转换就是, 给定了 Java Class 文件, 如何查看它的 ASM Source ? 这点对于我们验证 ASM Code 生成的 class 是否正确很有用。ASM 提供了另外一个工具 TraceClassVisitor, 来获得一个 Java Class 对应的 ASM Source。代码如下: 清单 8. 利用 TraceClassVisitor 查看 class 文件的 ASM Source01

//file 是 class 文件的全路径名

02

public

void

showClassSource(String file) { 03

FileInputStream is ;

04

ClassReader cr; 05

06

try

{ 07

is =

new

FileInputStream(file);

08

cr =

new

ClassReader(is); 09

TraceClassVisitor trace =

new

TraceClassVisitor(

new

PrintWriter(System.out));

10

cr.accept(trace, ClassReader.SKIP_DEBUG); 11

12

is.close() ; 13

}

catch

(Exception e) {

14

e.printStackTrace() ; 15

}

16

} 17

{

18

e.printStackTrace(); 19

}

20

}

ASM 类注入

ASM 类注入是指修改一个现有的 class 文件,在其中加入自己的代码。按通常思维,我们需要利用 ASM 提供的 reader 类去读取所要修改的类文件, 找到要修改的地方,如方法名或属性名,然后在该处插入自己的代码或修改现有的代码,这种思路将使问题复杂化。 ASM 为我们提供了访问类文件的 Visitor 模式,来遍历一个 class 文件,Visitor 是一个实现 ClassVisitor 接口的类。 要注入一个 class 文件,先要找到一个合适的 Visitor 做向导, ASM 提供了好几种 Visitor, 最常用的是 ClassWriter。同时我们需要提供一个适配器类 ClassAdapter, Visitor 会带着这个 Adapter 一起去遍历, 然后在遍历过程中回调 Adapter 提供的方法。我们在 Adapter 的这些方法中就可以实现我们的修改和定制逻辑。当然,如果你不想做任何修改,那 Visitor 遍历完后将得到一个和被遍历的 class 完全一样的拷贝。 清单 9. 类注入的完整过程01

public

void

addAnnotationToExistingClass() {

02

FileInputStream is ; 03

ClassReader cr;

04

try

{
05

String classfile =

"E:\SayHelloImpl.class"

;

// 待遍历的类

06

showClassSource(classfile) ;

// 打印该类的 ASM source 07

08

is =

new

FileInputStream(classfile);
09

cr =

new

ClassReader(is);

10

// 此处我们使用 ClassWriter 做 Visitor 11

ClassWriter cw =

new

ClassWriter(ClassWriter.COMPUTE_MAXS);

12

// 给 Visitor 提供一个 Adapter 13

AddAnnotationAdapter adapter =

new

AddAnnotationAdapter(cw);

14

cr.accept(adapter, ClassReader.SKIP_DEBUG);
15

16

// 遍历完后,生成的 class 保存在字节数组中 17

byte

[] b = cw.toByteArray();

18

19

try

{

20

// 将字节数组输出到文件中 21

// 为了不至于覆盖掉原有的 SayHelloImpl.class 文件,我们将结果输出到 _SayHelloImpl.class

22

FileOutputStream fout =

new

FileOutputStream(

"E:\_SayHelloImpl.class"

); 23

fout.write(b) ;

24

fout.flush(); 25

fout.close() ;

26

}

catch

(Exception e) { 27

e.printStackTrace() ;

28

} 29

30

// 验证 _SayHelloImpl.class 是否包含我们注入的代码 31

showClassSource(

"E:\_SayHelloImpl.class"

) ;

32

}

catch

(Exception e) { 33

e.printStackTrace();

34

} 35

}

web services 标记注入过程

我们已经知道了如何往 class 中注入自己的代码,那对于一个已有的 Java bean,怎么往里注入 @WebService 和 @WebMethod 呢?根据上面我们讲的转换关系,我们可以先写一个带 annotation 的 Java Source,编译得到 Java Class, 再得到其 ASM Code。下面是利用 ASM 给一个不带任何 Annotation 的 class 添加 @WebService 和 @WebMethod 的步骤。

  1. 首先在 SayHelloImpl.java 中添加 @WebService 和 @WebMethod: 清单 10. 带 web services 标注的 SayHelloImpl 类1

@WebService

2

public

class

SayHelloImpl { 3

@WebMethod

4

public

String sayHello(String s) { 5

return

"Hello: "

  • s ;

6

} 7

}

  1. 通过 ASMifierClassVisitor 工具获得 SayHelloImpl class 的 ASM Code,见清单 6。
  2. 写一个 ClassAdapter 类, ClassAdapter 其实也是一个 Visitor。根据 ASM Code 的提示, 我们就可以知道如何利用 ASM API 去获得我们希望的 class 文件。下面是 Adapter 的示例代码。 清单 11. 添加 Annotation 的 Adapter 类01

public

class

AddAnnotationAdapter

extends

ClassAdapter

implements

Opcodes{

02

//private String annotationDesc; 03

private

boolean

isAnnotationPresent;

04

05

public

AddAnnotationAdapter(ClassVisitor cv) {

06

super

(cv); 07

}

08

09

@Override

10

public

void

visit(

int

version,

int

access, String name, String signature, 11

String superName, String[] interfaces) {

12

13

cv.visit(V1_6, ACC_PUBLIC + ACC_SUPER, name, signature, superName,

14

interfaces); 15

AnnotationVisitor av0;

16

av0 = cv.visitAnnotation(

"Ljavax/jws/WebService;"

,

true

); 17

av0.visitEnd();

18

} 19

20

@Override 21

public

AnnotationVisitor visitAnnotation(String desc,

boolean

visible) {

22

return

cv.visitAnnotation(desc, visible); 23

}

24

25

@Override

26

public

void

visitInnerClass(String name, String outerName, 27

String innerName,

int

access) {

28

cv.visitInnerClass(name, outerName, innerName, access); 29

}

30

31

@Override

32

public

FieldVisitor visitField(

int

access, String name, String desc, 33

String signature, Object value) {

34

return

cv.visitField(access, name, desc, signature, value); 35

}

36

37

@Override

38

public

MethodVisitor visitMethod(

int

access, String name, String desc, 39

String signature, String[] exceptions) {

40

MethodVisitor mv = cv.visitMethod(access, name, desc, signature, 41

exceptions);

42

if

(!name.equals(

""

)) { 43

AnnotationVisitor av0;

44

av0 = mv.visitAnnotation(

"Ljavax/jws/WebMethod;"

,

true

); 45

av0.visitEnd();

46

} 47

return

mv;

48

} 49

50

@Override 51

public

void

visitEnd() {

52

cv.visitEnd(); 53

}

54

55

}

最后,我们采用上面讲的类注入办法,即可得到一个带 @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 编程模型上的项目有一定的参考价值。在基于 JAX-WS 标准的 web services 的开发中,不少实际场景都是希望采用自底向上的开发方式, 即基于已有的 Java bean 来创建 web services 。WebSphere Application Server ( 以下简称 WAS ) 提供了命令行的工具 wsgen 和相对应的 Ant task 来支持这种开发过程,而且这两个工具比较适合大型项目的自动化构建。 这两个工具的使用前提是 Java bean 中事先添加有 web services 的 Annotation 标注,而在现有的业务系统中,class 文件一般是不带 Annotation 的,这就需要开发人员去修改现有的代码,以手工方式添加 Annotation,但这样带来的工作量太大且容易出错。本文介绍了一种解决途径,可以不用修改源代码,而是利用字节码工具 ASM 直接修改 class 文件, 在字节码文件中自动注入 Annotation ,然后再利用 wsgen 工具就可以很方便地生成 web services 应用。 本文同时也总结了使用 ASM 的一些实践。

本文首先介绍了和 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 类1

public

class

SayHelloImpl {

2

public

String sayHello(String s) { 3

return

“Hello: ” + s ;

4

} 5

}

再用 wsgen 来创建 web services,wsgen 的使用如下: 清单 2. wsgen 实例1

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 Source01

// class version 50.0 (50)

02

// access flags 33 03

public

class

com/ibm/was/asm/SayHelloImpl {

04

05

// access flags 1

06

public

()V 07

ALOAD

0

08

INVOKESPECIAL java/lang/Object. ()V 09

RETURN

10

MAXSTACK =

1 11

MAXLOCALS =

1

12

13

// access flags 1

14

public

sayHello(Ljava/lang/String;)Ljava/lang/String; 15

NEW java/lang/StringBuilder

16

DUP 17

LDC

"Hello:"

18

INVOKESPECIAL java/lang/StringBuilder. (Ljava/lang/String;)V 19

ALOAD

1

20

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) 21

Ljava/lang/StringBuilder;

22

INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 23

ARETURN

24

MAXSTACK =

3 25

MAXLOCALS =

2

26

}清单 6. 生成 SayHelloImpl.class 的 ASM Code01

package

asm.com.ibm.was.asm;

02

import

java.util./*; 03

import

org.objectweb.asm./*;

04

import

org.objectweb.asm.attrs./*; 05

public

class

SayHelloImplDump

implements

Opcodes {

06

public

static

byte

[] dump ()

throws

Exception { 07

ClassWriter cw =

new

ClassWriter(

0

);

08

FieldVisitor fv; 09

MethodVisitor mv;

10

AnnotationVisitor av0; 11

12

cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER,

"com/ibm/was/asm/SayHelloImpl"

, 13

null

,

"java/lang/Object"

,

null

);

14

15

{

16

mv = cw.visitMethod(ACC_PUBLIC,

""

,

"()V"

,

null

,

null

); 17

mv.visitCode();

18

mv.visitVarInsn(ALOAD,

0

); 19

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/Object"

,

""

,

"()V"

);

20

mv.visitInsn(RETURN); 21

mv.visitMaxs(

1

,

1

);

22

mv.visitEnd(); 23

}

24

{ 25

mv = cw.visitMethod(ACC_PUBLIC,

"sayHello"

,

26

"(Ljava/lang/String;)Ljava/lang/String;"

,

null

,

null

); 27

mv.visitCode();

28

mv.visitTypeInsn(NEW,

"java/lang/StringBuilder"

); 29

mv.visitInsn(DUP);

30

mv.visitLdcInsn(

"Hello:"

); 31

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/StringBuilder"

,

32

""

,

"(Ljava/lang/String;)V"

); 33

mv.visitVarInsn(ALOAD,

1

);

34

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 35

"append"

,

"(Ljava/lang/String;)Ljava/lang/StringBuilder;"

);

36

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 37

"toString"

,

"()Ljava/lang/String;"

);

38

mv.visitInsn(ARETURN); 39

mv.visitMaxs(

3

,

2

);

40

mv.visitEnd(); 41

}

42

cw.visitEnd(); 43

return

cw.toByteArray();

44

} 45

}

图 1 描述了 Java Source 文件、Java Class 文件、ASM Code 文件和 ASM Source 文件之间的转换关系。 图 1. 四种文件的转换图 图 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 Codeview sourceprint?

01

public

void

showClassASMProcess() {

02

ClassReader cr; 03

final

String n = SayHelloImpl.

class

.getName();

04

05

try

{

06

cr =

new

ClassReader(n);
07

cr.accept(

new

ASMifierClassVisitor(

new

PrintWriter(System.out)),

08

new

Attribute[

0

], 09

ClassReader.SKIP_DEBUG);

10

11

}

catch

(Exception e) {

12

e.printStackTrace(); 13

}

14

}

另外一个重要的转换就是, 给定了 Java Class 文件, 如何查看它的 ASM Source ? 这点对于我们验证 ASM Code 生成的 class 是否正确很有用。ASM 提供了另外一个工具 TraceClassVisitor, 来获得一个 Java Class 对应的 ASM Source。代码如下: 清单 8. 利用 TraceClassVisitor 查看 class 文件的 ASM Source01

//file 是 class 文件的全路径名

02

public

void

showClassSource(String file) { 03

FileInputStream is ;

04

ClassReader cr; 05

06

try

{ 07

is =

new

FileInputStream(file);

08

cr =

new

ClassReader(is); 09

TraceClassVisitor trace =

new

TraceClassVisitor(

new

PrintWriter(System.out));

10

cr.accept(trace, ClassReader.SKIP_DEBUG); 11

12

is.close() ; 13

}

catch

(Exception e) {

14

e.printStackTrace() ; 15

}

16

} 17

{

18

e.printStackTrace(); 19

}

20

}

ASM 类注入

ASM 类注入是指修改一个现有的 class 文件,在其中加入自己的代码。按通常思维,我们需要利用 ASM 提供的 reader 类去读取所要修改的类文件, 找到要修改的地方,如方法名或属性名,然后在该处插入自己的代码或修改现有的代码,这种思路将使问题复杂化。 ASM 为我们提供了访问类文件的 Visitor 模式,来遍历一个 class 文件,Visitor 是一个实现 ClassVisitor 接口的类。 要注入一个 class 文件,先要找到一个合适的 Visitor 做向导, ASM 提供了好几种 Visitor, 最常用的是 ClassWriter。同时我们需要提供一个适配器类 ClassAdapter, Visitor 会带着这个 Adapter 一起去遍历, 然后在遍历过程中回调 Adapter 提供的方法。我们在 Adapter 的这些方法中就可以实现我们的修改和定制逻辑。当然,如果你不想做任何修改,那 Visitor 遍历完后将得到一个和被遍历的 class 完全一样的拷贝。 清单 9. 类注入的完整过程01

public

void

addAnnotationToExistingClass() {

02

FileInputStream is ; 03

ClassReader cr;

04

try

{
05

String classfile =

"E:\SayHelloImpl.class"

;

// 待遍历的类

06

showClassSource(classfile) ;

// 打印该类的 ASM source 07

08

is =

new

FileInputStream(classfile);
09

cr =

new

ClassReader(is);

10

// 此处我们使用 ClassWriter 做 Visitor 11

ClassWriter cw =

new

ClassWriter(ClassWriter.COMPUTE_MAXS);

12

// 给 Visitor 提供一个 Adapter 13

AddAnnotationAdapter adapter =

new

AddAnnotationAdapter(cw);

14

cr.accept(adapter, ClassReader.SKIP_DEBUG);
15

16

// 遍历完后,生成的 class 保存在字节数组中 17

byte

[] b = cw.toByteArray();

18

19

try

{

20

// 将字节数组输出到文件中 21

// 为了不至于覆盖掉原有的 SayHelloImpl.class 文件,我们将结果输出到 _SayHelloImpl.class

22

FileOutputStream fout =

new

FileOutputStream(

"E:\_SayHelloImpl.class"

); 23

fout.write(b) ;

24

fout.flush(); 25

fout.close() ;

26

}

catch

(Exception e) { 27

e.printStackTrace() ;

28

} 29

30

// 验证 _SayHelloImpl.class 是否包含我们注入的代码 31

showClassSource(

"E:\_SayHelloImpl.class"

) ;

32

}

catch

(Exception e) { 33

e.printStackTrace();

34

} 35

}

web services 标记注入过程

我们已经知道了如何往 class 中注入自己的代码,那对于一个已有的 Java bean,怎么往里注入 @WebService 和 @WebMethod 呢?根据上面我们讲的转换关系,我们可以先写一个带 annotation 的 Java Source,编译得到 Java Class, 再得到其 ASM Code。下面是利用 ASM 给一个不带任何 Annotation 的 class 添加 @WebService 和 @WebMethod 的步骤。

  1. 首先在 SayHelloImpl.java 中添加 @WebService 和 @WebMethod: 清单 10. 带 web services 标注的 SayHelloImpl 类1

@WebService

2

public

class

SayHelloImpl { 3

@WebMethod

4

public

String sayHello(String s) { 5

return

"Hello: "

  • s ;

6

} 7

}

  1. 通过 ASMifierClassVisitor 工具获得 SayHelloImpl class 的 ASM Code,见清单 6。
  2. 写一个 ClassAdapter 类, ClassAdapter 其实也是一个 Visitor。根据 ASM Code 的提示, 我们就可以知道如何利用 ASM API 去获得我们希望的 class 文件。下面是 Adapter 的示例代码。 清单 11. 添加 Annotation 的 Adapter 类01

public

class

AddAnnotationAdapter

extends

ClassAdapter

implements

Opcodes{

02

//private String annotationDesc; 03

private

boolean

isAnnotationPresent;

04

05

public

AddAnnotationAdapter(ClassVisitor cv) {

06

super

(cv); 07

}

08

09

@Override

10

public

void

visit(

int

version,

int

access, String name, String signature, 11

String superName, String[] interfaces) {

12

13

cv.visit(V1_6, ACC_PUBLIC + ACC_SUPER, name, signature, superName,

14

interfaces); 15

AnnotationVisitor av0;

16

av0 = cv.visitAnnotation(

"Ljavax/jws/WebService;"

,

true

); 17

av0.visitEnd();

18

} 19

20

@Override 21

public

AnnotationVisitor visitAnnotation(String desc,

boolean

visible) {

22

return

cv.visitAnnotation(desc, visible); 23

}

24

25

@Override

26

public

void

visitInnerClass(String name, String outerName, 27

String innerName,

int

access) {

28

cv.visitInnerClass(name, outerName, innerName, access); 29

}

30

31

@Override

32

public

FieldVisitor visitField(

int

access, String name, String desc, 33

String signature, Object value) {

34

return

cv.visitField(access, name, desc, signature, value); 35

}

36

37

@Override

38

public

MethodVisitor visitMethod(

int

access, String name, String desc, 39

String signature, String[] exceptions) {

40

MethodVisitor mv = cv.visitMethod(access, name, desc, signature, 41

exceptions);

42

if

(!name.equals(

""

)) { 43

AnnotationVisitor av0;

44

av0 = mv.visitAnnotation(

"Ljavax/jws/WebMethod;"

,

true

); 45

av0.visitEnd();

46

} 47

return

mv;

48

} 49

50

@Override 51

public

void

visitEnd() {

52

cv.visitEnd(); 53

}

54

55

}

最后,我们采用上面讲的类注入办法,即可得到一个带 @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 编程模型上的项目有一定的参考价值。在基于 JAX-WS 标准的 web services 的开发中,不少实际场景都是希望采用自底向上的开发方式, 即基于已有的 Java bean 来创建 web services 。WebSphere Application Server ( 以下简称 WAS ) 提供了命令行的工具 wsgen 和相对应的 Ant task 来支持这种开发过程,而且这两个工具比较适合大型项目的自动化构建。 这两个工具的使用前提是 Java bean 中事先添加有 web services 的 Annotation 标注,而在现有的业务系统中,class 文件一般是不带 Annotation 的,这就需要开发人员去修改现有的代码,以手工方式添加 Annotation,但这样带来的工作量太大且容易出错。本文介绍了一种解决途径,可以不用修改源代码,而是利用字节码工具 ASM 直接修改 class 文件, 在字节码文件中自动注入 Annotation ,然后再利用 wsgen 工具就可以很方便地生成 web services 应用。 本文同时也总结了使用 ASM 的一些实践。

本文首先介绍了和 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 类1

public

class

SayHelloImpl {

2

public

String sayHello(String s) { 3

return

“Hello: ” + s ;

4

} 5

}

再用 wsgen 来创建 web services,wsgen 的使用如下: 清单 2. wsgen 实例1

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 Source01

// class version 50.0 (50)

02

// access flags 33 03

public

class

com/ibm/was/asm/SayHelloImpl {

04

05

// access flags 1

06

public

()V 07

ALOAD

0

08

INVOKESPECIAL java/lang/Object. ()V 09

RETURN

10

MAXSTACK =

1 11

MAXLOCALS =

1

12

13

// access flags 1

14

public

sayHello(Ljava/lang/String;)Ljava/lang/String; 15

NEW java/lang/StringBuilder

16

DUP 17

LDC

"Hello:"

18

INVOKESPECIAL java/lang/StringBuilder. (Ljava/lang/String;)V 19

ALOAD

1

20

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) 21

Ljava/lang/StringBuilder;

22

INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 23

ARETURN

24

MAXSTACK =

3 25

MAXLOCALS =

2

26

}清单 6. 生成 SayHelloImpl.class 的 ASM Code01

package

asm.com.ibm.was.asm;

02

import

java.util./*; 03

import

org.objectweb.asm./*;

04

import

org.objectweb.asm.attrs./*; 05

public

class

SayHelloImplDump

implements

Opcodes {

06

public

static

byte

[] dump ()

throws

Exception { 07

ClassWriter cw =

new

ClassWriter(

0

);

08

FieldVisitor fv; 09

MethodVisitor mv;

10

AnnotationVisitor av0; 11

12

cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER,

"com/ibm/was/asm/SayHelloImpl"

, 13

null

,

"java/lang/Object"

,

null

);

14

15

{

16

mv = cw.visitMethod(ACC_PUBLIC,

""

,

"()V"

,

null

,

null

); 17

mv.visitCode();

18

mv.visitVarInsn(ALOAD,

0

); 19

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/Object"

,

""

,

"()V"

);

20

mv.visitInsn(RETURN); 21

mv.visitMaxs(

1

,

1

);

22

mv.visitEnd(); 23

}

24

{ 25

mv = cw.visitMethod(ACC_PUBLIC,

"sayHello"

,

26

"(Ljava/lang/String;)Ljava/lang/String;"

,

null

,

null

); 27

mv.visitCode();

28

mv.visitTypeInsn(NEW,

"java/lang/StringBuilder"

); 29

mv.visitInsn(DUP);

30

mv.visitLdcInsn(

"Hello:"

); 31

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/StringBuilder"

,

32

""

,

"(Ljava/lang/String;)V"

); 33

mv.visitVarInsn(ALOAD,

1

);

34

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 35

"append"

,

"(Ljava/lang/String;)Ljava/lang/StringBuilder;"

);

36

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 37

"toString"

,

"()Ljava/lang/String;"

);

38

mv.visitInsn(ARETURN); 39

mv.visitMaxs(

3

,

2

);

40

mv.visitEnd(); 41

}

42

cw.visitEnd(); 43

return

cw.toByteArray();

44

} 45

}

图 1 描述了 Java Source 文件、Java Class 文件、ASM Code 文件和 ASM Source 文件之间的转换关系。 图 1. 四种文件的转换图 图 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 Codeview sourceprint?

01

public

void

showClassASMProcess() {

02

ClassReader cr; 03

final

String n = SayHelloImpl.

class

.getName();

04

05

try

{

06

cr =

new

ClassReader(n);
07

cr.accept(

new

ASMifierClassVisitor(

new

PrintWriter(System.out)),

08

new

Attribute[

0

], 09

ClassReader.SKIP_DEBUG);

10

11

}

catch

(Exception e) {

12

e.printStackTrace(); 13

}

14

}

另外一个重要的转换就是, 给定了 Java Class 文件, 如何查看它的 ASM Source ? 这点对于我们验证 ASM Code 生成的 class 是否正确很有用。ASM 提供了另外一个工具 TraceClassVisitor, 来获得一个 Java Class 对应的 ASM Source。代码如下: 清单 8. 利用 TraceClassVisitor 查看 class 文件的 ASM Source01

//file 是 class 文件的全路径名

02

public

void

showClassSource(String file) { 03

FileInputStream is ;

04

ClassReader cr; 05

06

try

{ 07

is =

new

FileInputStream(file);

08

cr =

new

ClassReader(is); 09

TraceClassVisitor trace =

new

TraceClassVisitor(

new

PrintWriter(System.out));

10

cr.accept(trace, ClassReader.SKIP_DEBUG); 11

12

is.close() ; 13

}

catch

(Exception e) {

14

e.printStackTrace() ; 15

}

16

} 17

{

18

e.printStackTrace(); 19

}

20

}

ASM 类注入

ASM 类注入是指修改一个现有的 class 文件,在其中加入自己的代码。按通常思维,我们需要利用 ASM 提供的 reader 类去读取所要修改的类文件, 找到要修改的地方,如方法名或属性名,然后在该处插入自己的代码或修改现有的代码,这种思路将使问题复杂化。 ASM 为我们提供了访问类文件的 Visitor 模式,来遍历一个 class 文件,Visitor 是一个实现 ClassVisitor 接口的类。 要注入一个 class 文件,先要找到一个合适的 Visitor 做向导, ASM 提供了好几种 Visitor, 最常用的是 ClassWriter。同时我们需要提供一个适配器类 ClassAdapter, Visitor 会带着这个 Adapter 一起去遍历, 然后在遍历过程中回调 Adapter 提供的方法。我们在 Adapter 的这些方法中就可以实现我们的修改和定制逻辑。当然,如果你不想做任何修改,那 Visitor 遍历完后将得到一个和被遍历的 class 完全一样的拷贝。 清单 9. 类注入的完整过程01

public

void

addAnnotationToExistingClass() {

02

FileInputStream is ; 03

ClassReader cr;

04

try

{
05

String classfile =

"E:\SayHelloImpl.class"

;

// 待遍历的类

06

showClassSource(classfile) ;

// 打印该类的 ASM source 07

08

is =

new

FileInputStream(classfile);
09

cr =

new

ClassReader(is);

10

// 此处我们使用 ClassWriter 做 Visitor 11

ClassWriter cw =

new

ClassWriter(ClassWriter.COMPUTE_MAXS);

12

// 给 Visitor 提供一个 Adapter 13

AddAnnotationAdapter adapter =

new

AddAnnotationAdapter(cw);

14

cr.accept(adapter, ClassReader.SKIP_DEBUG);
15

16

// 遍历完后,生成的 class 保存在字节数组中 17

byte

[] b = cw.toByteArray();

18

19

try

{

20

// 将字节数组输出到文件中 21

// 为了不至于覆盖掉原有的 SayHelloImpl.class 文件,我们将结果输出到 _SayHelloImpl.class

22

FileOutputStream fout =

new

FileOutputStream(

"E:\_SayHelloImpl.class"

); 23

fout.write(b) ;

24

fout.flush(); 25

fout.close() ;

26

}

catch

(Exception e) { 27

e.printStackTrace() ;

28

} 29

30

// 验证 _SayHelloImpl.class 是否包含我们注入的代码 31

showClassSource(

"E:\_SayHelloImpl.class"

) ;

32

}

catch

(Exception e) { 33

e.printStackTrace();

34

} 35

}

web services 标记注入过程

我们已经知道了如何往 class 中注入自己的代码,那对于一个已有的 Java bean,怎么往里注入 @WebService 和 @WebMethod 呢?根据上面我们讲的转换关系,我们可以先写一个带 annotation 的 Java Source,编译得到 Java Class, 再得到其 ASM Code。下面是利用 ASM 给一个不带任何 Annotation 的 class 添加 @WebService 和 @WebMethod 的步骤。

  1. 首先在 SayHelloImpl.java 中添加 @WebService 和 @WebMethod: 清单 10. 带 web services 标注的 SayHelloImpl 类1

@WebService

2

public

class

SayHelloImpl { 3

@WebMethod

4

public

String sayHello(String s) { 5

return

"Hello: "

  • s ;

6

} 7

}

  1. 通过 ASMifierClassVisitor 工具获得 SayHelloImpl class 的 ASM Code,见清单 6。
  2. 写一个 ClassAdapter 类, ClassAdapter 其实也是一个 Visitor。根据 ASM Code 的提示, 我们就可以知道如何利用 ASM API 去获得我们希望的 class 文件。下面是 Adapter 的示例代码。 清单 11. 添加 Annotation 的 Adapter 类01

public

class

AddAnnotationAdapter

extends

ClassAdapter

implements

Opcodes{

02

//private String annotationDesc; 03

private

boolean

isAnnotationPresent;

04

05

public

AddAnnotationAdapter(ClassVisitor cv) {

06

super

(cv); 07

}

08

09

@Override

10

public

void

visit(

int

version,

int

access, String name, String signature, 11

String superName, String[] interfaces) {

12

13

cv.visit(V1_6, ACC_PUBLIC + ACC_SUPER, name, signature, superName,

14

interfaces); 15

AnnotationVisitor av0;

16

av0 = cv.visitAnnotation(

"Ljavax/jws/WebService;"

,

true

); 17

av0.visitEnd();

18

} 19

20

@Override 21

public

AnnotationVisitor visitAnnotation(String desc,

boolean

visible) {

22

return

cv.visitAnnotation(desc, visible); 23

}

24

25

@Override

26

public

void

visitInnerClass(String name, String outerName, 27

String innerName,

int

access) {

28

cv.visitInnerClass(name, outerName, innerName, access); 29

}

30

31

@Override

32

public

FieldVisitor visitField(

int

access, String name, String desc, 33

String signature, Object value) {

34

return

cv.visitField(access, name, desc, signature, value); 35

}

36

37

@Override

38

public

MethodVisitor visitMethod(

int

access, String name, String desc, 39

String signature, String[] exceptions) {

40

MethodVisitor mv = cv.visitMethod(access, name, desc, signature, 41

exceptions);

42

if

(!name.equals(

""

)) { 43

AnnotationVisitor av0;

44

av0 = mv.visitAnnotation(

"Ljavax/jws/WebMethod;"

,

true

); 45

av0.visitEnd();

46

} 47

return

mv;

48

} 49

50

@Override 51

public

void

visitEnd() {

52

cv.visitEnd(); 53

}

54

55

}

最后,我们采用上面讲的类注入办法,即可得到一个带 @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 编程模型上的项目有一定的参考价值。在基于 JAX-WS 标准的 web services 的开发中,不少实际场景都是希望采用自底向上的开发方式, 即基于已有的 Java bean 来创建 web services 。WebSphere Application Server ( 以下简称 WAS ) 提供了命令行的工具 wsgen 和相对应的 Ant task 来支持这种开发过程,而且这两个工具比较适合大型项目的自动化构建。 这两个工具的使用前提是 Java bean 中事先添加有 web services 的 Annotation 标注,而在现有的业务系统中,class 文件一般是不带 Annotation 的,这就需要开发人员去修改现有的代码,以手工方式添加 Annotation,但这样带来的工作量太大且容易出错。本文介绍了一种解决途径,可以不用修改源代码,而是利用字节码工具 ASM 直接修改 class 文件, 在字节码文件中自动注入 Annotation ,然后再利用 wsgen 工具就可以很方便地生成 web services 应用。 本文同时也总结了使用 ASM 的一些实践。

本文首先介绍了和 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 类1

public

class

SayHelloImpl {

2

public

String sayHello(String s) { 3

return

“Hello: ” + s ;

4

} 5

}

再用 wsgen 来创建 web services,wsgen 的使用如下: 清单 2. wsgen 实例1

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 Source01

// class version 50.0 (50)

02

// access flags 33 03

public

class

com/ibm/was/asm/SayHelloImpl {

04

05

// access flags 1

06

public

()V 07

ALOAD

0

08

INVOKESPECIAL java/lang/Object. ()V 09

RETURN

10

MAXSTACK =

1 11

MAXLOCALS =

1

12

13

// access flags 1

14

public

sayHello(Ljava/lang/String;)Ljava/lang/String; 15

NEW java/lang/StringBuilder

16

DUP 17

LDC

"Hello:"

18

INVOKESPECIAL java/lang/StringBuilder. (Ljava/lang/String;)V 19

ALOAD

1

20

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) 21

Ljava/lang/StringBuilder;

22

INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 23

ARETURN

24

MAXSTACK =

3 25

MAXLOCALS =

2

26

}清单 6. 生成 SayHelloImpl.class 的 ASM Code01

package

asm.com.ibm.was.asm;

02

import

java.util./*; 03

import

org.objectweb.asm./*;

04

import

org.objectweb.asm.attrs./*; 05

public

class

SayHelloImplDump

implements

Opcodes {

06

public

static

byte

[] dump ()

throws

Exception { 07

ClassWriter cw =

new

ClassWriter(

0

);

08

FieldVisitor fv; 09

MethodVisitor mv;

10

AnnotationVisitor av0; 11

12

cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER,

"com/ibm/was/asm/SayHelloImpl"

, 13

null

,

"java/lang/Object"

,

null

);

14

15

{

16

mv = cw.visitMethod(ACC_PUBLIC,

""

,

"()V"

,

null

,

null

); 17

mv.visitCode();

18

mv.visitVarInsn(ALOAD,

0

); 19

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/Object"

,

""

,

"()V"

);

20

mv.visitInsn(RETURN); 21

mv.visitMaxs(

1

,

1

);

22

mv.visitEnd(); 23

}

24

{ 25

mv = cw.visitMethod(ACC_PUBLIC,

"sayHello"

,

26

"(Ljava/lang/String;)Ljava/lang/String;"

,

null

,

null

); 27

mv.visitCode();

28

mv.visitTypeInsn(NEW,

"java/lang/StringBuilder"

); 29

mv.visitInsn(DUP);

30

mv.visitLdcInsn(

"Hello:"

); 31

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/StringBuilder"

,

32

""

,

"(Ljava/lang/String;)V"

); 33

mv.visitVarInsn(ALOAD,

1

);

34

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 35

"append"

,

"(Ljava/lang/String;)Ljava/lang/StringBuilder;"

);

36

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 37

"toString"

,

"()Ljava/lang/String;"

);

38

mv.visitInsn(ARETURN); 39

mv.visitMaxs(

3

,

2

);

40

mv.visitEnd(); 41

}

42

cw.visitEnd(); 43

return

cw.toByteArray();

44

} 45

}

图 1 描述了 Java Source 文件、Java Class 文件、ASM Code 文件和 ASM Source 文件之间的转换关系。 图 1. 四种文件的转换图 图 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 Codeview sourceprint?

01

public

void

showClassASMProcess() {

02

ClassReader cr; 03

final

String n = SayHelloImpl.

class

.getName();

04

05

try

{

06

cr =

new

ClassReader(n);
07

cr.accept(

new

ASMifierClassVisitor(

new

PrintWriter(System.out)),

08

new

Attribute[

0

], 09

ClassReader.SKIP_DEBUG);

10

11

}

catch

(Exception e) {

12

e.printStackTrace(); 13

}

14

}

另外一个重要的转换就是, 给定了 Java Class 文件, 如何查看它的 ASM Source ? 这点对于我们验证 ASM Code 生成的 class 是否正确很有用。ASM 提供了另外一个工具 TraceClassVisitor, 来获得一个 Java Class 对应的 ASM Source。代码如下: 清单 8. 利用 TraceClassVisitor 查看 class 文件的 ASM Source01

//file 是 class 文件的全路径名

02

public

void

showClassSource(String file) { 03

FileInputStream is ;

04

ClassReader cr; 05

06

try

{ 07

is =

new

FileInputStream(file);

08

cr =

new

ClassReader(is); 09

TraceClassVisitor trace =

new

TraceClassVisitor(

new

PrintWriter(System.out));

10

cr.accept(trace, ClassReader.SKIP_DEBUG); 11

12

is.close() ; 13

}

catch

(Exception e) {

14

e.printStackTrace() ; 15

}

16

} 17

{

18

e.printStackTrace(); 19

}

20

}

ASM 类注入

ASM 类注入是指修改一个现有的 class 文件,在其中加入自己的代码。按通常思维,我们需要利用 ASM 提供的 reader 类去读取所要修改的类文件, 找到要修改的地方,如方法名或属性名,然后在该处插入自己的代码或修改现有的代码,这种思路将使问题复杂化。 ASM 为我们提供了访问类文件的 Visitor 模式,来遍历一个 class 文件,Visitor 是一个实现 ClassVisitor 接口的类。 要注入一个 class 文件,先要找到一个合适的 Visitor 做向导, ASM 提供了好几种 Visitor, 最常用的是 ClassWriter。同时我们需要提供一个适配器类 ClassAdapter, Visitor 会带着这个 Adapter 一起去遍历, 然后在遍历过程中回调 Adapter 提供的方法。我们在 Adapter 的这些方法中就可以实现我们的修改和定制逻辑。当然,如果你不想做任何修改,那 Visitor 遍历完后将得到一个和被遍历的 class 完全一样的拷贝。 清单 9. 类注入的完整过程01

public

void

addAnnotationToExistingClass() {

02

FileInputStream is ; 03

ClassReader cr;

04

try

{
05

String classfile =

"E:\SayHelloImpl.class"

;

// 待遍历的类

06

showClassSource(classfile) ;

// 打印该类的 ASM source 07

08

is =

new

FileInputStream(classfile);
09

cr =

new

ClassReader(is);

10

// 此处我们使用 ClassWriter 做 Visitor 11

ClassWriter cw =

new

ClassWriter(ClassWriter.COMPUTE_MAXS);

12

// 给 Visitor 提供一个 Adapter 13

AddAnnotationAdapter adapter =

new

AddAnnotationAdapter(cw);

14

cr.accept(adapter, ClassReader.SKIP_DEBUG);
15

16

// 遍历完后,生成的 class 保存在字节数组中 17

byte

[] b = cw.toByteArray();

18

19

try

{

20

// 将字节数组输出到文件中 21

// 为了不至于覆盖掉原有的 SayHelloImpl.class 文件,我们将结果输出到 _SayHelloImpl.class

22

FileOutputStream fout =

new

FileOutputStream(

"E:\_SayHelloImpl.class"

); 23

fout.write(b) ;

24

fout.flush(); 25

fout.close() ;

26

}

catch

(Exception e) { 27

e.printStackTrace() ;

28

} 29

30

// 验证 _SayHelloImpl.class 是否包含我们注入的代码 31

showClassSource(

"E:\_SayHelloImpl.class"

) ;

32

}

catch

(Exception e) { 33

e.printStackTrace();

34

} 35

}

web services 标记注入过程

我们已经知道了如何往 class 中注入自己的代码,那对于一个已有的 Java bean,怎么往里注入 @WebService 和 @WebMethod 呢?根据上面我们讲的转换关系,我们可以先写一个带 annotation 的 Java Source,编译得到 Java Class, 再得到其 ASM Code。下面是利用 ASM 给一个不带任何 Annotation 的 class 添加 @WebService 和 @WebMethod 的步骤。

  1. 首先在 SayHelloImpl.java 中添加 @WebService 和 @WebMethod: 清单 10. 带 web services 标注的 SayHelloImpl 类1

@WebService

2

public

class

SayHelloImpl { 3

@WebMethod

4

public

String sayHello(String s) { 5

return

"Hello: "

  • s ;

6

} 7

}

  1. 通过 ASMifierClassVisitor 工具获得 SayHelloImpl class 的 ASM Code,见清单 6。
  2. 写一个 ClassAdapter 类, ClassAdapter 其实也是一个 Visitor。根据 ASM Code 的提示, 我们就可以知道如何利用 ASM API 去获得我们希望的 class 文件。下面是 Adapter 的示例代码。 清单 11. 添加 Annotation 的 Adapter 类01

public

class

AddAnnotationAdapter

extends

ClassAdapter

implements

Opcodes{

02

//private String annotationDesc; 03

private

boolean

isAnnotationPresent;

04

05

public

AddAnnotationAdapter(ClassVisitor cv) {

06

super

(cv); 07

}

08

09

@Override

10

public

void

visit(

int

version,

int

access, String name, String signature, 11

String superName, String[] interfaces) {

12

13

cv.visit(V1_6, ACC_PUBLIC + ACC_SUPER, name, signature, superName,

14

interfaces); 15

AnnotationVisitor av0;

16

av0 = cv.visitAnnotation(

"Ljavax/jws/WebService;"

,

true

); 17

av0.visitEnd();

18

} 19

20

@Override 21

public

AnnotationVisitor visitAnnotation(String desc,

boolean

visible) {

22

return

cv.visitAnnotation(desc, visible); 23

}

24

25

@Override

26

public

void

visitInnerClass(String name, String outerName, 27

String innerName,

int

access) {

28

cv.visitInnerClass(name, outerName, innerName, access); 29

}

30

31

@Override

32

public

FieldVisitor visitField(

int

access, String name, String desc, 33

String signature, Object value) {

34

return

cv.visitField(access, name, desc, signature, value); 35

}

36

37

@Override

38

public

MethodVisitor visitMethod(

int

access, String name, String desc, 39

String signature, String[] exceptions) {

40

MethodVisitor mv = cv.visitMethod(access, name, desc, signature, 41

exceptions);

42

if

(!name.equals(

""

)) { 43

AnnotationVisitor av0;

44

av0 = mv.visitAnnotation(

"Ljavax/jws/WebMethod;"

,

true

); 45

av0.visitEnd();

46

} 47

return

mv;

48

} 49

50

@Override 51

public

void

visitEnd() {

52

cv.visitEnd(); 53

}

54

55

}

最后,我们采用上面讲的类注入办法,即可得到一个带 @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 编程模型上的项目有一定的参考价值。在基于 JAX-WS 标准的 web services 的开发中,不少实际场景都是希望采用自底向上的开发方式, 即基于已有的 Java bean 来创建 web services 。WebSphere Application Server ( 以下简称 WAS ) 提供了命令行的工具 wsgen 和相对应的 Ant task 来支持这种开发过程,而且这两个工具比较适合大型项目的自动化构建。 这两个工具的使用前提是 Java bean 中事先添加有 web services 的 Annotation 标注,而在现有的业务系统中,class 文件一般是不带 Annotation 的,这就需要开发人员去修改现有的代码,以手工方式添加 Annotation,但这样带来的工作量太大且容易出错。本文介绍了一种解决途径,可以不用修改源代码,而是利用字节码工具 ASM 直接修改 class 文件, 在字节码文件中自动注入 Annotation ,然后再利用 wsgen 工具就可以很方便地生成 web services 应用。 本文同时也总结了使用 ASM 的一些实践。

本文首先介绍了和 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 类1

public

class

SayHelloImpl {

2

public

String sayHello(String s) { 3

return

“Hello: ” + s ;

4

} 5

}

再用 wsgen 来创建 web services,wsgen 的使用如下: 清单 2. wsgen 实例1

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 Source01

// class version 50.0 (50)

02

// access flags 33 03

public

class

com/ibm/was/asm/SayHelloImpl {

04

05

// access flags 1

06

public

()V 07

ALOAD

0

08

INVOKESPECIAL java/lang/Object. ()V 09

RETURN

10

MAXSTACK =

1 11

MAXLOCALS =

1

12

13

// access flags 1

14

public

sayHello(Ljava/lang/String;)Ljava/lang/String; 15

NEW java/lang/StringBuilder

16

DUP 17

LDC

"Hello:"

18

INVOKESPECIAL java/lang/StringBuilder. (Ljava/lang/String;)V 19

ALOAD

1

20

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) 21

Ljava/lang/StringBuilder;

22

INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 23

ARETURN

24

MAXSTACK =

3 25

MAXLOCALS =

2

26

}清单 6. 生成 SayHelloImpl.class 的 ASM Code01

package

asm.com.ibm.was.asm;

02

import

java.util./*; 03

import

org.objectweb.asm./*;

04

import

org.objectweb.asm.attrs./*; 05

public

class

SayHelloImplDump

implements

Opcodes {

06

public

static

byte

[] dump ()

throws

Exception { 07

ClassWriter cw =

new

ClassWriter(

0

);

08

FieldVisitor fv; 09

MethodVisitor mv;

10

AnnotationVisitor av0; 11

12

cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER,

"com/ibm/was/asm/SayHelloImpl"

, 13

null

,

"java/lang/Object"

,

null

);

14

15

{

16

mv = cw.visitMethod(ACC_PUBLIC,

""

,

"()V"

,

null

,

null

); 17

mv.visitCode();

18

mv.visitVarInsn(ALOAD,

0

); 19

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/Object"

,

""

,

"()V"

);

20

mv.visitInsn(RETURN); 21

mv.visitMaxs(

1

,

1

);

22

mv.visitEnd(); 23

}

24

{ 25

mv = cw.visitMethod(ACC_PUBLIC,

"sayHello"

,

26

"(Ljava/lang/String;)Ljava/lang/String;"

,

null

,

null

); 27

mv.visitCode();

28

mv.visitTypeInsn(NEW,

"java/lang/StringBuilder"

); 29

mv.visitInsn(DUP);

30

mv.visitLdcInsn(

"Hello:"

); 31

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/StringBuilder"

,

32

""

,

"(Ljava/lang/String;)V"

); 33

mv.visitVarInsn(ALOAD,

1

);

34

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 35

"append"

,

"(Ljava/lang/String;)Ljava/lang/StringBuilder;"

);

36

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 37

"toString"

,

"()Ljava/lang/String;"

);

38

mv.visitInsn(ARETURN); 39

mv.visitMaxs(

3

,

2

);

40

mv.visitEnd(); 41

}

42

cw.visitEnd(); 43

return

cw.toByteArray();

44

} 45

}

图 1 描述了 Java Source 文件、Java Class 文件、ASM Code 文件和 ASM Source 文件之间的转换关系。 图 1. 四种文件的转换图 图 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 Code01

public

void

showClassASMProcess() {

02

ClassReader cr; 03

final

String n = SayHelloImpl.

class

.getName();

04

05

try

{

06

cr =

new

ClassReader(n);
07

cr.accept(

new

ASMifierClassVisitor(

new

PrintWriter(System.out)),

08

new

Attribute[

0

], 09

ClassReader.SKIP_DEBUG);

10

11

}

catch

(Exception e) {

12

e.printStackTrace(); 13

}

14

}

另外一个重要的转换就是, 给定了 Java Class 文件, 如何查看它的 ASM Source ? 这点对于我们验证 ASM Code 生成的 class 是否正确很有用。ASM 提供了另外一个工具 TraceClassVisitor, 来获得一个 Java Class 对应的 ASM Source。代码如下: 清单 8. 利用 TraceClassVisitor 查看 class 文件的 ASM Source01

//file 是 class 文件的全路径名

02

public

void

showClassSource(String file) { 03

FileInputStream is ;

04

ClassReader cr; 05

06

try

{ 07

is =

new

FileInputStream(file);

08

cr =

new

ClassReader(is); 09

TraceClassVisitor trace =

new

TraceClassVisitor(

new

PrintWriter(System.out));

10

cr.accept(trace, ClassReader.SKIP_DEBUG); 11

12

is.close() ; 13

}

catch

(Exception e) {

14

e.printStackTrace() ; 15

}

16

} 17

{

18

e.printStackTrace(); 19

}

20

}

ASM 类注入

ASM 类注入是指修改一个现有的 class 文件,在其中加入自己的代码。按通常思维,我们需要利用 ASM 提供的 reader 类去读取所要修改的类文件, 找到要修改的地方,如方法名或属性名,然后在该处插入自己的代码或修改现有的代码,这种思路将使问题复杂化。 ASM 为我们提供了访问类文件的 Visitor 模式,来遍历一个 class 文件,Visitor 是一个实现 ClassVisitor 接口的类。 要注入一个 class 文件,先要找到一个合适的 Visitor 做向导, ASM 提供了好几种 Visitor, 最常用的是 ClassWriter。同时我们需要提供一个适配器类 ClassAdapter, Visitor 会带着这个 Adapter 一起去遍历, 然后在遍历过程中回调 Adapter 提供的方法。我们在 Adapter 的这些方法中就可以实现我们的修改和定制逻辑。当然,如果你不想做任何修改,那 Visitor 遍历完后将得到一个和被遍历的 class 完全一样的拷贝。 清单 9. 类注入的完整过程01

public

void

addAnnotationToExistingClass() {

02

FileInputStream is ; 03

ClassReader cr;

04

try

{
05

String classfile =

"E:\SayHelloImpl.class"

;

// 待遍历的类

06

showClassSource(classfile) ;

// 打印该类的 ASM source 07

08

is =

new

FileInputStream(classfile);
09

cr =

new

ClassReader(is);

10

// 此处我们使用 ClassWriter 做 Visitor 11

ClassWriter cw =

new

ClassWriter(ClassWriter.COMPUTE_MAXS);

12

// 给 Visitor 提供一个 Adapter 13

AddAnnotationAdapter adapter =

new

AddAnnotationAdapter(cw);

14

cr.accept(adapter, ClassReader.SKIP_DEBUG);
15

16

// 遍历完后,生成的 class 保存在字节数组中 17

byte

[] b = cw.toByteArray();

18

19

try

{

20

// 将字节数组输出到文件中 21

// 为了不至于覆盖掉原有的 SayHelloImpl.class 文件,我们将结果输出到 _SayHelloImpl.class

22

FileOutputStream fout =

new

FileOutputStream(

"E:\_SayHelloImpl.class"

); 23

fout.write(b) ;

24

fout.flush(); 25

fout.close() ;

26

}

catch

(Exception e) { 27

e.printStackTrace() ;

28

} 29

30

// 验证 _SayHelloImpl.class 是否包含我们注入的代码 31

showClassSource(

"E:\_SayHelloImpl.class"

) ;

32

}

catch

(Exception e) { 33

e.printStackTrace();

34

} 35

}

web services 标记注入过程

我们已经知道了如何往 class 中注入自己的代码,那对于一个已有的 Java bean,怎么往里注入 @WebService 和 @WebMethod 呢?根据上面我们讲的转换关系,我们可以先写一个带 annotation 的 Java Source,编译得到 Java Class, 再得到其 ASM Code。下面是利用 ASM 给一个不带任何 Annotation 的 class 添加 @WebService 和 @WebMethod 的步骤。

  1. 首先在 SayHelloImpl.java 中添加 @WebService 和 @WebMethod: 清单 10. 带 web services 标注的 SayHelloImpl 类1

@WebService

2

public

class

SayHelloImpl { 3

@WebMethod

4

public

String sayHello(String s) { 5

return

"Hello: "

  • s ;

6

} 7

}

  1. 通过 ASMifierClassVisitor 工具获得 SayHelloImpl class 的 ASM Code,见清单 6。
  2. 写一个 ClassAdapter 类, ClassAdapter 其实也是一个 Visitor。根据 ASM Code 的提示, 我们就可以知道如何利用 ASM API 去获得我们希望的 class 文件。下面是 Adapter 的示例代码。 清单 11. 添加 Annotation 的 Adapter 类01

public

class

AddAnnotationAdapter

extends

ClassAdapter

implements

Opcodes{

02

//private String annotationDesc; 03

private

boolean

isAnnotationPresent;

04

05

public

AddAnnotationAdapter(ClassVisitor cv) {

06

super

(cv); 07

}

08

09

@Override

10

public

void

visit(

int

version,

int

access, String name, String signature, 11

String superName, String[] interfaces) {

12

13

cv.visit(V1_6, ACC_PUBLIC + ACC_SUPER, name, signature, superName,

14

interfaces); 15

AnnotationVisitor av0;

16

av0 = cv.visitAnnotation(

"Ljavax/jws/WebService;"

,

true

); 17

av0.visitEnd();

18

} 19

20

@Override 21

public

AnnotationVisitor visitAnnotation(String desc,

boolean

visible) {

22

return

cv.visitAnnotation(desc, visible); 23

}

24

25

@Override

26

public

void

visitInnerClass(String name, String outerName, 27

String innerName,

int

access) {

28

cv.visitInnerClass(name, outerName, innerName, access); 29

}

30

31

@Override

32

public

FieldVisitor visitField(

int

access, String name, String desc, 33

String signature, Object value) {

34

return

cv.visitField(access, name, desc, signature, value); 35

}

36

37

@Override

38

public

MethodVisitor visitMethod(

int

access, String name, String desc, 39

String signature, String[] exceptions) {

40

MethodVisitor mv = cv.visitMethod(access, name, desc, signature, 41

exceptions);

42

if

(!name.equals(

""

)) { 43

AnnotationVisitor av0;

44

av0 = mv.visitAnnotation(

"Ljavax/jws/WebMethod;"

,

true

); 45

av0.visitEnd();

46

} 47

return

mv;

48

} 49

50

@Override 51

public

void

visitEnd() {

52

cv.visitEnd(); 53

}

54

55

}

最后,我们采用上面讲的类注入办法,即可得到一个带 @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 编程模型上的项目有一定的参考价值。在基于 JAX-WS 标准的 web services 的开发中,不少实际场景都是希望采用自底向上的开发方式, 即基于已有的 Java bean 来创建 web services 。WebSphere Application Server ( 以下简称 WAS ) 提供了命令行的工具 wsgen 和相对应的 Ant task 来支持这种开发过程,而且这两个工具比较适合大型项目的自动化构建。 这两个工具的使用前提是 Java bean 中事先添加有 web services 的 Annotation 标注,而在现有的业务系统中,class 文件一般是不带 Annotation 的,这就需要开发人员去修改现有的代码,以手工方式添加 Annotation,但这样带来的工作量太大且容易出错。本文介绍了一种解决途径,可以不用修改源代码,而是利用字节码工具 ASM 直接修改 class 文件, 在字节码文件中自动注入 Annotation ,然后再利用 wsgen 工具就可以很方便地生成 web services 应用。 本文同时也总结了使用 ASM 的一些实践。

本文首先介绍了和 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 类1

public

class

SayHelloImpl {

2

public

String sayHello(String s) { 3

return

“Hello: ” + s ;

4

} 5

}

再用 wsgen 来创建 web services,wsgen 的使用如下: 清单 2. wsgen 实例1

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 Source01

// class version 50.0 (50)

02

// access flags 33 03

public

class

com/ibm/was/asm/SayHelloImpl {

04

05

// access flags 1

06

public

()V 07

ALOAD

0

08

INVOKESPECIAL java/lang/Object. ()V 09

RETURN

10

MAXSTACK =

1 11

MAXLOCALS =

1

12

13

// access flags 1

14

public

sayHello(Ljava/lang/String;)Ljava/lang/String; 15

NEW java/lang/StringBuilder

16

DUP 17

LDC

"Hello:"

18

INVOKESPECIAL java/lang/StringBuilder. (Ljava/lang/String;)V 19

ALOAD

1

20

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) 21

Ljava/lang/StringBuilder;

22

INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 23

ARETURN

24

MAXSTACK =

3 25

MAXLOCALS =

2

26

}清单 6. 生成 SayHelloImpl.class 的 ASM Code01

package

asm.com.ibm.was.asm;

02

import

java.util./*; 03

import

org.objectweb.asm./*;

04

import

org.objectweb.asm.attrs./*; 05

public

class

SayHelloImplDump

implements

Opcodes {

06

public

static

byte

[] dump ()

throws

Exception { 07

ClassWriter cw =

new

ClassWriter(

0

);

08

FieldVisitor fv; 09

MethodVisitor mv;

10

AnnotationVisitor av0; 11

12

cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER,

"com/ibm/was/asm/SayHelloImpl"

, 13

null

,

"java/lang/Object"

,

null

);

14

15

{

16

mv = cw.visitMethod(ACC_PUBLIC,

""

,

"()V"

,

null

,

null

); 17

mv.visitCode();

18

mv.visitVarInsn(ALOAD,

0

); 19

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/Object"

,

""

,

"()V"

);

20

mv.visitInsn(RETURN); 21

mv.visitMaxs(

1

,

1

);

22

mv.visitEnd(); 23

}

24

{ 25

mv = cw.visitMethod(ACC_PUBLIC,

"sayHello"

,

26

"(Ljava/lang/String;)Ljava/lang/String;"

,

null

,

null

); 27

mv.visitCode();

28

mv.visitTypeInsn(NEW,

"java/lang/StringBuilder"

); 29

mv.visitInsn(DUP);

30

mv.visitLdcInsn(

"Hello:"

); 31

mv.visitMethodInsn(INVOKESPECIAL,

"java/lang/StringBuilder"

,

32

""

,

"(Ljava/lang/String;)V"

); 33

mv.visitVarInsn(ALOAD,

1

);

34

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 35

"append"

,

"(Ljava/lang/String;)Ljava/lang/StringBuilder;"

);

36

mv.visitMethodInsn(INVOKEVIRTUAL,

"java/lang/StringBuilder"

, 37

"toString"

,

"()Ljava/lang/String;"

);

38

mv.visitInsn(ARETURN); 39

mv.visitMaxs(

3

,

2

);

40

mv.visitEnd(); 41

}

42

cw.visitEnd(); 43

return

cw.toByteArray();

44

} 45

}

图 1 描述了 Java Source 文件、Java Class 文件、ASM Code 文件和 ASM Source 文件之间的转换关系。 图 1. 四种文件的转换图 图 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 Code01

public

void

showClassASMProcess() {

02

ClassReader cr; 03

final

String n = SayHelloImpl.

class

.getName();

04

05

try

{

06

cr =

new

ClassReader(n);
07

cr.accept(

new

ASMifierClassVisitor(

new

PrintWriter(System.out)),

08

new

Attribute[

0

], 09

ClassReader.SKIP_DEBUG);

10

11

}

catch

(Exception e) {

12

e.printStackTrace(); 13

}

14

}

另外一个重要的转换就是, 给定了 Java Class 文件, 如何查看它的 ASM Source ? 这点对于我们验证 ASM Code 生成的 class 是否正确很有用。ASM 提供了另外一个工具 TraceClassVisitor, 来获得一个 Java Class 对应的 ASM Source。代码如下: 清单 8. 利用 TraceClassVisitor 查看 class 文件的 ASM Source01

//file 是 class 文件的全路径名

02

public

void

showClassSource(String file) { 03

FileInputStream is ;

04

ClassReader cr; 05

06

try

{ 07

is =

new

FileInputStream(file);

08

cr =

new

ClassReader(is); 09

TraceClassVisitor trace =

new

TraceClassVisitor(

new

PrintWriter(System.out));

10

cr.accept(trace, ClassReader.SKIP_DEBUG); 11

12

is.close() ; 13

}

catch

(Exception e) {

14

e.printStackTrace() ; 15

}

16

} 17

{

18

e.printStackTrace(); 19

}

20

}

ASM 类注入

ASM 类注入是指修改一个现有的 class 文件,在其中加入自己的代码。按通常思维,我们需要利用 ASM 提供的 reader 类去读取所要修改的类文件, 找到要修改的地方,如方法名或属性名,然后在该处插入自己的代码或修改现有的代码,这种思路将使问题复杂化。 ASM 为我们提供了访问类文件的 Visitor 模式,来遍历一个 class 文件,Visitor 是一个实现 ClassVisitor 接口的类。 要注入一个 class 文件,先要找到一个合适的 Visitor 做向导, ASM 提供了好几种 Visitor, 最常用的是 ClassWriter。同时我们需要提供一个适配器类 ClassAdapter, Visitor 会带着这个 Adapter 一起去遍历, 然后在遍历过程中回调 Adapter 提供的方法。我们在 Adapter 的这些方法中就可以实现我们的修改和定制逻辑。当然,如果你不想做任何修改,那 Visitor 遍历完后将得到一个和被遍历的 class 完全一样的拷贝。 清单 9. 类注入的完整过程01

public

void

addAnnotationToExistingClass() {

02

FileInputStream is ; 03

ClassReader cr;

04

try

{
05

String classfile =

"E:\SayHelloImpl.class"

;

// 待遍历的类

06

showClassSource(classfile) ;

// 打印该类的 ASM source 07

08

is =

new

FileInputStream(classfile);
09

cr =

new

ClassReader(is);

10

// 此处我们使用 ClassWriter 做 Visitor 11

ClassWriter cw =

new

ClassWriter(ClassWriter.COMPUTE_MAXS);

12

// 给 Visitor 提供一个 Adapter 13

AddAnnotationAdapter adapter =

new

AddAnnotationAdapter(cw);

14

cr.accept(adapter, ClassReader.SKIP_DEBUG);
15

16

// 遍历完后,生成的 class 保存在字节数组中 17

byte

[] b = cw.toByteArray();

18

19

try

{

20

// 将字节数组输出到文件中 21

// 为了不至于覆盖掉原有的 SayHelloImpl.class 文件,我们将结果输出到 _SayHelloImpl.class

22

FileOutputStream fout =

new

FileOutputStream(

"E:\_SayHelloImpl.class"

); 23

fout.write(b) ;

24

fout.flush(); 25

fout.close() ;

26

}

catch

(Exception e) { 27

e.printStackTrace() ;

28

} 29

30

// 验证 _SayHelloImpl.class 是否包含我们注入的代码 31

showClassSource(

"E:\_SayHelloImpl.class"

) ;

32

}

catch

(Exception e) { 33

e.printStackTrace();

34

} 35

}

web services 标记注入过程

我们已经知道了如何往 class 中注入自己的代码,那对于一个已有的 Java bean,怎么往里注入 @WebService 和 @WebMethod 呢?根据上面我们讲的转换关系,我们可以先写一个带 annotation 的 Java Source,编译得到 Java Class, 再得到其 ASM Code。下面是利用 ASM 给一个不带任何 Annotation 的 class 添加 @WebService 和 @WebMethod 的步骤。

  1. 首先在 SayHelloImpl.java 中添加 @WebService 和 @WebMethod: 清单 10. 带 web services 标注的 SayHelloImpl 类1

@WebService

2

public

class

SayHelloImpl { 3

@WebMethod

4

public

String sayHello(String s) { 5

return

"Hello: "

  • s ;

6

} 7

}

  1. 通过 ASMifierClassVisitor 工具获得 SayHelloImpl class 的 ASM Code,见清单 6。
  2. 写一个 ClassAdapter 类, ClassAdapter 其实也是一个 Visitor。根据 ASM Code 的提示, 我们就可以知道如何利用 ASM API 去获得我们希望的 class 文件。下面是 Adapter 的示例代码。 清单 11. 添加 Annotation 的 Adapter 类01

public

class

AddAnnotationAdapter

extends

ClassAdapter

implements

Opcodes{

02

//private String annotationDesc; 03

private

boolean

isAnnotationPresent;

04

05

public

AddAnnotationAdapter(ClassVisitor cv) {

06

super

(cv); 07

}

08

09

@Override

10

public

void

visit(

int

version,

int

access, String name, String signature, 11

String superName, String[] interfaces) {

12

13

cv.visit(V1_6, ACC_PUBLIC + ACC_SUPER, name, signature, superName,

14

interfaces); 15

AnnotationVisitor av0;

16

av0 = cv.visitAnnotation(

"Ljavax/jws/WebService;"

,

true

); 17

av0.visitEnd();

18

} 19

20

@Override 21

public

AnnotationVisitor visitAnnotation(String desc,

boolean

visible) {

22

return

cv.visitAnnotation(desc, visible); 23

}

24

25

@Override

26

public

void

visitInnerClass(String name, String outerName, 27

String innerName,

int

access) {

28

cv.visitInnerClass(name, outerName, innerName, access); 29

}

30

31

@Override

32

public

FieldVisitor visitField(

int

access, String name, String desc, 33

String signature, Object value) {

34

return

cv.visitField(access, name, desc, signature, value); 35

}

36

37

@Override

38

public

MethodVisitor visitMethod(

int

access, String name, String desc, 39

String signature, String[] exceptions) {

40

MethodVisitor mv = cv.visitMethod(access, name, desc, signature, 41

exceptions);

42

if

(!name.equals(

""

)) { 43

AnnotationVisitor av0;

44

av0 = mv.visitAnnotation(

"Ljavax/jws/WebMethod;"

,

true

); 45

av0.visitEnd();

46

} 47

return

mv;

48

} 49

50

@Override 51

public

void

visitEnd() {

52

cv.visitEnd(); 53

}

54

55

}

最后,我们采用上面讲的类注入办法,即可得到一个带 @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 编程模型上的项目有一定的参考价值。

JVM调优总结(十)

Posted on

JVM调优总结(十)-调优方法

JVM调优工具

Jconsole,jProfile,VisualVM


Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。详细说明参考这里

JProfiler:商业软件,需要付费。功能强大。详细说明参考这里

VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。

如何调优

观察内存释放情况、集合类检查、对象树

上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功能

堆信息查看

可查看堆空间大小分配(年轻代、年老代、持久代分配)

提供即时的垃圾回收功能

垃圾监控(长时间监控回收情况)

查看堆内类、对象信息查看:数量、类型等

对象引用情况查看

有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:

--年老代年轻代大小划分是否合理

--内存泄漏

--垃圾回收算法设置是否合理

线程监控

线程信息监控:系统线程数量。

线程状态监控:各个线程都处在什么样的状态下

Dump线程详细信息:查看线程内部运行情况

死锁检查

热点分析

CPU热点:检查系统哪些方法占用的大量CPU时间

内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)

这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。

快照

快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题

举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。

内存泄漏检查

内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。

内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。

内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。

需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。

年老代堆空间被占满

异常: java.lang.OutOfMemoryError: Java heap space

说明:

这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。

如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)

解决:

这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。

持久代被占满

异常:java.lang.OutOfMemoryError: PermGen space

说明:

Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。

更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。

解决:

1. -XX:MaxPermSize=16m

2. 换用JDK。比如JRocket。

堆栈溢出

异常:java.lang.StackOverflowError

说明:这个就不多说了,一般就是递归没返回,或者循环调用造成

线程堆栈满

异常:Fatal: Stack size too small

说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。

解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。

系统内存被占满

异常:java.lang.OutOfMemoryError: unable to create new native thread

说明

这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。

分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。

解决:

1. 重新设计系统减少线程数量。

2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。

JVM调优总结(十二)

Posted on

JVM调优总结(十二)-参考资料

能整理出上面一些东西,也是因为站在巨人的肩上。下面是一些参考资料,供大家学习,大家有更好的,可以继续完善:)

· Java 理论与实践: 垃圾收集简史

· Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning

· Improving Java Application Performance and Scalability by Reducing Garbage Collection Times and Sizing Memory Using JDK 1.4.1

· Hotspot memory management whitepaper

· Java Tuning White Paper

· Diagnosing a Garbage Collection problem

· Java HotSpot VM Options

· A Collection of JVM Options

· Garbage-First Garbage Collection

· Frequently Asked Questions about Garbage Collection in the HotspotTM JavaTM Virtual Machine

· JProfiler试用手记

· Java6 JVM参数选项大全

· 《深入Java虚拟机》。虽然过去了很多年,但这本书依旧是经典。

这里是本系列的最后一篇了,很高兴大家能够喜欢这系列的文章。期间也提了很多问题,其中有些是我之前没有想到的或者考虑欠妥的,感谢提出这些问题的朋友,我也学到的不少东西。

JVM调优总结(六)

Posted on

JVM调优总结(六)-分代垃圾回收详述2

分代垃圾回收流程示意

选择合适的垃圾收集算法

串行收集器

用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。可以使用-XX:+UseSerialGC打开。

并行收集器

对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。并行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中进行了增强--可以对年老代进行并行收集。如果年老代不使用并发收集的话,默认是使用单线程进行垃圾回收,因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。

使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。

此收集器可以进行如下配置: 最大垃圾回收暂停:指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=指定。为毫秒.如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。

吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=来设定,公式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。

并发收集器

可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。

并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器 会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。

并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。

在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。



**浮动垃圾:**由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。



**Concurrent Mode Failure:**并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。



**启动并发收集器:**因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=<N>指定还有多少剩余堆时开始执行并发收集

小结

串行处理器:

--适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。 --缺点:只能用于小型应用

并行处理器:

--适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。 --缺点:垃圾收集过程中应用响应时间可能加长

并发处理器:

--适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。

JVM调优总结(五)

Posted on

JVM调优总结(五)-分代垃圾回收详述1

为什么要分代

分代的垃圾回收策略,是基于这样一个事实:**不同的对象的生命周期是不一样的**。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。



在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。



试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

如何分代

如图所示:

虚拟机中的共划分为三个代:**年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)**。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

年轻代:

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

年老代:

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

持久代:

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。

什么情况下触发垃圾回收

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:**Scavenge GC**和**Full GC**。

Scavenge GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

· 年老代(Tenured)被写满

· 持久代(Perm)被写满

· System.gc()被显示调用

·上一次GC之后Heap的各域分配策略动态变化