JavaEE 进阶第十二期:Spring Ioc & DI,从会用容器到成为容器(上)

专栏:JavaEE 进阶跃迁营

个人主页:手握风云

目录

[一、Spring 是什么](#一、Spring 是什么)

[1.1. 容器](#1.1. 容器)

[1.2. IoC](#1.2. IoC)

[二、IoC 介绍](#二、IoC 介绍)

[2.1. 传统程序开发](#2.1. 传统程序开发)

[2.2. 解决方案](#2.2. 解决方案)

[2.3. IoC 程序开发](#2.3. IoC 程序开发)

[2.4. IoC 优势](#2.4. IoC 优势)

[三、DI 介绍](#三、DI 介绍)

[四、Bean 的存储](#四、Bean 的存储)

[4.1. @Controller(存储器存储)](#4.1. @Controller(存储器存储))

[4.2. @Service(服务存储)](#4.2. @Service(服务存储))

[4.3. @Component(组件存储)](#4.3. @Component(组件存储))

[4.4. @Repository(仓库存储)](#4.4. @Repository(仓库存储))

[4.5. @Configuration(配置存储)](#4.5. @Configuration(配置存储))

[4.6. 为什么需要多类注解](#4.6. 为什么需要多类注解)


一、Spring 是什么

Spring 框架就像一个帮你打理代码里 "工具" 的智能收纳盒,核心作用是让写程序变得更简单。平时写代码时,你要用到的各种 "功能模块"(比如处理数据的组件、响应网页请求的组件),本来得自己一个个 "造出来"(比如手动创建对象),还得费劲琢磨这些模块之间怎么配合 ------ 比如要造辆车,得自己先做轮子、再做底盘、最后拼车身,少一步都不行,改个轮子尺寸还得重新调整底盘和车身。但有了 Spring,你不用自己 "造模块" 了:只要告诉 Spring 哪些模块需要它管,它就会把这些模块存进 "收纳盒" 里。等你要用某个模块时,也不用自己去找,Spring 会主动把你需要的模块 "递到手上"(这就是依赖注入)。要是某个模块要换版本、改功能,你也不用动其他代码,Spring 会帮你把新模块换好,不用你从头调整所有关联的部分。

1.1. 容器

"容器"泛指可容纳物品的工具,在IT领域特指一种轻量级、可移植的软件封装技术,将应用及其所有依赖(代码、库、配置文件等)打包,实现跨环境一致运行。

1.2. IoC

IoC 全称是控制反转,是 Spring 框架的核心思想,本质是把对象创建和管理的 "控制权" 从开发者编写的业务代码中,转移到了 Spring 容器手里。以前开发时,要用到某个对象(比如造汽车需要的轮子、底盘),得自己用 new 关键字手动创建,一旦底层对象有变化(比如轮子尺寸改了),所有依赖它的上层代码(底盘、车身、汽车)都得跟着改,代码耦合度很高。而有了 IoC,我们不用自己创建对象了,只需通过注解(比如 @Controller、@Service)告诉 Spring 哪些对象要交给它管理,Spring 启动时会自动创建这些对象并保存;等程序需要用某个对象时,也不用自己找,直接从 Spring 容器里获取(比如用 @Autowired 注入),哪怕底层对象变了,上层代码也不用修改,这样就解耦了代码,让程序更易维护。

以下是 Spring IoC 的官方文档:

二、IoC 介绍

如果我们现在有这么个需求,造一辆车。

2.1. 传统程序开发

按照传统程序开发的流程,我们想造一辆车,需要先造出车身,而车身需要依赖底盘,底盘又需要依赖轮子。

java 复制代码
public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.run();
    }
}
java 复制代码
public class Car {
    private FrameWork frameWork;

    public Car() {
        this.frameWork = new FrameWork();
        System.out.println("Car init......");
    }

    public void run() {
        System.out.println("Car run......");
    }
}
java 复制代码
public class FrameWork {
    private Bottom bottom;

    public FrameWork() {
        this.bottom = new Bottom();
        System.out.println("FrameWork init......");
    }
}
java 复制代码
public class Bottom {
    public Tire tire;

    public Bottom() {
        this.tire = new Tire();
        System.out.println("Tire init......");
    }
}
java 复制代码
public class Tire {
    private int size = 20;

    public Tire() {
        System.out.println("The size of tire: " + size);
    }
}

如果我们想给轮胎构造方法添加一个 size 参数,那么就需要把前面几个类里面的对象也需要修改,这就是"高耦合"。

2.2. 解决方案

在传统程序开发中,汽车(Car)、车身(Framework)、底盘(Bottom)、轮胎(Tire)存在严格的上层依赖下层关系,一旦最底层的轮胎(如尺寸)发生变化,整个调用链上的所有类都需同步修改,耦合度极高。针对这一问题,1.2.3节提出的解决方案核心是**倒置依赖关系并通过"注入传递"替代"内部创建"以实现解耦**。具体而言,首先改变依赖逻辑,从传统的"根据轮胎设计底盘、根据底盘设计车身、根据车身设计汽车",转变为"先确定汽车整体需求,再依次推导车身、底盘、轮胎的设计要求",使依赖关系倒置为"轮胎依赖底盘、底盘依赖车身、车身依赖汽车";其次,摒弃每个类自行创建下级依赖对象的方式,改为在类的外部创建下级依赖对象后,通过传递(注入)的方式将其提供给当前类(例如,汽车类不再内部创建车身对象,而是通过构造函数接收外部已创建好的车身对象)。这种方式下,即使下级依赖类的创建逻辑发生变化(如参数增减),当前类也无需修改任何代码,彻底切断了类与类之间的强耦合关联,最终实现程序的灵活适配与易维护性提升。

java 复制代码
public class Main {
    public static void main(String[] args) {
        Tire tire = new Tire(20);
        Bottom bottom = new Bottom(tire);
        FrameWork frameWork = new FrameWork(bottom);
        Car car = new Car(frameWork);
        car.run();
    }
}
java 复制代码
public class Tire {
    private int size = 20;

    public Tire(int size) {
        this.size = size;
        System.out.println("The size of tire: " + size);
    }
}
java 复制代码
public class Bottom {
    public Tire tire;

    public Bottom(Tire tire) {
        this.tire = tire;
        System.out.println("Tire init......");
    }
}
java 复制代码
public class FrameWork {
    private Bottom bottom;

    public FrameWork(Bottom bottom) {
        this.bottom = bottom;
        System.out.println("FrameWork init......");
    }
}
java 复制代码
public class Car {
    private FrameWork frameWork;

    public Car(FrameWork frameWork) {
        this.frameWork = frameWork;
        System.out.println("Car init......");
    }

    public void run() {
        System.out.println("Car run......");
    }
}

2.3. IoC 程序开发

在传统程序开发中,对象之间的依赖关系由开发者主动通过 new 关键字创建,例如上面"造汽车" 的案例:Car 需要自行创建 FrameWork,FrameWork 再创建 Bottom,Bottom 又创建 Tire,形成 "上层类控制下层类" 的依赖链。这种模式下,若底层类(如Tire的尺寸需要修改)发生变化,整个依赖链上的所有类(Bottom、FrameWork、Car)都需同步修改,导致代码耦合度极高,维护成本高。

而 IoC 程序开发彻底反转了这种控制权:不再由使用方类创建依赖对象,而是由 IoC 容器统一负责所有对象(即 Spring 中的 "Bean")的创建、初始化与生命周期管理。当应用程序中的某个类(如 Car)需要依赖其他对象(如 FrameWork)时,无需自行创建,只需通过依赖注入的方式(如构造函数注入、属性注入、Setter 注入),由 IoC 容器将预先创建好的依赖对象 "注入" 到当前类中。例如文档中改造后的造车案例,IoC 容器先创建 Tire 对象,再将其注入 Bottom,Bottom 注入 FrameWork,FrameWork 最终注入 Car,此时即便 Tire 的实现逻辑修改,Bottom、FrameWork、Car 等上层类无需任何调整,彻底解决了传统开发的高耦合问题。

2.4. IoC 优势

IoC 实现了资源的集中化管理,减少重复开发与配置成本。IoC 容器作为第三方管理者,统一负责对象(Bean)的创建、初始化、生命周期管控与配置,开发者无需在代码中重复编写 new 对象的逻辑,也无需关注对象创建的细节。

并且,IoC 提升了代码的灵活性与通用性,适配更多开发场景。一方面,依赖关系的 "倒置" 让程序设计更符合 "开闭原则"------ 新增功能时,只需在容器中新增 Bean 配置,现有业务逻辑无需修改,即可通过容器注入新的依赖实例;另一方面,IoC 的实现不依赖特定框架,例如构造方法注入基于 JDK 原生规范,即便更换开发框架,核心的依赖注入逻辑仍可复用,降低了技术选型的约束成本。

三、DI 介绍

Spring DI(Dependency Injection,依赖注入)是 Spring 框架核心思想 IoC(控制反转)的具体实现,其核心是容器在运行期间动态为应用程序提供运行时所依赖的资源(通常是对象),从而实现对象之间的解耦,让开发者无需手动创建依赖对象,专注于业务逻辑开发。

IoC 是一种设计思想,强调 "对象的控制权由开发者转移到 Spring 容器",DI 是 IoC 思想的落地实现,从 "应用程序视角" 描述 ------ 当应用程序需要某个依赖对象时,容器主动将该对象 "注入" 到应用程序中,而非应用程序自行创建。

四、Bean 的存储

共有两类注解类型可以实现:

  • 类注解:@Controller、@Service、@Repository、@Component、@Configuration.
  • 方法注解:@Bean

4.1. @Controller(存储器存储)

java 复制代码
package com.yang.test1_22_1.controller;

import org.springframework.stereotype.Controller;

@Controller
public class UserController {
    public void hello() {
        System.out.println("hello userController");
    }
}
java 复制代码
package com.yang.test1_22_1;

import com.yang.test1_22_1.controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1221Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1221Application.class, args);
        // 从上下文中获取 UserController 的 Bean 实例
        UserController bean = context.getBean(UserController.class);
        bean.hello();
    }

}

这里的 ApplicationContext(应用上下文)是 Spring 框架的核心接口之一,属于 IoC(控制反转)容器的高级形态。这个"应用上下文"可以理解为当程序正在运行时,"围绕着程序转的所有东西"------ 包括程序里的各种 "工具"(Bean)、工具之间的搭配规则、程序需要的配置等。

除了上述方式,还有其他的方式可以获取 Bean 实例。

|-------------------------------------------------|--------------------------------|---------|
| 方法 | 描述 | 返回值 |
| getBean(Class<T> requiredType) | 如果存在,返回唯一匹配给定对象类型的bean实例。 | <T> T |
| getBean(String name) | 返回指定bean的实例,该实例可以是共享的,也可以是独立的。 | Object |
| getBean(String name, @Nullable object ... args) | 返回指定bean的实例,该实例可以是共享的,也可以是独立的。 | Object |

java 复制代码
package com.example.test1_25_1;

import com.example.test1_25_1.user.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1251Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1251Application.class, args);
        // 从上下文中获取 UserController 的 Bean 实例
        UserController bean = context.getBean(UserController.class);
        bean.setName("Yang");
        bean.hello();

        UserController userController1 = (UserController) context.getBean("userController");
        userController1.hello();

        UserController userController2 = context.getBean("userController", UserController.class);
        userController2.hello();

        System.out.println(bean);
        System.out.println(userController1);
        System.out.println(userController2);
    }

}

我们会发现3个对象的内存地址的16进制的结果是一样的,这个其实也是一种实现单例模式的思想。

Spring Bean 命名核心规则:每个 Bean 拥有一个或多个标识符(主名 + 别名),所有标识符在托管该 Bean 的 IoC 容器中必须唯一;通常一个 Bean 仅需一个主标识符,多个标识符时额外的视为别名。

通用命名约定:遵循Java 实例字段的驼峰命名法 ,首字母小写,后续单词首字母大写,如accountManageruserDaologinController;统一命名可提升配置可读性,也便于 Spring AOP 按名称批量匹配 Bean。

自动命名规则:类路径组件扫描时,容器会为未显式命名 的组件自动生成 Bean 名,规则为:取类的简单类名 ,并按 java.beans.Introspector.decapitalize 规则转换:常规情况下,将简单类名首字母转为小写;特殊情况下,若类名前两个字符均为大写,则保留原始大小写。

java 复制代码
public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
        return name;
    }
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
            Character.isUpperCase(name.charAt(0))){
        return name;
    }
    char[] chars = name.toCharArray();
    chars[0] = Character.toLowerCase(chars[0]);
    return new String(chars);
}

如果我们把上面传入的参数改为"UserController",再次运行程序,就会出现"No bean named 'UserController' available"的异常,这就是因为Spring 容器在注册 Bean 时,默认会使用类名首字母小写作为 Bean 的名称。

还有一种错误,如果我们忘加了 @Controller 注解,就会出现"No qualifying bean of type 'com.example.test1_25_1.user.UserController' available"的异常,这就是没有将 UserController 实例化作为 Bean 管理起来。

4.2. @Service(服务存储)

java 复制代码
package com.yang.test1_25_2.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void hello() {
        System.out.println("Hello UserService...");
    }
}
java 复制代码
package com.yang.test1_25_2;

import com.yang.test1_25_2.service.UserService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1252Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1252Application.class, args);
        UserService bean = context.getBean(UserService.class);
        bean.hello();
    }

}

4.3. @Component(组件存储)

java 复制代码
package com.yang.test1_25_3.component;

import org.springframework.stereotype.Component;

@Component
public class UserComponent {
    public void hello() {
        System.out.println("Hello UserComponent...");
    }
}
java 复制代码
package com.yang.test1_25_3;

import com.yang.test1_25_3.component.UserComponent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1253Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1253Application.class, args);
        UserComponent bean = context.getBean(UserComponent.class);
        bean.hello();
    }

}

