Reading Time: 11 minutes

Reflection trong Java cung cấp khả năng kiểm tra và thay đổi hành vi của ứng dụng tại thời điểm chạy (runtime). Đây là một chủ đề nâng cao trong lập trình Java.

Reflection trong Java

Sử dụng Reflection, ta có thể kiểm tra một class, interface, enum, lấy thông tin về cấu trúc, các phương thức và trường của chúng tại runtime, ngay cả khi class đó không thể truy cập được tại thời điểm biên dịch (compile time). Ta cũng có thể dùng reflection để khởi tạo một đối tượng, gọi các phương thức của nó và thay đổi giá trị của các trường.

Reflection trong Java

Dù ít được sử dụng trong lập trình thông thường, Reflection là một tính năng mạnh mẽ được dùng trong hầu hết các framework Java và J2EE. Một số framework sử dụng nó bao gồm:

  1. JUnit: sử dụng Reflection để phân tích annotation @Test nhằm lấy các phương thức test rồi gọi chúng.
  2. Spring: dùng cho dependency injection (chèn phụ thuộc).
  3. Web container trong Tomcat: dùng để chuyển tiếp request đến module phù hợp bằng cách phân tích các file web.xml và URI của request.
  4. Eclipse: dùng cho tính năng tự động hoàn thành tên phương thức.
  5. Struts
  6. Hibernate

Ngoài các cái tên trên, danh sách framework sử dụng Reflection còn dài vô tận vì chúng không có thông tin và quyền truy cập vào các class, interface, phương thức, v.v. do người dùng định nghĩa. Ta không nên sử dụng Reflection trong các tình huống lập trình thông thường khi đã có quyền truy cập vào các class và interface vì những nhược điểm sau:

  • Hiệu suất kém: Vì Reflection phân giải các kiểu dữ liệu một cách động, nó đòi hỏi các bước xử lý như quét classpath để tìm và nạp class, dẫn đến hiệu suất kém.
  • Hạn chế về bảo mật: Reflection yêu cầu các quyền tại thời điểm chạy và các quyền này có thể không có sẵn trên hệ thống đang chạy dưới một security manager (trình quản lý bảo mật). Điều này có thể khiến ứng dụng của bạn gặp lỗi tại runtime do security manager.
  • Vấn đề bảo mật: Sử dụng Reflection, ta có thể truy cập vào những phần code mà đáng lẽ không được phép, ví dụ như truy cập các trường riêng tư của một class và thay đổi giá trị của chúng. Điều này có thể tạo ra một lỗ hổng bảo mật nghiêm trọng và khiến ứng dụng của bạn hoạt động bất thường.
  • Khó bảo trì: Code sử dụng Reflection rất khó hiểu và debug. Thêm vào đó, mọi vấn đề với code đều không thể được phát hiện tại thời điểm biên dịch vì các class có thể chưa tồn tại lúc đó, làm cho code kém linh hoạt và khó bảo trì hơn.

Sử dụng Reflection với Class

Trong Java, mọi đối tượng đều thuộc một kiểu nguyên thủy (primitive) hoặc kiểu tham chiếu. Tất cả các class, enum, mảng đều là kiểu tham chiếu và kế thừa từ java.lang.Object. Các kiểu nguyên thủy bao gồm boolean, byte, short, int, long, char, float, và double.

java.lang.Class là điểm khởi đầu cho mọi tiến trình Reflection. Với mỗi kiểu đối tượng, JVM sẽ khởi tạo một thực thể bất biến (immutable) của java.lang.Class. Thực thể này cung cấp các phương thức để kiểm tra các thuộc tính của đối tượng tại thời gian chạy, tạo đối tượng mới, gọi phương thức và lấy/gán giá trị cho trường.

Trong phần này, chúng ta sẽ xem xét các phương thức quan trọng của Class. Để tiện lợi, ta sẽ tạo một số class và interface với hệ thống kế thừa.

package com.journaldev.reflection;

public interface BaseInterface {
	
	public int interfaceInt=0;
	
	void method1();
	
	int method2(String str);
}
package com.journaldev.reflection;

