Spring Boot依赖注入[DI]和相关注解 [Annotations]

这篇文章,让我们讨论一下Spring boot中的依赖注入(Dependency Injection)和相关注解(Annotations)的使用。

演示例子源代码 演示案例

概念

Spring Boot的一个重要概念和实现--依赖注入。我列举了几个和依赖注入相关的概念,面向接口编程、IOC和依赖倒转。这些都是Spring Boot 依赖注入机制实现的理论基础。更多的了解这些概念将有助于深入理解Spring Boot 的依赖注入。

由于讲述所有这些概念是另一个更大的关于设计模式的话题,以后有机会抽时间再写一下相关文章。有兴趣的朋友也可以给我留言,大家共同讨论。

相关概念解释:

  • 面对接口编程
    IOP: Interface Oriented Programming
    系统应该依赖接口(Interface)或抽象类进行编程,而不是具体的实现类(Class),即将抽象和具体分离,以便增强系统的可扩展性。
  • 依赖倒转
    IOC: Inverse of Control
    面向对象的一种设计思想,它提出在设计中对象交由一个容器控制在运行时动态创建和管理,而不是在设计时由程序代码直接管理对象。
  • 依赖注入:
    DI: Dependency Injection
    在IOC的设计思想基础上,DI提供对象管理容器并在运行时提供相关查找、定位、初始化、装配对象的过程。也就是,你在写代码的时候只需要关心接口,不需要关心对象如何被装配的。

依赖注入

解决的问题

在一个应用系统中,通常会存在分层、分模块的情况,通常一个系统都有由很多不同模块共同合作构建而成。比如,经典的三层模式,它包括服务层、业务层、数据层,层次之间存在大量的对象引用(依赖)。这导致:

  1. 存在大量的对象强依赖(引用),不利于维护、变更和扩展。
  2. 设计时,需要关注大量对象的创建,这是一件繁杂且需要耐心的事情。
  3. 系统维护随着系统变大变得更加艰难。
  4. 如果不使用依赖注入很难做到面向接口编程,包和类的引用管理都需要人为或程序进行管理。
    依赖注入就是解决上述问题的。通过依赖注入机制,程序员在编程时(设计时)只需要面向接口进行编程,而无需关心具体实现对象如何被初始化和加载的(装配过程)。

原理

“没有对比就没有伤害。”,我们来对比下面两种编程模式,以便知道为什么需要依赖注入。

传统编程
面向接口编程
  • 第一种模式中,上层的调用方(Controller)需要明确知道下层的实际实现类(OrderService)以及明确所在的包。这个方案的最大问题就是耦合度太高,一旦程序需要更改,调用方和被调用方都需修改代码,并且存在对包的版本管理。

  • 第二种模式中,类的实现和接口的定义被分割到不同的包中。在设计时,上层的调用方(Controller)仅需要知道接口(OrderService)的存在,至于需要加载哪个包,使用哪个具体的实现类是不需要关心的。具体的实现类的引用、初始化被“延迟”到程序运行时。程序运行时可利用Factory模式或者依赖注入机制选择具体的实现类并加载(延迟确定)。由于在运行时动态确定和装配,使得程序灵活度更高,通过配置就可以更改和替换逻辑和规则,实现内聚合、外开放

示意图

接下来,看看依赖注入机制都做了什么。

依赖注入机制

以上图例中:

  1. OrderServiceImpl类实现了OrderService接口。
  2. OrderControler作为上层模块依赖OrderService接口。
  3. 1、2的信息在设计时通过多种可能的途径被配置,通常依赖注入机制会提供相应的配置机制。
  4. 依赖注入机制在程序启动 运行时(runtime),读取相关配置信息并分析、定位和装配相关对象。
  5. 最后,当OrderController被调用的时候,如期得到来之OrderServiceImpl的支持。

在上述过程中,借助依赖注入机制,运行时程序也只关注于接口定义,依赖注入容器会在运行时完成自动装配过程。

  • 设计时,OrderController作为上层调用者只需要知道OrderService接口的存在,并做相关“依赖配置”。
  • 运行时(runtime),依赖注入机制根据配置信息,自动 装配 所需对象。

原理对比

为更加清楚的说明,我们将依赖注入机制和自行车的制造过程做一个类比。下图描述了自行车从设计到生产的大概过程。

依赖注入对比
  1. 自行车的设计(设计时)类似我们编写代码的时候定义的接口、逻辑、以及配置文件等等。
  2. 自行车厂家做完自行车设计后,将零件设计标准提供给零配件厂家,厂家按照设计标准进行生产。当然,在符合标准的情况厂家也可以做一些调整,比如,颜色。这个过程类似我们将符合接口的实现类分解到不同的包中,或者请“外部团队”根据接口要求编写和构建包。
  3. 装配车辆的过程(运行时)就是将零配件按照设计进行实际装配。这类似Spring Boot的依赖注入机制,它根据接口的定义、实现类(制作好的零件)以及相关的配置文件在程序运行时进行动态装配。

上面生产自行车的过程如果用代码编写出来的话,我们只需要定义好做好设计标准并生产出相应的零件就好,自行车的装配工作就交给依赖注入机制完成就好了。

演示例子源代码 演示案例

Spring boot 依赖注入

看完前面的描述,spring boot 依赖注入机制就显得很简单。它提供了一个容器( spring container)管理一堆的bean对象。spring container通过“配置信息“知道如何将一个普通对象变成一个bean对象并放入到容器中。同时,spring container也知道如何装配一个对象到另一个对象中(可能是字段、构造函数或属性)

