用Java反射(Reflection)解释Spring Boot 中依赖注入的原理

首先我们在学习原理前,需要先了解下反射

Reflection(反射)

在有些场景中,需要在运行时动态的操作类的成员,比如在运行时根据数据库中提供具体的类名或方法名,或者根据字符串来动态实例化对象或者调用方法时,就不能通过new()对象,的方式 创建类或者 是使用类中的方法了

关键思路在于创建 class对象

我们写的每一个类如下被虚拟机解析

JVM是如何创建 Class 对象的

首先通过.java 编译成 .class 文件,然后通过 Class Loader 加载到内存, 随后JVM根据内存中的信息创建响应的Class 对象。

如何获取某个类的Class 对象
  • 直接用类名.class, 就是某个类的class对象

    java 复制代码
    Class<User> userClass = User.class;
  • 通过对象的.getClass() 方法,我们在编译阶段无法准确地判断Class对象的确切类型

    java 复制代码
    Class<?> clazz = user.getClass(); //使用通配符
  • 通过Class.forName() 方法获取 类型

    java 复制代码
    Class<?> class = Class.forName("org.blcu.taco.User")
    • 参数为完全类的限定名
    • 通过这种方法获取类的对象时,类的静态代码块会立即执行。
某个类的Class对象的常用操作

假设我们有一个类为:

java 复制代码
public class Son {
    private final String idNum;
    public String school;
    public String name;
    private static final String staticField = "Son static field"
    public Son (String idNum,String name) {
        this.idNum = idNum;
        this.name = name;
    }
    static {
        System.out.println("Son static block");
    }

    public static void sayHello() {
        System.out.println("Son say hello");
    }

    public void sayHi(String name) {
        System.out.println("Son say hi to " + name);
    }

    private static void sayBye() {
        System.out.println("Son sayBye");
    }

    private static void sayBye(String what) {
        System.out.println("Son".concat(what));
    }

    private void sayBye1() {
        System.out.println(this.name+"Son sayBye1");
    }

}

那当我们获取了Son 的 Class 对象后,我们能做什么操作呢?

  • 获取所有字段名

    java 复制代码
    // 获取Son类的Class对象
    Class<?> aClass = Class.forName("studydemo.reflection.Son");
    // 通过这种方法获取类的对象时,类的静态代码块会立即执行, 此时我们的静态代码块就会执行,输出:son static block
    
    
    // 获取反射类中所有的公有字段
    Field[] fields = aClass.getFields();
    for (Field field : fields) {
        System.out.println(field.getName());
    }
    
    // 获取所有的字段名,包括共有和私有
    Field[] declaredFields1 = aClass.getDeclaredFields();
    for (Field field : declaredFields1) {
        System.out.println(field.getName());
    }
  • 获取本类中所有的方法名

    java 复制代码
    // 获取Son类的Class对象
    Class<?> aClass = Class.forName("studydemo.reflection.Son");
    // 获取所有的方法名
    Method[] declaredMethods = aClass.getDeclaredMethods();
    for (Method declaredMethod : declaredMethods) {
        System.out.println(declaredMethod.getName());
    }
  • 通过名称访问静态变量和静态方法(静态变量、静态方法、代码块在类加载时就已经被初始化了,所以才可以访问)

    • 访问静态变量

      java 复制代码
      // 按照名称获取变量或方法
      Field declaredField = aClass.getDeclaredField("staticField");
      // 因为字段是私有的所以要给它访问的权限
      declaredField.setAccessible(true);
      // 获取的是静态变量并要取值
      Object o = declaredField.get(null);
      System.out.println(o);
      // 输出:Son static field
    • 访问静态方法

      java 复制代码
      // invoke函数是执行通过一个类的class对象获取的方法的
      Method sayBye = aClass.getDeclaredMethod("sayBye");
      // 因为方法时私有的,所以要给它访问的权限
      sayBye.setAccessible(true);
      sayBye.invoke(null);
      // 输出:"Son sayBye"
      
      // 访问带参的静态方法
      Method sayBye1 = aClass.getDeclaredMethod("sayBye", String.class);
      sayBye1.setAccessible(true);
      sayBye1.invoke(null,"88");
      // 输出 say88
  • 使用反射类创建类的实例

    主要就是Class对象有两个函数:getConstructor() 和 newInstance(), 第一个用来获取构造函数,第二个用来使用该构造器创建类的实例

    • 使用反射来创建类的实例(无参构造方法)

      java 复制代码
      Constructor<?> constructor = aClass.getConstructor();
      Object o1 = constructor.newInstance();
    • 使用反射来创建类的实例(有参构造方法)

      java 复制代码
      Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class, String.class);
      Object o2 = declaredConstructor.newInstance("01", "syl");
  • 创建实例之后执行类的私有方法(不是静态私有方法,这里要和静态私有方法做区分,静态私有方法是不用先实例化对象的)

    java 复制代码
    Method sayBye11 = aClass.getDeclaredMethod("sayBye1");
    Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class, String.class);
    Object o2 = declaredConstructor.newInstance("01", "syl");
    // 私有方法,设置访问劝降为 true
    sayBye11.setAccessible(true);
    // 传入的对象就为实例化的对象
    sayBye11.invoke(o2);
    // 输出:sylSon sayBye1
  • 创建实例之后访问类的final型私有变量

    java 复制代码
    Field declaredField1 = aClass.getDeclaredField("idNum");
    declaredField1.setAccessible(true);
    declaredField1.set(o2,"11111");
    System.out.println(declaredField1.get(o2));

    我们可以发现,在使用反射的情况下,final型变量也可以被修改,我们在平常使用中,要避免出现这种情况。

  • 如何在运行时进行类型转化,在反射过程中如何保证类型安全

    • 如何在运行时进行类型转换

      java 复制代码
      Method sayBye11 = aClass.getDeclaredMethod("sayBye1");
      Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class, String.class);
      Object o2 = declaredConstructor.newInstance("01", "syl");
      // 进行类型转换
      Son son = (Son) o2;
    • 转化时如何保证类型安全

      java 复制代码
      if(o2 instanceof Son) {
          Son son = (Son) o2;
      }

