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

专栏:JavaEE 进阶跃迁营

个人主页:手握风云

目录

[一、方法注解 @Bean](#一、方法注解 @Bean)

[1.1. 需要配合类注解使用](#1.1. 需要配合类注解使用)

[1.2. 定义多个对象](#1.2. 定义多个对象)

[1.3. 重命名 Bean](#1.3. 重命名 Bean)

二、扫描路径

[三、DI 详解](#三、DI 详解)

[3.1. 属性注入](#3.1. 属性注入)

[3.2. 构造方法注入](#3.2. 构造方法注入)

[3.3. Setter 注入](#3.3. Setter 注入)


一、方法注解 @Bean

类注解需要添加到类上,但是有些外部包里面的类,这些文件是只读的,还有一个类可以 new 多个对象。这是我们就可以使用方法注解 @Bean。

1.1. 需要配合类注解使用

java 复制代码
package com.yang.test1_26_1.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
    private int age;
    private String name;

    // 表示这个方法返回的实例将被Spring管理
    @Bean
    public UserInfo userInfo() {
        return new UserInfo(18, "Jack");
    }
}
java 复制代码
package com.yang.test1_26_1;

import com.yang.test1_26_1.model.UserInfo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1261Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1261Application.class, args);

        UserInfo bean = context.getBean(UserInfo.class);
        System.out.println(bean);
    }

}

但我们一运行就会出现"NoSuchBeanDefinitionException"。

我们只需加上五大类注解就可以解决这个问题。

1.2. 定义多个对象

如果说在多数据源的场景下,类是同一个,但是配置不同,会指向不同的数据源

java 复制代码
package com.yang.test1_26_2;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Controller
public class UserInfo {
    private int age;
    private String name;

    @Bean
    public UserInfo userInfo1() {
        return new UserInfo(20, "Crane");
    }

    @Bean
    public UserInfo userInfo2() {
        return new UserInfo(18, "Larry");
    }
}
java 复制代码
package com.yang.test1_26_2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1262Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1262Application.class, args);

        UserInfo bean1 = context.getBean(UserInfo.class);
        System.out.println(bean1);
    }

}

报错信息中显示,期望只有一个匹配,结果发现了两个对象 userInfo1 和 userInfo2,从报错信息中可以看出,通过 @Bean 定义的对象, Bean Name 就是方法名。

java 复制代码
package com.yang.test1_26_2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1262Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1262Application.class, args);

        UserInfo bean1 = (UserInfo) context.getBean("userInfo1");
        System.out.println(bean1);

        UserInfo bean2 = (UserInfo) context.getBean("userInfo2");
        System.out.println(bean2);
    }

}

1.3. 重命名 Bean

可以通过设置 name 属性给 Bean 对象进行重命名操作。我们先来看下类注解的重命名。

java 复制代码
package com.yang.test1_27_1;

import org.springframework.stereotype.Controller;

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1271Application {

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

}

方法注解的重命名:

java 复制代码
package com.yang.test1_27_1;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Controller
public class UserInfo {
    private int age;
    private String name;

    @Bean("userInfoOne")
    public UserInfo userInfo1() {
        return new UserInfo(20, "Crane");
    }

    @Bean("userInfoTwo")
    public UserInfo userInfoT2() {
        return new UserInfo(18, "Larry");
    }
}
java 复制代码
package com.yang.test1_27_1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1271Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1271Application.class, args);

        UserInfo bean1 = (UserInfo) context.getBean("userInfoOne");
        System.out.println(bean1);

        UserInfo bean2 = (UserInfo) context.getBean("userInfoTwo");
        System.out.println(bean2);
    }

}

@Bean 注解的属性值,可以起多个别名。默认情况下,Bean 的名字是方法名,但是,一旦显式指定了 value 或 name 属性,方法名将不再是 Bean 的名字,Bean 的名字将严格由数组中的元素决定。这里的别名集合实际上是一个字符串数组,在 Spring 中,如果数组里面只有一个元素,"{}" 可以省略不写。

java 复制代码
package com.yang.test1_28_1;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Controller
public class UserInfo {
    private int age;
    private String name;

    @Bean({"userInfoOne", "u1"})
    public UserInfo userInfo1() {
        return new UserInfo(20, "Crane");
    }

    @Bean({"userInfoTwo", "u2"})
    public UserInfo userInfoT2() {
        return new UserInfo(18, "Larry");
    }
}
java 复制代码
package com.yang.test1_28_1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1281Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1281Application.class, args);

        UserInfo bean1 = (UserInfo) context.getBean("u1");
        System.out.println(bean1);

        UserInfo bean2 = (UserInfo) context.getBean("u1");
        System.out.println(bean2);
    }

}

二、扫描路径

