2023-08-05

使用 映射 來存取私有資訊

最近由於學習編寫 Android應用程式 所以再次學習 Java
發現一些功能不能直接使用,而是需要使用 映射(Reflection) 來執行
在下因此尋找 映射 資料
預覽
Java 的一般情況下,當 類別(Class)方法(Method)屬性(Field)私有(private) 的情況下宣告
其他類別都不能存取該資料
但當使用 (映射)Reflection 時,即使使用 私有 ,仍能被存取

獲取私有靜態屬性

public class MyClass {
	private static boolean MY_BOOLEAN = true;
}
靜態(static) 屬性屬於 類別 ,能以 Class.Field 的方式存取,但設定為 私有 時便無法讓其他 類別 存取

public class Test {
	public static void main(String[] args) throws Exception {
		System.out.println(MyClass.MY_BOOLEAN);
	}
}
測試靜態屬性

編譯結果:
Test.java:3: error: MY_BOOLEAN has private access in MyClass
		System.out.println(MyClass.MY_BOOLEAN);
		                          ^
1 error
由於 MY_BOOLEAN 為 私有,因此 MY_BOOLEAN 只能在 MyClass 中使用,導致編譯出錯

但如果使用 映射 時,便可以存取 私有屬性:
import java.lang.reflect.Field;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		Field field = clazz.getDeclaredField("MY_BOOLEAN");
		field.setAccessible(true);
		System.out.println(field.get(null));
	}
}
field.setAccessible(true) 令 私有屬性 能被存取
再使用 field.get() 便能獲取 私有屬性 的資料
true

不過 field.get() 只會傳回 Object類別,而輸出內容只會將物件的 toString 顯示,並不能確定型態
例如:
public class MyClass {
	private static boolean PRIMITIVE_BOOLEAN = true;
	private static Boolean CLASS_BOOLEAN = true;
	private static String CLASS_String = "true";
}
import java.lang.reflect.Field;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		Field[] fields = {
			clazz.getDeclaredField("PRIMITIVE_BOOLEAN"),
			clazz.getDeclaredField("CLASS_BOOLEAN"),
			clazz.getDeclaredField("CLASS_String"),
		};
		for (Field field : fields) {
			field.setAccessible(true);
			System.out.println(field.get(null));
		}
	}
}
執行結果:
true
true
true

所有資料輸出都只會顯示 true ,不能確認傳回是 原始boolean 或 Boolean類別 或 String類別
因此還需要使用 field.getType().getName() 的確定型態
import java.lang.reflect.Field;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		Field[] fields = {
			clazz.getDeclaredField("PRIMITIVE_BOOLEAN"),
			clazz.getDeclaredField("CLASS_BOOLEAN"),
			clazz.getDeclaredField("CLASS_String"),
		};
		for (Field field : fields) {
			field.setAccessible(true);
			System.out.println(field.getType().getName());
		}
	}
}
執行結果:
boolean
java.lang.Boolean
java.lang.String

確認類型後,便可以 強行變更型態 來執行其他操作

既然能夠獲取 私有屬性 ,因此 封裝(<default>)保護(protected) 的資料亦能夠相同方法獲取
因此不重覆測試

更改私有靜態屬性

使用 映射 除了能夠獲取 屬性,還可以使用 field.set() 來修改 屬性
import java.lang.reflect.Field;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		Field field = clazz.getDeclaredField("PRIMITIVE_BOOLEAN");
		field.setAccessible(true);
		System.out.println(field.get(null));
		field.set(null, false);
		System.out.println(field.get(null));
	}
}
執行結果:
true
false

獲取及更改物件私有屬性

public class MyClass {
	private boolean myBoolean = true;
}
沒有 static 的屬性不再是靜態屬性,不能直接以類別方式存取
而是需要先建立該類別的物件,再在物件中存取
import java.lang.reflect.Field;
public class Test {
	public static void main(String[] args) throws Exception {
		MyClass myClass = new MyClass();
		Class clazz = myClass.getClass();
		Field field = clazz.getDeclaredField("myBoolean");
		field.setAccessible(true);
		System.out.println(field.get(myClass));
		field.set(myClass, false);
		System.out.println(field.get(myClass));
	}
}
使用 field.get() 時,需要指定物件參數,才能存取該物件對應的屬性

