Động lực học lập trình Java, Phần 4: Chuyển đổi lớp bằng Javassist pps

17 383 1
Động lực học lập trình Java, Phần 4: Chuyển đổi lớp bằng Javassist pps

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

Độ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, bởi 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 có 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 sẽ gọi toàn bộ lĩnh vực của chủ đề này là hoạt động lớp (classworking). Tôi sẽ bắ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ó trở thà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 để là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 nghệ mà 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 là 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) 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 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 sử dụ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ó thể sử 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 chí có 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 lớp đượ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 lớp 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ì bạn 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ông có cá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 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ừ mã đượ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 xử lý 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 không 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 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. Liệt kê 1 hiển thị một phương thức ví dụ (xấu) mà tôi sẽ sử dụng như là 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 sẽ nó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 cũ 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 sẽ cho 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 thời 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 private String buildString(int length) { long start = System.currentTimeMillis(); String result = ""; for (int i = 0; i < length; i++) { result += (char)(i%26 + 'a'); } System.out.println("Call to buildString took " + (System.currentTimeMillis()-start) + " ms."); return result; } Thực hiện nó với Javassist Khai thác cùng tác dụng như khi sử dụng Javassist để xử lí bytecode lớp có vẻ như nó sẽ dễ dàng. Javassist cung cấp nhiều cách để thêm mã vào ở đầu và cuối của các phương thức, sau tất cả, mã này phải chính xác với những gì mà tôi đã thực hiện trong mã nguồn để thêm thông tin tính thời gian cho phương thức này. Tuy nhiên, cũng có một khó khăn. Khi tôi đã mô tả cách Javassist để cho phép bạn thêm mã, tôi đã nói rằng mã thêm vào không thể tham chiếu đến các biến cục bộ đã xác định ở những nơi khác trong phương thức. Sự hạn chế này ngăn cản tôi triển khai thực hiện mã tính thời gian trong Javassist theo cùng cách mà tôi đã thực hiện trong mã nguồn; trong trường hợp đó, tôi đã xác định một biến cục bộ mới trong mã được thêm vào ở lúc bắt đầu và đã tham chiếu biến đó trong mã được thêm vào ở cuối. Vậy tôi có thể sử dụng cách tiếp cận khác nào để nhận được tác dụng tương tự? Đúng, tôi có thể thêm một trường thành viên mới vào lớp đó và sử dụng nó thay cho biến cục bộ. Tuy nhiên đó là hơi hướng của giải pháp và phạm phải một số hạn chế về sử dụng chung. Ví dụ, hãy xem điều gì sẽ xảy ra với một phương thức đệ quy. Mỗi lần phương thức tự gọi chính nó, giá trị thời gian bắt đầu được lưu trữ từ lần gọi cuối cùng sẽ bị ghi đè và bị mất. May mắn thay có một giải pháp tốt hơn. Tôi có thể giữ cho mã phương thức ban đầu không thay đổi và chỉ cần thay đổi tên phương thức, sau đó thêm một phương thức mới bằng cách sử dụng tên ban đầu. Phương thức interceptor (chặn) này có thể sử dụng cùng chữ ký như phương thức ban đầu, kể cả trả về giá trị như nhau. Liệt kê 3 cho thấy một phiên bản mã nguồn của phương thức này sẽ trông như sau: Liệt kê 3. Thêm một phương thức chặn trong mã nguồn 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 took " + (System.currentTimeMillis()-start) + " ms."); return result; } Cách tiếp cận của việc sử dụng một phương thức chặn này hoạt động tốt với Javassist. Vì toàn bộ phần thân của phương thức này là một khối, tôi có thể xác định và sử dụng các biến cục bộ trong phần thân mà không có bất kỳ vấn đề nào. Tạo mã nguồn cho phương thức chặn này cũng dễ dàng; chỉ cần một vài thay đổi để làm việc với phương thức có thể bất kì. Chạy phương thức chặn Triển khai thực hiện các đoạn mã để thêm vào tính thời gian của phương thức sử dụng một số các Javassist API đã mô tả trong những điều cơ bản về Javassist. Liệt kê 4 cho thấy mã này, dưới dạng một ứng dụng lấy một cặp các đối số dòng lệnh cho tên lớp và tên phương thức được tính thời gian. Phần thân phương thức main() chỉ tìm thấy các thông tin lớp và sau đó chuyển nó tới phương thức addTiming() để xử lý các sửa đổi thực sự. Phương thức addTiming() đầu tiên đặt lại tên cho phương thức hiện tại bằng cách gắn thêm "$impl" vào cuối tên, sau đó tạo một bản sao của phương thức khi sử dụng tên ban đầu. Sau đó nó thay thế thân phương thức đã sao chép với mã có tính thời gian đang bao bọc một cuộc gọi đến phương thức ban đầu đã đổi tên. Liệt kê 4. Thêm phương thức chặn với Javassist public class JassistTiming { public static void main(String[] argv) { if (argv.length == 2) { try { // start by getting the class file and method CtClass clas = ClassPool.getDefault().get(argv[0]); if (clas == null) { System.err.println("Class " + argv[0] + " not found"); } else { // add timing interceptor to the class addTiming(clas, argv[1]); clas.writeFile(); System.out.println("Added timing to method " + argv[0] + "." + argv[1]); } } catch (CannotCompileException ex) { ex.printStackTrace(); } catch (NotFoundException ex) { ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } } else { System.out.println("Usage: JassistTiming class method-name"); } } private static void addTiming(CtClass clas, String mname) throws NotFoundException, CannotCompileException { // get the method information (throws exception if method with // given name is not declared directly by this class, returns // arbitrary choice if more than one with the given name) CtMethod mold = clas.getDeclaredMethod(mname); // rename old method to synthetic name, then duplicate the // method with original name for use as interceptor String nname = mname+"$impl"; mold.setName(nname); CtMethod mnew = CtNewMethod.copy(mold, mname, clas, null); // start the body text generation by saving the start time // to a local variable, then call the timed method; the // actual code generated needs to depend on whether the // timed method returns a value String type = mold.getReturnType().getName(); StringBuffer body = new StringBuffer(); [...]... đổi số lượng lớn các lớp và cho việc sửa đổi đang thực hiện như là các lớp được nạp trong thời gian chạy Đây là những tính năng cung cấp cho Javassist một công cụ quan trọng để triển khai thực hiện các khía cạnh trong các ứng dụng của bạn, vì vậy hãy chắc chắn rằng bạn nắm bắt được các phần tiếp theo với câu chuyện đầy đủ về công cụ mạnh mẽ này Mục lục  Những điều cơ bản về Javassist  Hoạt động lớp. .. chặn Phần văn bản thân trên thực tế trông giống như mã Java tiêu chuẩn trừ các cuộc gọi đến (được đổi tên) phương thức ban đầu Đây là một dòng body.append(nname + "($$);\n"); trong đoạn mã đó, ở đây nname là tên đã sửa đổi cho phương thức ban đầu Trình nhận dạng $$ được sử dụng trong cuộc gọi là cách mà Javassist trình bày danh sách các tham số cho phương thức trong xây dựng Bằng cách sử dụng trình. .. cuộc gọi này đến phương thức chặn được chuyển qua tới phương thức ban đầu Liệt kê 5 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 sửa đổi, sau đó chạy chương trình JassistTiming để thêm thông tin tính thời gian và cuối cùng là chạy chương trình StringBuilder sau khi nó được sửa đổi Bạn có thể thấy cách StringBuilder chạy sau thay đổi về các thời gian thực hiện và bao... cung cấp cho Javassist Tuy nhiên thật quan trọng để nhận ra rằng Javassist sẽ không nhất thiết phải bắt giữ bất kỳ lỗi nào trong mã này và rằng các kết quả của một lỗi có thể khó dự đoán Nhìn về phía trước Có rất nhiều thứ với Javassist hơn những gì chúng tôi đã trình bày trong bài viết này Tháng tới, chúng ta sẽ nghiên cứu sâu hơn một chút bằng việc xem xét một số các kết nối đặc biệt mà Javassist cung... nguồn, Luke? Javassist thực hiện một công việc rất lớn làm cho hoạt động lớp dễ dàng bằng cách cho phép bạn làm việc với mã nguồn chứ không phải là các danh sách hướng dẫn bytecode thực sự Nhưng điều dễ sử dụng này đi kèm với một số hạn chế Như tôi đã đề cập lại trong mã nguồn của tất cả các bytecode, mã nguồn được Javassist sử dụng không chính xác là ngôn ngữ Java Bên cạnh việc thừa nhận các trình nhận... System.out.println("Interceptor method body:"); System.out.println(body.toString()); } } Xây dựng phần thân phương thức chặn sử dụng một java.lang.StringBuffer để tích lũy phần văn bản thân (chỉ ra một cách thích hợp để xử lý xây dựng String , trái với cách tiếp cận đã dùng trong StringBuilder) Phần thân văn bản thay đổi dựa vào phương thức ban đầu có trả về một giá trị hay không Nếu nó thực sự trả về một... chấp nhận mã nguồn và biến đổi nó thành bytecode hợp lệ, nhưng các thời gian kết quả là sai Nếu bạn đã thử biên dịch trực tiếp nhiệm vụ này vào trong một chương trình Java, bạn sẽ nhận được một lỗi biên dịch vì nó vi phạm một trong các quy tắc của ngôn ngữ Java: một nhiệm vụ hẹp đòi hỏi một khuôn mẫu Liệt kê 6 Lưu trữ một biến long trong một biến int [dennis]$ java -cp javassist. jar: JassistTiming... Javassist triển khai thực hiện kiểm tra thời gian biên dịch lỏng lẻo trên mã hơn được đặc tả ngôn ngữ Java yêu cầu Vì điều này, nó sẽ tạo bytecode từ mã nguồn theo những cách có thể có các kết quả gây ngạc nhiên nếu bạn không cẩn thận Ví dụ, Liệt kê 6 cho thấy những gì xảy ra khi tôi thay đổi kiểu của biến cục bộ thường sử dụng cho thời gian bắt đầu phương thức trong mã chặn từ biến long sang int Javassist. .. xây dựng, do đoạn mã xây dựng chuỗi không hiệu quả Liệt kê 5 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 javassist. jar: JassistTiming StringBuilder buildString Interceptor method body:... string of length 16000 Tùy thuộc vào những gì bạn làm trong mã nguồn, thậm chí bạn có thể nhận được Javassist để tạo bytecode không hợp lệ Liệt kê 7 cho thấy một ví dụ về điều này, ở đây tôi đã vá lỗi đoạn mã JassistTiming để luôn luôn xử lý phương thức có tính thời gian như sự trả về một giá trị int Javassist một lần nữa chấp nhận mã nguồn mà không đưa ra lỗi, nhưng bytecode không thực hiện xác nhận . Độ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,. đó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. 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& quot; (02.2004) Phần 6, "Các thay đổi hướng-khía cạnh với Javassist& quot;

Ngày đăng: 07/08/2014, 10:22

Từ khóa liên quan

Tài liệu cùng người dùng

Tài liệu liên quan