当前位置: 首页 > article >正文

Spring 6 第4章——原理:手写IoC

Spring框架的IoC是根据Java的反射机制实现的 

一、回顾Java反射

  1. Java反射机制是在运行状态中,对于任何一个类,都能够知道这个类的属性和方法;对于任何一个对象,都能够调用它的任意方法和属性
  2. 这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制
  3. 简单来说,反射机制就是程序在运行时,能够获取自身信息
  4. 想要解剖一个类,必须先要获取到该类的Class对象(即字节码文件)。Class对象是反射的根源
  5. 剖析一个类或者使用反射解决具体的问题就是使用相关的API(1)java.lang.Class(2)java.lang.reflect
  6. 自定义一个类(该类中有三个私有属性,分别为它们提供相应的getter和setter方法,为这个类提供有参构造器、无参构造器,还有一个私有方法run):
    package com.atguigu.reflect;
    
    public class Car {
        private String name;
        private int age;
        private String color;
    
        public Car() {
        }
    
        public Car(String name, int age, String color) {
            this.name = name;
            this.age = age;
            this.color = color;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getColor() {
            return color;
        }
    
        public void setColor(String color) {
            this.color = color;
        }
    
        private void run(){
            System.out.println("私有方法-run...");
        }
    }
  7. 获取字节码文件的三种方法(1)类名.class(2)对象.getClass()(3)Class.forName("全路径")(全路径是指包名+类名)
  8. 通过字节码进行对象实例化的方法:
  9. 如果通过getConstructors方法得到的构造器,只能是public修饰的构造器。而getDeclaredConstructors方法得到的构造器,没有这种限制。此时,我们把Car类中的有参构造器改成私有的,然后进行测试,测试方法如下:
    @Test
        public void test02() throws Exception{
            Class clazz1 = Car.class;
            //获取所有构造,该方法返回的是一个Constructor类型的数组
            Constructor[] constructors = clazz1.getConstructors();
            for (Constructor constructor : constructors) {
                System.out.println("方法名称:"+constructor.getName()+" 参数个数:"+constructor.getParameterCount());
            }
            System.out.println("-------------------------------------------------------------");
            Constructor[] declaredConstructors = clazz1.getDeclaredConstructors();
            for (Constructor declaredConstructor : declaredConstructors) {
                System.out.println("方法名称:"+declaredConstructor.getName()+" 参数个数:"+declaredConstructor.getParameterCount());
            }
        }

  10. getConstructors方法和getDeclaredConstructors方法返回的是多个构造器,即构造器数组。而getConstructor方法和getDeclaredConstructor方法返回的是一个构造器。如果没有Declared,就只能找到公有的构造器,如果有Declared,就没有这一限制,可以找到所有构造器。在获取一个构造器时,如果getConstructor方法和getDeclaredConstructor方法中没有参数,那就代表获取的是无参构造器,如果想获取有参构造器,方法如下(此时,Car类中的有参构造器还是public的):
  11. 如果我们现在把Car类中的有参构造器改成private的,那么getConstructors(String.class,int.class,String.class)就找不到这个构造器了
  12. 如果我们想使用类中被private修饰的构造器,还要加上setAccessible(true)。因为被private修饰的构造器,只能在本类中使用,而此时我们是想在其它类中使用这个构造器,所以必须设置一下
  13. 获取指定构造器的测试方法:
    @Test
        public void test02() throws Exception{
            Class clazz1 = Car.class;
            //指定有参数构造器创建对象
            //1.构造public
    //        Constructor c1 = clazz1.getConstructor(String.class, int.class, String.class);
    //        Car car = (Car)c1.newInstance("夏利", 10, "红色");
    //        System.out.println(car);
            //2.构造private
            Constructor c2 = clazz1.getDeclaredConstructor(String.class, int.class, String.class);
            c2.setAccessible(true);
            Car car2 = (Car)c2.newInstance("捷达", 15, "白色");
            System.out.println(car2);
        }
  14. 获取属性,以及给属性赋值的测试方法:
    @Test
        public void test03() throws Exception{
            Class clazz = Car.class;
            //用无参构造器实例化一个对象
            Car car = (Car)clazz.getDeclaredConstructor().newInstance();
            //获取所有public属性
            //Field[] fields = clazz.getFields();
            //获取所有属性
            Field[] fields = clazz.getDeclaredFields();
            //给属性赋值
            for (Field field : fields) {
                if(field.getName().equals("name")){
                    field.setAccessible(true);
                    field.set(car,"五菱宏光");
                }
            }
            System.out.println(car);
        }
  15. 也就是说,我们先要得到类对应的字节码文件。我们可以通过字节码文件得到该类中的无参构造器,用无参构造器实例化对象。我们还可以通过字节码文件得到该类中的属性,如果属性是private修饰的,但是我们还想在其它类中给该属性赋值,那么就要setAccessible(true)
  16. 获取方法的测试方法:
    @Test
        public void test04() throws Exception {
            //现在Car类中的有参构造器是public修饰的
            Car car = new Car("奔驰",10,"黑色");
            Class clazz = car.getClass();
            //1.public方法
    //        Method[] methods = clazz.getMethods();
    //        for (Method method : methods) {
    //            //System.out.println(method.getName());
    //            if(method.getName().equals("toString")){
    //                String info = (String)method.invoke(car);//因为此时toString方法没有形参列表,并且toString方法的返回值是String类型的
    //                System.out.println("toString方法执行了:" + info);
    //            }
    //        }
            //2.所有方法,不仅限于被public修饰的方法。但是这些方法中不包括从Object类中继承的方法
            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method m1 : declaredMethods) {
                if(m1.getName().equals("run")){
                    m1.setAccessible(true);
                    m1.invoke(car);
                }
            }
        }
  17. 总结:当我们获取到私有的构造器/属性/方法,如果我们想使用它们,就必须先setAccessible(true)。执行方法的方式是:方法.invoke(对象,形参)

