Java基础—深入理解反射机制的基本概念与使用

1.从Spring容器的核心谈起

使用过Spring框架进行web开发的应该都知道,Spring的两大核心技术是IOC和AOP。而其中IOC又是AOP的支撑。IOC要求由容器来帮我们自动创建Bean实例并完成依赖注入。IOC容器的代码在实现时肯定不知道要创建哪些Bean,并且这些Bean之间的依赖关系是怎样的(如果写死在里面,这框架还能用吗?)。所以其必须在运行期通过扫描配置文件或注解的方式来确定需要为哪些类创建实例。


通俗的说,必须在运行时为编译期还不能确定的类创建实例。再直白一点,必须提供一种new Object()之外的创建对象的方法。依赖注入存在类似的问题,容器必须能够在运行时发现所有标注有@Autowired或@Resource的字段或方法,并且能够在不知道对象的任何类型信息的情况下调用其setter方法完成依赖的注入(默认bean的字段都会实现setter方法)。总结一下IOC容器在实现时必须做到的三件看起来“不太可能的事”。


1)提供new之外的创建对象的方法,这个对象的类型在编译期不能确定。

2)能够在运行期知道类的结构信息,包括:方法,字段以及其上的注解信息等。

3)能够在运行期对编译期不能确定任何类型或接口信息的对象进行方法调用。

而这些,在java的反射技术下成为了可能。应该说反射技术并不仅仅在IOC容器中被使用,它是整个Spring框架的底层核心技术之一,是Spring实现通用型和扩展性的基石。


2. 反射技术初探

2.1 什么是反射技术

下面这段是官方的定义

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.

The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.


总结下官方的定义,我们可以知道反射技术的核心就两点:

1)反射使程序能够在运行时探知类的结构信息:构造器,方法,字段等。

2)并且依赖这些结构信息完成相应的操作,比如创建对象,方法调用,字段赋值等。


2.2 类结构信息和java对象的映射

回顾下类加载的过程:当JVM需要使用某个类,但内存中不存在时,会将该类的字节码文件加载进内存的方法区中,并在堆区创建一个Class对象。Class对象相当于存储于方法区的字节码信息的映射。我们可以通过该Class对象获得关于该类的所有描述信息:类名,访问权限,类注解,构造方法,字段,方法等等,尽管真实的类信息并不存在于该对象中。但通过它我们能获得想要的东西,并进行相关的操作,某种程度可以认为它们逻辑上等价。对类的结构进一步细分,类主要由构造方法,方法,字段构成。所以也必须存在和它们建立逻辑关系的映射。java在反射包下定义了Constructor类,Method类和Field类来建立和构造方法,方法,字段的映射。Constructor对象映射构造器方法,Method对象映射静态或实例方法,Field对象映射类的字段,而Class对象映射整个字节码文件(从字节码文件中抽取的方法区的运行时数据结构),通过Class对象又可以获得Method,Constructor,Field对象。它们之间的关系如下图所示。

image.png

通过这个图像我们对反射可以建立更加直观的认识。堆中的对象就像一面镜子,反射出类全部或某一部分的面貌。通过这些对象,我们可以在运行时获取类的全部信息;并且同样通过这些对象,可以完成创建类的实例,方法调用,字段赋值等操作。


3 .Class对象的获取及需要注意的地方

我们知道Class对象是进行反射操作的入口,所以首先必须获得Class对象。除了通过实例获取外,Class对象主要由以下几种方法获得:


1)通过类加载器加载class文件

Class<?> clazz = Thread.currentThread().getContextClassLoader().
        loadClass("com.takumiCX.reflect.ClassTest");

2)通过静态方法Class.forName()获取,需要传入类的全限定名字符串作参数

Class<?> clazz = Class.forName("com.takumiCX.reflect.ClassTest");

3)通过类.class获得类的Class对象

Class<ClassTest> clazz = ClassTest.class;

除了获得的Class对象的泛型类型信息不一样外,还有一个不同点值得注意。只有“2”在获得class对象的同时会引起类的初始化,而1和3都不会。还记得获得jdbc连接前注册驱动的操作吗?这就是完成驱动注册的代码

