深入浅出Spring依赖注入(DI)和控制反转(IOC)

1 概要

作为一个Spring工程师,我们几乎每天都在使用Spring的两大功能——依赖注入(DI)和控制反转(IOC)。这两个名词看上去很高大上,其实就是一些很简单的东西。专家就是爱装逼,整了这两个名词。

2 依赖注入(DI)

2.1 什么叫依赖?

回到Java本身,在Java的世界里,对一类事物的抽象叫做类(Class),这个抽象化的概念类(Class)包含了事物的属性(Field)行为(Method),而这个抽象化的过程(归纳一类事物的属性和行为)叫做封装
我们在初始化一个类的实例的时候,实际上就是初始化类的属性,为属性赋具体的值,所以类和实例的关系也就是从抽象和具体的关系。从抽象(类)到具体(实例)的过程中,我们需要的这些属性的值就是依赖

2.2 什么叫依赖注入

我们初始化一个类的时候,需要对类的属性进行赋值,对属性赋值的过程就叫依赖注入。

2.3 依赖注入的三种方式

  • 通过属性(Field)注入
    这种注入方式的局限性是需要对注入的属性有访问权限,如果name是private修饰的,就无法通过Field的方式注入了
class Student { public String name; } // 实例化一个Student对象 Student stu = new Student(); // 通过访问Field注入依赖 stu.name = "张三"
  • 通过构造器(Constructor)注入
    这种注入方式的局限性是需要有相应的构造器
class Student { private String name; private int id; public Student(String name, int id) { this.name = name; this.id = id; } } // 通过构造器初始化一个Student对象并注入依赖 Student student = new Student("张三", 5);
  • 通过方法注入
    这种注入方式的局限性是需要有对应的set方法
class Student { private String name; private int id; public void setName(String name) { this.name = name; } public void setId(int id) { this.id = id; } } // 初始化一个Student对象并通过方法注入依赖 Student stu = new Student(); stu.setName("张三"); stu.setId(3);

2.4 总结

依赖注入一共有三种方式,分别是通过属性注入,通过构造器注入和通过方法注入。这三种注入方式都有它的局限性,通过属性注入需要有访问权限,通过方法构造器注入需要提供对应参数的构造器,通过方法注入需要提供对应的set方法

3 控制反转(IOC)

3.1 什么叫控制反转

控制反转就是把初始化一个类的实例的权利交给框架(比如Spring)

3.2 为什么要控制反转

通常,开发者都是自己控制初始化一个类的实例的过程,通常我们会这么做

// 在某个Java文件里 private Service service = new ServiceImpl(args...); public void fuc1() { service.function(args...) } // 在另一个Java文件里 private Service service = new ServiceImpl(args...); public void fuc2() { service.function(args...) }

如果这样做,如果有一天我们使用了Service的另一种实现来替换ServiceImpl,我们又需要找到这些地方然后进行更改。
但是如果交给Spring控制我们会这么做:

// 有一个Service接口和它的实现 public interface Service { void function(Args args...) } @Component public class ServiceImpl implements Service{ @Override public void function(Args args...) { // TODO } } // 在一个java文件里 @Autowired // 这里仅为展示方便,不建议在private修饰的Field上使用该注解,之后会提到 private Service service; public fun1() { service.function(args); } // 在另一个java文件里 @Autowired private Service service; public fun1() { service.function(args); }

如果有一天我们需要使用用另一种实现,我们要做的就是把原来的实现类删掉或者把@Component注解去掉,在新的实现类上使用@Component就可以了。
控制反转的好处就是松耦合,Spring通过加上或移除@Component注解使Service变成了易插拔的依赖,而不需要在依赖它的类文件里修改一个字。

3.3 IOC容器

IOC容器就是管理依赖的容器,里面存放了交给框架管理的一些类的实例。比如Spring中可以通过@Component,@Service, @Bean等注解将一个类的实例交给Spring的IOC容器管理。我们需要使用某个实例时,可以从IOC容器中取出使用,这样就达到了对实例的复用。

3.4 总结

控制反转是为了实现依赖的松耦合设计的,而IOC容器实现了对实例的复用。

4 简易实现

以下三段代码都在在控制台上打印出
Student(id=4, name=张三, score=90.5)

4.1 通过属性注入

