您现在的位置是:首页 >技术杂谈 >【Spring】三大依赖注入(@Autowired,Setter,构造方法)网站首页技术杂谈

【Spring】三大依赖注入(@Autowired,Setter,构造方法)

你不应该热爱 2023-06-03 12:00:03
简介【Spring】三大依赖注入(@Autowired,Setter,构造方法)

目录

一、属性注入(@Autowired)

1.1 优点分析

1.2 缺点分析

1.2.1 无法实现final修饰的变量注入。

1.2.2 兼容性不好

1.2.3 (可能违背)设计原则问题

1.2.4 代码举例:

1.2.5 出现循环依赖该怎么办?

二、Setter注入

2.1 优点分析

2.2 缺点分析

2.2.1 不能注入不可变对象

2.2.2 注入对象可被修改

三、构造方法注入

3.1 优点分析

3.1.1 可注入不可变对象

3.1.2 注入对象不会被修改

3.1.3 注入对象会被完全初始化

3.1.4 通用性更好


一、属性注入(@Autowired)

属性注入是使用@Autowired实现的,如下:将UserService类注入到UserController类中:

启动类如下:

运行结果如下:

1.1 优点分析

属性注入的最大优点就是实现简单、使用简单。只需要给变量加上一个@Autowired,就可以在不new对象的情况下,直接获得注入的对象——这也正是DI的功能及其魅力所在。

1.2 缺点分析

1.2.1 无法实现final修饰的变量注入。

原因也很简单,在Java中final对象(不可变)要么直接赋值,要么在构造方法中赋值,所以当使用属性注入final对象时,它不符合Java中final的使用规范,所以也就无法注入成功。

问:那么如果要注入一个不可变的对象,该如何实现呢?

答;使用构造方法注入。

1.2.2 兼容性不好

只适用于IoC容器。如果将属性注入的代码移植到其他非 IoC 的框架中,那么代码就无效了,所以属性注入的通用性不是很好。

1.2.3 (可能违背)设计原则问题

属性注入容易违背单一设计原则是因为它会导致类的职责不够单一,在属性注入中,一个类可能会同时拥有多个依赖,这些依赖可能与该类的主要职责无关,导致类的职责不够单一,使代码变得难以维护。

而单一设计原则的核心思想是:一个类应该只有一个职责,只有这样才能使代码易于维护和扩展,因此,在使用属性注入时,特别需要注意类的职责是否清晰,以及注入的属性是否与类的主要职责相关。

当然,也不是说一定会出现违背单一原则的情况,但是不可否认的是:注入实现越简单,那么滥用它的概率也越大,所以出现违背单一职责原则的概率也越大。 注意:这里强调的是违背设计原则(单一职责)的可能性,而不是一定会违背设计原则,二者有着本质的区别。

1.2.4 代码举例:

当使用@Autowired注入时,一个类可能会依赖于多个其他类或接口,这样会导致这个类的职责过重,违反了单一设计原则。下面是一个例子:

假设有一个OrderService类,它需要依赖于一个UserService和一个ProductService来完成一些业务逻辑。如果使用@Autowired注入这两个依赖,那么OrderService将会依赖于UserService和ProductService两个类,导致OrderService职责过重。

@Service
public class OrderService {
 
    @Autowired
    private UserService userService;
 
    @Autowired
    private ProductService productService;
 
    public void placeOrder() {
        // use userService and productService to place order
    }
}

这里OrderService类的职责包含了用户管理和商品管理,这违反了单一设计原则。如果将UserService和ProductService作为方法参数传递,而不是使用@Autowired注入,那么OrderService就只需要关注订单管理相关的逻辑,而不需要依赖其他的类或接口,符合单一设计原则。

1.2.5 出现循环依赖该怎么办?

什么是循环依赖?

在Spring中,当一个bean被初始化时,如果依赖的bean还未初始化,Spring会把该bean放入一个专门用来存储正在初始化的bean的缓存中,以便在依赖的bean初始化完成后再将其注入。但如果两个bean相互依赖,那么它们的初始化顺序就会产生死锁,导致应用程序无法启动。

@Autowired注入可以让类和类之间的关系变得紧密,容易出现循环依赖和复杂的依赖关系,从而违反了单一设计原则中的“高内聚,低耦合”的原则。

以下是一个例子:

public class OrderService {
    @Autowired
    private UserService userService;
    
    public void createOrder() {
        // 创建订单代码
        User user = userService.getUser();
        // 订单相关代码
    }
}

public class UserService {
    @Autowired
    private OrderService orderService;
    
    public User getUser() {
        // 获取用户代码
        Order order = orderService.getOrder();
        // 用户相关代码
    }
}

在这个例子中,OrderService和UserService互相依赖,存在循环依赖关系(具体来说,当容器创建bean A时,会发现A需要依赖B,于是容器会先创建bean B,但是创建B又发现B需要依赖A,于是容器又会回去创建bean A,这样就形成了循环依赖的问题。)。这使得系统中类之间的依赖关系变得复杂,不利于代码的维护和扩展。

为了解决这个问题,可以采用构造函数注入来替代@Autowired注入。构造函数注入避免循环依赖问题,并且更容易维护和测试。例如,可以通过以下方式来进行改进:

public class OrderService {
    private UserService userService;
    