Class.forName("com.mysql.jdbc.Driver");

该方法引起了com.mysql.jdbc.Driver类被加载进内存,同时引起了类的初始化,而注册驱动的逻辑就是在Driver类中的静态代码块中完成的,

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

而通过类.class或classLoad.loadClass()虽然会引起类加载进内存,但不会引起类的初始化。通过下面的例子可以清楚的看到它们之间的区别:

/**
 * @author: takumiCX
 * @create: 2018-07-27
 **/
public class ClassInitializeTest {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<InitialTest> clazz = InitialTest.class;
        System.out.println("InitialTest.class:如果之前打印了初始化语句,说明该操作引起了类的初始化!");
        Thread.currentThread().getContextClassLoader().
                loadClass("com.takumiCX.reflect.InitialTest");
        System.out.println("classLoader.loadClass:如果之前打印了初始化语句,说明该操作引起了类的初始化!");
        Class.forName("com.takumiCX.reflect.InitialTest");
        System.out.println("Class.forName:如果之前打印了初始化语句,说明该操作引起了类的初始化!");
    }
}
class InitialTest{
    static {
        System.out.println("ClassTest 初始化!");
    }
}

测试结果如下

image.png


4. 运行时反射获取类的结构信息

Class类里的方法比较多,如要是围绕如何获得Method对象,Field对象,Constructor对象,Annotation对象的方法及其重载方法,当然也可以获得类的父类,类实现的接口等信息。

/**
 * @author: takumiCX
 * @create: 2018-07-27
 **/
public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException {
        Class<User> clazz = User.class;
        //根据构造参数类型获得指定的Constructor对象(包括非公有构造方法)
        Constructor<User> constructor = clazz.getDeclaredConstructor(String.class);
        System.out.println("获得带String参数的Constructor:"+constructor);
        //获得指定字段名的Field对象(包括非公有字段)
        Field name = clazz.getDeclaredField("name");
        System.out.println("获得字段名为name的Field:"+name);
        //根据方法名和方法参数类型获得指定的Method对象(包括非公有方法)
        Method method = clazz.getDeclaredMethod("setName", String.class);
        System.out.println("获得带String类型参数且方法名为setName的Method:"+method);
        //获得类上指定的注解
        MyAnnotation myAnnotation = clazz.getAnnotation(MyAnnotation.class);
        System.out.println("获得类上MyAnnotation类型的注解:"+myAnnotation);
        //获得类的所有实现接口
        Class<?>[] interfaces = clazz.getInterfaces();
        System.out.println("获得类实现的所有接口:"+interfaces);
        //获得包对象
        Package apackage = clazz.getPackage();
        System.out.println("获得类所在的包:"+apackage);
    }
    @MyAnnotation
    public static class User implements Iuser{
        private String name;
        public User() {
        }
        public User(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    static @interface MyAnnotation{
    }
    static interface Iuser {
    }
}


测试结果

image.png


5. 运行时反射获取泛型的真实类型

除了获得类的常规信息外,类的参数类型(泛型)信息也可以通过反射在运行时获得。泛型类不能用Class来表示,必须借助于反射包下关于类型概念的其他抽象结构。反射包下对类型这个复杂的概念进行了不同层次的抽象,我们有必要知道这种抽象的层次结构以及不同的抽象对应着什么样的类型信息。


1)反射包下对类型概念的抽象层次结构

反射包下对类型这个概念进行了不同层级的抽象,它们之间的关系可以用下面这张图表示

image.png


Class:可以表示类,枚举,注解,接口,数组等,但是其不能带泛型参数。

GenericArrayType:表示带泛型参数的数组类型,如T[ ].

ParameterizedType:表示带泛型参数的类型,如List< String > ,java.lang.Comparable<? super T>.

TypeVariable:表示类型变量,比如T,T entends Serializable

WildcardType:表示通配符类型表达式,比如?,? extends Number等

Type:关于类型概念的顶级抽象,可以用Type表示所有类型。


2)运行时获取带泛型的类,字段,方法参数,方法返回值的真实类型信息

/**
 * @author: takumiCX
 * @create: 2018-07-27
 **/