我们之间提到过不建议在用private关键字修饰的Field上使用@Autowired,这样做程序当然可以运行,但是却破坏了java的封装原则

package com.yeyeck; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; public class FieldInject { public static void main(String[] args) { // 构造一个简单容器 Map<Class<?>, Object> container = new HashMap<>(); container.put(String.class, "张三"); container.put(int.class, 4); container.put(double.class, 90.5); try { Class<?> stuClass = Student.class; // 通过默认构造器实例化一个对象 Object instance = stuClass.getConstructor().newInstance(); // 通过属性注入 Field[] fields = stuClass.getDeclaredFields(); for (Field field : fields) { if (!field.canAccess(instance)) { // 因为Student的属性是private的,强制访问,这种方式是不建议的,违背了封装的初衷 field.setAccessible(true); } Object value = container.get(field.getType()); field.set(instance, value); } // 把实例化之后的Student实例装入容器中 container.put(Student.class, instance); } catch (NoSuchMethodException | SecurityException | IllegalArgumentException | IllegalAccessException | InstantiationException | InvocationTargetException e) { e.printStackTrace(); } // 从容器中取出Student实例并使用 Student stu = (Student)container.get(Student.class); System.out.println(stu.toString()); } } class Student{ private int id; private String name; private double score; public String toString() { return "Student(id=" + this.id +", name=" + this.name + ", score=" + this.score + ")"; } }

4.2 通过构造器注入

package com.yeyeck; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; public class ConstructorInject { public static void main(String[] args) { // 构造一个简单容器 Map<Class<?>, Object> container = new HashMap<>(); container.put(String.class, "张三"); container.put(int.class, 4); container.put(double.class, 90.5); try { // 通过构造器实例化一个对象 Class<?> stuClass = Student.class; Constructor constructor = stuClass.getConstructors()[0]; Class<?>[] paramTypes = constructor.getParameterTypes(); // 获取参数列表 Object[] values = new Object[paramTypes.length]; for (int i = 0; i < paramTypes.length; i++) { values[i] = container.get(paramTypes[i]); } Object instance = constructor.newInstance(values); // 存入容器中 container.put(stuClass, instance); } catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } // 从容器中取出并使用 Student student = (Student)container.get(Student.class); System.out.println(student.toString()); } } class Student{ private int id; private String name; private double score; public Student(int id, String name, double score) { this.id = id; this.name = name; this.score = score; } public String toString() { return "Student(id=" + this.id +", name=" + this.name + ", score=" + this.score + ")"; } }

4.3 通过方法注入

package com.yeyeck; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; public class MethodInject { public static void main(String[] args) { // 构造一个简单容器 Map<Class<?>, Object> container = new HashMap<>(); container.put(String.class, "张三"); container.put(int.class, 4); container.put(double.class, 90.5); try { Class<?> stuClass = Student.class; Constructor constructor = stuClass.getConstructor(); Object instance = constructor.newInstance(); Method[] methods = stuClass.getMethods(); for (Method method: methods) { // 为了简便演示,我们只使用set作为标记,在Spring中,可以使用@Autowired注解来筛选 String name = method.getName(); if (name.startsWith("set")) { Class<?>[] paramTypes = method.getParameterTypes(); Object[] values = new Object[paramTypes.length]; for (int i = 0; i < paramTypes.length; i++) { values[i] = container.get(paramTypes[i]); } method.invoke(instance, values); } } // 存入容器中 container.put(stuClass, instance); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } // 从容器中取出并使用 Student student = (Student)container.get(Student.class); System.out.println(student.toString()); } } class Student{ private int id; private String name; private double score; public void setId(int id) { this.id = id; } public void setName(String name) { this.name = name; } public void setScore(double score) { this.score = score; } public String toString() { return "Student(id=" + this.id +", name=" + this.name + ", score=" + this.score + ")"; } }

总结

三种依赖注入的方式中,通过Field注入私有属性违背了java设计封装的原则,所以是最不推荐的方式.通过方法注入需要多次调用invoke方法,而通过Constructor注入只需要调用一次.所以建议使用构造器注入,Spring也是建议使用构造器注入.
这只是一个简单的功能实现,Spring为控制反转和依赖注入设计了很多规则来实现一个可用性和可拓展性极高的系统。

阅读(24)
评论(0)
updated@2020-11-19
评论区
目录