public class BaseClass {

	public int baseInt;
	
	private static void method3(){
		System.out.println("Method3");
	}
	
	public int method4(){
		System.out.println("Method4");
		return 0;
	}
	
	public static int method5(){
		System.out.println("Method5");
		return 0;
	}
	
	void method6(){
		System.out.println("Method6");
	}
	
	// inner public class
	public class BaseClassInnerClass{}
		
	//member public enum
	public enum BaseClassMemberEnum{}
}
package com.journaldev.reflection;

@Deprecated
public class ConcreteClass extends BaseClass implements BaseInterface {

	public int publicInt;
	private String privateString="private string";
	protected boolean protectedBoolean;
	Object defaultObject;
	
	public ConcreteClass(int i){
		this.publicInt=i;
	}

	@Override
	public void method1() {
		System.out.println("Method1 impl.");
	}

	@Override
	public int method2(String str) {
		System.out.println("Method2 impl.");
		return 0;
	}
	
	@Override
	public int method4(){
		System.out.println("Method4 overriden.");
		return 0;
	}
	
	public int method5(int i){
		System.out.println("Method4 overriden.");
		return 0;
	}
	
	// inner classes
	public class ConcreteClassPublicClass{}
	private class ConcreteClassPrivateClass{}
	protected class ConcreteClassProtectedClass{}
	class ConcreteClassDefaultClass{}
	
	//member enum
	enum ConcreteClassDefaultEnum{}
	public enum ConcreteClassPublicEnum{}
	
	//member interface
	public interface ConcreteClassPublicInterface{}

}

Hãy cùng xem một số phương thức Reflection quan trọng cho class dưới đây.

Lấy đối tượng Class

Ta có thể lấy Class của một đối tượng bằng ba cách: thông qua biến tĩnh class, sử dụng phương thức getClass() của đối tượng, và java.lang.Class.forName(String fullyClassifiedClassName). Đối với các kiểu nguyên thủy và mảng, ta có thể sử dụng biến tĩnh class. Các lớp bọc (wrapper) cung cấp một biến tĩnh khác là TYPE để lấy class.

// Get Class using reflection
Class<?> concreteClass = ConcreteClass.class;
concreteClass = new ConcreteClass(5).getClass();
try {
	// below method is used most of the times in frameworks like JUnit
	//Spring dependency injection, Tomcat web container
	//Eclipse auto completion of method names, hibernate, Struts2 etc.
	//because ConcreteClass is not available at compile time
	concreteClass = Class.forName("com.journaldev.reflection.ConcreteClass");
} catch (ClassNotFoundException e) {
	e.printStackTrace();
}
System.out.println(concreteClass.getCanonicalName()); // prints com.journaldev.reflection.ConcreteClass

//for primitive types, wrapper classes and arrays
Class<?> booleanClass = boolean.class;
System.out.println(booleanClass.getCanonicalName()); // prints boolean

Class<?> cDouble = Double.TYPE;
System.out.println(cDouble.getCanonicalName()); // prints double

Class<?> cDoubleArray = Class.forName("[D");
System.out.println(cDoubleArray.getCanonicalName()); //prints double[]

Class<?> twoDStringArray = String[][].class;
System.out.println(twoDStringArray.getCanonicalName()); // prints java.lang.String[][]

getCanonicalName() trả về tên chính tắc của class cơ sở. Lưu ý rằng java.lang.Class sử dụng Generics. Điều này giúp các framework đảm bảo rằng Class được lấy ra là một subclass của framework Base Class.

Lấy Super Class

Phương thức getSuperclass() của một đối tượng Class trả về super class (class cha) của class đó. Nếu Class này đại diện cho lớp Object, một interface, một kiểu nguyên thủy, hoặc void, phương thức sẽ trả về null. Nếu đối tượng này là một lớp mảng (array class), phương thức sẽ trả về đối tượng Class đại diện cho lớp Object.

