Spring 6 第4章——原理:手写IoC
Spring框架的IoC是根据Java的反射机制实现的
一、回顾Java反射
- Java反射机制是在运行状态中,对于任何一个类,都能够知道这个类的属性和方法;对于任何一个对象,都能够调用它的任意方法和属性
- 这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制
- 简单来说,反射机制就是程序在运行时,能够获取自身信息
- 想要解剖一个类,必须先要获取到该类的Class对象(即字节码文件)。Class对象是反射的根源
- 剖析一个类或者使用反射解决具体的问题就是使用相关的API(1)java.lang.Class(2)java.lang.reflect
- 自定义一个类(该类中有三个私有属性,分别为它们提供相应的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..."); } }
- 获取字节码文件的三种方法(1)类名.class(2)对象.getClass()(3)Class.forName("全路径")(全路径是指包名+类名)
- 通过字节码进行对象实例化的方法:
- 如果通过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()); } }
- getConstructors方法和getDeclaredConstructors方法返回的是多个构造器,即构造器数组。而getConstructor方法和getDeclaredConstructor方法返回的是一个构造器。如果没有Declared,就只能找到公有的构造器,如果有Declared,就没有这一限制,可以找到所有构造器。在获取一个构造器时,如果getConstructor方法和getDeclaredConstructor方法中没有参数,那就代表获取的是无参构造器,如果想获取有参构造器,方法如下(此时,Car类中的有参构造器还是public的):
- 如果我们现在把Car类中的有参构造器改成private的,那么getConstructors(String.class,int.class,String.class)就找不到这个构造器了
- 如果我们想使用类中被private修饰的构造器,还要加上setAccessible(true)。因为被private修饰的构造器,只能在本类中使用,而此时我们是想在其它类中使用这个构造器,所以必须设置一下
- 获取指定构造器的测试方法:
@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); }
- 获取属性,以及给属性赋值的测试方法:
@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); }
- 也就是说,我们先要得到类对应的字节码文件。我们可以通过字节码文件得到该类中的无参构造器,用无参构造器实例化对象。我们还可以通过字节码文件得到该类中的属性,如果属性是private修饰的,但是我们还想在其它类中给该属性赋值,那么就要setAccessible(true)
- 获取方法的测试方法:
@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); } } }
- 总结:当我们获取到私有的构造器/属性/方法,如果我们想使用它们,就必须先setAccessible(true)。执行方法的方式是:方法.invoke(对象,形参)
二、实现Spring的IoC
- 实现过程:
- 第一步:创建子模块atguigu-spring
- 第二步:创建测试类service dao
- 第三步:创建两个注解:(1)@Bean用于创建对象(2)@Di用于属性注入
- 第四步:创建bean容器接口ApplicationContext,定义方法,返回对象
- 第五步:实现bean容器接口(1)返回对象(2)根据包会加载bean(比如包com.atguigu,扫描com.atguigu这个包和它的子包里面的所有类,看这个类的上面是否有@Bean注解,如果有,就把这个类通过反射实例化)
- 第一步和第二步:
- 注解有两个元注解(@Target和@Retention),@Target注解表示这个注解能用在哪些地方(比如ElementType.TYPE代表这个注解能用在类上,ElementType.FIELD代表这个注解作用在属性上),@Retention表示这个注解的作用范围(比如RetentionPolicy.RUNTIME代表这个注解在运行时生效)
- 注意:字节码文件属于Class类
- 四个包:anno包用于放两个注解,service包用于发UserService接口和UserServiceImpl类,dao包用于放UserDao接口和UserDaoImpl类,bean包用于放ApplicationContext接口和AnnotationApplicationContext类
- 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 { }
- 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......"); } }
- 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(); } }
- 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(); } } } } } }
- 首先,我们要创建两个注解@Bean和@Di,@Bean注解用于实例化对象,作用在类上,在运行时生效;@Di注解用于属性注入,作用在字段上,在运行时生效
- 然后我们创建一个接口,叫ApplicationContext,在里面创建一个getBean的方法。再创建一个AnnotationApplicationContext,在里面(1)创建一个Map集合,用于存放某个包中所有有@Bean修饰的类的实例对象,这个Map集合的元素的键是对象所属的类的字节码文件(如果该类有接口,就是接口的字节码文件),这个Map集合的元素的值是对象(2)重写getBean方法(3)创建AnnotationApplicationContext有参构造器(4)写loadBean方法(5)写loadDi方法
- getBean方法:getBean方法是用于获取对象的,而这个包及其子类中所有有@Bean注解的类生成的对象都存放在Map集合中。所以我们要从Map集合中获取对象,而对象是值,我们通过键获取值。因此getBean的形参是字节码文件
- 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对象了
- 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集合(注意键:键是对象所属的类的字节码文件,如果所属的类有接口,那就是接口的字节码文件)
- loadDi方法没有形参。我们要想给对象注入属性,就要先从Map集合得到对象。(1)遍历Map集合(2)通过entrySet.getValue()得到对象,然后通过对象得到类的字节码文件(3)根据字节码文件得到所有字段(4)遍历字段数组,给每个字段赋值,如果字段是private,别忘了setAccessible