likes
comments
collection
share

想要造轮子, 你知道反射机制吗?

作者站长头像
站长
· 阅读数 25

平时写代码的过程中,我们使用不同的工具框架来提升开发效率,除了基础框架之外,我们自己也想造轮子,封装各种业务平台功能;

一旦需造轮子的时候,那么就需要使用Java造轮子利器:反射;

一些项目中常见的反射应用场景:

  • 泛化调用: 提前不知道目标RPC的接口和方法,而是开发在后台输入值,根据输入的配置动态请求。 这也是提升效率的一部分,因为不可能所以得RPC接口都要亲自对接的,总要有一部分可以灵活的调用不同接口。

  • 自测入口: 我们的逻辑代码一般会散落在应用的不同位置,如果想要debug,一般会有一个核心入口,但是如果核心入口太长,想要进入我们的逻辑分支很难呢? 我们可以统一收口下测试入口,这个测试入口在开发环境下就是可以访问到所有的对象,服务,组件等功能,直接对目标逻辑进行调试即可, 这个也是需要反射.

其实上面两个场景也说明了反射应用场景的一个特点: 我不知道现在要调用哪个类和哪个方法,等到运行时才知道要调用的类和方法。

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

Oracle文档: 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。

反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。

测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。

概念上的东西就说这么多,回到实际的coding部分:

想要造轮子, 你知道反射机制吗?

反射使用

1. 准备测试对象


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() 返回属性的泛型类型

获取泛型具体的Class

@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());
    }
}

想要造轮子, 你知道反射机制吗?

总结

  1. 反射在Java的框架中非常常见,如果说自己想要封装一些通用框架,需要了解反射相关的知识。
  2. 本文主要是知识层面的内容,介绍了反射常用的API,泛型的一些API;
  3. 最后就可以验证下泛型是比直接调用性能差一些,主要原因是要校验权限,无法JIT优化,自动装箱拆箱等导致的。
  4. 泛型的优点是强大且灵活,缺点是难用并且慢...

希望能对大家有所帮助;

参考:

转载自:https://juejin.cn/post/7148813463308435463
评论
请登录