程序员重复造轮子没意义:你知道反射机制吗
程序员重复造轮子没意义:你知道反射机制吗概念上的东西就说这么多,回到实际的coding部分:测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。Oracle文档: 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
平时写代码的过程中,我们使用不同的工具框架来提升开发效率,除了基础框架之外,我们自己也想造轮子,封装各种业务平台功能;
一旦需造轮子的时候,那么就需要使用Java造轮子利器:反射;
一些项目中常见的反射应用场景:
- 泛化调用: 提前不知道目标RPC的接口和方法,而是开发在后台输入值,根据输入的配置动态请求。 这也是提升效率的一部分,因为不可能所以得RPC接口都要亲自对接的,总要有一部分可以灵活的调用不同接口。
- 自测入口: 我们的逻辑代码一般会散落在应用的不同位置,如果想要debug,一般会有一个核心入口,但是如果核心入口太长,想要进入我们的逻辑分支很难呢? 我们可以统一收口下测试入口,这个测试入口在开发环境下就是可以访问到所有的对象,服务,组件等功能,直接对目标逻辑进行调试即可, 这个也是需要反射.
其实上面两个场景也说明了反射应用场景的一个特点: 我不知道现在要调用哪个类和哪个方法,等到运行时才知道要调用的类和方法。
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
Oracle文档: 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。
概念上的东西就说这么多,回到实际的coding部分:
import io.mybatis.provider.Entity;
import javax.persistence.Id;
import java.io.Serializable;
@Entity.Table(value = "Blog" autoResultMap = true)
public class Blog implements Serializable {
@Id
@Entity.Column
private Long id;
@Entity.Column
private String title;
public Blog() {
}
private Blog(Long id) {
this.id = id;
}
public Blog(Long id String title) {
this.id = id;
this.title = title;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public String toString() {
return "Blog{"
"id=" id
" title='" title '\''
'}';
}
}
复制代码
2. 拿到Class信息
有三种方式可以拿到Class信息:
- 第一种方法有限制条件:需要导入类的包;
- 第二种:需要知道对象的全路径,也是最常使用的方式
- 第三种方法Blog对象,仅仅想拿到Class信息,一般不需要反射。
并且在同一个运行环境中,三种方式拿到的Class对象是同一个,每个类只生成一个Class对象,Class对象一般在JVM的metaspace区域中。
@Test
public void testClass() throws ClassNotFoundException {
Class<Blog> blogClass = Blog.class;
Class<?> blogClassForName = Class.forName("me.aihe.bizim.dal.bean.Blog");
Class<? extends Blog> blogClassForGet = new Blog().getClass();
System.out.println(blogClassForGet == blogClass);
System.out.println(blogClassForGet == blogClassForName);
}
复制代码
3. 关键API
拿到Class信息之后,我们基本上就可以对这个对象做各自想要的操作了;
反射中有两种命名方式:
- getXXX 获取公共的对象,即标记为public的
- getDeclaredXXX 获取已经声明的,也就是对象中所有的XXX
setAccessible 函数用于动态获取访问权限,一般对象如果声明为private的需要setAccessible(true)才可以进行调用。
构造方法操作 - Constructor如果类的构造方法一定要传参数,可以根据获取到的Constructor来创建对象;
@Test
public void testConstruct() throws ClassNotFoundException NoSuchMethodException InvocationTargetException InstantiationException IllegalAccessException {
Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog");
Constructor<?>[] constructors = aClass.getConstructors();
Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println("getConstructors:" constructor);
}
for (Constructor<?> constructor : declaredConstructors) {
System.out.println("getDeclaredConstructors:" constructor);
}
// 使用构造方法
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Long.class);
declaredConstructor.setAccessible(true);
Object blog = declaredConstructor.newInstance(1L);
System.out.println(blog);
}
复制代码
属性操作 - Field
拿到了对象的属性之后,可以通过Field进行set和get操作,在更新数据库的时候,如果要带上创建人修改人,可以根据请求上下文中的用户,同feild方式写入到数据库的对象中(对象的命名保持统一)
@Test
public void testFiled() throws ClassNotFoundException IllegalAccessException NoSuchFieldException {
Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog");
Field title = aClass.getDeclaredField("title");
title.setAccessible(true);
Blog obj = new Blog();
System.out.println(obj);
title.set(obj "测试");
System.out.println(obj);
System.out.println(title.get(obj));
}
复制代码
一个可以往某个对象根据fieldName设置值,和获取值的工具方法:
public static Object getFiledValue(Object obj String filedName){
try {
Class<?> clazz = obj.getClass();
Field field = null;
do {
try {
field = clazz.getDeclaredField(filedName);
if (field != null){
field.setAccessible(true);
break;
}
}catch (Exception e){
}
clazz = clazz.getSuperclass();
} while (!clazz.equals(Object.class));
if (field != null){
Object o = field.get(obj);
return o;
}
}catch (Exception e){
log.error("getFiledValue error" e);
return null;
}
return null;
}
public static void setField(Object obj String filedName Object value){
Class<?> clazz = obj.getClass();
Field field = null;
do {
try {
field = clazz.getDeclaredField(filedName);
if (field != null){
field.setAccessible(true);
}
}catch (Exception e){
}
clazz = clazz.getSuperclass();
} while (!clazz.equals(Object.class));
if (field != null){
try {
field.set(obj value);
}catch (Exception e){
log.error("setField error " e);
}
}
}
复制代码
方法操作 - Method
和属性操作类似,不同的是方法可能有多个参数;
@Test
public void testMethod() throws ClassNotFoundException IllegalAccessException NoSuchMethodException InvocationTargetException {
Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog");
Method setTitle = aClass.getMethod("setTitle" String.class);
Method getTitle = aClass.getMethod("getTitle");
Blog obj = new Blog();
System.out.println(obj);
setTitle.invoke(obj "测试");
System.out.println(obj);
System.out.println(getTitle.invoke(obj));
}
复制代码
注解操作 - Annoation
注解可以加在字段上,方法上,类上,有时候我们拿到了对象或者方法也不知道做什么,通过注解,可以很好的做想做的事情。当然注解已经不算是反射的范畴了,但是可以让反射的功能更加强大。
- 比如Spring中标记了@RequestMapping注解的方法,我们知道这个是用来处理Http请求的,就可以在框架内做一些处理。
- 一些自定义的注解,在启动的时候扫描到这些注解,将这些对象放到某个集合中统一处理。
获取类、方法、属性上的注解:
@Test
public void testAnnotation() throws ClassNotFoundException NoSuchMethodException NoSuchFieldException {
Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog");
Annotation[] annotations = aClass.getAnnotations();
Method getTitle = aClass.getMethod("getTitle");
Annotation[] annotations1 = getTitle.getAnnotations();
Field title = aClass.getDeclaredField("title");
Annotation[] annotations2 = title.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println("类注解:" annotation);
}
for (Annotation annotation : annotations1) {
System.out.println("方法注解:" annotation);
}
for (Annotation annotation : annotations2) {
System.out.println("属性注解:" annotation);
}
}
复制代码
4. 泛型操作 - Type
有时候在代码中写的是泛型,不确定具体是什么对象,也可以通过泛型拿到泛型信息: 泛型类型相对比较复杂。
泛型常用API在类、方法、构造器、属性上都可以获取到泛型:
java.lang.Class中的相关方法:
- Type[] getGenericInterfaces() 返回类实例的接口的泛型类型
- Type getGenericSuperclass() 返回类实例的父类的泛型类型
java.lang.reflect.Constructor中的相关方法:
- Type[] getGenericExceptionTypes() 返回构造器的异常的泛型类型
- Type[] getGenericParameterTypes() 返回构造器的方法参数的泛型类型
java.lang.reflect.Method中的相关方法:
- Type[] getGenericExceptionTypes() 返回方法的异常的泛型类型
- Type[] getGenericParameterTypes() 返回方法参数的泛型类型
- Type getGenericReturnType() 返回方法返回值的泛型类型
java.lang.reflect.Field中的相关方法:
- Type getGenericType() 返回属性的泛型类型
@Test
public void testGeneric(){
BlogImpl blog = new BlogImpl();
Class kClass = blog.getKClass();
Class vClass = blog.getVClass();
System.out.println(kClass);
System.out.println(vClass);
}
public static abstract class BaseClass<K V>{
public Class getKClass(){
Type genericSuperclass = this.getClass().getGenericSuperclass();
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
Class tClass = (Class) actualTypeArguments[0];
return tClass;
}
public Class getVClass(){
Type genericSuperclass = this.getClass().getGenericSuperclass();
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
Class tClass = (Class) actualTypeArguments[1];
return tClass;
}
public abstract V execute(K k);
}
public static class BlogImpl extends BaseClass<Blog Long>{
@Override
public Long execute(Blog blog) {
return blog.getId();
}
}
复制代码
获取泛型的Class信息
private static Class<?> getDetailClass(Type type int i) {
if (type instanceof ParameterizedType) {
// 处理泛型类型
ParameterizedType parameterizedType = (ParameterizedType)type;
Type actualTypeArgument = parameterizedType.getActualTypeArguments()[i];
return getDetailClass(actualTypeArgument i);
} else if (type instanceof GenericArrayType) {
// 处理数组泛型
return (Class<?>)((GenericArrayType)type).getGenericComponentType();
} else if (type instanceof TypeVariable) {
// 处理泛型擦除对象<R>
return (Class<?>)getDetailClass(((TypeVariable)type).getBounds()[0] 0);
} else {
return (Class<?>)type;
}
}
复制代码
5. 反射工具类
在commons-lang3包中,也已经封装了对应的反射工具,如果已经引入了这个包,直接使用即可:
可以看到泛型性能比直接调用差很多,为什么泛型的性能会比正常调用差?
- 泛型在执行的时候会校验方法、字段名称,校验是否有对应的权限,会比直接调用多一部分逻辑。
- 无法被jit优化,JIT可以帮助java的字节码到原生的机器码层面上,这样的话减少了java字节码的再解析操作,而反射方法是无法被jit优化的。
- 调用过程中的封装与解封操作,invoke 方法的参数是 Object[] 类型,在调用的时候需要进行一次封装。产生了额外的开销。
性能测试代码:
import org.apache.commons.lang3.time.StopWatch;
import org.junit.jupiter.api.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class PerformanceTest {
private static int COUNT = 100000;
@Test
public void testReflect() throws ClassNotFoundException NoSuchMethodException InstantiationException IllegalAccessException InvocationTargetException {
StopWatch started = StopWatch.createStarted();
for (int i = 0; i < COUNT; i ) {
Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog");
Method setTitle = aClass.getMethod("setTitle" String.class);
Object obj = aClass.newInstance();
setTitle.invoke(obj "测试");
}
System.out.println("泛型 " COUNT "次耗时(ms):" started.getTime());
}
@Test
public void testDirectUse() throws ClassNotFoundException NoSuchMethodException InstantiationException IllegalAccessException InvocationTargetException {
StopWatch started = StopWatch.createStarted();
for (int i = 0; i < COUNT; i ) {
Blog blog = new Blog();
blog.setTitle("测试");
}
System.out.println("直接调用 " COUNT "次耗时(ms):" started.getTime());
}
}
复制代码
- 反射在Java的框架中非常常见,如果说自己想要封装一些通用框架,需要了解反射相关的知识。
- 本文主要是知识层面的内容,介绍了反射常用的API,泛型的一些API;
- 最后就可以验证下泛型是比直接调用性能差一些,主要原因是要校验权限,无法JIT优化,自动装箱拆箱等导致的。
- 泛型的优点是强大且灵活,缺点是难用并且慢...
希望能对大家有所帮助
原文链接:https://juejin.cn/post/7148813463308435463#comment