反射面試題-請了解下

猿天地發表於2018-05-15

什麼是反射?

反射就是動態載入物件,並對物件進行剖析。在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法,這種動態獲取資訊以及動態呼叫物件方法的功能成為Java反射機制。

反射的基本操作

建立一個類,用於演示反射的基本操作,程式碼如下:

package fs;
public class Student {
	private long id;
	private String name;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}
複製程式碼

獲取類中的所有方法

public static void main(String[] args) {
	try {
		Class<?> clz = Class.forName("fs.Student");
		Method[] methods = clz.getMethods();
		for (Method method : methods) {
			System.out.println("方法名:" + method.getName());
		}
	} catch (ClassNotFoundException e) {
			e.printStackTrace();
	}
}
複製程式碼
  • Class.forName("fs.Student"):初始化指定的類
  • clz.getMethods():獲取類中所有的方法(包括其繼承類的方法)

如果只需要獲取載入類中的方法,不要父類的方法,可以使用下面的程式碼:

Method[] methods = clz.getDeclaredMethods();
複製程式碼

Method是方法類,可以獲取方法相關的資訊,除了我們上面的方法名稱,我們還可以獲取其他的一些資訊,比如:

  • 方法返回型別:method.getReturnType().getName()
  • 方法修飾符:Modifier.toString(method.getModifiers())
  • 方法引數資訊: method.getParameters()
  • 方法上的註解: method.getAnnotations()
  • 等等.......

操作方法

除了可以獲取Class中方法的資訊,還可以通過反射來呼叫方法,接下來看看怎麼呼叫方法:

try {
	Class<?> clz = Class.forName("fs.Student");
	Student stu = (Student) clz.newInstance();
	System.out.println(stu.getName());
	Method method = clz.getMethod("setName", String.class);
	method.invoke(stu, "猿天地");
	System.out.println(stu.getName());
} catch (Exception e) {
	e.printStackTrace();
} 
複製程式碼

通過class的newInstance()方法構造一個Student物件,然後呼叫getName()方法,這個時候輸出的是null,然後通過方法名獲取到setName方法,通過invoke呼叫方法,傳入引數,然後呼叫getName()方法可以看到輸出的就是我們設定的值“猿天地”。

獲取類中的所有屬性

Class<?> clz = Class.forName("fs.Student");
Field[] fields = clz.getFields();
for (Field field : fields) {
	System.out.println("屬性名:" + field.getName());
}
複製程式碼

clz.getFields()只能獲取public的屬性,包括父類的。

如果需要獲取自己宣告的各種欄位,包括public,protected,private得用clz.getDeclaredFields()

Field是屬性類,可以獲取屬性相關的資訊,比如:

  • 屬性型別:field.getType().getName()
  • 屬性修飾符:Modifier.toString(field.getModifiers())
  • 屬性上的註解: field.getAnnotations()
  • 等等.......

操作屬性

try {
	Class<?> clz = Class.forName("fs.Student");
	Student stu = (Student) clz.newInstance();
	Field field = clz.getDeclaredField("name");
	field.setAccessible(true);
	System.out.println(field.get(stu));
	field.set(stu, "猿天地");
	System.out.println(field.get(stu));
} catch (Exception e) {
	e.printStackTrace();
} 
複製程式碼

通過clz.getDeclaredField("name");獲取name屬性,呼叫get方法獲取屬性的值,第一次肯定是沒有值的,然後呼叫set方法設定值,最後再次獲取就有值了,在get之前有field.setAccessible(true);這個程式碼,如果不加的話就會報下面的錯誤資訊:

Class fs.Test can not access a member of class fs.Student with modifiers "private"
複製程式碼

setAccessible(true);以取消Java的許可權控制檢查,讓我們在用反射時可以訪問訪問私有變數

反射的優缺點?

優點

  • 反射提高了程式的靈活性和擴充套件性,在底層框架中用的比較多,業務層面的開發過程中儘量少用。

缺點

  • 效能不好 反射是一種解釋操作,用於欄位和方法接入時要遠慢於直接程式碼,下面通過2段簡單的程式碼來比較下執行的時間就可以體現出效能的問題

直接建立物件,呼叫方法設定值,然後獲取值,時間在300ms左右

long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
	Student stu = new Student();
	stu.setName("猿天地");
	System.out.println(stu.getName());
}
long end = System.currentTimeMillis();
System.out.println(end - start);
複製程式碼

利用反射來實現上面的功能,時間在500ms左右,我是在我本機測試的