abstract class GenericType<T> {
}
public class TestGenericType extends GenericType<String> {
    private Map<String, Integer> map;
    public Map<String,Integer> getMap(){
        return map;
    }
    public void setMap(Map<String, Integer> map) {
        this.map = map;
    }
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
        //获取Class对象
        Class<TestGenericType> clazz = TestGenericType.class;
        System.out.println("获取类的参数化类型信息:");
        //1.获取类的参数化类型信息
        Type type = clazz.getGenericSuperclass();//获取带泛型的父类类型
        if (type instanceof ParameterizedType) { //判断是否参数化类型
            Type[] types = ((ParameterizedType) type).getActualTypeArguments(); //获得参数的实际类型
            for (Type type1 : types) {
                System.out.println(type1);
            }
        }
        System.out.println("--------------------------");
        System.out.println("获取字段上的参数化类型信息:");
        //获取字段上的参数化类型信息
        Field field = clazz.getDeclaredField("map");
        Type type1 = field.getGenericType();
        Type[] types = ((ParameterizedType) type1).getActualTypeArguments();
        for(Type type2:types){
            System.out.println(type2);
        }
        System.out.println("--------------------------");
        System.out.println("获取方法参数的参数化类型信息:");
        //获取方法参数的参数化类型信息
        Method method = clazz.getDeclaredMethod("setMap",Map.class);
        Type[] types1 = method.getGenericParameterTypes();
        for(Type type2:types1){
            if(type2 instanceof ParameterizedType){
                Type[] typeArguments = ((ParameterizedType) type2).getActualTypeArguments();
                for(Type type3:typeArguments){
                    System.out.println(type3);
                }
            }
        }
        System.out.println("--------------------------");
        System.out.println("获取方法返回值的参数化类型信息:");
        //获取方法返回值得参数化类型信息
        Method method1 = clazz.getDeclaredMethod("getMap");
        Type returnType = method1.getGenericReturnType();
        if(returnType instanceof ParameterizedType){
            Type[] arguments = ((ParameterizedType) returnType).getActualTypeArguments();
            for(Type type2:arguments){
                System.out.println(type2);
            }
        }
    }
}


测试结果

image.png


3)运行时泛型父类获取子类的真实类型信息

/**
 * @author: takumiCX
 * @create: 2018-07-27
 **/
abstract class GenericType2<T> {
    protected Class<T> tClass;
    public GenericType2() {
        Class<? extends GenericType2> aClass = this.getClass();
        Type superclass = aClass.getGenericSuperclass();
        if(superclass instanceof ParameterizedType){
            Type[] typeArguments = ((ParameterizedType) superclass).getActualTypeArguments();
            tClass=(Class<T>) typeArguments[0];
        }
    }
}
public class TestGenericType2 extends GenericType2<String>{
    public static void main(String[] args) {
        TestGenericType2 type2 = new TestGenericType2();
        System.out.println(type2.tClass);
    }
}


测试结果

image.png


4)泛型的类型信息不是编译期间就擦除了吗

java里的的泛型只在源码阶段存在,编译的时候就会被擦除,声明中的泛型类型信息会变成Object或泛型上界的类型,而使用时都用Object替换,如果要返回泛型类型,则通过强转的方式完成。我们可以写一个泛型类,将其编译成字节码文件后再反编译看看发生了什么。


源码如下:

/**
 * @author: takumiCX
 * @create: 2018-07-27
 **/
abstract class GenericType<T> {
}
public class TestGenericType extends GenericType<String> {
    private Map<String, Integer> map;
    public Map<String,Integer> getMap(){
        return map;
    }
    public void setMap(Map<String, Integer> map) {
        this.map = map;
    }
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
    }
}

反编译后的源码

package com.takumiCX.reflect;
import java.util.ArrayList;
import java.util.Map;
// Referenced classes of package com.takumiCX.reflect:
//            GenericType
public class TestGenericType extends GenericType
{
    public TestGenericType()
    {
    }
    public Map getMap()
    {
        return map;
    }
    public void setMap(Map map)
    {
        this.map = map;
    }
    public static void main(String args[])
    {
        ArrayList list = new ArrayList();
    }
    private Map map;
}

