Tải bản đầy đủ (.pdf) (24 trang)

Động lực học lập trình Java, Phần 7: Kỹ thuật bytecode với BCEL Apache BCEL cho phép bạn đi đến các chi tiết về ngôn ngữ assembler của JVM cho hoạt động lớp doc

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (276.94 KB, 24 trang )

Động lực học lập trình Java, Phần 7: Kỹ thuật bytecode với BCEL
Apache BCEL cho phép bạn đi đến các chi tiết về ngôn ngữ assembler của JVM
cho hoạt động lớp
Dennis Sosnoski, Nhà tư vấn, Sosnoski Software Solutions, Inc.
Tóm tắt: Apache Byte Code Engineering Library (BCEL-Thư viện kỹ thuật mã
byte) cho phép bạn nghiên cứu bytecode của các lớp Java. Bạn có thể sử dụng nó
để biến đổi các biểu diễn lớp hiện tại hoặc xây dựng các lớp mới, và vì BCEL làm
việc ở mức các lệnh JVM riêng biệt, nó sẽ cho bạn sức mạnh tối đa trên mã của
bạn. Mặc dù sức mạnh đó đi kèm với một chi phí về độ phức tạp. Trong bài này,
nhà tư vấn Java Dennis Sosnoski cung cấp cho bạn những điều cơ bản về BCEL
và hướng dẫn bạn thông qua một ví dụ ứng dụng BCEL để cho bạn có thể tự quyết
định xem sức mạnh có tương xứng với sự phức tạp không.
Trong ba bài viết mới đây của loạt bài này, tôi đã cho bạn thấy cách sử dụng
khung công tác Javassist cho hoạt động lớp (classworking). Bây giờ tôi sẽ trình
bày một cách tiếp cận rất khác để xử lí bytecode, đó là sử dụng Apache Byte Code
Engineering Library (BCEL). BCEL hoạt động ở mức các lệnh JVM thực sự,
không giống như giao diện mã nguồn được Javassist hỗ trợ. Cách tiếp cận mức
thấp làm cho BCEL rất tốt khi bạn thực sự muốn kiểm soát mọi bước thực hiện
chương trình, nhưng nó cũng làm cho hoạt động với BCEL phức tạp hơn nhiều so
với sử dụng Javassist cho các trường hợp ở đó cả hai cùng làm việc.
Tôi sẽ bắt đầu bằng cách trình bày kiến trúc BCEL cơ bản, sau đó dành hầu hết bài
viết này cho việc xây dựng lại ví dụ hoạt động lớp Javassist đầu tiên của tôi bằng
BCEL. Tôi sẽ kết thúc bằng việc xem xét một số các công cụ có trong gói BCEL
và một vài ứng dụng mà các nhà phát triển đã xây dựng ở trên BCEL.
Truy cập lớp BCEL
BCEL cung cấp cho bạn tất cả các khả năng cơ bản giống như Javassist kiểm tra,
chỉnh sửa và tạo các lớp Java nhị phân. Sự khác biệt rõ ràng với BCEL là mọi thứ
được thiết kế để làm việc ở mức ngôn ngữ chương trình dịch hợp ngữ (assembler)
của JVM, hơn là giao diện mã nguồn do Javassist cung cấp. Có một số khác biệt
sâu hơn dưới các vỏ bọc, gồm việc sử dụng hai hệ thống phân cấp riêng của các
thành phần trong BCEL một cái kiểm tra mã hiện có và cái khác để tạo mã mới.