SpringBoot 有个特点:约定大于配置。每个 Spring FrameWork 的启动类的路径非常关键,这个决定了那个路径对应下的对象有效。使用五大类注解声明的 Bean,并非一定能在 Spring 容器中生效,关键前提是这些 Bean 所在的类必须被 Spring 扫描到。若未被扫描,即使添加了类注解,Spring 也无法识别并管理该 Bean,会出现 "找不到 Bean" 的异常。

扫描路径的配置方式,使用 @ComponentScan 注解。

java 复制代码
@ComponentScan({"com.example.demo.component", "com.example.demo.service"}) // 扫描多个包
@SpringBootApplication
public class SpringIocDemoApplication { ... }

日常开发中,多数场景无需显式添加 @ComponentScan,核心原因是 @SpringBootApplication 的源码包含 @ComponentScan ,默认扫描范围是 SpringBoot 启动类所在的包及其所有子包

三、DI 详解

依赖注入是一个过程,是指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象。

关于依赖注入, Spring 提供了3种方式:

  1. 属性注入(Field Injection)
  2. 构造方法注入(Constructor Injection)
  3. Setter 注入(Setter Injection)

3.1. 属性注入

属性注入是 Spring 依赖注入(DI)的三种核心方式之一,其本质是通过 @Autowired 注解,将 IoC 容器中已管理的 Bean(对象)直接注入到目标类的成员属性中,实现目标类对依赖对象的使用,无需手动创建依赖对象实例。比如下面的代码将 UserService 类注入 UserRepository。在 UserRepository 类的私有成员属性上添加 @Autowired 注解,Spring 会自动从容器中找到对应类型的 Bean 并注入。

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

import org.springframework.stereotype.Service;

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

import com.yang.test1_28_2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    @Autowired
    private UserService userService;

    public void hello() {
        userService.hello();
        System.out.println("hello, Repository...");
    }
}
java 复制代码
package com.yang.test1_28_2;

import com.yang.test1_28_2.repo.UserRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Test1282Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1282Application.class, args);

        UserRepository bean = context.getBean(UserRepository.class);
        bean.hello();
    }

}

通过 Spring 上下文获取 UserRepository 对象并调用方法,可观察到注入成功:

如果去掉 @Autowired 的注解,Spring 不会注入依赖对象,调用方法时就会抛出空指针异常 NullPointerException。

优点:

  • 简洁易用 :代码侵入性低,仅需在属性上添加 @Autowired 注解,无需编写构造方法或 Setter 方法,实现快速注入。

缺点:

  • 依赖 IoC 容器 :属性注入仅在 Spring IoC 容器环境中生效,若脱离容器(如手动 new 目标类),依赖对象无法注入,会出现 NPE。
  • 无法注入 final 属性 :如上文所述,final 属性需初始化时赋值,与属性注入的执行时机冲突。
  • 潜在的注入失败风险:注入逻辑隐藏在注解中,若依赖 Bean 不存在或类型不匹配,仅在运行时才会暴露异常(编译期无法感知)。

3.2. 构造方法注入

构造方法注入是 Spring 依赖注入(DI)的核心方式之一,其核心逻辑是:通过目标类的构造方法,将 IoC 容器中已管理的依赖 Bean 传入目标类。IoC 容器在创建目标类实例时,会自动解析构造方法的参数类型,从容器中找到匹配的依赖 Bean 并完成注入,确保目标类在初始化阶段就拥有所需的依赖资源。

如果只有一个构造方法,@Autowired 注解可以省略掉,因为Spring 容器也会自动通过该构造方法注入匹配的依赖 Bean;若目标类存在多个构造方法,必须在需要用于注入的构造方法上添加 @Autowired 注解,明确指定容器使用的构造方法,否则会报错。

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

import com.yang.test1_28_2.service.UserService;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    private UserService userService;

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

    public void hello() {
        userService.hello();
        System.out.println("hello, Controller...");
    }
}
java 复制代码
package com.yang.test1_28_2;

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

@SpringBootApplication
public class Test1282Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1282Application.class, args);

        UserController bean = context.getBean(UserController.class);
        bean.hello();
    }

}

优点:

  1. 支持注入 final 修饰的属性 final修饰的属性需在对象初始化阶段(构造方法执行时)完成赋值,而构造方法注入恰好在此阶段执行,因此可注入 final 依赖(解决了属性注入无法注入 final 属性的问题)。示例:

    java 复制代码
    @Controller
    public class UserController2 {
        private final UserService userService; // final 修饰的依赖
        
        // 构造方法注入 final 属性
        public UserController2(UserService userService) {
            this.userService = userService; 
        }
    }
  2. 注入的依赖对象不可修改依赖对象通过构造方法赋值后,若未提供 Setter 方法,外部无法修改该依赖对象,避免了依赖被意外篡改的风险,保证了对象的稳定性。

  3. 依赖对象初始化完全构造方法是类实例化的核心逻辑,依赖注入在构造方法中执行,意味着目标类在创建完成后,所有依赖已完全初始化,不会出现 "依赖未就绪就被使用" 的情况(避免了属性注入可能出现的运行时空指针风险)。

  4. 通用性强构造方法是 JDK 原生支持的语法,不依赖 Spring 框架特性,即使更换其他 IoC 框架,构造方法注入的逻辑依然可用,兼容性更高。

