学完Spring框架回头再来看反射你会发现真的不一样

Source

前言

你还记得你的Spring入门案例吗?
在这里插入图片描述
字符串经过解析得到了对象并放入到了Bean中,然后,从IOC里按照id拿对象(getBean("…"))就变成了一件耦合度极低的事情。为什么耦合度降低了?因为"new"的地方不一样了,它是如何实现的呢?
反射,功不可没
在以前,调用类成员的方式基本上是通过"new()."的方式,这样做修改了源码,不符合OCP原则,反射的引入可以通过外部配置文件,在不修改源码的基础上控制程序,符合OCP原则。它是Java中很多高级特性的基础,比如注解、动态代理。各类的ORM框架、RPC框架、Spring的IOC、AOP都是以反射作为技术基础的。

一.什么是反射?

简单来讲,反射机制指的是程序在运行时能够获取自身的信息。对于任何一个类,反射可以帮助我们在运行期获得其所有的方法和变量
毫不夸张地说,只要给定类的名字(路径),就可以通过反射机制来获得类的所有属性和方法,就像这样:

   String classPath="xxx.xxx.xxx.类名";
   //(1) 加载类, 返回Class类型的对象cls
   Class cls = Class.forName(classfullpath);
   //(2) 通过 cls 得到你加载的类 xxx.xxx.xxx.类名 的对象实例
   Object o = cls.newInstance();
   //(3) 通过 cls 得到你加载的类 xxx.xxx.xxx.类名 的 methodName"hi"  的方法对象
   //    即:在反射中,可以把方法视为对象(万物皆对象)
   Method method1 = cls.getMethod(methodName);
   //(4) 通过method1 调用方法: 即通过方法对象来实现调用方法
   method1.invoke(o); //传统方法 对象.方法() , 反射机制 方法.invoke(对象)

传统的方法都是通过对象.方法()的方式来调用,而反射机制则是通过方法.invoke(对象)来调用。
在这里插入图片描述
我们都知道Java程序在计算机中有三个阶段:编译阶段、加载阶段、运行阶段
在这里插入图片描述
加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。
这个Class类对象就像一面镜子,透过这个镜子看到类的结构,所以形象的称之为:反射

二.如何实现反射?

2.1java.lang.Class

java.lang.Class类是Java反射机制的基础,必须通过Class类才能获得在运行期一个类的相关信息,怎样获取一个Java类的Class对象呢?
1.调用对象的getClass()方法获取Class对象:

TestObject obj=new TestObject();
Class cls=obj.getClass();

2.根据类名.class获取Class对象:

Class cls=TestObject.class

3.根据Class中的静态方法Class.forName()获取Class对象:

Class cls=Class.forName("xxx.TestObject");

很多人喜欢把Class类和class关键字混淆,千万不要搞错!
JVM会为每个类都创建一个Class对象,在程序运行后JVM首先检查要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将类的Class对象载入。

2.2通过反射创建对象

在以前,通常使用new的方式来创建对象,反射也可以创建对象,并且有两种方式。
方式一:
Class类的newInstance方法,这个newInstance方法是通过调用类中定义的无参构造函数创建对象

Class cls=TestObject.class;
TestObject obj=cls.newInstance();

方式二:
和Class类的newInstance类似,第二种方式则是利用java.lang.reflect.Constructor类的newInstance方法,通过构造器的方式就是方式一的细化

Constructor<TestObject> constructor=TestObject.class.getConstructor();
TestObject obj=constructor.newInstance();

2.3通过反射获取类成员

在Class类中,用Field表示类中的属性,用Method表示类中的方法,用Annotation表示类中的注解,用Constructor表示类的构造函数,可以得到下列方法:

Field[] getFields()
Method[] getMethod()
Annotation[] getAnnotations()
Constructor[] getConstructor()