Tôi sẽ giả định bạn quen với Javassist từ những bài viết trước trong loạt bài này
(xem phần bên cạnh Đừng bỏ lỡ phần còn lại của loạt bài này). Vì vậy tôi sẽ tập
trung vào những sự khác biệt có khả năng gây nhầm lẫn cho bạn khi bạn bắt đầu
làm việc với BCEL.
Như với Javassist, các khía cạnh kiểm tra lớp của BCEL về cơ bản lặp lại những gì
có sẵn trực tiếp trong nền tảng Java qua Reflection API. Điều trùng lắp này là cần
thiết trong một bộ công cụ hoạt động lớp vì bạn thường không muốn nạp các lớp
mà bạn đang làm việc với chúng cho đến sau khi chúng đã được thay đổi.
Đừng bỏ lỡ phần còn lại của loạt bài này
Phần 1, "Các lớp Java và nạp lớp" (04.2003)
Phần 2, "Giới thiệu sự phản chiếu" (06.2003)
Phần 3, "Ứng dụng sự phản chiếu" (07.2003)
Phần 4, "Chuyển đổi lớp bằng Javassist" (09.2003)
Phần 5, "Việc chuyển các lớp đang hoạt động" (02.2004)
Phần 6, "Các thay đổi hướng-khía cạnh với Javassist" (03.2004)
Phần 8, "Thay thế sự phản chiếu bằng việc tạo mã" (06.2004)
BCEL cung cấp một số định nghĩa không thay đổi cơ bản trong gói
org.apache.bcel, nhưng không kể những định nghĩa này, tất cả các mã kiểm tra có
liên quan nằm trong gói org.apache.bcel.classfile. Điểm khởi đầu trong gói này là
lớp JavaClass. Lớp này đóng vai trò giống như trong việc truy cập thông tin lớp
bằng cách sử dụng BCEL như lớp java.lang.Class thực hiện khi dùng sự phản
chiếu của Java chuẩn. JavaClass xác định các phương thức để nhận được thông tin
trường và phương thức cho lớp này, cũng như thông tin theo cấu trúc về siêu lớp
và các giao diện. Không giống như java.lang.Class, JavaClass cũng cung cấp
quyền truy cập tới các thông tin nội bộ cho lớp đó, gồm nhóm hằng số và các
thuộc tính và biểu diễn lớp nhị phân đầy đủ như một luồng byte.
Các cá thể JavaClass thường được tạo bằng cách phân tích cú pháp lớp nhị phân
hiện có. BCEL cung cấp lớp org.apache.bcel.Repositoryđể thực hiện phân tích cú
pháp cho bạn. Theo mặc định, BCEL phân tích cú pháp và lưu trữ nhanh các biểu
diễn của các lớp được tìm thấy trong đường dẫn lớp (classpath) JVM, nhận được

các biểu diễn lớp nhị phân thực sự từ một cá thể org.apache.bcel.util.Repository
(lưu ý sự khác biệt trong tên gói). Hiện tại org.apache.bcel.util.Repository là một
giao diện cho một nguồn biểu diễn lớp nhị phân. Bạn có thể thay thế các đường
dẫn khác để tìm kiếm các tệp lớp hoặc các cách truy cập thông tin lớp khác, thay
cho nguồn mặc định có sử dụng đường dẫn lớp.
Thay đổi các lớp
Bên cạnh việc truy cập kiểu-phản chiếu tới các thành phần lớp,
org.apache.bcel.classfile.JavaClass cũng cung cấp các phương thức để thay đổi lớp
đó. Bạn có thể sử dụng những phương thức này để thiết lập bất kỳ các thành phần
lớp này với các giá trị mới. Mặc dù, chúng thường không sử dụng trực tiếp, vì các
lớp khác trong gói đó không hỗ trợ cho việc xây dựng các phiên bản mới của các
thành phần theo bất kỳ cách hợp lý nào. Thay vào đó, có một tập riêng biệt đầy đủ
các lớp trong gói org.apache.bcel.generic để cung cấp phiên bản có thể chỉnh sửa
được của các thành phần được các lớp org.apache.bcel.classfile biểu diễn.
Cũng như org.apache.bcel.classfile.JavaClass là điểm khởi đầu cho việc sử dụng
BCEL để kiểm tra các lớp hiện có, org.apache.bcel.generic.ClassGen là điểm bắt
đầu của bạn để tạo các lớp mới. Nó cũng thay đổi các lớp hiện tại để xử lý
trường hợp đó, có một hàm tạo lấy một cá thể JavaClass và sử dụng nó để khởi tạo
thông tin lớp ClassGen. Một khi bạn đã hoàn tất các thay đổi lớp của bạn, bạn có
thể nhận được một sự biểu diễn lớp thích hợp từ cá thể ClassGen bằng cách gọi
một phương thức trả về một JavaClass, nó có thể lần lượt được chuyển đổi sang
biểu diễn lớp nhị phân.
Hỏi chuyên gia: Dennis Sosnoski về các vấn đề JVM và bytecode
Đối với các ý kiến hay các câu hỏi về tài liệu được trình bày trong loạt bài này,
cũng như bất cứ điều gì khác có liên quan đến Java bytecode, định dạng lớp nhị
phân Java hoặc các vấn đề JVM chung, hãy truy cập vào diễn đàn thảo luận JVM
và Bytecode, do Dennis Sosnoski kiểm soát.
Nói lộn xộn quá phải không? Tôi nghĩ như vậy. Trong thực tế, việc quay lại và
tiến lên giữa hai gói này là một trong những khía cạnh làm việc bất tiện nhất với
BCEL. Các cấu trúc lớp sao chép lại hướng theo cách này, vì thế nếu bạn đang làm

