Động lực học lập trình Java, Phần 4: Chuyển đổi lớp bằng Javassist
Sử dụng Javassist để chuyển đổi các phương thức theo bytecode
Dennis Sosnoski, Nhà tư vấn, Sosnoski Software Solutions, Inc.
Tóm tắt: Thật buồn tẻ với các lớp Java chỉ thực hiện theo cách mã nguồn đã được
viết phải không? Sau đó, hãy vui vẻ lên, bi vì bạn sắp sửa thấy việc kết hợp các
lớp theo các hình dạng chưa bao giờ được trình biên dịch dự kiến! Trong bài viết
này, nhà tư vấn Java Dennis Sosnoski đóng góp loại bài động lực học lập trình
Java của mình vào việc tăng nhanh tốc độ xem xét Javassist, thư viện thao tác mã
byte (bytecode), đây là cơ sở cho các tính năng lập trình hướng-khía cạnh được bổ
sung cho máy chủ ứng dụng JBoss được sử dụng rộng rãi. Bạn sẽ tìm ra những
điều cơ bản về việc chuyển đổi các lớp hiện với Javassist và nhận thấy cả sức
mạnh lẫn hạn chế của cách tiếp cận mã nguồn mở của khung công tác này với hoạt
động lớp (classworking).
Sau khi trình bày những điều căn bản của việc định dạng lớp Java và truy cập
trong lúc chạy qua phản chiếu, đây là lúc để di chuyển loạt bài này theo hướng tới
nhiều chủ đề cao cấp hơn. Trong số tháng này tôi sẽ bắt đầu vào phần thứ hai của
loạt bài, ở đây các thông tin về lớp Java chỉ trở thành một dạng cấu trúc dữ liệu
khác được các ứng dụng xử lí. Tôi sgọi toàn bộ lĩnh vực của chủ đề này là hoạt
động lớp (classworking).
Tôi sbắt đầu trình bày hoạt động lớp với thư viện thao tác bytecode Javassist.
Javassist không phải là thư viện duy nhất để làm việc với bytecode, mà nó còn có
một tính năng cụ thể làm cho nó trthành một điểm khởi đầu quan trọng cho các
thí nghiệm hoạt động lớp: bạn có thể sử dụng Javassist để m thay đổi bytecode
của một lớp Java mà trên thực tế không cần tìm hiểu bất cứ điều gì về kiến trúc
bytecode hoặc kiến trúc máy ảo Java (JVM). Đây là một điều may mắn lẫn trong
một số chi tiết cụ thể -- nói chung tôi không tán thành can thiệp vào công nghmà
bạn không hiểu -- nhưng chắc chắn nó làm cho việc thao tác bytecode có khả năng
truy cập nhiều hơn so với các khung công tác mà ở đó bạn làm việc ở mức các
hướng dẫn riêng.
Những điều cơ bản về Javassist
Javassist cho phép bạn kiểm tra, chỉnh sửa và tạo các lớp Java nhị phân. Khía cạnh
kiểm tra chủ yếu lặp lại chính xác những gì có sẵn trực tiếp trong Java thông qua
Reflection API, nhưng việc có cách khác để truy cập thông tin này là rất có ích khi
trên thực tế bạn đang sửa đổi các lớp thay vì chỉ cần thực hiện chúng. Điều này
do thiết kế JVM không cung cấp cho bạn bất kỳ quyền truy cập nào vào dữ liệu
lớp thô sau khi nó được nạp vào JVM. Nếu bạn sắp làm việc với các lớp như là dữ
liệu, bạn cần phải làm như thế bên ngoài JVM.
Đừ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)
Phn 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 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 7, "Kỹ thuật bytecode với BCEL" (04.2004)
Phần 8, "Thay thế sự phản chiếu bằng việc tạo mã" (06.2004)
Javassist sdụng lớp javassist.ClassPool để theo dõi và kiểm soát các lớp bạn
đang thao tác. Lớp này làm việc rất giống như một trình nạp lớp (classloader) của
JVM, nhưng có sự khác biệt quan trọng khác hơn việc kết nối các lớp đã nạp để
thực hiện như một phần của ứng dụng của bạn, nhóm lớp giúp cho các lớp đã nạp
có thsử dụng như là dữ liệu thông qua Javassist API. Bạn có thể sử dụng một
nhóm lớp mặc định để nạp từ đường dẫn tìm kiếm JVM hoặc xác định một đường
dẫn để tìm kiếm danh sách các đường dẫn riêng của bạn. Bạn thậm ccó thể tải
trực tiếp các lớp nhị phân từ các mảng hoặc luồng byte và tạo các lớp mới từ đầu.
Các lp được nạp trong một nhóm lớp được các cá thể javassist.CtClass đại diện.
Như với lớp Java tiêu chuẩn java.lang.Class, CtClass cung cấp các phương thức để
kiểm tra dữ liệu lớp như các trường và các phương thức. Đó chỉ là sự khởi đầu cho
CtClass, tuy nhiên, cũng định nghĩa các phương thức để thêm vào các trường, các
phương thức và các hàm tạo mới cho lớp đó và để thay đổi tên lớp, siêu lp và các
giao diện. Thật kỳ quặc, Javassist không cung cấp bất kỳ cách nào để xóa các
trường, các phương thức hoặc các hàm tạo từ một lớp.
Các trường, các phương thức và các hàm tạo được các cá thể tương ứng
javassist.CtField, javassist.CtMethod và javassist.CtConstructor biểu diễn. Các lớp
này xác định các phương thức để sửa đổi tất cả các khía cạnh của mục được lớp đó
đại diện, bao gồm cả phần bytecode thực sự của một phương thức hoặc hàm tạo.
Mã nguồn của tất cả bytecode
Javassist cho phép bạn thay thế hoàn toàn phần thân bytecode của một phương
thức hoặc hàm tạo hoặc thêm vào khả năng chọn lọc bytecode ở đầu hoặc cuối của
phần thân hiện có (cùng với một cặp các biến khác cho các hàm tạo). Dù bằng
cách nào, các bytecode mới được chuyển qua như là một câu lệnh hay khối mã
nguồn giống như Java trong một String. Các phương thức Javassist biên dịch có
hiệu quả mã nguồn mà bạn cung cấp trong bytecode của Java, sau đó chúng chèn
bytecode này vào trong phần thân của phương thức hoặc hàm tạo đích.
Mã nguồn được Javassist chấp nhận không khớp chính xác với ngôn ngữ Java,
nhưng sự khác biệt chính là việc bổ sung một số trình nhận dạng đặc biệt dùng để
mô tả các tham số của phương thức hoặc hàm tạo, giá trị trả về phương thức và
các mục khác mà bạn có thể muốn sử dụng trong mã chèn vào của bạn. Tất cả các
trình nhận dạng đặc biệt này bắt đầu bằng biểu tượng $, vì vậy chúng sẽ không can
thiệp tới bất cứ điều gì mà bạn đã làm khác đi trong mã của bạn.
Cũng có một số hạn chế về những gì bn có thể làm trong mã nguồn mà bạn
chuyển tới Javassist. Hạn chế đầu tiên là định dạng thực tế, nó phải là một câu
lệnh hay một khối. Đây là một hạn chế không tốt đối với hầu hết các mục đích, vì
bạn có thể đặt bất cứ chuỗi các câu lệnh nào mà bạn muốn trong một khối. Dưới
đây là một ví dụ khi sử dụng trình nhận dạng Javassist đặc biệt cho hai giá trị tham
số phương thức đầu tiên để hiển thị cách điều này hoạt động:
{
System.out.println("Argument 1: " + $1);
System.out.println("Argument 2: " + $2);
}
Một hạn chế đáng kể hơn về mã nguồn là khôngcách nào tham chiếu đến các
biến cục bộ đã khai báo ngoài câu lệnh hoặc khối được thêm vào. Điều này có
nghĩa rằng nếu bạn đang thêm ở cả hai phần bắt đầu và kết thúc của một
phương thức, ví dụ, nói chung bạn sẽ không có khả năng chuyển các thông tin từ
được thêm vào lúc bắt đầu đến mã được thêm vào ở lúc kết thúc. Có nhiều
cách giải quyết xung quanh hạn chế này, nhưng cách giải quyết rất lộn xộn -- nói
chung bạn cần phải tìm một cách để kết hợp mã riêng chèn vào trong một khối.
Hoạt động lớp với Javassist
Với một ví dụ về việc áp dụng Javassist, tôi sẽ sử dụng một nhiệm vụ mà tôi đã
thường xuyên xlý trực tiếp trong mã nguồn: đo thời gian đã mất để thực hiện
một phương thức. Phép đo này là đủ dễ dàng để thực hiện trong mã nguồn; bạn chỉ
cần ghi lại thời gian hiện tại ở đầu phương thức, sau đó kiểm tra lại thời gian hiện
tại ở cuối phương thức và tìm thấy sự khác biệt giữa hai giá trị đó. Nếu bạn kng
có mã nguồn, sẽ khó khăn hơn nhiều để có được kiểu thông tin tính thời gian này.
Đó là nơi mà hoạt động lớp có ích -- nó cho phép bạn thực hiện các thay đổi giống
như điều này với phương thức bất kỳ, mà không cần mã nguồn.
Hỏi chuyên gia: Dennis Sosnoski vcá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 kim soát.
Liệt kê 1 hin thị một phương thức ví dụ (xấu) mà tôi sẽ sử dụng như một thí
nghiệm tính toán thời gian của tôi: phương thức buildString của lớp StringBuilder.
Phương thức này xây dựng một String với độ dài 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 snói bạn không phải làm -
- nó nhiều lần gắn thêm ch một ký tự vào cuối chuỗi để tạo một chuỗi dài hơn. Do
các chuỗi không thể thay đổi được, 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 lần qua vòng lặp, với các dữ liệu được sao chép từ chuỗi và
chỉ một ký tự được thêm vào cuối. Tác động cuối cùng là phương thức này gặp
phải 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());
}
}
}
Thêm tính thời gian phương thức
Vì tôi có sẵn mã nguồn cho phương thức này, tôi scho bạn thấy cách tôi sẽ trực
tiếp thêm vào thông tin tính thời gian. Điều này cũng sẽ dùng làm mô hình cho
những gì mà tôi muốn làm khi sử dụng Javassist. Liệt kê 2 cho thấy phương thức
buildString() có bổ sung thêm tính thời gian. Điều này chẳng có giá trị thay đổi
nào. Mã được thêm vào này lưu trữ thời gian bắt đầu cho một biến cục bộ, sau đó
tính thi gian trôi qua ở cuối của phương thức và in nó ra bàn điều khiển.
Liệt kê 2. Phương thức có tính thời gian