一个bean对象,记录了被注入的对象的类型、如何初始化以及这个对象依赖其他的哪些对象(严格意义上来说应该是哪些bean对象)。

容器如何知道哪些类型需要被转变成bean呢?这就是下面我们需要讲的部分。

spring boot只在应用程序启动的时刻注入所有的bean对象以及进行装配。

注解(Annotation)

Spring container依赖三种配置方式收集哪些类型需要被转化成bean对象并注入到spring container中进行管理。

注解方式,是spring boot 依赖注入中三种静态bean配置其中的一种。下面列举依赖注入的三种配置方式。

配置bean(注入bean)的方式

  • 通过注解进行配置 这章节讲述的
  • 通过编码方式,使用@Configuraiton的方式进行配置,它可以取代下面基于xml的配置方式。
  • 通过bean xml配置文件形式配置 (现代应用程序中很少使用这样方式)

相关的注解

使用注解方式配置依赖注入bean配置一般涉及到以下几类注解。

用于把对象配置为bean对象的标签

  1. @Component
    • 它由Spring boot框架,是元注解,可以用于标注其他注解(元注解)
    • 它是@Service, @Controller, @Respository的注解标签
    • 被@Component的注解的对象以及被它注解的注解,如@Service, @Controller, @Respository都将被组件扫描(Component scanning)过程收集注册为bean对象。
  2. @Controller, @Service, @Respository
    • 它们在经典三层模式中带有特殊含义,分别代API接口组件、服务组件和持久化组件。
  • 其用途和@Component相当。
  1. @Primary
    • 在一个接口有多个实现的情况下,通过@Primary指定默认实现此接口的实现类作为主要的bean。
  2. @Order
    • 控制bean的加载次序,有时候一个对象需要另一个对象被加载后在能被加载。
  3. Conditional 【单独描述和举例】
    • Spring 4.0引入的条件注解,满足某个特定条件下创建特定的Bean。而我们实现的方式就是实现Condition接口。

用于指示需要装配bean对象的标签

  1. @Autowired 和 @Qualifier
    • 上面两各标签由Spring boot框架
    • @Autowired通过类型查找bean并进行装配,如果同一个类型有多个bean,将引发错误。
    • 如果@Autowired和@Qualifier一起使用将使用名字的方式查找bean并装配,如果没有找到,将报错。
  2. @Resource
    • 属于JDK提供的标签,位于javax.annotation.Resource, 最早实现的DI是作为JDK的一部分。
    • 默认用名字来查找bean并进行装配,如果查找失败,将回退到@Autowired通过类型查找并装配。

第一个例子

这个例子中将基于典型三层,使用 @Autowired、@Compoent、@Service, @Controller, @Respository的注解进行bean对象的配置和装配。

演示例子源代码 演示案例

例子讲解

  • 项目结构
    从图中可以看出来,这是一个典型的三层结构的 rest api项目。


    截屏2020-02-1800.16.59.png
  • 先从定义 bean开始

UserService接口

package com.juqimiao.di.demos.d01.daos;

public interface UserDao {
    String select(String name);
}

定义UserDaoImpl实现类,它实现了UserDao接口,并且将这个类打上@ Repository注解。它将被作为bean注入到spring container 中。

@Repository
public class UserDaoImpl implements UserDao {

    @Override
    public String select(String name) {
        return "User's information " + name;
    }
}

接下来,我们定义UserService接口。

package com.juqimiao.di.demos.d01.services;

public interface UserService {
    String getInfo(String name);
}

定义UserServiceImpl实现类,它实现了UserService接口,并且在这个类上面大上了@Service注解。它将被作为bean注入到spring container 中。

package com.juqimiao.di.demos.d01.services.Impl;

import com.juqimiao.di.demos.d01.daos.UserDao;
import com.juqimiao.di.demos.d01.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    private UserDao userDao;

    /**
     * 演示构造函数注入方式。
     * @param userDao {@link UserDao}
     */
    @Autowired
    public UserServiceImpl(UserDao userDao){
        this.userDao = userDao;
    }

    @Override
    public String getInfo(String name) {
        return userDao.select(name);
    }
}

你看到了代码中,类的构造函数上被打上了@Autowired注解,构造函数中有一个参数 “UserDao userDao”。这意味着,当程序运行的时候,UserDaoImpl的一个实例会被自动装配给这个参数。

最后,我们创建UserController,这是典型的rest api的Controller。

package com.juqimiao.di.demos.d01.controllers;

import com.juqimiao.di.demos.d01.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
    @GetMapping("/info")
    public String getInfo(@RequestParam String name){
        return userService.getInfo(name);
    }
}

这里,你可以看到@Autowired注解被打在了userService 字段上面,意味着UserServiceImpl对象将被装配到这个字段上。

同时,我们也在项目中通过,CustomerController/CustomService和CustomerDao演示了@Component是如何工作的。

例子小结

  1. @Service、@ Repository和@Component都用于声明一个对象作为bean对象的注解。
  2. @Autowired告诉spring container它所需要依赖的bean并要求container自动查找并装配。
  3. bean可以通过 构造函数、字段、属性进行装配,但装配的bean对象并不能保证一定是不为null的。
  4. UserServiceImpl和UserController中相应引用的是UserDao和UserServic接口,而不是具体实现类。因为,如果直接指向具体实现类,依赖注入本身其实就失去了意义,毕竟它是为面向接口编程而生的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容