việc với BCEL, thật bõ công để viết các lớp của trình bao bọc (wrapper), mà nó có
thể ẩn dấu một số các sự khác nhau này. Với bài viết này, tôi sẽ chủ yếu làm việc
với các lớp của gói org.apache.bcel.generic và tránh sử dụng các trình bao bọc,
nhưng với bạn đây là một điều để ghi nhớ cho công việc riêng của mình.
Ngoài ClassGen, gói org.apache.bcel.generic định nghĩa các lớp để quản lý việc
xây dựng các thành phần lớp khác nhau. Các lớp xây dựng này gồm
ConstantPoolGen để xử lý nhóm hằng số FieldGen và MethodGen cho các trường
và các phương thức và InstructionList để làm việc với các chuỗi của các lệnh
JVM. Cuối cùng, gói org.apache.bcel.generic cũng định nghĩa các lớp để biểu diễn
mọi kiểu của các lệnh JVM. Bạn có thể trực tiếp tạo các cá thể của các lớp này
hoặc trong một số trường hợp bằng cách sử dụng lớp của trình trợ giúp (helper)
org.apache.bcel.generic.InstructionFactory. Lợi thế của việc sử dụng
InstructionFactory là nó xử lý các chi tiết tạo sổ sách của việc xây dựng lệnh cho
bạn (gồm cả việc thêm các mục vào nhóm hằng số khi cần thiết cho các lệnh). Bạn
sẽ thấy cách làm cho tất cả các lớp này hoạt động cùng nhau trong phần tiếp theo.


Hoạt động lớp với BCEL
Đối với một ví dụ về việc áp dụng BCEL, tôi sẽ sử dụng cùng một nhiệm vụ mà
tôi đã sử dụng như một ví dụ Javassist trong Phần 4 việc đo thời gian được dùng
để thực hiện một phương thức. Tôi thậm chí sẽ sử dụng cùng một cách tiếp cận mà
tôi đã sử dụng với Javassist: tôi sẽ tạo một bản sao của phương thức ban đầu có
tính thời gian khi sử dụng một tên đã thay đổi, sau đó thay thế phần thân của
phương thức ban đầu với mã bao bọc các tính toán đếm thời gian xung quanh một
cuộc gọi đến phương thức đã đổi tên.
Chọn một vật thí nghiệm
Liệt kê 1 đưa ra một phương thức ví dụ mà tôi sẽ sử dụng cho các mục đích giải
thích: phương thức buildString của lớp StringBuilder. Như tôi đã nói trong Phần 4,
phương thức này xây dựng một String có độ dài yêu cầu bất kỳ bằng cách thực
hiện chính xác những gì mà bất kỳ chuyên gia hiệu năng Java nào khuyên bạn

không nên làm nó liên tục gắn thêm một ký tự vào cuối của một chuỗi để tạo
một chuỗi dài hơn. Vì các chuỗi không thay đổi được, nên cách tiếp cận này có
nghĩa là một chuỗi mới sẽ được xây dựng mỗi khi qua vòng lặp, với các dữ liệu
được sao chép từ chuỗi cũ và một ký tự được thêm vào cuối. Ảnh hưởng cuối cùng
là phương thức này phải chịu chi phí hoạt động càng ngày càng nhiều khi nó được
sử dụng để tạo các chuỗi dài hơn.

Liệt kê 1. Phương thức có tính giờ

public class StringBuilder
{
private String buildString(int length) {
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
return result;
}

public static void main(String[] argv) {
StringBuilder inst = new StringBuilder();
for (int i = 0; i < argv.length; i++) {
String result = inst.buildString(Integer.parseInt(argv[i]));
System.out.println("Constructed string of length " +
result.length());
}
}
}

Liệt kê 2 cho thấy mã nguồn tương đương với sự thay đổi hoạt động lớp mà tôi sẽ

làm với BCEL. Ở đây phương thức của trình bao bọc (wrapper) chỉ lưu trữ thời
gian hiện tại, sau đó gọi phương thức ban đầu đã đổi tên và in ra một thông báo
thời gian trước khi trả về kết quả của cuộc gọi đến phương thức ban đầu.