Class<?> superClass = Class.forName("com.journaldev.reflection.ConcreteClass").getSuperclass();
System.out.println(superClass); // prints "class com.journaldev.reflection.BaseClass"
System.out.println(Object.class.getSuperclass()); // prints "null"
System.out.println(String[][].class.getSuperclass());// prints "class java.lang.Object"

Lấy các Class của thành viên công khai

Phương thức getClasses() của một đối tượng Class trả về một mảng chứa các đối tượng Class đại diện cho tất cả các class, interface và enum công khai là thành viên của class được đại diện bởi đối tượng Class này. Mảng này bao gồm các class và interface công khai được kế thừa từ các super class cũng như các thành viên công khai do chính class đó khai báo.

Phương thức này trả về một mảng có độ dài bằng 0 nếu đối tượng Class này không có class hay interface thành viên công khai nào, hoặc nếu nó đại diện cho một kiểu nguyên thủy, một lớp mảng, hoặc void.

Class<?>[] classes = concreteClass.getClasses();
//[class com.journaldev.reflection.ConcreteClass$ConcreteClassPublicClass, 
//class com.journaldev.reflection.ConcreteClass$ConcreteClassPublicEnum, 
//interface com.journaldev.reflection.ConcreteClass$ConcreteClassPublicInterface,
//class com.journaldev.reflection.BaseClass$BaseClassInnerClass, 
//class com.journaldev.reflection.BaseClass$BaseClassMemberEnum]
System.out.println(Arrays.toString(classes));

Lấy các Class đã khai báo

Phương thức getDeclaredClasses() trả về một mảng các đối tượng Class. Nó phản ánh tất cả các class và interface được khai báo là thành viên của class được đại diện bởi đối tượng Class này. Mảng trả về không bao gồm các class được khai báo trong các class và interface kế thừa.

//getting all of the classes, interfaces, and enums that are explicitly declared in ConcreteClass
Class<?>[] explicitClasses = Class.forName("com.journaldev.reflection.ConcreteClass").getDeclaredClasses();
//prints [class com.journaldev.reflection.ConcreteClass$ConcreteClassDefaultClass, 
//class com.journaldev.reflection.ConcreteClass$ConcreteClassDefaultEnum, 
//class com.journaldev.reflection.ConcreteClass$ConcreteClassPrivateClass, 
//class com.journaldev.reflection.ConcreteClass$ConcreteClassProtectedClass, 
//class com.journaldev.reflection.ConcreteClass$ConcreteClassPublicClass, 
//class com.journaldev.reflection.ConcreteClass$ConcreteClassPublicEnum, 
//interface com.journaldev.reflection.ConcreteClass$ConcreteClassPublicInterface]
System.out.println(Arrays.toString(explicitClasses));

Lấy Class khai báo

Phương thức getDeclaringClass() trả về đối tượng Class đại diện cho class mà trong đó nó được khai báo.

Class<?> innerClass = Class.forName("com.journaldev.reflection.ConcreteClass$ConcreteClassDefaultClass");
//prints com.journaldev.reflection.ConcreteClass
System.out.println(innerClass.getDeclaringClass().getCanonicalName());
System.out.println(innerClass.getEnclosingClass().getCanonicalName());

Lấy tên Package

Phương thức getPackage() trả về package của class này. Loader (trình nạp lớp) của class này được sử dụng để tìm package. Ta có thể gọi phương thức getName() của Package để lấy tên của package đó.

//prints "com.journaldev.reflection"
System.out.println(Class.forName("com.journaldev.reflection.BaseInterface").getPackage().getName());

Lấy các Modifier của Class

Phương thức getModifiers() trả về một số int đại diện cho các modifier của class. Ta có thể sử dụng phương thức java.lang.reflect.Modifier.toString() để lấy chuỗi định dạng của chúng như trong mã nguồn.

System.out.println(Modifier.toString(concreteClass.getModifiers())); //prints "public"
//prints "public abstract interface"
System.out.println(Modifier.toString(Class.forName("com.journaldev.reflection.BaseInterface").getModifiers())); 

Lấy các tham số Type

getTypeParameters() trả về một mảng TypeVariable nếu có bất kỳ tham số Type nào liên quan đến class. Các tham số type được trả về theo đúng thứ tự chúng được khai báo.

