反射面试题-请了解下

3,296 阅读8分钟

什么是反射?

反射就是动态加载对象,并对对象进行剖析。在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能成为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中。

更多技术分享请关注微信公众号:猿天地

猿天地