long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
	Class<?> clz = Class.forName("fs.Student");
	Student stu = (Student) clz.newInstance();
	Method method = clz.getMethod("setName", String.class);
	method.invoke(stu, "猿天地");
	System.out.println(stu.getName());
}
long end = System.currentTimeMillis();
System.out.println(end - start);
複製程式碼
  • 程式邏輯有影響

使用反射操作會模糊化程式的內部邏輯,從程式碼的維護角度來講,我們更希望在原始碼中看到程式的邏輯,反射相當於繞過了原始碼的方式,因此會帶來維護難度比較大的問題。

反射的使用場景有哪些?

  • 實現RPC框架
  • 實現ORM框架
  • 拷貝屬性值(BeanUtils.copyProperties)
  • ......

實現RPC框架

RPC是遠端過程呼叫的簡稱,廣泛應用在大規模分散式應用中。提到RPC框架在我腦海裡第一閃現的就是Dubbo,遠端過程呼叫的實現原理簡單無非就是當客戶端呼叫的時候通過動態代理向服務提供方傳送呼叫的資訊(Netty通訊),服務提供方收到後根據客戶端需要呼叫的方法,呼叫本地方法,拿到結果組裝返回。這裡就涉及到動態方法的呼叫,反射也就可以排上用場了。

至於Dubbo中是怎麼動態呼叫的我就不太清楚啦,沒去研究過Dubbo的原始碼哈,我臨時看了下,找到了2個相關的類JdkProxyFactory和JavassistProxyFactory。

JdkProxyFactory就是用的method.invoke(proxy, arguments);

public class JdkProxyFactory extends AbstractProxyFactory {

    @Override
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));
    }

    @Override
    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName,
                                      Class<?>[] parameterTypes,
                                      Object[] arguments) throws Throwable {
                Method method = proxy.getClass().getMethod(methodName, parameterTypes);
                return method.invoke(proxy, arguments);
            }
        };
    }

}
複製程式碼

JavassistProxyFactory是用的Javassist框架來實現的

public class JavassistProxyFactory extends AbstractProxyFactory {

    @Override
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

    @Override
    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
        final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName,
                                      Class<?>[] parameterTypes,
                                      Object[] arguments) throws Throwable {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }

}
複製程式碼

實現ORM框架

關於ORM的概念本文就不做過多的介紹了,主要給大家介紹下如何用反射實現ORM的核心功能,我們以保持操作來進行講解,也就是定義一個與資料庫表對應的實體類,寫一個save方法,傳入我們實體類就可以將這個物件中的屬性值儲存到資料庫中,變成一條資料。

還是以上面的Student來作為與表對應的實體類,下面我們看如何實現save方法中的邏輯:

public static void save(Object data, Class<?> entityClass) throws Exception {
	String sql = "insert into {0}({1}) values({2})";
	String tableName = entityClass.getSimpleName();
		
	List<String> names = new ArrayList<>();
	List<String> fs = new ArrayList<>();
	List<Object> values = new ArrayList<>();
		
	Field[] fields = entityClass.getDeclaredFields();
	for (Field field : fields) {
		names.add(field.getName());
		fs.add("?");
		field.setAccessible(true);
		values.add(field.get(data));
	}
		
	String fieldStr = names.stream().collect(Collectors.joining(","));
	String valueStr = fs.stream().collect(Collectors.joining(","));
	System.err.println(MessageFormat.format(sql, tableName, fieldStr, valueStr));
	values.forEach(System.out::println);
}
	
public static void main(String[] args) {
	try {
		Student stu = new Student();
		stu.setId(1);
		stu.setName("猿天地");
		save(stu, Student.class);
	} catch (Exception e) {
		e.printStackTrace();
	} 
}
複製程式碼

執行main方法,輸出結果如下:

insert into Student(id,name) values(?,?)
1
猿天地
複製程式碼

當然我上面只是最簡單的程式碼,考慮也沒那麼全面,為的只是讓大家熟悉反射的使用方式和場景,接下來我們再配合註解做一個小小的優化,註解不熟的同學可以參考我的這篇文章:《註解面試題-請了解下》

優化2點,定義一個TableName註解,用於描述表的資訊,上面我們是直接用的類名作為表名,實際使用中很有可能表名是stu_info這樣的 ,還有就是定義一個Field用於描述欄位的資訊,原理同上。

定義TableName註解:

import java.lang.annotation.*;
/**
 * 表名
 * @author yinjihuan
 *
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableName {
	
	/**
	 * 表名
	 * @return
	 */
	String value();

}
複製程式碼