Liệt kê 2. Tính thời gian đã thêm vào phương thức ban đầu

public class StringBuilder
{
private String buildString$impl(int length) {
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
return result;
}

private String buildString(int length) {
long start = System.currentTimeMillis();
String result = buildString$impl(length);
System.out.println("Call to buildString$impl took " +
(System.currentTimeMillis()-start) + " ms.");
return result;
}

public static void main(String[] argv) {
StringBuilder inst = new StringBuilder();
for (int i = 0; i < argv.length; i++) {
String result = inst.buildString(Integer.parseInt(argv[i]));
System.out.println("Constructed string of length " +
result.length());

}
}
}

Mã hóa phép chuyển đổi
Triển khai thực hiện mã để thêm việc tính thời gian phương thức sử dụng các
BCEL API mà tôi đã nêu ra trong phần Truy cập lớp BCEL. Làm việc ở mức các
lệnh JVM làm cho đoạn mã dài hơn rất nhiều so với ví dụ Javassist trong Phần 4,
do đó ở đây tôi sẽ duyệt qua nó từng đoạn một trước khi cung cấp cho bạn việc
thực hiện đầy đủ. Trong đoạn mã cuối cùng, tất cả các đoạn này sẽ tạo nên chỉ một
phương thức, một phương thức lấy một cặp tham số: cgen, một cá thể của lớp
org.apache.bcel.generic.ClassGen được khởi tạo bằng các thông tin hiện có cho
các lớp đang được thay đổi; và method (phương thức), một cá thể
org.apache.bcel.classfile.Method cho phương thức tôi sắp tính thời gian.
Liệt kê 3 có đoạn mã đầu tiên cho phương thức chuyển đổi. Như bạn thấy từ các ý
kiến, phần đầu tiên chỉ khởi tạo các thành phần BCEL cơ bản mà tôi sắp sử dụng,
gồm việc khởi tạo một cá thể org.apache.bcel.generic.MethodGen mới bằng cách
sử dụng các thông tin cho phương thức có tính giờ. Tôi thiết lập một danh sách
lệnh rỗng cho MethodGen, để sau này tôi sẽ điền vào đó với mã tính thời gian thực
tế. Trong phần thứ hai, tôi tạo một cá thể org.apache.bcel.generic.MethodGen thứ
hai từ phương thức ban đầu, sau đó loại bỏ phương thức ban đầu khỏi lớp đó.
Trong cá thể MethodGen thứ hai này, tôi chỉ cần thay đổi tên để sử dụng một hậu
tố "$impl", sau đó gọi phương thức getMethod() để chuyển đổi thông tin phương
thức có thể thay đổi được thành một dạng cố định như một cá thể
org.apache.bcel.classfile.Method. Sau đó tôi sử dụng cuộc gọi addMethod() để
thêm phương thức đã đổi tên vào lớp đó.

Liệt kê 3. Thêm phương thức chặn

// set up the construction tools

InstructionFactory ifact = new InstructionFactory(cgen);
InstructionList ilist = new InstructionList();
ConstantPoolGen pgen = cgen.getConstantPool();
String cname = cgen.getClassName();
MethodGen wrapgen = new MethodGen(method, cname, pgen);
wrapgen.setInstructionList(ilist);

// rename a copy of the original method
MethodGen methgen = new MethodGen(method, cname, pgen);
cgen.removeMethod(method);
String iname = methgen.getName() + "$impl";
methgen.setName(iname);
cgen.addMethod(methgen.getMethod());

Liệt kê 4 cung cấp các đoạn mã tiếp theo cho phương thức chuyển đổi. Ở đây phần
đầu tiên tính không gian bị chiếm bởi các tham số gọi phương thức trên ngăn xếp.
Vì đoạn này cần thiết để lưu trữ thời gian bắt đầu trên khung ngăn xếp trước khi
gọi phương thức bao bọc mà tôi cần phải biết có thể sử dụng khoảng trống nào cho
một biến cục bộ (lưu ý rằng tôi có thể sử dụng việc xử lý biến cục bộ của BCEL để
nhận được cùng tác dụng, nhưng với bài viết này tôi thích một cách tiếp cận rõ
ràng). Phần thứ hai của đoạn mã này tạo cuộc gọi đến
java.lang.System.currentTimeMillis() để nhận được thời gian bắt đầu, lưu nó vào
khoảng trống của biến cục bộ đã được tính toán trong khung ngăn xếp.
Có lẽ bạn tự hỏi tại sao tôi kiểm tra xem phương thức đó có tĩnh hay không ở lúc
bắt đầu tính toán kích thước tham số của tôi, sau đó khởi tạo khe hở khung ngăn
xếp là 0 nếu nó có (trái ngược với một khe hở nếu nó không có). Cách tiếp cận này
liên quan đến cách xử lý các cuộc gọi phương thức ngôn ngữ Java. Đối với các
phương thức không tĩnh, tham số (ẩn) đầu tiên trên mỗi cuộc gọi là tham chiếu này
cho đối tượng đích, mà tôi cần phải tính đến khi tính toán kích thước tập tham số
đầy đủ trên khung ngăn xếp.


