【Spring Boot】深入浅出Spring Boot中的控制反转与依赖注入

文章目录

  • 前言
  • 一、控制反转
    • [1.1 为什么需要控制反转?](#1.1 为什么需要控制反转?)
    • [1.2 Spring Boot里的IOC管理](#1.2 Spring Boot里的IOC管理)
      • [1.2.1 Bean注册](#1.2.1 Bean注册)
      • [1.2.2 Bean扫描](#1.2.2 Bean扫描)
  • 二、依赖注入
    • [2.1 依赖注入的意义·](#2.1 依赖注入的意义·)
    • [2.2 Spring Boot里实现依赖注入](#2.2 Spring Boot里实现依赖注入)
    • [2.3 Bean优先级](#2.3 Bean优先级)
  • [三、Spring Boot是如何启动一个容器](#三、Spring Boot是如何启动一个容器)
  • 总结

前言

一个复杂且具备高可维护性的系统,离不开对模块架构的精细化设计。单个模块内部功能联系紧密,围绕核心功能高度统一。同时,各个模块之间需保持适度的独立性,通过标准化的接口实现必要协作。实现低依赖。这种高内聚、低耦合的设计思想,同样是现代编程中控制反转(IOC)与依赖注入(DI)模式的核心出发点。

控制反转(Inversion of Control ,IOC)和依赖注入(Dependency Injection ,DI)是两个紧密相关的设计概念,常被一同提及但又容易混淆。依赖注入是控制反转实现思想的实现方式。依赖注入的提出是为了简化模块的组装过程,降低模块之间的耦合度。


一、控制反转

1.1 为什么需要控制反转?

传统项目我们常常会大量的重复new一个对象,控制权在自身。这样当依赖变化时,必须修改对象的代码。若修改依赖的实现,所有使用该依赖的类都需修改代码,牵一发而动全身,随着项目的持续推进要修改的代码量会呈现指数型上升。重复创建对象也会存在浪费内存的现象和资源泄漏的风险。并且高层模块直接依赖低层模块,而不是抽象接口,也违背了依赖倒置原则。毕竟相较于相对于低层实现类细节的多变性,抽象的东西要稳定的多。

如果要修改对日志(LogOperation)依赖的实现 ,则所有使用该依赖的类都需修改代码,耦合度非常高

csharp 复制代码
@RequestMapping("/users/{id}")
    public String deleteUser(@PathVariable int id) {
        LogOperation logOperation = new LogOperation();
        // 执行删除操作
        boolean result = userService.deleteUser(id);
        if (result) {
            logOperation.log("用户删除成功,ID: " + id);
            return "删除成功";
        } else {
            logOperation.log("用户删除失败,ID: " + id);
            return "删除失败,用户不存在";
        }
    }

1.2 Spring Boot里的IOC管理

1.2.1 Bean注册

在Spring Boot中要将某个对象交给IOC容器管理分成的简洁,可以通过@Componet注解声明这个对象交由IOC容器管理。

java 复制代码
@Component
public class UserServiceImpl implements UserService {
}

交由IOC容器管理的对象称之为bean。我们可以借助idea观测到。将程序运行起来,下方调试窗口里找到Beans。这里面就包含注册到IOC容器里的对象,可以看到有很多是系统自动注册的,如果我们仔细观测,会发现Controller本身也被作为bean对象交由IOC容器管理。

但是回想一下在Spring Boot开发中,我们的控制器上一般只会声明一个@RestController注解,并没有@Component注解。这是因为@RestController注解本身又使用到了Controller注解,定位到Controller注解里,就能见到Component注解本身。

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

其实在Spring Boot中有三种@Component的衍生注解,分别是:

  • @Controller:控制器层
  • @Service:业务服务层
  • @Repository:数据访问层类
    这三种衍生注解仅用于明确类的职责分层,功能上与@Component一致

1.2.2 Bean扫描

在主程序入口上一个@SpringBootApplication注解

java 复制代码
@SpringBootApplication
public class SpringbootWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebApplication .class, args);
    }
}

其内部有个@ComponentScan注解,这个用于指定Spring容器扫描组件的范围,将标注了 @Component、@Controller、@Service、@Repository 等注解的类注册为 IOC 容器中的 Bean,默认扫描当前类所在包及其子包。

java 复制代码
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
java 复制代码
public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

最终是ComponentScanAnnotationParser方法调用@ComponentScan里定义的属性,比方说basePackages和basePackageClasses,如果都是空默认扫描当前类所在包及其子包。

二、依赖注入

2.1 依赖注入的意义·

控制反转的提出本质上是为了提升系统灵活性和扩展性。忽略细节,依赖抽象。体现在实际开发中便是一个控制权的转变, 从 如何创建一个对象改变成我要一个对象,无需关心这个对象生成的细节。

在Spring中,依赖注入是实现控制反转的具体方式:容器在创建其管理的对象(Bean)时,会自动将该对象所依赖的、同样由容器管理的其他对象(Bean)注入到当前对象中。

一般而言只有容器内的Bean之间,才能享受自动注入的能力。若 A 依赖 B,且 A 由容器管理(是 Bean),则 B 必须也是容器中的 Bean 才能被注入;若 B 还依赖 C,则 C 也需是 Bean,以此类推,形成依赖链的 "传染性"。

可以简单的说依赖注入具有传染性,这一点和.NET里的ServiceProvider很相似。一条依赖链上的对象,往往会因为前一个对象需要被注入,而传染到后一个对象也需要被容器管理。

2.2 Spring Boot里实现依赖注入

在Spring Boot中实现依赖注入一般分为三种方式,分别是构造函数注入,属性注入和Setter注入。下面分别演示下。

  • 构造函数注入(最推荐)。通过在构造函数使用@Autowired注解,将定义好的私有不可变字段通过构造函数实现依赖注入,这样就能直接获得一个实例。通过final关键字修饰依赖字段,一旦注入后无法被修改,非常的安全。但是如果依赖是非必需的,仍需在构造函数中传入null
java 复制代码
@RestController
public class UserController {
    private final UserService userService ;
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
  • 属性注入。这种方式是最为简洁的注入方式,但是不直观,也是需要@Autowired注解。字段无法用 final 修饰,可能被意外修改,破坏对象稳定性。并且因为不依赖于构造函数,从类的外部无法直观得知依赖关系
java 复制代码
public class UserController {
		@Autowired
    private UserService userService ;
  • Setter注入。Setter注入是通过一个setXXX()方法注入依赖。支持可选依赖(可通过 @Autowired(required = false) 标记),但是需手动调用 Setter 方法设置依赖,步骤稍多
java 复制代码
@RestController
public class UserController {
    private UserService userService ;
    @Autowired 
    public void setUserService (UserService userService) {
        this.userService = userService;
    }

2.3 Bean优先级

想象一下,一个接口的实现类有多种。每个实现类都被我们在IOC容器里注册成为bean对象。那么在依赖注入的时候Spring框架是如何区分待注入的bean对象呢。在Spring中,我们可以依据两个注解来实现Bean的优先级

  • @Primary。设置Bean的优先级
java 复制代码
//接口
public interface UserService {}

//实现类1
@Service
@Primary
public class UserServiceImpl1 implements UserService {}

//实现类2
@Service
public class UserServiceImpl2 implements UserService {}

//注入时,因为UserServiceImpl1有Primary注解
@Autowired
private UserService userService; // 所以实际注入的是UserServiceImpl1
  • @Qualifier。注解Qualifier里有一个参数,运行通过指定Bean 的名称来精确选择要注入的 Bean。比起@Primary注解,它能更加精细化的控制
java 复制代码
@Autowired
@Qualifier("userServiceImpl2 ") 
private UserService userService;

一般情况下bean对象名称和注入类的名称一致,但是首字母小写

三、Spring Boot是如何启动一个容器

在springboot里实现控制反转依赖于一个容器,这个容器用于存储对象,被存储的就称之为bean对象。

在我们启动springboot服务的时候。

Spring boot SpringbootWeb1Application主方法会调用run方法:

java 复制代码
@SpringBootApplication
public class SpringbootWebApplication{

    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebApplication.class, args);
    }

}

实际重载执行SpringApplication内部的run方法:

java 复制代码
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return (new SpringApplication(primarySources)).run(args);
}

run() 方法会创建SpringApplication实例并触发容器初始化,核心逻辑在SpringApplication的run方法中:

java 复制代码
public ConfigurableApplicationContext run(String... args) {
        /***省略***/
        try {
            /***省略***/
            //创建应用程序上下文触发容器初始化
            context = this.createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
            //准备应用程序上下文
            this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            //刷新应用程序上下文
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            /***省略***/
        } catch (Throwable ex) {
            throw this.handleRunFailure(context, ex, listeners);
        }
    }

在createApplicationContext里会根据当前应用的类型生成不同的ApplicationContext,比如AnnotationConfigServletWebServerApplicationContext。最后都是基础自GenericApplicationContext这个类,在这个GenericApplicationContext类里就包含着一个极为重要的BeanFactory。

java 复制代码
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
    private final DefaultListableBeanFactory beanFactory;
    @Nullable
    private ResourceLoader resourceLoader;
    private boolean customClassLoader;
    private final AtomicBoolean refreshed;

    public GenericApplicationContext() {
        this.customClassLoader = false;
        this.refreshed = new AtomicBoolean();
        this.beanFactory = new DefaultListableBeanFactory();
    }

BeanFactory是 IOC 容器的底层接口,负责 Bean 的创建、依赖注入等核心功能。


总结

通过IOC容器管理对象和依赖关系,实现 "高内聚、低耦合",提升系统的可维护性和扩展性。

相关推荐
shepherd1115 小时前
破局延时任务(上):为什么选择Spring Boot + DelayQueue来自研分布式延时队列组件?
java·spring boot·后端
技术杠精5 小时前
Docker Swarm之Java 应用部署与平滑更新
java·docker·容器
beyond阿亮5 小时前
nacos支持MCP Server注册与发现
java·python·ai·nacos·mcp
非凡ghost5 小时前
EaseUS Fixo(易我视频照片修复)
前端·javascript·后端
非凡ghost5 小时前
Avast Cleanup安卓版(手机清理优化)
前端·javascript·后端
豆苗学前端5 小时前
长时间不操作自动退出登录(系统非活跃状态下自动登出机制的企业级设计方案)
前端·后端·面试
用户68545375977695 小时前
🚀⚡ 预计算与预加载:让程序提前"热身"的智慧
后端
非凡ghost5 小时前
Atlantis Word Processor(文字处理软件)
前端·javascript·后端
zl9798995 小时前
SpringBoot-数据访问之JDBC
java·spring boot