4.4. @Repository(仓库存储)

java 复制代码
package com.yang.test1_25_4.reposity;

import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    public void hello() {
        System.out.println("Hello UserRepository");
    }
}
java 复制代码
package com.yang.test1_25_4;

import com.yang.test1_25_4.reposity.UserRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1254Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1254Application.class, args);
        UserRepository bean = context.getBean(UserRepository.class);
        bean.hello();
    }

}

4.5. @Configuration(配置存储)

java 复制代码
package com.yang.test1_25_5.configuration;

import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfiguration {
    public void hello() {
        System.out.println("Hello UserConfiguration...");
    }
}
java 复制代码
package com.yang.test1_25_5;

import com.yang.test1_25_5.configuration.UserConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1255Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1255Application.class, args);
        UserConfiguration bean = context.getBean(UserConfiguration.class);
        bean.hello();
    }

}

4.6. 为什么需要多类注解

这个也是和咱们前面讲的应用分层是呼应的。让程序员看到类注解之后,就能直接了解当前类的用途。

  • @Controller:控制层,接收请求,对请求进行处理,并进行响应;
  • @Service:业务逻辑层,处理具体的业务逻辑;
  • @Repository:数据访问层,也称为持久层。负责数据访问操作;
  • @Configuration:配置层,处理项目中的一些配置信息;
  • @Component:组件。