使用私有靜態功能

public class MyClass {
	private static void myMethod() {
		System.out.println("I am private static method.");
	}
}
與先前的例子相同,私有方法 不能被其他類別存取

import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		Method method = clazz.getDeclaredMethod("myMethod");
		method.setAccessible(true);
		method.invoke(null);
	}
}
與 field.get() 相似,不過改為使用 method.invoke()
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
編譯時,由於 clazz.getDeclaredMethod() 有機會出現不安全的操作,因此會出現提示,但仍然能夠編譯
如果想避免編譯時出現提示,可以將 Test類別 修改成:
import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		@SuppressWarnings("unchecked")
		Method method = clazz.getDeclaredMethod("myMethod");
		method.setAccessible(true);
		method.invoke(null);
	}
}
執行結果:
I am private static method.
由於 method.invoke() 不會區分 void傳回資料
public class MyClass {
	private static void voidMethod() {
		return;
	}
	private static String nullMethod() {
		return null;
	}
	private static String stringMethod() {
		return "null";
	}
}
import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		@SuppressWarnings("unchecked")
		Method[] methods = {
			clazz.getDeclaredMethod("voidMethod"),
			clazz.getDeclaredMethod("nullMethod"),
			clazz.getDeclaredMethod("stringMethod"),
		};
		for (Method method : methods) {
			method.setAccessible(true);
			System.out.println(method.getReturnType().getName());
			System.out.println(method.invoke(null));
		}
	}
}
執行結果:
void
null
java.lang.String
null
java.lang.String
null
使用 method.invoke() 時,即使功能是 void 仍會傳回 null 資料
與屬性相似,使用 method.getReturnType().getName() 來確認傳回類型

使用私有物件功能

public class MyClass {
	private void objectMethod() {
		System.out.println("I am private object method.");
	}
}
import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		MyClass myClass = new MyClass();
		Class<MyClass> clazz = myClass.getClass();
		@SuppressWarnings("unchecked")
		Method method = clazz.getDeclaredMethod("objectMethod");
		method.setAccessible(true);
		method.invoke(myClass);
	}
}
以 映射 使用物件的功能 與獲取物件的資料相似
使用 method.invoke() 時,同樣需要指定物件參數,才能使用該物件對應的方法

使用附有參數的功能

public class MyClass {
	private static int square(int value) {
		return value * value;
	}
}
import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		@SuppressWarnings("unchecked")
		Method method = clazz.getDeclaredMethod("square", int.class);
		method.setAccessible(true);
		System.out.println(method.invoke(null, 16));
	}
}
使用 映射 時,由於 方法 允許 重載(Overload)
因此在 clazz.getDeclaredMethod() 除了傳入功能名稱的參數外,可以因應參數的類型(或以陣列)順序傳入以使用對應重載的 方法
最後在 method.invoke() 除了第一個參數傳入 null 或物件外,亦需要將對應的參數類型的資料(或以陣列)順序傳入

使用私有建構子建立物件

如果類別只有私有 建構子(Constructor),又沒有靜態方法或靜態屬性獲取該類別的物件,即是該類別是無法以正規方法建立物件
但透過 映射 就能夠將建立該類別的物件
public class MyClass {
	private MyClass(int edge) {
		System.out.println(edge * 4);
	}
}
import java.lang.reflect.Constructor;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		Constructor<MyClass> constructor = clazz.getDeclaredConstructor(int.class);
		constructor.setAccessible(true);
		MyClass myClass = constructor.newInstance(8);
	}
}
clazz.getDeclaredConstructor() 的使用方法與 clazz.getDeclaredMethod() 相似,只是不需要傳入名稱
如果需要附加參數,同樣將參數的類型順序傳入,再在 constructor.newInstance() 順序傳入對應資料即可

使用私有靜態內部類別的方法