定義Field註解:

import java.lang.annotation.*;
/**
 * 欄位名
 * @author yinjihuan
 *
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Field {
	
	/**
	 * 欄位名稱
	 * @return
	 */
	String value();
	
}
複製程式碼

修改實體類,增加註解的使用:

@TableName("stu_info")
public class Student {
	
	private long id;
	
	@Field("stu_name")
	private String name;
	
	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

複製程式碼

save方法中就需要考慮到有註解的情況,修改程式碼,增加獲取註解中值的邏輯:

public static void save(Object data, Class<?> entityClass) throws Exception {
	String sql = "insert into {0}({1}) values({2})";
	String tableName = entityClass.getSimpleName();
	if (entityClass.isAnnotationPresent(TableName.class)) {
		tableName = entityClass.getAnnotation(TableName.class).value();
	}
	List<String> names = new ArrayList<>();
	List<String> fs = new ArrayList<>();
	List<Object> values = new ArrayList<>();
		
	Field[] fields = entityClass.getDeclaredFields();
	for (Field field : fields) {
		String fieldName = field.getName();
		if (field.isAnnotationPresent(fs.Field.class)) {
			fieldName = field.getAnnotation(fs.Field.class).value();
		}
		names.add(fieldName);
		fs.add("?");
		field.setAccessible(true);
		values.add(field.get(data));
	}
		
	String fieldStr = names.stream().collect(Collectors.joining(","));
	String valueStr = fs.stream().collect(Collectors.joining(","));
	System.err.println(MessageFormat.format(sql, tableName, fieldStr, valueStr));
	values.forEach(System.out::println);
}
複製程式碼

通上面的修改,如果有註解的情況下以註解中的值為主,沒有的話就用Class中的。 執行main方法,輸出結果如下:

insert into stu_info(id,stu_name) values(?,?)
1
猿天地

複製程式碼

更完整的反射實現的ORM可以參考我的框架:https://github.com/yinjihuan/smjdbctemplate

拷貝屬性值(BeanUtils.copyProperties)

在開發過程中,我們會遇到各種bean之間的轉換,比如用ORM框架查詢出來的資料,對應的bean,需要轉換成Dto返回給呼叫方,這個時候就需要進行bean的轉換了,下面通過簡單的虛擬碼來講解下:

Student stu = dao.get();
StudentDto dto = new StudentDto();
dto.setName(stu.getName());
dto.setXXX(stu.getXXX());
dto.set......
return dto;
複製程式碼

如果屬性多的話,光寫set方法就要寫很多行,有沒有優雅的方式呢?

這個時候我們可以用Spring中的BeanUtils.copyProperties來實現上面的需求,只需要一行程式碼即可,關於BeanUtils.copyProperties的詳細使用不做過多講解:

Student stu = dao.get();
StudentDto dto = new StudentDto();
BeanUtils.copyProperties(stu, dto);
複製程式碼

這個功能就是反射的功勞了,我們可以通過原始碼來驗證下是否是通過反射來實現的

private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
			throws BeansException {

		Assert.notNull(source, "Source must not be null");
		Assert.notNull(target, "Target must not be null");

		Class<?> actualEditable = target.getClass();
		if (editable != null) {
			if (!editable.isInstance(target)) {
				throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
						"] not assignable to Editable class [" + editable.getName() + "]");
			}
			actualEditable = editable;
		}
		PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
		List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

		for (PropertyDescriptor targetPd : targetPds) {
			Method writeMethod = targetPd.getWriteMethod();
			if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
				PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
				if (sourcePd != null) {
					Method readMethod = sourcePd.getReadMethod();
					if (readMethod != null &&
							ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
						try {
							if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
								readMethod.setAccessible(true);
							}
							Object value = readMethod.invoke(source);
							if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
								writeMethod.setAccessible(true);
							}
							writeMethod.invoke(target, value);
						}
						catch (Throwable ex) {
							throw new FatalBeanException(
									"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
						}
					}
				}
			}
	}
}
複製程式碼

原始碼不做過多解釋,我們看最關鍵的2行程式碼,第一行是:

Object value = readMethod.invoke(source);
複製程式碼

通過呼叫讀的方法將source中的值讀取出來

第二行關鍵的是:

writeMethod.invoke(target, value);
複製程式碼

通過呼叫寫的方法進行復制到target中。

更多技術分享請關注微信公眾號:猿天地

猿天地

相關文章