这就像车牌号一样,车牌号是唯一的,用于标识一辆车。不同的省之间,车牌号的第一位的省份缩写不一样,紧跟的第二位字母也会因各省的地区不同。这样既可以节约号码,又可以直接了解这辆车的所属地。

同样地,在一些企业当中,分为表现层、业务逻辑层、数据层。同样可以根据不同的注解,提高源码的可读性,又能精准定位问题所在。

我们来查看下类注解直接的关系:

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

    boolean proxyBeanMethods() default true;

    /** @deprecated */
    @Deprecated(
        since = "7.0"
    )
    boolean enforceUniqueMethods() default true;
}

我们会发现,除了 Compnent 注解,其余的4个注解都依托于 Compnent,是 @Compnent 的一个元注解,这些注解也被称为 @Compnent 的衍生注解。

相关推荐
2 小时前
java关于键盘录入
java·开发语言
马猴烧酒.2 小时前
JAVA后端对象存储( 图片分享平台)详解
java·开发语言·spring·腾讯云
梅梅绵绵冰2 小时前
springboot初步2
java·spring boot·后端
独自破碎E2 小时前
【纵向扫描】最长公共前缀
java·开发语言
pp起床2 小时前
【苍穹外卖】Day03 菜品管理
java·数据库·mybatis
IT空门:门主2 小时前
Spring AI Alibaba使用教程
java·人工智能·spring
yaoxin5211232 小时前
303. Java Stream API - 查找元素
java·windows·python
weixin_462446232 小时前
Linux/Mac 一键自动配置 JAVA_HOME 环境变量(含 JDK 完整性校验)
java·linux·macos
虾说羊2 小时前
JWT的使用方法
java·开发语言