//Get Type parameters (generics)
TypeVariable<?>[] typeParameters = Class.forName("java.util.HashMap").getTypeParameters();
for(TypeVariable<?> t : typeParameters)
System.out.print(t.getName()+",");

Lấy các Interface đã triển khai

Phương thức getGenericInterfaces() trả về một mảng các interface được class triển khai cùng với thông tin về generic type (kiểu tổng quát). Ta cũng có thể sử dụng getInterfaces() để lấy đối tượng lớp đại diện của tất cả các interface đã được triển khai.

Type[] interfaces = Class.forName("java.util.HashMap").getGenericInterfaces();
//prints "[java.util.Map<K, V>, interface java.lang.Cloneable, interface java.io.Serializable]"
System.out.println(Arrays.toString(interfaces));
//prints "[interface java.util.Map, interface java.lang.Cloneable, interface java.io.Serializable]"
System.out.println(Arrays.toString(Class.forName("java.util.HashMap").getInterfaces()));		

Lấy tất cả phương thức công khai

Phương thức getMethods() trả về một mảng các phương thức công khai của Class, bao gồm cả các phương thức công khai của các superclass và super interface của nó.

Method[] publicMethods = Class.forName("com.journaldev.reflection.ConcreteClass").getMethods();
//prints public methods of ConcreteClass, BaseClass, Object
System.out.println(Arrays.toString(publicMethods));

Lấy tất cả constructor (hàm khởi tạo) công khai

Phương thức getConstructors() trả về danh sách các constructor công khai của class được tham chiếu.

//Get All public constructors
Constructor<?>[] publicConstructors = Class.forName("com.journaldev.reflection.ConcreteClass").getConstructors();
//prints public constructors of ConcreteClass
System.out.println(Arrays.toString(publicConstructors));

Lấy tất cả trường công khai

Phương thức getFields() trả về mảng các trường công khai của class, bao gồm cả các trường công khai của các superclass và super interface của nó.

//Get All public fields
Field[] publicFields = Class.forName("com.journaldev.reflection.ConcreteClass").getFields();
//prints public fields of ConcreteClass, it's superclass and super interfaces
System.out.println(Arrays.toString(publicFields));

Lấy tất cả Annotation

Phương thức getAnnotations() trả về tất cả các annotation cho một phần tử. Ta có thể sử dụng nó với class, trường và cả phương thức.

Lưu ý rằng chỉ những annotation có retention policy (chính sách bảo lưu) là RUNTIME mới có thể được truy cập bằng Reflection. Chúng ta sẽ tìm hiểu kỹ hơn về vấn đề này trong các phần sau.

java.lang.annotation.Annotation[] annotations = Class.forName("com.journaldev.reflection.ConcreteClass").getAnnotations();
//prints [@java.lang.Deprecated()]
System.out.println(Arrays.toString(annotations));

Sử dụng Reflection với trường

Reflection API cung cấp nhiều phương thức để phân tích các trường của Class và sửa đổi giá trị của chúng tại runtime. Trong phần này, chúng ta sẽ xem xét một số hàm Reflection thường dùng cho các phương thức.

Lấy trường công khai

Trong phần trước, chúng ta đã thấy cách lấy danh sách tất cả các trường công khai của một class. Reflection API cũng cung cấp phương thức để lấy một trường công khai cụ thể của một class thông qua phương thức getField(). Phương thức này sẽ tìm kiếm trường trong Class được chỉ định, sau đó trong các super interface và cuối cùng là trong các superclass.

Field field = Class.forName("com.journaldev.reflection.ConcreteClass").getField("interfaceInt");

Lệnh trên sẽ trả về trường từ BaseInterfaceConcreteClass đã triển khai. Nếu không tìm thấy trường nào, nó xe đưa ra lỗi NoSuchFieldException.

Class khai báo trường

Ta có thể sử dụng phương thức getDeclaringClass() của đối tượng trường để lấy class đã khai báo trường đó.