內部類別(Inner Class) 與一般類別相同,只是內部類別存在於一個類別中
而且內部類別允許以私有方式製作
public class MyClass {
	private static class MySubClass {
		private MySubClass() {
		}
		private void mySubMethod() {
			System.out.println(this.getClass().getName());
		}
	}
}
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = Class.forName("MyClass$MySubClass");
		Constructor constructor = clazz.getDeclaredConstructor();
		constructor.setAccessible(true);
		Object innerObject = constructor.newInstance();
		Class innerClass = innerObject.getClass();
		Method innerMethod = innerClass.getDeclaredMethod("mySubMethod");
		innerMethod.setAccessible(true);
		innerMethod.invoke(innerObject);
	}
}
執行結果:
MyClass$MySubClass
由於 私有內部類別 無法被匯入,因此物件只能以 Object類別 建立
但 Object類別 不會擁有 內部類別的 屬性 或 方法
而且由於無法強制轉變類型,即使是 公有(public) 的 屬性 或 方法 都不能直接使用
整過操作程式,都需要使用 映射 來使用方法或獲取屬性

另外,內部類別 的名稱使用 $(錢符號(dollar)) 連接,而非 .(點(Period)) 來連接

使用私有內部類別物件的方法

一般情況很少會設計私有內部類別物件,但技術上是允許這種設計

public class MyClass {
	private MyClass() {
	}
	private class MySubClass {
		private MySubClass() {
		}
		private void mySubMethod() {
			System.out.println(this.getClass().getName());
		}
	}
}
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		Class<MyClass> outerClass = MyClass.class;
		Constructor<MyClass> outerConstructor = outerClass.getDeclaredConstructor();
		outerConstructor.setAccessible(true);
		MyClass myClass = outerConstructor.newInstance();
		Class innerClass = Class.forName("MyClass$MySubClass");
		Constructor innerConstructor = innerClass.getDeclaredConstructor(MyClass.class);
		innerConstructor.setAccessible(true);
		Object innerObject = innerConstructor.newInstance(myClass);
		Method innerMethod = innerClass.getDeclaredMethod("mySubMethod");
		innerMethod.setAccessible(true);
		innerMethod.invoke(innerObject);
	}
}
執行結果:
MyClass$MySubClass
如果將例子中所有 private 改成 public
MyClass myClass = new MyClass();
MyClass.MySubClass mySubClass = myClass.new MySubClass();
mySubClass.mySubMethod();
語法上已經有點麻煩,如果必須以 映寫 方法建立物入更加繁複的步驟

更改物件常數屬性

宣告以 常數(final) 的變數,其地址只能被指派資料一次
class MyClass {
	public final int VALUE = 0;
}
public class Test {
	public static void main(String[] args) throws Exception {
		MyClass myClass = new MyClass();
		myClass.VALUE = 1;
	}
}
執行結果:
Test.java:4: error: cannot assign a value to final variable VALUE
		myClass.VALUE = 1;
		       ^
1 error
當地址被重覆指派時,編譯時會出現錯誤

但使用 映射 仍能透過 映射 更改
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class Test {
	public static void main(String[] args) throws Exception {
		MyClass myClass = new MyClass();
		Field field = clazz.getDeclaredField("VALUE");
		field.setAccessible(true);
		System.out.println(field.get(myClass));
		field.set(myClass, 1);
		System.out.println(field.get(myClass));
	}
}
執行結果:
0
1

更改靜態常數屬性

使用 映射 更改靜態常數屬性則比較麻煩
public class MyClass {
    public static final int VALUE = 0;
}
import java.lang.reflect.Field;
public class Test {
    public static void main(String[] args) throws Exception {
        Class clazz = MyClass.class;
        Field field = clazz.getDeclaredField("VALUE");
        field.setAccessible(true);
        System.out.println(field.get(null));
        field.set(null, 1);
        System.out.println(field.get(null));
    }
}
執行結果:
0
Exception in thread "main" java.lang.IllegalAccessException: Can not set static final int field MyClass.VALUE to java.lang.Integer
	at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
	at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
	at java.base/jdk.internal.reflect.UnsafeQualifiedStaticIntegerFieldAccessorImpl.set(UnsafeQualifiedStaticIntegerFieldAccessorImpl.java:77)
	at java.base/java.lang.reflect.Field.set(Field.java:780)
	at Test.main(Test.java:8)