Liệt kê 4. Thiết lập cho cuộc gọi được bao bọc

// compute the size of the calling parameters
Type[] types = methgen.getArgumentTypes();
int slot = methgen.isStatic() ? 0 : 1;
for (int i = 0; i < types.length; i++) {
slot += types[i].getSize();
}

// save time prior to invocation
ilist.append(ifact.createInvoke("java.lang.System",
"currentTimeMillis", Type.LONG, Type.NO_ARGS,
Constants.INVOKESTATIC));
ilist.append(InstructionFactory.createStore(Type.LONG, slot));

Liệt kê 5 cho thấy đoạn mã để tạo cuộc gọi đến phương thức được bao bọc và lưu
kết quả (nếu có). Phần đầu tiên của đoạn mã này sẽ kiểm tra xem phương thức này
có tĩnh không. Nếu phương thức không tĩnh, tôi tạo mã để nạp tài liệu tham khảo
đối tượng này cho ngăn xếp đó và cũng có thể thiết lập kiểu gọi phương thức là ảo
(virtual) (chứ không phải tĩnh (static)). Vòng lặp for sau đó tạo mã để sao chép tất
cả các giá trị tham số cuộc gọi tới ngăn xếp đó, phương thức createInvoke() tạo
cuộc gọi thực sự tới phương thức được bao bọc và câu lệnh ifcuối cùng sẽ lưu giá
trị kết quả đến vị trí biến cục bộ khác trong khung ngăn xếp (nếu kiểu kết quả
không phải là rỗng ).

Liệt kê 5. Gọi phương thức được bao bọc

// call the wrapped method
int offset = 0;

short invoke = Constants.INVOKESTATIC;
if (!methgen.isStatic()) {
ilist.append(InstructionFactory.createLoad(Type.OBJECT, 0));
offset = 1;
invoke = Constants.INVOKEVIRTUAL;
}
for (int i = 0; i < types.length; i++) {
Type type = types[i];
ilist.append(InstructionFactory.createLoad(type, offset));
offset += type.getSize();
}
Type result = methgen.getReturnType();
ilist.append(ifact.createInvoke(cname,
iname, result, types, invoke));

// store result for return later
if (result != Type.VOID) {
ilist.append(InstructionFactory.createStore(result, slot+2));
}

Bây giờ đi vào hoàn tất. Liệt kê 6 tạo mã để tính toán trên thực tế số mili giây trôi
qua kể từ thời gian bắt đầu và để in nó ra như thông báo có định dạng hoàn chỉnh.
Phần này sẽ rất phức tạp, nhưng hầu hết các hoạt động thực sự chỉ là viết các
thông báo kết quả riêng biệt. Nó không cung cấp một số kiểu hoạt động mà tôi đã
không sử dụng trong mã trước đó, gồm một sự truy cập trường (đến
java.lang.System.out) và một vài kiểu lệnh khác nhau. Hầu hết trong số các kiểu
lệnh này nên dễ hiểu nếu bạn nghĩ về JVM như là một bộ xử lý dựa vào-ngăn xếp,
vì vậy tôi sẽ không đi vào chi tiết tại đây.

Liệt kê 6. Tính và in ra thời gian đã sử dụng