try {
	Field field = Class.forName("com.journaldev.reflection.ConcreteClass").getField("interfaceInt");
	Class<?> fieldClass = field.getDeclaringClass();
	System.out.println(fieldClass.getCanonicalName()); //prints com.journaldev.reflection.BaseInterface
} catch (NoSuchFieldException | SecurityException e) {
	e.printStackTrace();
}

Lấy kiểu của trường

Phương thức getType() trả về đối tượng Class cho kiểu đã khai báo của trường. Nếu trường thuộc kiểu nguyên thủy, nó sẽ trả về đối tượng lớp bọc tương ứng.

Field field = Class.forName("com.journaldev.reflection.ConcreteClass").getField("publicInt");
Class<?> fieldType = field.getType();
System.out.println(fieldType.getCanonicalName()); //prints int			

Lấy/Gán giá trị trường công khai

Ta có thể lấy và gán giá trị của một trường trong một đối tượng bằng Reflection.

Field field = Class.forName("com.journaldev.reflection.ConcreteClass").getField("publicInt");
ConcreteClass obj = new ConcreteClass(5);
System.out.println(field.get(obj)); //prints 5
field.setInt(obj, 10); //setting field value to 10 in object
System.out.println(field.get(obj)); //prints 10

Phương thức get() trả về một Object, vì vậy nếu trường thuộc kiểu nguyên thủy, nó sẽ trả về lớp bọc tương ứng. Nếu trường là static, ta có thể truyền null cho tham số Object trong phương thức get().

Có một số phương thức set*() để gán một Object vào trường hoặc gán các kiểu nguyên thủy khác nhau cho trường. Ta có thể lấy kiểu của trường và sau đó gọi hàm phù hợp để gán giá trị cho trường một cách chính xác. Nếu trường là final, các phương thức set() sẽ cho ra lỗi java.lang.IllegalAccessException.

Lấy/Gán giá trị trường riêng tư

Ta biết rằng các trường và phương thức riêng tư không thể được truy cập từ bên ngoài class. Tuy nhiên, bằng cách sử dụng Reflection, ta có thể lấy/gán giá trị cho một trường riêng tư bằng cách vô hiệu hóa cơ chế kiểm tra truy cập (access check) của Java đối với các modifier của trường đó.

Field privateField = Class.forName("com.journaldev.reflection.ConcreteClass").getDeclaredField("privateString");
//turning off access check with below method call
privateField.setAccessible(true);
ConcreteClass objTest = new ConcreteClass(1);
System.out.println(privateField.get(objTest)); // prints "private string"
privateField.set(objTest, "private string updated");
System.out.println(privateField.get(objTest)); //prints "private string updated"

Sử dụng Reflection với phương thức

Sử dụng Reflection, ta có thể lấy thông tin về một phương thức và gọi nó. Trong phần này, chúng ta sẽ học các cách khác nhau để lấy, gọi một phương thức cũng như truy cập các phương thức private.

Lấy phương thức công khai

Ta có thể sử dụng getMethod() để lấy một phương thức công khai của class. Ta cần truyền vào tên phương thức và các kiểu tham số của nó. Nếu phương thức không được tìm thấy trong class, Reflection API sẽ tìm kiếm trong superclass.

Trong ví dụ dưới đây, ta lấy phương thức put() của HashMap bằng Reflection. Ví dụ cũng cho thấy cách lấy các kiểu tham số, modifier và kiểu trả về của một phương thức.

Method method = Class.forName("java.util.HashMap").getMethod("put", Object.class, Object.class);
//get method parameter types, prints "[class java.lang.Object, class java.lang.Object]"
System.out.println(Arrays.toString(method.getParameterTypes()));
//get method return type, return "class java.lang.Object", class reference for void
System.out.println(method.getReturnType());
//get method modifiers
System.out.println(Modifier.toString(method.getModifiers())); //prints "public"

Gọi phương thức công khai

Ta có thể sử dụng phương thức invoke() của đối tượng Method để gọi một phương thức. Trong đoạn code ví dụ dưới đây, tôi đang gọi phương thức put trên HashMap bằng reflection.