使用類似更改物件常數屬性的方法更改靜態常數屬性會出現錯誤
編譯時顯示 不能更改靜態常數屬性

要更改類別靜態屬性需要先將更改 屬性 的 修飾(Modifier) ,將 常數位元取消 才能更改
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		Field field = clazz.getDeclaredField("VALUE");
		field.setAccessible(true);
		Class modifiersClass = field.getClass();
		Field modifiersField = modifiersClass.getDeclaredField("modifiers");
		modifiersField.setAccessible(true);
		// remove final modifier
		modifiersField.set(field, field.getModifiers() & ~Modifier.FINAL);
		System.out.println(field.get(null));
		field.set(null, 1);
		// gain final modifier
		modifiersField.set(field, field.getModifiers() & ~Modifier.FINAL);
		System.out.println(field.get(null));
	}
}
執行結果:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by Test (file:/path/of/file/) to field java.lang.reflect.Field.modifiers
WARNING: Please consider reporting this to the maintainers of Test
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
0
1
但即是能夠修改,執行程式時,會出現警告資訊

補充資料

在測試各種以 映射 更改存取資料的操作中,會發現當使用 私有方法 會出現 不安全提示 及 更改 靜態常數屬性 會出現 警告

由於 私有方法 有機會是 讓內部其他 方法 使用,當中如果沒有檢查資料,便會出現非預期結果

public class MyClass {
	private static int privateDivision(int dividend, int divider) {
		return dividend / divider;
	}
	public static void publicDivision(int dividend, int divider) {
		if (divider == 0) {
			System.out.println("Cannot divided by 0");
		} else {
			System.out.println(MyClass.privateDivision(dividend, divider));
		}
	}
}
public class Test {
	public static void main(String[] args) throws Exception {
		MyClass.publicDivision(1, 0);
		MyClass.publicDivision(1, 1);
	}
}
執行結果:
Cannot divided by 0
1
由於 publicDivision() 在運算有檢查傳入的參數的況狀來避免執行時出現錯誤
import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = MyClass.class;
		Method method = clazz.getDeclaredMethod("privateDivision", int.class, int.class);
		method.setAccessible(true);
		System.out.println(method.invoke(null, 1, 0));
		System.out.println(method.invoke(null, 1, 1));
	}
}
執行結果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:119)
	at java.base/java.lang.reflect.Method.invoke(Method.java:578)
	at Test.main(Test.java:7)
Caused by: java.lang.ArithmeticException: / by zero
	at MyClass.privateDivision(Test.java:7)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	... 2 more
由於 privateDivision() 在運算沒有檢查傳入的參數的況狀,執行時會出現異常情況

更改其他類別常數屬性,是非常危險
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class Test {
	public static void main(String[] args) throws Exception {
		Class clazz = Boolean.class;
		Field field = clazz.getDeclaredField("TRUE");
		field.setAccessible(true);
		Class modifiersClass = field.getClass();
		Field modifiersField = modifiersClass.getDeclaredField("modifiers");
		modifiersField.setAccessible(true);
		modifiersField.set(field, field.getModifiers() & ~Modifier.FINAL);
		System.out.println(Boolean.TRUE);
		field.set(null, false);
		System.out.println(Boolean.TRUE);
		modifiersField.set(field, field.getModifiers() & ~Modifier.FINAL);
	}
}
執行結果:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by Test (file:/path/of/file/) to field java.lang.reflect.Field.modifiers
WARNING: Please consider reporting this to the maintainers of Test
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
true
false
如同上述例子,將 Boolean類別 的 TRUE靜態常數屬性 更改為 false
當其他類別需要使用 Boolean.TRUE 時,便會出現嚴重問題

