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

简介: 平时写代码的过程中,我们使用不同的工具框架来提升开发效率,除了基础框架之外,我们自己也想造轮子,封装各种业务平台功能;一旦需造轮子的时候,那么就需要使用Java造轮子利器:反射;一些项目中常见的反射应用场景:• 泛化调用: 提前不知道目标RPC的接口和方法,而是开发在后台输入值,根据输入的配置动态请求。 这也是提升效率的一部分,因为不可能所以得RPC接口都要亲自对接的,总要有一部分可以灵活的调用不同接口。

image.png

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

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

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

  • 泛化调用: 提前不知道目标RPC的接口和方法,而是开发在后台输入值,根据输入的配置动态请求。 这也是提升效率的一部分,因为不可能所以得RPC接口都要亲自对接的,总要有一部分可以灵活的调用不同接口。
  • 自测入口: 我们的逻辑代码一般会散落在应用的不同位置,如果想要debug,一般会有一个核心入口,但是如果核心入口太长,想要进入我们的逻辑分支很难呢? 我们可以统一收口下测试入口,这个测试入口在开发环境下就是可以访问到所有的对象,服务,组件等功能,直接对目标逻辑进行调试即可, 这个也是需要反射.

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

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。Oracle文档: 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。

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

image.png

反射使用

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包中,也已经封装了对应的反射工具,如果已经引入了这个包,直接使用即可:


image.png


泛型的性能为什么比直接调用差?

可以看到泛型性能比直接调用差很多,为什么泛型的性能会比正常调用差?

  • 泛型在执行的时候会校验方法、字段名称,校验是否有对应的权限,会比直接调用多一部分逻辑。
  • 无法被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());
    }
}
复制代码


image.png


总结

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

希望能对大家有所帮助

相关文章
|
4月前
|
设计模式
学会了这个设计模式,再也不是只会写if/else了
本文详细介绍了责任链设计模式(Chain of Responsibility Pattern),这是一种行为型设计模式,用于创建一个接收者对象的链,通过解耦请求的发送者和接收者,允许沿着链传递请求,直到某个接收者能够处理它。
学会了这个设计模式,再也不是只会写if/else了
|
4月前
|
Java
Java 新手入门:重载和重写傻傻分不清?一篇文章带你清晰理解!
Java 新手入门:重载和重写傻傻分不清?一篇文章带你清晰理解!
41 0
Java 新手入门:重载和重写傻傻分不清?一篇文章带你清晰理解!
|
5月前
|
安全 Java API
JAVA反射:不只是“看”,还能“动手”改变!
【7月更文挑战第1天】Java反射提供运行时类信息查看与动态操作:获取类名、创建对象、调用未知方法、修改字段。虽强大但有性能和安全风险,需谨慎使用。
27 0
|
7月前
|
Java
Java反射的详细解析之三
面试题: 你觉得反射好不好?好,有两个方向 第一个方向:无视修饰符访问类中的内容。但是这种操作在开发中一般不用,都是框架底层来用的。 第二个方向:反射可以跟配置文件结合起来使用,动态的创建对象,动态的调用方法。
45 0
|
前端开发 Java 编译器
Java的第十六篇文章——枚举、反射和注解(后期再学一遍)
Java的第十六篇文章——枚举、反射和注解(后期再学一遍)
|
Java Spring 容器
SpringIOC注入三种方式灵活运用(第十四课)
SpringIOC注入三种方式灵活运用(第十四课)
107 0
|
Java Spring
再也不用重复造轮子了 一个Spring注解轻松解决
再也不用重复造轮子了 一个Spring注解轻松解决
73 0
|
XML 缓存 Java
Java注解怎么用
Java注解怎么用
231 0
|
Java
Java面向对象进阶4——多态的弊端及解决方法
多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。
133 0
Java面向对象进阶4——多态的弊端及解决方法