通过上述如此多的例子,我们对反射的基本使用有了一些了解,那么我们用一个案例来模拟spring boot 自动注入的流程

  • 创建一个Config类 (对标Spring Boot 的@Configure + @Bean), Config中的类,都会被装配到我们的Container类中(对标SpringBoot的容器)

    java 复制代码
    package studydemo.reflection.autowired;
    
    import org.springframework.context.annotation.Bean;
    
    public class Config {
    
        @Bean
        public Customer customer() {
            return new Customer("syl","2819719869@qq.com");
        }
    
        @Bean
        public Address address() {
            return new Address("345","467100");
        }
    
        public Message message() {
            return  new Message("hi there!");
        }
    }
  • 创建一个Container类(对标Spring Boot的容器),其中的 init()方法,相当于Spring Boot 的run 方法, 这里面的 init()、 getServiceInstanceByClass()和createInstance() 时浓缩的精华,我们慢慢来看

    java 复制代码
    package studydemo.reflection.autowired;
    
    import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
    import lombok.AllArgsConstructor;
    import lombok.NoArgsConstructor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map;
    
    
    // 相当于Spring Boot 容器
    @NoArgsConstructor
    public class Container {
    
        // Method 存储的方法本身,而不是方法执行后返回的实例,具体的实例将在使用时执行这些方法来获取,这种方法可以节约资源并提高性能
        private Map<Class<?>, Method> methods;
        private Object config;
    
        private Map<Class<?>,Object> services;
    
    
        // 相当于Spring Boot 的 run 方法
        public void init() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            this.methods = new HashMap<>();
            this.services = new HashMap<>();
            Class<?> clazz = Class.forName("studydemo.reflection.autowired.Config");
            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
                if(ObjectUtils.isNotNull(declaredMethod.getAnnotation(Bean.class) != null)) {
                    this.methods.put(declaredMethod.getReturnType(),declaredMethod);
                }
                System.out.println(declaredMethod.getName());
            }
            this.config = clazz.getConstructor().newInstance();
        }
    
        // 通过类的Class 对象,获取相应的的服务实例
        // 执行该方法后,我们确实获取了该方法的对象,我们每次调用这个方法时,都会创造一个实例,这在性能上可能会有点问题,因为许多服务都是全局的,这里在一个容器中应该用单例模式
        public Object getServiceInstanceByClass(Class<?> clazz) throws InvocationTargetException, IllegalAccessException {
            if(this.services.containsKey(clazz)) {
                return this.services.get(clazz);
            }
            if(this.methods.containsKey(clazz)) {
                Method method = this.methods.get(clazz);
                Object obj = method.invoke(this.config);
                this.services.put(clazz,obj);
                return obj;
            }
            return null;
        }
    
        // 用于通过Class对象创建普通实例,并且实现将服务自动注入到对象里面
        public Object createInstance(Class<?> clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
            Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
            for (Constructor<?> declaredConstructor : declaredConstructors) {
                if(ObjectUtils.isNotNull(declaredConstructor.getAnnotation(Autowired.class))) {
                    // 首先获取构造器所有的参数类型
                    Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
                    Object[]  arguments = new Object[parameterTypes.length];
                    for (int i = 0; i < parameterTypes.length; i++) {
                        arguments[i] = getServiceInstanceByClass(parameterTypes[i]);
                    }
                    return declaredConstructor.newInstance(arguments);
                }
            }
            return clazz.getDeclaredConstructor().newInstance();
        }
    }
  • 变量的解释

    java 复制代码
    // 存储Config 类中的方法
    private Map<Class<?>, Method> methods;
    // Config类的实例化对象
    private Object config;
    // 
    private Map<Class<?>,Object> services;

    Method 存储的方法本身,而不是方法执行后返回的实例,具体的实例将在使用时执行这些方法来获取,这种方法可以节约资源并提高性能

  • init() 函数,相当于Spring Boot 的run方法

    java 复制代码
    // 相当于Spring Boot 的 run 方法
        public void init() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            // 初始化变量
            this.methods = new HashMap<>();
            this.services = new HashMap<>();
            // 利用反射获取Config类 的 Class对象
            Class<?> clazz = Class.forName("studydemo.reflection.autowired.Config");
            // 获取Config类中所有的方法,大家可以回去看一下Config类,Config的每个方法都是用来创建不同类的实例的
            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
                // 加了Bean注解的方法才会被放到methods(Spring Boot 中为什么加了 @Bean注解的方法会被放到容器中呢,本质也是这个原理)
                if(ObjectUtils.isNotNull(declaredMethod.getAnnotation(Bean.class) != null)) {
                    this.methods.put(declaredMethod.getReturnType(),declaredMethod);
                }
                System.out.println(declaredMethod.getName());
            }
            // 实例化Config类对象,并赋值给 成员变量 config
            this.config = clazz.getConstructor().newInstance();
        }
  • getServiceInstanceByClass()

    java 复制代码
    // 通过类的Class 对象,获取相应的的服务实例
        // 执行该方法后,我们确实获取了该方法的对象,我们每次调用这个方法时,都会创造一个实例,这在性能上可能会有点问题,因为许多服务都是全局的,这里在一个容器中应该用单例模式
        public Object getServiceInstanceByClass(Class<?> clazz) throws InvocationTargetException, IllegalAccessException {
            // this.services 存储的是创建过的实例,如果已经创建过了,那就返回原来创建过的,否则再新创建
            if(this.services.containsKey(clazz)) {
                return this.services.get(clazz);
            }
            // this.methods 是一个Map , key为Class<?>
            if(this.methods.containsKey(clazz)) {
                // this.methods 中存储的都是 Config类的方法,执行的时候必须要传入一个实例化后的 Config对象,this.config 正好是 Config 的实例化对象
                Method method = this.methods.get(clazz);
                // 指向该方法,通过 类的 Class 对象,就能获取到这个类的对象
                Object obj = method.invoke(this.config);
                this.services.put(clazz,obj);
                return obj;
            }
            return null;
        }
  • createInstance() --- 用于通过Class 对象创建普通实例,并且自动实现将所需的服务自动注入到对象里面

    java 复制代码
    // 用于通过Class对象创建普通实例,并且实现将服务自动注入到对象里面
        public Object createInstance(Class<?> clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
            // 获取到Class对象所指代的类所有的构造方法
            Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
            // 遍历构造方法
            for (Constructor<?> declaredConstructor : declaredConstructors) {
                // 找到加了 @Autowried 注解的构造器
                if(ObjectUtils.isNotNull(declaredConstructor.getAnnotation(Autowired.class))) {
                    // 首先获取构造器所有的参数类型,也就是需要注入什么(特别重要)
                    Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
                    Object[]  arguments = new Object[parameterTypes.length];
                    for (int i = 0; i < parameterTypes.length; i++) {
                        // arguments[i] 就是实例化的类
                        arguments[i] = getServiceInstanceByClass(parameterTypes[i]);
                    }
                    // 返回注入了所需服务的实例化对象
                    return declaredConstructor.newInstance(arguments);
                }
            }
            // 默认返回一个无参构造器所创建的对象
            return clazz.getDeclaredConstructor().newInstance();
        }

至此,我们调用 Container 的 createIntance() 函数,就能够按照我们的规则创建 已经注入了所需服务的对象。

那么你们能通过代码来看出要按照什么规则吗?

  1. 对象要有一个带有 @Autowired 注解的有参构造方法(参数就是这个类依赖的服务类),如果没有依赖其他服务,可以不写
  2. 需要注册服务的类,要在 Config类中加 @Bean 注解,这样才能被注册成为一个服务。
相关推荐
Cachel wood3 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
Code哈哈笑6 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
gb42152878 分钟前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶9 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_4336184411 分钟前
shell 编程(二)
开发语言·bash·shell
zfoo-framework17 分钟前
【jenkins插件】
java
风_流沙22 分钟前
java 对ElasticSearch数据库操作封装工具类(对你是否适用嘞)
java·数据库·elasticsearch
charlie11451419126 分钟前
C++ STL CookBook
开发语言·c++·stl·c++20
袁袁袁袁满26 分钟前
100天精通Python(爬虫篇)——第113天:‌爬虫基础模块之urllib详细教程大全
开发语言·爬虫·python·网络爬虫·爬虫实战·urllib·urllib模块教程