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..
Trang 1Độ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
Trang 2Như 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
Trang 3Bê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
Trang 4bạ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) {
Trang 5String 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 " +
Liệt kê 2 Tính thời gian đã thêm vào phương thức ban đầu
Trang 6public 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();
Trang 7for (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 đó
Trang 8Liệ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);
Trang 9java.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++) {
Trang 10Liệ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
for (int i = 0; i < types.length; i++) {
Type type = types[i];
Trang 11java.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
Trang 12ilist.append(new PUSH(pgen, text));
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