// print time required for method call
ilist.append(ifact.createFieldAccess("java.lang.System", "out",
new ObjectType("java.io.PrintStream"), Constants.GETSTATIC));
ilist.append(InstructionConstants.DUP);
ilist.append(InstructionConstants.DUP);
String text = "Call to method " + methgen.getName() + " took ";
ilist.append(new PUSH(pgen, text));
ilist.append(ifact.createInvoke("java.io.PrintStream", "print",
Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL));
ilist.append(ifact.createInvoke("java.lang.System",
"currentTimeMillis", Type.LONG, Type.NO_ARGS,
Constants.INVOKESTATIC));
ilist.append(InstructionFactory.createLoad(Type.LONG, slot));
ilist.append(InstructionConstants.LSUB);
ilist.append(ifact.createInvoke("java.io.PrintStream", "print",
Type.VOID, new Type[] { Type.LONG }, Constants.INVOKEVIRTUAL));
ilist.append(new PUSH(pgen, " ms."));
ilist.append(ifact.createInvoke("java.io.PrintStream", "println",
Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL));

Sau khi mã thông báo tính thời gian được tạo, tất cả công việc để lại cho Liệt kê 7
là hoàn thành mã phương thức của trình bao bọc (wrapper) với một sự trả về giá trị
kết quả đã lưu trữ (nếu có) từ cuộc gọi phương thức được bao bọc, tiếp theo là
hoàn thành phương thức của trình bao bọc đã được xây dựng. Phần cuối cùng này
liên quan đến một vài bước. Cuộc gọi đến stripAttributes(true) chỉ ra lệnh cho
BCEL không tạo các thông tin gỡ rối cho phương thức được xây dựng, trong khi
các cuộc gọi setMaxStack() và setMaxLocals() tính toán và thiết lập thông tin về
cách sử dụng ngăn xếp cho phương thức này. Sau khi việc đó được thực hiện, tôi
thực sự có thể tạo phiên bản hoàn chỉnh của phương thức này và thêm nó vào lớp.


Liệt kê 7. Hoàn thành trình bao bọc

// return result from wrapped method call
if (result != Type.VOID) {
ilist.append(InstructionFactory.createLoad(result, slot+2));
}
ilist.append(InstructionFactory.createReturn(result));

// finalize the constructed method
wrapgen.stripAttributes(true);
wrapgen.setMaxStack();
wrapgen.setMaxLocals();
cgen.addMethod(wrapgen.getMethod());
ilist.dispose();

Mã đầy đủ
Liệt kê 8 cho thấy đoạn mã hoàn chỉnh (được định dạng lại một chút để khớp với
chiều rộng), trong đó có một phương thức main() để chọn tên của tệp lớp và
phương thức lớp được chuyển đổi:

Liệt kê 8. Mã chuyển đổi xong