反编译后的源码泛型信息全部消失了。说明编译器在编译源代码的时候已经把泛型的类型信息擦除。理论上来说,源码中指定的具体的泛型类型,在运行时是无法知道的。但是“2”和“3”的例子里我们确实通过反射在运行时得到了类,字段,方法参数以及方法返回值的泛型类型信息。那么问题出在哪里?关于这个问题我也是纳闷了好久,在网上找了很多资料才得出比较靠谱的答案。泛型如果被用来进行声明,比如说类上,字段上,方法参数和方法返回值上,这些属于类的结构信息其实是会被编译进Class文件中的;而泛型如果被用来使用,常见的方法体中带泛型的局部变量,其类型信息不会被编译进Class文件中。前者因为存在于Class文件中,所以运行时通过反射还是能够获得其类型信息的;而后者因为在Class文件中根本不存在,反射也就无能为力了。


6. 反射创建实例,方法调用,修改字段

/**
 * @author: takumiCX
 * @create: 2018-07-27
 **/
public class ReflectOpration {
    private String name;
    public ReflectOpration(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    @Override
    public String toString() {
        return "ReflectOpration{" +
                "name='" + name + '\'' +
                '}';
    }
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Class<ReflectOpration> clazz = ReflectOpration.class;
        //获取带参构造器
        Constructor<ReflectOpration> constructor = clazz.getConstructor(String.class);
        //反射创建实例,传入构造器参数takumiCX
        ReflectOpration instance = constructor.newInstance("takumiCX");
        System.out.println(instance);
        //根据方法名获取指定方法
        Method getName = clazz.getMethod("getName");
        //通过反射进行方法调用,传入进行调用的对象作参数,后面可跟上方法参数
        String res = (String) getName.invoke(instance);
        System.out.println(res);
        //获取Field对象
        Field field = clazz.getDeclaredField("name");
        //修改访问权限
        field.setAccessible(true);
        //反射修改字段,将名字改为全大写
        field.set(instance,"TAKUMICX");
        System.out.println(instance);
    }
}


运行结果

image.png


7. 反射的缺点

反射功能强大,使用它我们几乎可以做到java语言层面支持的任何事情。但要注意到这种强大是有代价的。过多的使用反射可能会带来严重的性能问题。曾今作支付平台的系统改造时就碰到过前人滥用反射留下的坑,因为模型对象在web,业务和持久层是不同,但其属性基本一样,所以原来的开发人员为了偷懒大量使用反射来进行这种对象属性的拷贝操作。开发时间是节省了,但给系统性能带来严重的负担,支付接口的调用时间太长,甚至会超时。后来我们将其改回了手动调用setter赋值的方式,虽然工作量不少,但是最后上线的系统性能有了很大的提高,接口调用的响应时间比原来少了近30%。这个例子说明了对反射合理使用的重要性:框架中大量使用反射是因为要提供一套通用的处理流程来减少开发者的工作量,且大部分都在准备或者说容器启动阶段,反射的使用虽然增加了容器启动时间,但因为提高了开发效率,所以是可以接受的;而在对性能有要求的业务代码层面,使用反射会降低业务处理的速度,拖慢接口的响应时间,很多时候是不可接受的。反射一定要在权衡了开发效率和执行性能后,视场景和性能要求谨慎使用。

关注下方微信公众号“Java精选”(w_z90110),回复关键字领取资料:如HadoopDubboCAS源码等等,免费领取资料视频和项目。 

涵盖:程序人生、搞笑视频、算法与数据结构、黑客技术与网络安全、前端开发、Java、Python、Redis缓存、Spring源码、各大主流框架、Web开发、大数据技术、Storm、Hadoop、MapReduce、Spark、elasticsearch、单点登录统一认证、分布式框架、集群、安卓开发、iOS开发、C/C++、.NET、Linux、Mysql、Oracle、NoSQL非关系型数据库、运维等。

评论

  1. #1

    采果子 (2018/09/02 10:54:05)回复
    最近也在学习java,太难了

分享:

支付宝

微信