缺点:

  • 注入多个依赖时代码繁琐 若目标类需要依赖多个 Bean(如同时依赖 UserServiceUserRepository),构造方法的参数会增多,代码可读性和维护性下降。
java 复制代码
// 依赖较多时,构造方法参数冗长
public UserController2(UserService userService, UserRepository userRepository, UserConfig userConfig) {
    this.userService = userService;
    this.userRepository = userRepository;
    this.userConfig = userConfig;
}

3.3. Setter 注入

Setter 注入是 Spring 依赖注入(DI)的三种核心方式之一,其核心逻辑是:通过目标类的 Setter 方法(属性的赋值方法)配合 @Autowired 注解,让 IoC 容器在创建目标类实例后,主动调用 Setter 方法将容器中的依赖 Bean 注入到目标类中

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

import com.yang.test1_28_2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    private UserService userService;

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

    public void hello() {
        userService.hello();
        System.out.println("hello, Controller...");
    }
}
java 复制代码
package com.yang.test1_28_2;

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

@SpringBootApplication
public class Test1282Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Test1282Application.class, args);

        UserController bean = context.getBean(UserController.class);
        bean.hello();
    }

}

优点:

  1. 支持实例化后重新配置依赖Setter 注入的核心优势是 "灵活性":目标对象创建后,可通过再次调用 Setter 方法修改依赖对象(例如切换数据源、更新配置类等),适合依赖需要动态调整的场景。

    java 复制代码
    // 对象创建后,可重新注入新的 UserService 实例
    UserController3 controller3 = context.getBean(UserController3.class);
    controller3.setUserService(new UserService() { // 新的实现
        @Override
        public void sayHi() {
            System.out.println("Hi, 新的 UserService 实现");
        }
    });
    controller3.sayHi(); // 输出:Hi, 新的 UserService 实现
  2. 代码可读性较强Setter 方法明确对应某个属性的赋值逻辑,通过方法名可直接关联依赖对象,便于理解 "哪个依赖通过哪个方法注入"。

缺点:

  1. 无法注入 final修饰的属性 final 修饰的属性需在对象初始化阶段(构造方法执行时)完成赋值,而 Setter 注入在对象创建后执行,此时 final 属性已无法修改,因此 Setter 注入不支持 final 依赖。

  2. 注入对象存在被篡改的风险由于 Setter 方法可被外部多次调用,若代码中存在误操作(如重复调用 Setter 方法覆盖依赖),可能导致依赖对象被意外修改,破坏对象的稳定性。

  3. 依赖对象可能未就绪 Setter 注入在对象创建后执行,若目标类在依赖注入前就调用了依赖对象的方法(如在构造方法中使用 userService),会因依赖未注入而抛出 NullPointerException,需额外注意代码执行顺序。

  4. 通用性较弱 Setter 注入依赖 Spring 的 @Autowired 注解,是 Spring 框架特有的注入方式,若脱离 Spring 容器(如手动 new 目标类),需手动调用 Setter 方法赋值,兼容性不如构造方法注入(构造方法是 JDK 原生支持)。

相关推荐
组合缺一2 小时前
论 AI Skills 分布式发展的必然性:从单体智能到“云端大脑”的跃迁
java·人工智能·分布式·llm·mcp·skills
砚边数影2 小时前
决策树原理(一):信息增益与特征选择 —— Java 实现 ID3 算法
java·数据库·决策树·机器学习·kingbase·数据库平替用金仓·金仓数据库
让我上个超影吧2 小时前
天机学堂——BitMap实现签到
java·数据库·spring boot·redis·spring cloud
迷路爸爸1802 小时前
无sudo权限远程连接Ubuntu服务器安装TeX Live实操记录(适配VS Code+LaTeX Workshop,含路径选择与卸载方案)
java·服务器·ubuntu·latex
有梦想的攻城狮2 小时前
maven中的os-maven-plugin插件的使用
java·maven·maven插件·os-maven-plugin·classifer
Carry灭霸2 小时前
【BUG】Redisson Connection refused 127.0.0.1
java·redis
消失的旧时光-19432 小时前
第九课实战版:异常与日志体系 —— 后端稳定性的第一道防线
java·后端
钦拆大仁2 小时前
Java设计模式-状态模式
java·设计模式·状态模式
人道领域2 小时前
javaWeb从入门到进阶(SpringBoot基础案例2)
java·开发语言·mybatis