public class BCELTiming
{
private static void addWrapper(ClassGen cgen, Method method) {

// set up the construction tools
InstructionFactory ifact = new InstructionFactory(cgen);
InstructionList ilist = new InstructionList();

ConstantPoolGen pgen = cgen.getConstantPool();
String cname = cgen.getClassName();
MethodGen wrapgen = new MethodGen(method, cname, pgen);
wrapgen.setInstructionList(ilist);

// rename a copy of the original method
MethodGen methgen = new MethodGen(method, cname, pgen);
cgen.removeMethod(method);
String iname = methgen.getName() + "$impl";
methgen.setName(iname);
cgen.addMethod(methgen.getMethod());
Type result = methgen.getReturnType();

// compute the size of the calling parameters
Type[] types = methgen.getArgumentTypes();
int slot = methgen.isStatic() ? 0 : 1;
for (int i = 0; i < types.length; i++) {
slot += types[i].getSize();
}

// save time prior to invocation
ilist.append(ifact.createInvoke("java.lang.System",
"currentTimeMillis", Type.LONG, Type.NO_ARGS,
Constants.INVOKESTATIC));
ilist.append(InstructionFactory.
createStore(Type.LONG, slot));

// call the wrapped method
int offset = 0;
short invoke = Constants.INVOKESTATIC;

if (!methgen.isStatic()) {
ilist.append(InstructionFactory.
createLoad(Type.OBJECT, 0));
offset = 1;
invoke = Constants.INVOKEVIRTUAL;
}
for (int i = 0; i < types.length; i++) {
Type type = types[i];
ilist.append(InstructionFactory.
createLoad(type, offset));
offset += type.getSize();
}
ilist.append(ifact.createInvoke(cname,
iname, result, types, invoke));

// store result for return later
if (result != Type.VOID) {
ilist.append(InstructionFactory.
createStore(result, slot+2));
}

// print time required for method call
ilist.append(ifact.createFieldAccess("java.lang.System",
"out", new ObjectType("java.io.PrintStream"),
Constants.GETSTATIC));
ilist.append(InstructionConstants.DUP);
ilist.append(InstructionConstants.DUP);
String text = "Call to method " + methgen.getName() +
" took ";
ilist.append(new PUSH(pgen, text));

ilist.append(ifact.createInvoke("java.io.PrintStream",
"print", Type.VOID, new Type[] { Type.STRING },
Constants.INVOKEVIRTUAL));
ilist.append(ifact.createInvoke("java.lang.System",
"currentTimeMillis", Type.LONG, Type.NO_ARGS,
Constants.INVOKESTATIC));
ilist.append(InstructionFactory.
createLoad(Type.LONG, slot));
ilist.append(InstructionConstants.LSUB);
ilist.append(ifact.createInvoke("java.io.PrintStream",
"print", Type.VOID, new Type[] { Type.LONG },
Constants.INVOKEVIRTUAL));
ilist.append(new PUSH(pgen, " ms."));
ilist.append(ifact.createInvoke("java.io.PrintStream",
"println", Type.VOID, new Type[] { Type.STRING },
Constants.INVOKEVIRTUAL));

// return result from wrapped method call
if (result != Type.VOID) {
ilist.append(InstructionFactory.
createLoad(result, slot+2));
}
ilist.append(InstructionFactory.createReturn(result));

// finalize the constructed method
wrapgen.stripAttributes(true);
wrapgen.setMaxStack();
wrapgen.setMaxLocals();
cgen.addMethod(wrapgen.getMethod());
ilist.dispose();

}

public static void main(String[] argv) {
if (argv.length == 2 && argv[0].endsWith(".class")) {
try {

JavaClass jclas = new ClassParser(argv[0]).parse();
ClassGen cgen = new ClassGen(jclas);
Method[] methods = jclas.getMethods();
int index;
for (index = 0; index < methods.length; index++) {
if (methods[index].getName().equals(argv[1])) {
break;
}
}
if (index < methods.length) {
addWrapper(cgen, methods[index]);
FileOutputStream fos =
new FileOutputStream(argv[0]);
cgen.getJavaClass().dump(fos);
fos.close();
} else {
System.err.println("Method " + argv[1] +
" not found in " + argv[0]);
}
} catch (IOException ex) {
ex.printStackTrace(System.err);
}

} else {

System.out.println
("Usage: BCELTiming class-file method-name");
}
}
}

Thực hiện một lượt
Liệt kê 9 cho thấy các kết quả của lần đầu tiên chạy chương trình StringBuilder ở
dạng chưa thay đổi, sau đó chạy chương trình BCELTiming để thêm thông tin về
thời gian và cuối cùng là chạy chương trình StringBuilder sau khi nó đã thay đổi.
Bạn có thể thấy cách StringBuilder bắt đầu thông báo các thời gian thực hiện sau
khi nó đã thay đổi và cách các thời gian tăng nhanh hơn nhiều so với chiều dài của
chuỗi được xây dựng do mã xây dựng chuỗi không hiệu quả.

Liệt kê 9.Chạy các chương trình

[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Constructed string of length 1000
Constructed string of length 2000
Constructed string of length 4000
Constructed string of length 8000
Constructed string of length 16000

[dennis]$ java -cp bcel.jar:. BCELTiming StringBuilder.class buildString

[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Call to method buildString$impl took 20 ms.
Constructed string of length 1000
Call to method buildString$impl took 79 ms.
Constructed string of length 2000

Call to method buildString$impl took 250 ms.
Constructed string of length 4000
Call to method buildString$impl took 879 ms.
Constructed string of length 8000
Call to method buildString$impl took 3875 ms.
Constructed string of length 16000



Tóm tắt BCEL
Có nhiều thứ với BCEL hơn chỉ là sự hỗ trợ hoạt động lớp cơ bản mà tôi đã thể
hiện trong bài viết này. Nó cũng gồm việc thực hiện trình kiểm tra đầy đủ để đảm
bảo rằng một lớp nhị phân là hợp lệ theo các đặc tả JVM (xem
org.apache.bcel.verifier.VerifierFactory), một trình phân tách (disassembler) tạo
một khung nhìn mức JVM được liên kết và có bố cục đẹp của một lớp nhị phân và
thậm chí một trình tạo chương trình BCEL cung cấp mã nguồn cho một chương
trình BCEL để xây dựng một lớp do bạn cung cấp. (Lớp
org.apache.bcel.util.BCELifier không nằm trong Javadocs, do đó hãy trông cậy
vào mã nguồn với cách sử dụng. Tính năng này hấp dẫn, nhưng kết quả có thể quá
khó hiểu để dùng cho hầu hết các nhà phát triển).
Theo cách sử dụng BCEL riêng của mình, tôi đã phát hiện thấy trình phân tách
HTML đặc biệt có ích. Để tiến hành thử nghiệm nó, chỉ cần thực hiện lớp
org.apache.bcel.util.Class2HTML với đường dẫn đến tệp lớp mà bạn muốn tách ra
như một đối số dòng lệnh. Nó sẽ tạo các tệp HTML trong thư mục hiện tại. Ví dụ
ở đây tôi sẽ phân tách lớp StringBuilder mà tôi đã sử dụng cho ví dụ tính thời gian
của tôi:
[dennis]$ java -cp bcel.jar org.apache.bcel.util.Class2HTML StringBuilder.class
Processing StringBuilder.class Done.

Hình 1 là một ảnh chụp màn hình của kết quả bố trí được trình phân tách tạo nên.

Trong ảnh chụp này khung lớn ở phía trên bên phải cho thấy sự phân tách của
phương thức trình bao bọc tính thời gian được thêm vào lớp StringBuilder. Kết
quả HTML đầy đủ có trong các tệp tải về chỉ cần mở tệp StringBuilder.html
trong một cửa sổ trình duyệt nếu bạn muốn xem trực tiếp điều này".

Hình 1. Phân tách StringBuilder

Hiện nay, BCEL có lẽ là khung công tác được sử dụng rộng rãi nhất cho hoạt động
lớp Java. Nó liệt kê một số dự án khác có sử dụng BCEL trên trang Web, gồm
trình biên dịch Xalan XSLT, mở rộng AspectJ cho ngôn ngữ lập trình Java và triển
khai thực hiện một số JDO. Nhiều dự án khác chưa được liệt kê cũng đang sử
dụng BCEL, gồm dự án liên kết dữ liệu JiBX XML riêng của tôi. Tuy nhiên, một
số các dự án được BCEL liệt kê từ đó đã chuyển sang thư viện khác, do vậy không
nằm trong danh sách như là một hướng dẫn đúng sự thực để phổ biến của BCEL.
Những lợi thế lớn của BCEL là giấy phép Apache tiện lợi cho thương mại của nó
và sự hỗ trợ mức lệnh JVM rộng rãi của nó. Các tính năng này, kết hợp với sự ổn
định và tuổi thọ của nó, đã làm cho BCEL trở thành một sự lựa chọn rất phổ biến
cho các ứng dụng hoạt động lớp. Nhưng BCEL dường như không phải là tất cả
được thiết kế tốt cho cả về tốc độ lẫn tính dễ sử dụng. Javassist cung cấp một API
thuận lợi hơn nhiều đối với hầu hết các mục đích, với tốc độ tương đương (hoặc
thậm chí tốt hơn), ít nhất là trong các thử nghiệm đơn giản của tôi. Nếu dự án của
bạn có thể tận dụng phần mềm có sử dụng Mozilla Public License (MPL) hoặc
GNU Lesser General Public License (LGPL), thì Javassist có thể là một lựa chọn
tốt hơn ngay bây giờ (nó có sẵn trong cả hai giấy phép này).


Phần tiếp theo
Bây giờ tôi đã giới thiệu cho bạn cả hai Javassist và BCEL, bài viết tiếp theo của
tôi trong loạt bài này sẽ nghiên cứu một ứng dụng hầu như có ích về hoạt động lớp
nhiều hơn những gì bạn đã nhìn thấy cho đến nay. Quay lại Phần 2, tôi đã giải

thích sự phản chiếu gọi đến các phương thức chậm hơn nhiều so với các cuộc gọi
trực tiếp như thế nào. Trong Phần 8, tôi sẽ chỉ ra cách bạn có thể sử dụng cả hai
BCEL và Javassist để thay thế các cuộc gọi sự phản chiếu với mã được tạo ra động
trong thời gian chạy với một sự cải tiến đáng kể về hiệu năng. Xem tiếp bài
tháng tới về một trải nghiệm khác của Động lực học lập trình Java để tìm hiểu các
chi tiết.


Mục lục

 Truy cập lớp BCEL
 Hoạt động lớp với BCEL
 Tóm tắt BCEL
 Phần tiếp theo

×