二、实现Spring的IoC

  1. 实现过程:
    1. 第一步:创建子模块atguigu-spring
    2. 第二步:创建测试类service dao
    3. 第三步:创建两个注解:(1)@Bean用于创建对象(2)@Di用于属性注入
    4. 第四步:创建bean容器接口ApplicationContext,定义方法,返回对象
    5. 第五步:实现bean容器接口(1)返回对象(2)根据包会加载bean(比如包com.atguigu,扫描com.atguigu这个包和它的子包里面的所有类,看这个类的上面是否有@Bean注解,如果有,就把这个类通过反射实例化)
  2. 第一步和第二步:
  3. 注解有两个元注解(@Target和@Retention),@Target注解表示这个注解能用在哪些地方(比如ElementType.TYPE代表这个注解能用在类上,ElementType.FIELD代表这个注解作用在属性上),@Retention表示这个注解的作用范围(比如RetentionPolicy.RUNTIME代表这个注解在运行时生效)
  4. 注意:字节码文件属于Class类
  5. 四个包:anno包用于放两个注解,service包用于发UserService接口和UserServiceImpl类,dao包用于放UserDao接口和UserDaoImpl类,bean包用于放ApplicationContext接口和AnnotationApplicationContext类
  6. anno包的代码:
    package com.atguigu.anno;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.TYPE)//作用在类上
    @Retention(RetentionPolicy.RUNTIME)//在运行时生效
    public @interface Bean {
    
    }
    package com.atguigu.anno;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.FIELD)//作用在字段上
    @Retention(RetentionPolicy.RUNTIME)//在运行时生效
    public @interface Di {
        
    }
  7. dao包代码:
    package com.atguigu.dao;
    
    public interface UserDao {
        public void add();
    }
    package com.atguigu.dao.impl;
    
    import com.atguigu.anno.Bean;
    import com.atguigu.dao.UserDao;
    
    @Bean
    public class UserDaoImpl implements UserDao {
        @Override
        public void add() {
            System.out.println("dao......");
        }
    }
  8. service包代码:
    package com.atguigu.service;
    
    public interface UserService {
        public void add();
    }
    import com.atguigu.anno.Bean;
    import com.atguigu.anno.Di;
    import com.atguigu.dao.UserDao;
    import com.atguigu.service.UserService;
    
    @Bean
    public class UserServiceImpl implements UserService {
        @Di
        private UserDao userDao;
        @Override
        public void add() {
            System.out.println("service......");
            //调用dao方法
            userDao.add();
        }
    }
  9. bean包:
    package com.atguigu.bean;
    
    public interface ApplicationContext {
        //context.getBean(类名.class)
        Object getBean(Class clazz);
    }
    package com.atguigu.bean;
    
    import com.atguigu.anno.Bean;
    import com.atguigu.anno.Di;
    
    import java.io.File;
    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.net.URL;
    import java.net.URLDecoder;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    
    public class AnnotationApplicationContext implements ApplicationContext{
        //创建Map集合,放bean对象
        private Map<Class,Object> beanFactory = new HashMap<>();
        private static String rootPath;
        //返回对象
        @Override
        public Object getBean(Class clazz) {
            return beanFactory.get(clazz);
        }
        //创建有参构造,传递包路径,设置包的扫描规则
        //当前包及其子包里面,哪个类有@Bean注解,就把这个类通过反射实例化
        public AnnotationApplicationContext(String basePackage) throws Exception {
            //com.atguigu
            //1.把.替换成\
            try {
                String packagePath = basePackage.replaceAll("\\.", "\\\\");
                //2.获取包的绝对路径
                Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(packagePath);
    
                while(urls.hasMoreElements()){
                    //将绝对路径从枚举类型中取出来
                    URL url = urls.nextElement();
                    //解码操作:/在url中变成了%5c,我们要通过解码操作,把它的%5c变回/
                    //filePath是带有盘符的包绝对路径
                    String filePath = URLDecoder.decode(url.getFile(), "utf-8");
                    //获取包前面路径的部分,字符串截取
                    rootPath = filePath.substring(0, filePath.length() - packagePath.length());
                    //包扫描
                    loadBean(new File(filePath));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            //属性注入
            loadDi();
        }
    
        //包扫描过程
        private void loadBean(File file) throws Exception {
            //1.判断当前是否是文件夹
            if(file.isDirectory()){//如果值为true,代表是文件夹,如果值为false,代表不是文件夹
                //2.获取文件夹里面所有内容
                File[] childrenFiles = file.listFiles();
    
                //3.判断文件夹里面为空,直接返回
                if(childrenFiles == null || childrenFiles.length == 0){
                    return;
                }
    
                //4.如果文件夹里面不为空,遍历文件夹里面所有内容
                for (File child : childrenFiles) {
                    //4.1遍历得到每个file对象,继续判断,如果还是文件,递归
                    if(child.isDirectory()){
                        //递归
                        loadBean(child);
                    }else {
                        //4.2遍历得到的file对象不是文件夹,是文件
                        //4.3得到包路径+类名称:字符串的截取过程
                        String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1);
                        //4.4判断当前文件的类型是否是.class
                        if(pathWithClass.contains(".class")){
                            //4.5如果是.class类型,把路径替换成.,把.class去掉
                            String allName = pathWithClass.replaceAll("\\\\", "\\.").replace(".class", "");
                            //4.6判断类上面是否有@Bean注解,如果有,就进行实例化
                            //4.6.1获取类的class
                            Class<?> clazz = Class.forName(allName);
                            //4.6.2判断不是接口
                            if(!clazz.isInterface()){
                                //4.6.3判断类上面是否有注解@Bean
                                Bean annotation = (Bean)clazz.getAnnotation(Bean.class);
                                if (annotation != null){
                                    //4.6.4实例化
                                    Object instance = clazz.getDeclaredConstructor().newInstance();
                                    //4.7把对象实例化后,放到Map集合beanFactory
                                    //4.7.1判断当前类如果有接口,就让接口的class作为map的key
                                    if(clazz.getInterfaces().length > 0){
                                        beanFactory.put(clazz.getInterfaces()[0],instance);
                                    }else{
                                        //4.7.2
                                        beanFactory.put(clazz,instance);
                                    }
                                }
                            }
                        }
                    }
                }
            }
    
        }
    
        public void loadDi(){
            //实例化对象都在beanFactory的map集合里面
            //1.遍历beanFactory的map集合
            Set<Map.Entry<Class, Object>> entries = beanFactory.entrySet();
            for (Map.Entry<Class, Object> entry : entries) {
                //2.获取map集合每个对象(value),每个对象的属性都获取到
                Object obj = entry.getValue();
                //获取对象Class
                Class<?> clazz = obj.getClass();
                Field[] declaredFields = clazz.getDeclaredFields();
                //3.遍历得到的每个对象属性数组,得到里面的每个属性
                for (Field field : declaredFields) {
                    //4.判断属性上面是否有@Di注解
                    Di annotation = field.getAnnotation(Di.class);
                    if(annotation != null){
                        //如果私有属性,可以设置值
                        field.setAccessible(true);
                        //5.如果有@Di注解,把对象进行设置(注入)
                        try {
                            field.set(obj,beanFactory.get(field.getType()));
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
  10. 首先,我们要创建两个注解@Bean和@Di,@Bean注解用于实例化对象,作用在类上,在运行时生效;@Di注解用于属性注入,作用在字段上,在运行时生效
  11. 然后我们创建一个接口,叫ApplicationContext,在里面创建一个getBean的方法。再创建一个AnnotationApplicationContext,在里面(1)创建一个Map集合,用于存放某个包中所有有@Bean修饰的类的实例对象,这个Map集合的元素的键是对象所属的类的字节码文件(如果该类有接口,就是接口的字节码文件),这个Map集合的元素的值是对象(2)重写getBean方法(3)创建AnnotationApplicationContext有参构造器(4)写loadBean方法(5)写loadDi方法
  12. getBean方法:getBean方法是用于获取对象的,而这个包及其子类中所有有@Bean注解的类生成的对象都存放在Map集合中。所以我们要从Map集合中获取对象,而对象是值,我们通过键获取值。因此getBean的形参是字节码文件
  13. AnnotationApplicationContext有参构造器里调用了loadBean方法和loadDi方法。调用loadBean方法之前还有一系列操作,因为loadBean的形参是一个File对象,而File对象是根据带有盘符的包绝对路径生成的,但是该有参构造器的形参是包名。所以我们要先得到带有盘符的包绝对路径:(1)把包名(com.atguigu)替换成(com\athuigu)的形式(2)获取带有盘符的包的绝对路径(Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(packagePath);)(3)绝对路径放在枚举类型里,所以我们要把它取出来(4)取出来以后还有一个解码操作:就是把url中的%5c变成/(String filePath = URLDecoder.decode(url.getFile(), "utf-8");)(5)然后我们就可以通过filePath去得到File对象了
  14. loadBean的形参就是File对象。(1)判断这个File对象(一开始是包)是不是文件夹(2)取出这个文件夹里的所有内容(3)判断这个文件夹里面是否为空,如果为空,则结束该方法(4)如果不为空,我们就遍历这个文件夹中的所有文件(5)把文件挨个遍历取出,如果取出的文件是文件夹,那么就递归(5)如果取出的文件不是文件夹,而是文件,那么就想办法得到它的包名+类名(得到的结果是包含.class这种的,接口和类的字节码文件都是以.class结尾的)(6)然后我们根据包名+类名里是否包含.class进行处理,我们先把包名+类名里的.class去掉,因为包名+类名里还有\,所以我们要把\替换成.,我们得到的就是包名+类名(不含class)(9)然后我们就可以据此通过Class.forName(包名+类名(不含class))得到类的字节码文件(10)通过得到的字节码文件可以判断是不是接口,如果不是接口,才能继续判断所得的类有没有被@Bean修饰(11)我们通过所得的类的字节码文件,得到该类的@Bean注解,如果@Bean注解存在,我们就把对象实例化(12)实例化完了还要放进Map集合(注意键:键是对象所属的类的字节码文件,如果所属的类有接口,那就是接口的字节码文件)
  15. loadDi方法没有形参。我们要想给对象注入属性,就要先从Map集合得到对象。(1)遍历Map集合(2)通过entrySet.getValue()得到对象,然后通过对象得到类的字节码文件(3)根据字节码文件得到所有字段(4)遍历字段数组,给每个字段赋值,如果字段是private,别忘了setAccessible

http://www.kler.cn/a/512893.html

相关文章:

  • 迅为龙芯2K1000开发板/核心板流畅运行Busybox、Buildroot、Loognix、QT5.12系统
  • 金融场景 PB 级大规模日志平台:中信银行信用卡中心从 Elasticsearch 到 Apache Doris 的先进实践
  • 解决npm install安装出现packages are looking for funding run `npm fund` for details问题
  • Flask简介与安装以及实现一个糕点店的简单流程
  • VSCode最新离线插件拓展下载方式
  • 国内汽车法规政策标准解读:GB 44495-2024《汽车整车信息安全技术要求》
  • 《开源与合作:驱动鸿蒙Next系统中人工智能技术创新发展的双引擎》
  • STM32单片机学习记录(1.17)
  • Failed to load API definition
  • vue 如何判断每次进入都会刷新页面
  • 【WPF】WPF设置自定义皮肤主题
  • 数据结构初 - 链表
  • 第01章 11 分别使用DCMTK和gdcm库,解析DICOM文件系列的dicom标准数据信息
  • SpringBoot 搭建 SSE
  • Numpy基础01(Jupyter基本用法/Ndarray创建与基本操作)
  • vue.draggable 拖拽
  • 2025年国产化推进.NET跨平台应用框架推荐
  • MyBatis操作数据库(入门)
  • 【Java实现导出Excel使用EasyExcel快速实现数据下载到Excel功能】
  • Qt之QDjango-db的简单使用
  • 三格电子——MODBUS TCP 转 CANOpen 协议网关
  • 网络通信---MCU移植LWIP
  • 从零开始:使用 Brain.js 创建你的第一个神经网络(一)
  • Redis - 通用命令
  • Spring Boot 整合 PageHelper 实现分页功能
  • 线程池遇到未处理的异常会崩溃吗?