Method method = Class.forName("java.util.HashMap").getMethod("put", Object.class, Object.class);
Map<String, String> hm = new HashMap<>();
method.invoke(hm, "key", "value");
System.out.println(hm); // prints {key=value}

Nếu phương thức là static, ta có thể truyền NULL làm đối số object.

Gọi phương thức private

Ta có thể sử dụng getDeclaredMethod() để lấy một phương thức private, sau đó vô hiệu hóa kiểm tra truy cập để gọi nó. Ví dụ dưới đây minh họa cách ta có thể gọi phương thức method3() của BaseClass (một phương thức static và không có tham số).

//invoking private method
Method method = Class.forName("com.journaldev.reflection.BaseClass").getDeclaredMethod("method3", null);
method.setAccessible(true);
method.invoke(null, null); //prints "Method3"

Sử dụng Reflection với constructor

Ta đã biết lấy tất cả các constructor công khai. Reflection API cũng cung cấp các phương thức để lấy các constructor của một class để phân tích, và chúng ta có thể tạo các thực thể mới của class bằng cách gọi constructor.

Lấy constructor công khai

Ta có thể sử dụng phương thức getConstructor() trên đối tượng Class để lấy một constructor công khai cụ thể. Ví dụ dưới đây cho thấy cách lấy constructor của ConcreteClass đã định nghĩa ở trên và constructor không tham số của HashMap. Nó cũng cho thấy cách lấy mảng chứa các kiểu tham số của constructor.

Constructor<?> constructor = Class.forName("com.journaldev.reflection.ConcreteClass").getConstructor(int.class);
//getting constructor parameters
System.out.println(Arrays.toString(constructor.getParameterTypes())); // prints "[int]"
		
Constructor<?> hashMapConstructor = Class.forName("java.util.HashMap").getConstructor(null);
System.out.println(Arrays.toString(hashMapConstructor.getParameterTypes())); // prints "[]"

Khởi tạo đối tượng bằng constructor

Ta có thể sử dụng phương thức newInstance() trên đối tượng constructor để khởi tạo một thực thể mới của class. Vì ta sử dụng Reflection khi không có thông tin về các class tại thời điểm biên dịch, ta có thể gán nó cho một Object và sau đó tiếp tục sử dụng Reflection để truy cập các trường và gọi các phương thức của nó.

Constructor<?> constructor = Class.forName("com.journaldev.reflection.ConcreteClass").getConstructor(int.class);
//getting constructor parameters
System.out.println(Arrays.toString(constructor.getParameterTypes())); // prints "[int]"
		
Object myObj = constructor.newInstance(10);
Method myObjMethod = myObj.getClass().getMethod("method1", null);
myObjMethod.invoke(myObj, null); //prints "Method1 impl."

Constructor<?> hashMapConstructor = Class.forName("java.util.HashMap").getConstructor(null);
System.out.println(Arrays.toString(hashMapConstructor.getParameterTypes())); // prints "[]"
HashMap<String,String> myMap = (HashMap<String,String>) hashMapConstructor.newInstance(null);

Sử dụng Reflection với Annotation

Annotation được giới thiệu trong Java 1.5 để cung cấp thông tin metadata của class, phương thức hoặc trường, và hiện nay nó được sử dụng rộng rãi trong các framework như Spring và Hibernate. Reflection API cũng đã được mở rộng để hỗ trợ phân tích các annotation tại runtime. Sử dụng Reflection API, ta có thể phân tích các annotation có retention policy là Runtime.

Tổng kết

Reflection trong Java mở ra khả năng thực thi phương thức và truy xuất trường một cách linh động, phục vụ nhu cầu xử lý nâng cao mà các kỹ thuật thông thường không đáp ứng được. Nó mang đến sự chủ động trong việc kiểm soát, truy xuất và điều khiển các thành phần bên trong của lớp Java khi chương trình đang chạy. Nếu có thắc mắc hoặc muốn biết thêm về các tình huống sử dụng cụ thể của API này, hãy để lại bình luận ở dưới để mọi người cùng thảo luận nhé.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất