反射面試題-請了解下

什麼是反射?

反射就是動態加載對象,並對對象進行剖析。在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法,這種動態獲取信息以及動態調用對象方法的功能成爲Java反射機制。java

反射的基本操做

建立一個類,用於演示反射的基本操做,代碼以下:git

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;
	}
}
複製代碼

獲取類中的全部方法github

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是方法類,能夠獲取方法相關的信息,除了咱們上面的方法名稱,咱們還能夠獲取其餘的一些信息,好比:sql

  • 方法返回類型:method.getReturnType().getName()
  • 方法修飾符:Modifier.toString(method.getModifiers())
  • 方法參數信息: method.getParameters()
  • 方法上的註解: method.getAnnotations()
  • 等等.......

操做方法數據庫

除了能夠獲取Class中方法的信息,還能夠經過反射來調用方法,接下來看看怎麼調用方法:bash

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()方法能夠看到輸出的就是咱們設置的值「猿天地」。微信

獲取類中的全部屬性app

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中。

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

猿天地
相關文章
相關標籤/搜索