Độ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

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.

Dennis Sosnoski, Nhà tư vấn, Sosnoski Software Solutions, Inc.

Dennis Sosnoski là một nhà tư vấn và nhà trợ giúp đào tạo chuyên về các dịch vụ Web và SOA dựa trên-Java. Kinh nghiệm phát triển phần mềm chuyên nghiệp của ông trải suốt hơn 30 năm qua, với một thập kỉ cuối tập trung vào các công nghệ XML và Java phía máy chủ. Dennis là nhà phát triển hàng đầu về dụng cụ liên kết dữ liệu XML JiBX mã nguồn mở, cũng là một người có duyên nợ với khung công tác của các dịch vụ Web Apache Axis2. Ông cũng là một trong những thành viên của nhóm chuyên gia đặc tả kỹ thuật của Jax-WS 2.0 và JAXB 2.0. Xem trang web của ông để có thông tin về các dịch vụ đào tạo và tư vấn của ông.



04 12 2009

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ố FieldGenMethodGen 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()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
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.


Tải về

Mô tảTênKích thước
Mã ví dụj-dyn7.zip462KB

Tài nguyên

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=452535
ArticleTitle=Động lực học lập trình Java, Phần 7: Kỹ thuật bytecode với BCEL
publish-date=12042009