    public OrderService(UserService userService) {
        this.userService = userService;
    }
    
    public void createOrder() {
        // 创建订单代码
        User user = userService.getUser();
        // 订单相关代码
    }
}

public class UserService {
    private OrderService orderService;
    
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
    
    public User getUser() {
        // 获取用户代码
        Order order = orderService.getOrder();
        // 用户相关代码
    }
}

在这个改进后的代码中,通过构造函数注入的方式来避免了循环依赖问题,并且更加符合单一设计原则中的“高内聚,低耦合”的原则。

可能有人会说,Spring框架不是提供了三级缓存的方案来解决循环依赖吗?

为了解决循环依赖的问题,Spring确实是使用了三级缓存来管理bean的创建和初始化过程:

具体来说,当Spring创建bean时,会将该bean放入三级缓存中,其中第一级缓存存储已经完成了实例化的bean,第二级缓存存储已经完成了属性注入的bean,第三级缓存存储还未完成属性注入的bean。在注入属性时,Spring会首先从第一级缓存中查找bean,如果找不到则继续到第二级缓存中查找,如果还是找不到则继续到第三级缓存中查找,如果最终还是找不到,则会抛出异常。

既然提供了三级缓存,为什么还要关心注入会不会出现依赖注入的情况呢?

虽然Spring框架提供了三级缓存的方案来解决循环依赖,但是使用@Autowired注解的确可能会引起循环依赖的问题。而且,如果循环依赖链条较长,缓存的效率会受到影响,甚至会导致系统性能下降。

另外,对于大型系统来说,循环依赖是一种设计上的问题,不应该在代码实现时去“绕过”这个问题,而应该通过优化代码结构、拆分模块等方式来解决。

总的来说,虽然Spring框架提供了循环依赖的解决方案,但是在实际使用时,应该避免产生循环依赖的情况,从设计层面上避免这个问题的发生,提高系统的稳定性和可维护性。

注意这里不能使用Setter注入来解决循环依赖问题:

setter注入可以一定程度上避免循环依赖的问题,但并不是完全解决。setter注入是通过在bean初始化完成之后,逐一为属性赋值来完成注入的,因此可以避免在构造函数中进行依赖注入时可能产生的循环依赖问题。

但是如果在setter注入中,存在多个bean相互依赖的情况,仍然有可能产生循环依赖。例如,beanA中有属性a,需要通过setter注入来完成,而属性a又需要引用beanB中的属性b,那么就会产生循环依赖的问题。

为了避免循环依赖的问题,可以通过三种方式解决:

  1. 构造函数注入:在构造函数中注入依赖,从而避免setter注入中可能出现的循环依赖问题。

  2. 使用延迟依赖注入:在注入时不直接注入依赖,而是通过代理对象实现延迟注入,当真正需要使用该依赖时,再进行注入。

  3. 使用工厂模式:通过工厂模式来管理bean的创建和依赖注入,从而解决循环依赖问题。

二、Setter注入

实现代码如下:

@RestController
public class UserController {
    // Setter 注入
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void sayHi() {
        System.out.println("com.java.demo -> do UserController sayHI()");
        userService.sayHi();
    }
}

2.1 优点分析

可以看出,Setter注入其实是比属性注入要麻烦的,但是其也是有优点的,那就是它完全符合单一职责的设计原则,因为每个Setter只针对一个对象。

2.2 缺点分析

2.2.1 不能注入不可变对象

与@Autowired相同,Setter注入是不能注入不可变对象的:

2.2.2 注入对象可被修改

Setter注入提供了setXXX的方法,意味着开发者可以在任意时刻,任何地方,通过调用setXXX方法来改变注入对象。

三、构造方法注入

构造方法注入是Spring官方从4.x之后推荐的注入方式,它的实现代码如下:

@Controller
public class UserController {
    // 构造方法注入
    private UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
    public void sayHi() {
        System.out.println("com.java.demo -> do UserController sayHI()");
        userService.sayHi();
    }
   
}

注意,如果当前的类中只有一个构造方法,那么@Autowired也可以省略,所以以上代码还可以这一写:

@Controller
public class UserController {
    // 构造方法注入
    private UserService userService;

   
    public UserController(UserService userService) {
        this.userService = userService;
    }
    public void sayHi() {
        System.out.println("com.java.demo -> do UserController sayHI()");
        userService.sayHi();
    }
   
}

3.1 优点分析

3.1.1 可注入不可变对象

使用构造方法是可以注入不可变对象的,以下代码实现:

@Controller
public class UserController {

    private final UserService userService;
    
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }


    public void sayHi() {
        System.out.println("com.java.demo -> do UserController sayHI()");
        userService.sayHi();
    }
}

3.1.2 注入对象不会被修改

由于构造方法只会在对象创建时候执行一次,避免了像Setter注入那样,不存在注入对象被随时(调用)修改的情况。

3.1.3 注入对象会被完全初始化

因为依赖对象是在构造方法中执行的,而构造方法是在对象创建之初执行的,因此被注入的对象在使用之前,会被完全初始化,这也是构造方法注入的优点之一。

3.1.4 通用性更好

构造方法和属性注入不同,构造方法注入可适用于任何环境,无论是IoC框架还是非IoC框架,构造方法注入的代码都是通用的,所以它的通用性更好。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。