上面的一组方法其实就是一面镜子,由Class类提供的镜子,当其他类来“照镜子”,就会得到该类在运行时期获得对应的一系列类成员的方法,就会看到镜子里的自己
为了获取私有的类成员,需要使用getDeclared系列的方法

Field[] getDeclaredFields()
Method[] getDeclaredMethod()
Annotation[] getDeclaredAnnotations()
Constructor[] getDeclaredConstructor()

当类“照镜子”过后,想要在运行时期获取指定的方法,属性,注解只需要在上述系列的方法形参列表里传入类成员对应的参数即可,并且返回类型不再是数组。
可以看到,我从外部很轻松地就访问到了类的私有属性,这其实是破坏封装性的一种表现

三.反射的性能

其实,使用反射获取类的成员性能是十分低的,他比通过new的方式耗时多得多,可以写两个方法来分别测试其执行用时:

 public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
    
      

        testTime01();
        testTime02();
    }
    //传统方式
    public static void testTime01(){
    
      
        Test test = new Test();
        long l1 = System.currentTimeMillis();
        for (int i = 0; i < 99999999; i++) {
    
      
            test.play();
        }
        long l2 = System.currentTimeMillis();
        System.out.println("传统方式执行用时:"+(l2-l1)+"ms");
    }
    //反射方式
    public static void testTime02() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    
      
        Class<?> aClass = Class.forName("javaReflection.Test");
        Object o = aClass.newInstance();
        Method play = aClass.getMethod("play");
        long l1 = System.currentTimeMillis();
        for (int i = 0; i < 99999999; i++) {
    
      
            play.invoke(o);
        }
        long l2 = System.currentTimeMillis();
        System.out.println("反射方式执行用时:"+(l2-l1)+"ms");
    }

各自执行耗时:
在这里插入图片描述
如果想要提升反射性能可以设置反射属性,关闭访问检查:xxx.setAccessible(true);
虽然他性能低,破坏封装性,但是反射的有点也比较明显,我们可以在运行时期获得类的信息并操作类的成员,提高了程序的扩展性和灵活性。

四.反射是如何破坏单例模式的?

我们都知道,单例模式保证一个类仅有一个实例,并提供一个访问他的全局访问点。
为了确保单例与线程安全,我们一般这样写:

public class Singletons {
    
      
    private static volatile Singletons singleton;
    private Singletons(){
    
      }

    public static Singletons getSingleton() {
    
      
        if (singleton==null){
    
      
            synchronized (Singletons.class){
    
      
                if (singleton==null){
    
      
                    singleton = new Singletons();
                }
            }
        }
        return singleton;
    }
}

在这里插入图片描述
通过写一个私有的构造器,在类的内部创建出一个单例对象,并加锁保证线程安全。
看完上面的反射再来看这个单例,他真的能做到实例的唯一性吗?很显然是不能的!
但是,反射可以在类的运行期获取并调用类的成员,当然也包括上述的私有构造器,拿到了构造器我就可以为所欲为。
所以,使用反射可以破坏单例模式,不信你看:

public class Break {
    
      
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    
      
        //得到单例
        Singletons singleton1 = Singletons.getSingleton();
        //通过反射获取构造器
        Constructor<Singletons> cons = Singletons.class.getDeclaredConstructor();
        //反射爆破
        cons.setAccessible(true);
        //通过构造器的newInstance 创建对象
        Singletons singleton2 = cons.newInstance();
        System.out.println("是同一个单例吗:"+(singleton1 == singleton2));
    }
}

从运行结果来看,单例模式已经被反射爆破所破坏,单例模式已经不再单例~
在这里插入图片描述

五.如何避免单例模式被反射破坏?

其实很简单,单例模式之所以遭到破坏是因为在类的运行期间我拿到的私有的构造器,并通过他创建了对象的实例,我可以在构造器里做点手脚就可以完美解决这个问题!
就像这样:

    private Singletons(){
    
      
        if (singleton!=null){
    
      
            throw new RuntimeException("单例模式中对象只能被创建一次哟~");
        }
    }