不過在 JDK 12+ 基於安全考慮,已經禁止存取 Field類別 的 modifiers屬性
但仍然能夠使用 sun.misc.Unsafe 類別 來更改 靜態屬性 ,不過比之前的方法更加複雜
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test {
	public static void main(String[] args) throws Exception {
		Class booleanClass = Boolean.class;
		Field fieldClass = booleanClass.getDeclaredField("TRUE");
		Class unsafeClass = Class.forName("sun.misc.Unsafe");
		Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
		unsafeField.setAccessible(true);
		Object unsafeObject = unsafeField.get(null);
		Method unsafeMethodBase = unsafeClass.getDeclaredMethod("staticFieldBase", fieldClass.getClass());
		Object unsafeMethodBaseObject = unsafeMethodBase.invoke(unsafeObject, fieldClass);
		Method unsafeMethodOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", fieldClass.getClass());
		Object unsafeMethodOffsetObject = unsafeMethodOffset.invoke(unsafeObject, fieldClass);
		Method unsafeMethodPutObject = unsafeClass.getDeclaredMethod("putObject", Object.class, long.class, Object.class);
		System.out.println(Boolean.TRUE);
		unsafeMethodPutObject.invoke(unsafeObject, unsafeMethodBaseObject, unsafeMethodOffsetObject, false);
		System.out.println(Boolean.TRUE);
	}
}
sun.misc.Unsafe 在 JDK 7 開始出現,但 不屬於標準 JDK ,因此無法找到 sun.misc.Unsafe 的 API

sun.misc.Unsafe 能存取記憶體及其他低階操作
由於 sun.misc.Unsafe 能夠繞過 JVM 的安全設定,如果使用不當,會影響 JVM 的穩定性或產生不安全操作
不建議在一般開發中胡亂使用

在下為了將來可以方便使用這些操作,而編寫一些簡化操作的方法
public static<T> T createObject(Class<T> clazz, Class[] parameterClasses, Object[] parameterValues) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
	Constructor<T> constructor = clazz.getDeclaredConstructor(parameterClasses);
	constructor.setAccessible(true);
	return constructor.newInstance(parameterValues);
}
public static Object invokeMethod(Class clazz, String methodName, Class[] parameterClasses, Object[] parameterValues, Object instance) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
	Method method = clazz.getDeclaredMethod(methodName, parameterClasses);
	method.setAccessible(true);
	return method.invoke(instance, parameterValues);
}
public static Object getValue(Class clazz, String fieldName, Object instance) throws NoSuchFieldException, IllegalAccessException {
	Field field = clazz.getDeclaredField(fieldName);
	field.setAccessible(true);
	return field.get(instance);
}
public static void setValue(Class clazz, String fieldName, Object value, Object instance) throws NoSuchFieldException, IllegalAccessException {
	Field field = clazz.getDeclaredField(fieldName);
	field.setAccessible(true);
	int modifiers = field.getModifiers();
	if ((modifiers & Modifier.FINAL) > 0) {
		Class modifiersClass = field.getClass();
		Field modifiersField = modifiersClass.getDeclaredField("modifiers");
		modifiersField.setAccessible(true);
		modifiersField.set(field, modifiers & ~Modifier.FINAL);
		field.set(instance, value);
		modifiersField.set(field, modifiers & ~Modifier.FINAL);
	} else {
		field.set(instance, value);
	}
}
public static void setStaticValue(Class clazz, String fieldName, Object value) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
	Field field = clazz.getDeclaredField(fieldName);
	Class unsafeClass = Class.forName("sun.misc.Unsafe");
	Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
	unsafeField.setAccessible(true);
	Object unsafeObject = unsafeField.get(null);
	Method unsafeMethodBase = unsafeClass.getDeclaredMethod("staticFieldBase", field.getClass());
	Object unsafeMethodBaseObject = unsafeMethodBase.invoke(unsafeObject, field);
	Method unsafeMethodOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", field.getClass());
	Object unsafeMethodOffsetObject = unsafeMethodOffset.invoke(unsafeObject, field);
	Method unsafeMethodPutObject = unsafeClass.getDeclaredMethod("putObject", Object.class, long.class, Object.class);
	unsafeMethodPutObject.invoke(unsafeObject, unsafeMethodBaseObject, unsafeMethodOffsetObject, false);
}

總結

編寫 Android 程式時,偶然發現一些功能,不是 公有方法 或 公有屬性
但這些功能在下希望能加入到自己編寫的 Android應用程式 中,因此尋找資料發現可以透過 映射 達至效果

參考資料

沒有留言 :

張貼留言