Trong bài báo này, Dennis cho bạn thấy cách kết hợp chuyển đổi với việc nạp các lớp thực sự bằng cách sử dụng khung công tác Javassist, để xử lý tính năng hướng khía cạnh "đúng thời gian
Trang 1Động lực học lập trình Java, Phần 5: Việc chuyển đổi các lớp đang hoạt động
Tìm hiểu cách thay đổi các lớp khi chúng đang được nạp bằng Javassist
Dennis Sosnoski, Nhà tư vấn, Sosnoski Software Solutions, Inc
Tóm tắt: Sau thời gian gián đoạn ngắn, Dennis Sosnoski trở lại với phần 5 của
loạt bài Động lực học lập trình Java của mình Bạn đã thấy cách viết một chương
trình chuyển đổi các tệp lớp Java để thay đổi hành vi mã Trong bài báo này, Dennis cho bạn thấy cách kết hợp chuyển đổi với việc nạp các lớp thực sự bằng cách sử dụng khung công tác Javassist, để xử lý tính năng hướng khía cạnh "đúng thời gian" linh hoạt Cách tiếp cận này cho phép bạn quyết định những gì bạn muốn thay đổi trong thời gian chạy và có khả năng thực hiện các thay đổi khác nhau mỗi khi bạn chạy một chương trình Theo cách này, bạn cũng sẽ xem xét sâu hơn vào các vấn đề chung của việc nạp lớp (classloading) trong JVM
Trong Phần 4, "Các phép biến đổi lớp bằng Javassist," bạn đã học được cách sử dụng khung công tác Javassist để chuyển đổi các tệp lớp Java do trình biên dịch tạo ra, viết lại các tệp lớp đã sửa đổi Bước chuyển đổi tệp lớp này rất quan trọng
để thực hiện các thay đổi liên tục, nhưng không nhất thiết phải tiện lợi khi bạn muốn thực hiện các thay đổi khác nhau mỗi khi bạn thực hiện ứng dụng của bạn Đối với các thay đổi thoáng qua như vậy, một cách tiếp cận hoạt động khi bạn thực sự khởi động ứng dụng của bạn là tốt hơn
Kiến trúc JVM cho chúng ta làm điều này thuận tiện bằng cách làm việc với việc thực hiện trình nạp lớp (classloader) Khi sử dụng các dấu móc của trình nạp lớp, bạn có thể ngăn chặn quá trình nạp các lớp vào JVM và chuyển đổi các biểu diễn lớp trước khi chúng thực sự được nạp Để minh họa cách làm việc này, đầu tiên tôi sẽ giải thích việc chặn nạp lớp trực tiếp, sau đó chỉ ra cách Javassist cung cấp một phím tắt thuận tiện để bạn có thể sử dụng trong các ứng dụng của bạn Theo cách này, tôi sẽ sử dụng các đoạn mã từ các bài viết trước trong loạt bài này
Đừ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)
Trang 2Phầ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)
Vùng nạp
Bình thường, bạn chạy một ứng dụng Java bằng cách xác định lớp chính như là một tham số cho JVM Điều này làm việc tốt với các hoạt động tiêu chuẩn, nhưng không cung cấp cách nối bất kỳ đúng lúc vào quá trình nạp lớp có ích cho hầu hết các ứng dụng Như tôi đã thảo luận trong Phần 1 "Các lớp và việc nạp lớp," nhiều lớp được nạp ngay trước khi lớp chính của bạn bắt đầu thực hiện Việc ngăn chặn nạp các lớp này đòi hỏi một mức gián tiếp trong việc thực hiện chương trình
May mắn thay, rất dễ dàng để sao chép công việc JVM đã thực hiện trong khi chạy lớp chính của ứng dụng của bạn Tất cả những thứ mà bạn cần làm là sử dụng sự phản chiếu (như đã trình bày trong Phần 2) để trước tiên tìm phương thức tĩnh main() trong lớp cụ thể, sau đó gọi nó bằng các đối số dòng lệnh mong muốn Liệt
kê 1 đưa ra mã ví dụ để thực hiện điều này (tôi đã để ngoài các phương thức nhập khẩu và các lỗi ngoại lệ để giữ cho đoạn mã này ngắn gọn):
Liệt kê 1 Trình chạy (runner) ứng dụng Java
public class Run
{
public static void main(String[] args) {
if (args.length >= 1) {
try {
// load the target class to be run
Trang 3Class clas = Run.class.getClassLoader()
loadClass(args[0]);
// invoke "main" method of target class
Class[] ptypes =
new Class[] { args.getClass() };
Method main =
clas.getDeclaredMethod("main", ptypes); String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length); main.invoke(null, new Object[] { pargs });
} catch
}
} else {
System.out.println
("Usage: Run main-class args ");
}
}
}
Trang 4Để chạy ứng dụng Java của bạn khi sử dụng lớp này, bạn chỉ cần đặt tên nó làm đích cho lệnh java, tiếp sau nó là lớp chính cho ứng dụng của bạn và bất kỳ đối số nào mà bạn muốn chuyển tới ứng dụng của bạn Nói cách khác, nếu lệnh mà bạn
sử dụng để khởi chạy ứng dụng Java của bạn thường là:
java test.Test arg1 arg2 arg3
Thì thay vào đó bạn khởi chạy nó khi sử dụng lớp Run bằng lệnh:
java Run test.Test arg1 arg2 arg3
Chặn nạp lớp
Thật đúng với riêng nó, lớp Run nhỏ bé từ Liệt kê 1 rất không thực sự có ích Để hoàn thành mục tiêu của tôi về việc chặn quá trình nạp lớp chúng ta cần phải tiến một bước xa hơn, bằng cách định nghĩa và sử dụng trình nạp lớp riêng của mình cho các lớp ứng dụng
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
Như chúng ta đã thảo luận trong Phần 1, các trình nạp lớp sử dụng một hệ thống phân cấp có cấu trúc cây Mỗi trình nạp lớp (trừ trình nạp lớp gốc được JVM sử dụng cho các lớp Java cốt lõi) có trình nạp lớp cha mẹ Các trình nạp lớp có nhiệm
vụ xác nhận lại trình nạp lớp cha mẹ của chúng trước khi nạp một lớp cho riêng mình, để ngăn ngừa các xung đột có thể nảy sinh khi cùng một lớp được nạp bởi nhiều hơn một trình nạp lớp trong một hệ thống phân cấp Quá trình này xác nhận
lại với trình nạp lớp cha mẹ đầu tiên được gọi là delegation (ủy quyền) các trình
nạp lớp ủy quyền trách nhiệm để nạp một lớp cho trình nạp lớp gần với trình nạp lớp gốc nhất có quyền truy cập vào thông tin lớp đó
Trang 5Khi chương trình Run từ Liệt kê 1 bắt đầu thực hiện, nó đã được trình nạp lớp Hệ thống (System) mặc định cho JVM (JVM loại bỏ đường dẫn lớp-classpath mà bạn xác định) nạp Để tuân theo nguyên tắc ủy quyền nạp lớp này, chúng ta cần phải tạo cho trình nạp lớp của mình một sự thay thế thật sự cho trình nạp lớp System, khi sử dụng tất cả các thông tin đường dẫn lớp tương tự và ủy thác cho các trình nạp lớp cha mẹ giống nhau May mắn thay, lớp java.net.URLClassLoader được các JVM hiện hành sử dụng để triển khai thực hiện trình nạp lớp System cung cấp một cách dễ dàng để lấy ra thông tin đường dẫn lớp, khi sử dụng phương thức getURLs() Để viết các trình nạp lớp của chúng ta, chúng ta có thể chỉ phân lớp java.net.URLClassLoadervà khởi tạo lớp cơ sở để sử dụng cùng một đường dẫn lớp và trình nạp lớp cha mẹ như là trình nạp lớp System để nạp lớp chính Liệt kê
2 cho thấy việc thực hiện thực sự của cách tiếp cận này:
Liệt kê 2 Một trình nạp lớp dài dòng
public class VerboseLoader extends URLClassLoader
{
protected VerboseLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public Class loadClass(String name)
throws ClassNotFoundException {
System.out.println("loadClass: " + name);
return super.loadClass(name);
}
Trang 6protected Class findClass(String name)
throws ClassNotFoundException {
Class clas = super.findClass(name);
System.out.println("findclass: loaded " + name + " from this loader");
return clas;
}
public static void main(String[] args) {
if (args.length >= 1) {
try {
// get paths to be used for loading
ClassLoader base =
ClassLoader.getSystemClassLoader(); URL[] urls;
if (base instanceof URLClassLoader) {
urls = ((URLClassLoader)base).getURLs(); } else {
urls = new URL[]
{ new File(".").toURI().toURL() }; }
Trang 7
// list the paths actually being used
System.out.println("Loading from paths:"); for (int i = 0; i < urls.length; i++) {
System.out.println(" " + urls[i]);
}
// load target class using custom class loader VerboseLoader loader =
new VerboseLoader(urls, base.getParent()); Class clas = loader.loadClass(args[0]);
// invoke "main" method of target class
Class[] ptypes =
new Class[] { args.getClass() };
Method main =
clas.getDeclaredMethod("main", ptypes); String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length); Thread.currentThread()
setContextClassLoader(loader);
main.invoke(null, new Object[] { pargs });
Trang 8
} catch
}
} else {
System.out.println
("Usage: VerboseLoader main-class args ");
}
}
}
Chúng ta đã phân lớp java.net.URLClassLoader bằng lớp riêng VerboseLoader của chúng ta để liệt kê ra tất cả các lớp đang được nạp, ghi nhận những lớp nào đã được nạp bởi cá thể trình nạp này (chứ không phải là một trình nạp lớp cha mẹ ủy quyền) Ở đây một lần nữa tôi đã bỏ qua các phương thức nhập khẩu và các lỗi ngoại lệ để giữ cho đoạn mã ngắn gọn
Hai phương thức đầu tiên trong lớp VerboseLoader, loadClass() và findClass() là quan trọng hơn các phương thức của trình nạp lớp tiêu chuẩn Phương thức
loadClass() được gọi cho mỗi lớp được yêu cầu từ trình nạp lớp Trong trường hợp này, chúng ta dùng nó chỉ để in một thông báo ra bàn điều khiển và sau đó gọi phiên bản lớp cơ sở để xử lý thực sự Phương thức lớp cơ sở triển khai thực hiện hành vi ủy quyền của trình nạp lớp tiêu chuẩn, đầu tiên kiểm tra xem trình nạp lớp cha mẹ có thể nạp lớp cần thiết không và chỉ cố gắng nạp lớp trực tiếp bằng cách
sử dụng phương thức findClass() có bảo vệ nếu trình nạp lớp cha mẹ bị hỏng Đối với việc thực hiện VerboseLoader của findClass(), trước tiên chúng ta gọi việc thực hiện lớp cơ sở quan trọng hơn, sau đó in ra một thông báo nếu cuộc gọi thành công (trả về mà không đưa ra một lỗi ngoại lệ)
Phương thức main() của VerboseLoader hoặc nhận được danh sách các địa chỉ URL của đường dẫn lớp từ trình nạp được sử dụng cho lớp đang có hoặc, nếu
Trang 9được sử dụng với một trình nạp không có một cá thể URLClassLoader, thì chỉ cần
sử dụng thư mục hiện tại làm lối vào đường dẫn lớp duy nhất Dù bằng cách nào đi nữa, nó sẽ liệt kê ra các đường dẫn đang được sử dụng trên thực tế, sau đó tạo một
cá thể của lớp VerboseLoader và sử dụng nó để nạp lớp đích có tên trên dòng lệnh Phần còn lại của logic này, để tìm và gọi phương thức main() của lớp đích, giống như mã Run của Liệt kê 1
Liệt kê 3 cho thấy một ví dụ về dòng lệnh VerboseLoader và kết quả được sử dụng
để gọi các ứng dụng Run từ Liệt kê 1:
Liệt kê 3 Ví dụ kết quả từ chương trình của Liệt kê 2
[dennis]$ java VerboseLoader Run
Loading from paths:
file:/home/dennis/writing/articles/devworks/dynamic/code5/
loadClass: Run
loadClass: java.lang.Object
findclass: loaded Run from this loader
loadClass: java.lang.Throwable
loadClass: java.lang.reflect.InvocationTargetException
loadClass: java.lang.IllegalAccessException
loadClass: java.lang.IllegalArgumentException
loadClass: java.lang.NoSuchMethodException
loadClass: java.lang.ClassNotFoundException
loadClass: java.lang.NoClassDefFoundError
loadClass: java.lang.Class
Trang 10loadClass: java.lang.String
loadClass: java.lang.System
loadClass: java.io.PrintStream
Usage: Run main-class args
Trong trường hợp này, lớp duy nhất được VerboseLoader nạp trực tiếp là lớp Run Tất cả các lớp khác được lớp Run sử dụng là các lớp Java lõi, các lớp lõi này được nạp bằng sự ủy quyền thông qua trình nạp lớp cha mẹ Hầu hết nếu không phải tất cả các lớp Java lõi trên thực tế được nạp trong quá trình tự khởi động của ứng dụng VerboseLoader vì vậy trình nạp lớp cha mẹ sẽ chỉ trả về một tham chiếu đến cá thể java.lang.Class được tạo ra trước đó
Javassist chặn
VerboseClassloader từ Liệt kê 2 cho thấy những điều căn bản về việc chặn nạp lớp Để thay đổi các lớp khi chúng đang được nạp, chúng ta có thể lấy thêm việc này, thêm mã vào phương thức findClass() để truy cập tệp lớp nhị phân như một tài nguyên và sau đó làm việc với các dữ liệu nhị phân Trên thực tế Javassist bao gồm mã để thực hiện trực tiếp kiểu chặn này, vì vậy hơn là tiếp tục ví dụ này, chúng ta sẽ xem cách sử dụng việc thực hiện Javassist để thay thế
Việc chặn nạp lớp với Javassist xây dựng trên cùng lớp javassist.ClassPool mà chúng ta đã làm trong Phần 4 Trong bài viết này, chúng ta đã yêu cầu một lớp theo tên trực tiếp từ ClassPool, tìm lại việc biểu diễnJavassist của lớp đó dưới dạng một cá thể javassist.CtClass Mặc dù, đây không phải là cách duy nhất để sử dụng một ClassPool Javassist cũng cung cấp một trình nạp lớp có sử dụng
ClassPool như là nguồn dữ liệu lớp của nó, dưới dạng lớp javassist.Loader
Để cho phép bạn làm việc với các lớp khi chúng đang được nạp ClassPool sử dụng một mẫu Trình quan sát (Observer) Bạn có thể chuyển một cá thể của giao diện trình quan sát mong muốn, javassist.Translator, tới hàm tạo ClassPool Mỗi khi một lớp mới được yêu cầu từ ClassPool nó gọi phương thức onWrite() của Trình quan sát để có thể thay đổi biểu diễn lớp trước khi nó được ClassPool phân phát Lớp javassist.Loader này có phương thức run() thuận tiện để nạp một lớp đích và gọi phương thức main() của lớp đó với một mảng các đối số được cung cấp (như trong mã của Liệt kê 1) Liệt kê 4 chứng tỏ việc sử dụng các lớp Javassist và
Trang 11phương thức này để nạp và chạy một lớp ứng dụng đích Việc thực hiện trình quan sát javassist.Translator đơn giản trong trường hợp này chỉ in ra một thông báo về lớp đang được yêu cầu
Liệt kê 4 Trình chạy ứng dụng Javassist
public class JavassistRun
{
public static void main(String[] args) {
if (args.length >= 1) {
try {
// set up class loader with translator
Translator xlat = new VerboseTranslator();
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);
// invoke "main" method of target class
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
loader.run(args[0], pargs);
} catch
Trang 12}
} else {
System.out.println
("Usage: JavassistRun main-class args ");
}
}
public static class VerboseTranslator implements Translator
{
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname) {
System.out.println("onWrite called for " + cname);
}
}
}
Dưới đây là một ví dụ về dòng lệnh JavassistRun và kết quả, khi sử dụng nó để gọi ứng dụng Run từ Liệt kê 1:
[dennis]$java -cp :javassist.jar JavassistRun Run