Spring IoC详解

发布于:2024-03-29 ⋅ 阅读:(11) ⋅ 点赞:(0)

1.什么是IoC

Spring IoC(Inversion of Control,控制反转)是Spring框架的核心之一,它是一种设计模式,也称为依赖注入(Dependency Injection,DI)。在传统的程序设计中,对象之间的依赖关系通常由程序员在类内部直接创建和管理,而在IoC容器中,对象的创建和管理由容器来负责,程序员只需要定义对象的依赖关系,由容器来实现对象的创建和组装,即把对象的控制权交给IoC容器,所以叫控制反转。

接下来我们通过一个例子来类比解释什么是IoC

1.1 传统程序的开发

需求:造一辆汽车。

实现思路:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘大小设计车身,最后根据车身设计好整个汽车。在这个过程中出现了一个“依赖”关系,汽车依赖车身,车身依赖底盘,底盘依赖轮子。

最后实现的代码如下:

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.run();
    }
}

class Car {
    private Framework framework;
    public Car() {
        framework = new Framework();
        System.out.println("car init");
    }
    public void run() {
        System.out.println("car run run!");
    }
}

//车身
class Framework {
    private Bottom bottom;
    public Framework() {
        bottom = new Bottom();
        System.out.println("Framework init");
    }
}

//底盘
class Bottom {
    private Tire tire;
    public Bottom() {
        tire = new Tire();
        System.out.println("bottom init");
    }
}

//轮子
class Tire {
    //轮子尺寸
    private int size;
    public Tire() {
        this.size = 15;
        System.out.println("tire init");
    }
}

如果现在要求轮子的大小要为20,我们就要给Tire类添加一个有参数的构造方法,与此同时,Bottom类,Framework类,Car类都需要修改,如果,我现在又要给轮子添加一个属性 颜色 此时所有依赖Tire类的类又都要修改,由此可见这样的开发方式是存在弊端的。 

1.2 IoC 程序开发

public class Main {
    public static void main(String[] args) {
        Tire tire = new Tire();
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
        car.run();
    }
}

class Car {

    public Car(Framework framework) {
        System.out.println("car init");
    }
    public void run() {
        System.out.println("car run run!");
    }
}

//车身
class Framework {
    public Framework(Bottom bottom) {
        System.out.println("Framework init");
    }
}

//底盘
class Bottom {
    public Bottom(Tire tire) {
        System.out.println("bottom init");
    }
}

//轮子
class Tire {
    //轮子尺寸
    private int size;
    public Tire() {
        this.size = 15;
        System.out.println("tire init");
    }
}

我们把创建对象交给Main类去做,达到了其他类之间的解耦合效果,无论其他类怎么变化,整个的调用链是不需要调整的,这里我们的Main类就可以称作IoC容器。

在Spring中,IoC容器负责管理Java对象之间的依赖关系。当应用程序启动时,IoC容器会读取配置文件或注解信息,根据这些信息来实例化对象,并将这些对象之间的依赖关系注入到对象中。这种方式使得应用程序的组件之间解耦,易于维护和扩展。

1.3 DI

DI,全称为依赖注入(Dependency Injection),是面向对象编程中的一种设计模式。它是控制反转(Inversion of Control,IoC)思想的一种具体实现方式。

简单来说,依赖注入是指在创建对象时,将其所依赖的其他对象的引用注入到对象中,而不是由对象自己创建或查找依赖的对象。这样做的目的是降低模块之间的耦合度,使得对象之间的依赖关系更加灵活、可维护和易于测试。

2. IoC/DI 使用

我们已经对IoC和DI有初步的了解,接下来我们具体学习Spring IoC 和 DI的代码实现。

Spring 容器管理的主要是对象,这些对象,我们称之为“Bean” ,我们把这些对象交由Spring管理,由Spring来负责对象的创建和销毁,我们程序只需要告诉Spring,哪些需要存,以及如何从Spring取出对象。

使用@Component注解,可以表示把某个类交给Spring来管理,使用@Autowired注解注入运行时依赖的对象:

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.run();
    }
}
@Component
class Car {
    @Autowired
    private Framework framework;
    public Car() {
        System.out.println("car init");
    }
    public void run() {
        System.out.println("car run run!");
    }
}

//车身
@Component
class Framework {
    @Autowired
    private Bottom bottom;
    public Framework() {
        System.out.println("Framework init");
    }
}

//底盘
@Component
class Bottom {
    @Autowired
    Tire tire;
    public Bottom() {
        System.out.println("bottom init");
    }
}

//轮子
@Component
class Tire {
    //轮子尺寸
    private int size;
    public Tire() {
        this.size = 15;
        System.out.println("tire init");
    }
}

3. IoC详解

3.1 Bean的存储

在上面的案例中,我们知道要把某个对象交给IoC容器管理,需要在类上添加一个注解:@Component 而Spring框架为了更好的服务Web程序,提供了更丰富的注解:

  1. 类注解:@Controller,@Service,@Repository,@Component,@Configuration
  2. 方法注解:@Bean
3.1.1  类注解
  1. @Controller
    @Controller 注解用于标识一个类作为Spring MVC框架中的控制器,负责处理用户请求,并返回相应的视图。通常用于标识处理HTTP请求的控制器类,可以与 @RequestMapping 注解结合使用,实现请求映射和页面跳转。
  2. @Service
    @Service 注解用于标识一个类作为业务层的组件,通常用于标识服务层的类,通过该注解告诉Spring该类是业务逻辑处理的组件,可以被注入到其他类中使用。
  3. @Repository
    @Repository 注解用于标识一个类作为数据访问层的组件,通常用于标识数据访问对象(DAO)类,通过该注解告诉Spring该类是用于数据库访问的组件,可以捕获数据库异常并将其重新抛出为Spring的数据访问异常。
  4. @Component
    @Component 注解是Spring中所有具体组件的通用形式,用于标识一个类作为Spring容器管理的组件,表示该类会自动被Spring加载,并可以通过依赖注入来使用。
  5. @Configuration
    @Configuration 注解用于标识一个类作为配置类,通常用于定义Bean的创建和依赖关系。该注解通常与 @Bean 注解一起使用,用于替代XML配置文件,实现Java Config的方式进行配置。

 这个和我们上期讲的应用分层是呼应的,让程序员看到类注解后,就能知道当前类的用途:

使用演示:

这些注解的使用方式都相同,我们通过@Controller来演示:

@Controller//将对象储存到Spring中
public class UserController {
    public void say() {
        System.out.println("Hello @Controller");
    }
}

如上述代码,我们只需在类上面加上@Controltroller 注解即可把该类的对象储存在Spring中。

我们如何知道这个对象已经存在Spring容器中了呢,接下来我们学习如何从Spring容器中获取对象:

@SpringBootApplication
public class J20240323SpringIocApplication {
    public static void main(String[] args) {
        //获取Spring上下文对象
        ApplicationContext context = SpringApplication.run(J20240323SpringIocApplication.class, args);
        //从Spring上下文中获取对象
        UserController userController = context.getBean(UserController.class);
        //调用say方法
        userController.say();
    }
}

可以看到,项目启动后成功打印了 Hello @Controller

解释:

ApplicationContext:译为程序上下文,用于表示 Spring 容器,并且负责管理 Bean 的生命周期和配置信息。即可以理解为包含了Spring容器里的所有内容,所有我们可以从ApplicationContext对象中获取Bean.

getBean()是BeanFactory接口中的一个方法,ApplicationContext实现了BeanFactory接口,我们打开BeanFactory的源码:

我们发现重载了五个getBean方法 ,我们刚才是通过类对象获取的Bean,所以我们使用的是第四个,我们接下来再学习第一个和第二个的使用:

1. getBean(String name)

根据Bean的名称获取Bean,Spring会给管理的Bean按照小驼峰的方式命名:

@SpringBootApplication
public class J20240323SpringIocApplication {
    public static void main(String[] args) {
        //获取Spring上下文对象
        ApplicationContext context = SpringApplication.run(J20240323SpringIocApplication.class, args);
        //从Spring上下文中获取对象

        //通过类对象获取
        UserController userController1 = context.getBean(UserController.class);
        userController1.say();

        //通过对象名获取
        UserController userController2 = (UserController)context.getBean("userController");
        userController2.say();
    }
}

注意:Spring官方文档中关于Bean的名称有这样一个注意事项:

 当类名的前两个字符都是大写字母时,将按照类起初的名字来命名。

2. getBean(String name, Class<T> requiredType)

通过名称和类对象获取Bean:

@SpringBootApplication
public class J20240323SpringIocApplication {
    public static void main(String[] args) {
        //获取Spring上下文对象
        ApplicationContext context = SpringApplication.run(J20240323SpringIocApplication.class, args);
        //从Spring上下文中获取对象

        //通过类对象获取
        UserController userController1 = context.getBean(UserController.class);
        userController1.say();

        //通过对象名获取
        UserController userController2 = (UserController)context.getBean("userController");
        userController2.say();

        //通过类对象和对象名获取
        UserController userController3 = context.getBean("userController", UserController.class);
        userController3.say();
    }
}

注意:上述几种方式获取到的对象都是同一个对象,把类交给Spring管理时,默认是单例模式。 

ApplicationContext对比BeanFactory:

上面我们演示的是使用ApplicationContext来获取Bean,我们也可以使用BeanFactory获取Bean,接下来我们讲解一下它们的区别和联系

区别:

  1. 初始化时机:BeanFactory 是在第一次请求获取 Bean 时才进行实例化和初始化,延迟加载;而 ApplicationContext 在容器启动时就将所有 Bean 进行实例化和初始化。
  2. 性能:ApplicationContext 比 BeanFactory 更强大也更复杂,因为 ApplicationContext 支持更多的特性,如国际化处理、事件传播、AOP 管理等,所以在性能上 BeanFactory 会更轻量级一些。
  3. 功能:ApplicationContext 继承自 BeanFactory,并且提供了更多的高级特性,如自动装配、消息资源处理、事件传播等。因此,ApplicationContext 功能更加强大。

联系:

  1. ApplicationContext 可以看作是 BeanFactory 的扩展版本,提供了更多功能和便利,是更高级的容器接口。
  2. 无论是 BeanFactory 还是 ApplicationContext,它们都负责管理 Bean 实例的生命周期、依赖注入等工作,是 Spring 框架中重要的组件。
  3. 在实际开发中,如果应用对性能要求较高,可以使用 BeanFactory;如果需要更多的企业级特性和功能,建议使用 ApplicationContext。通常情况下,我们更倾向于使用 ApplicationContext,因为它提供了更多的便利和功能,能够更好地支持复杂的应用场景。 
3.1.2 方法注解

类注解是添加到类上面的,但是存在两个问题:

  • 使用外部包里的类,没办法添加类注解
  • 一个类需要多个对象

这种场景就需要使用方法注解@Bean

使用方法:

@Bean注解需要在类注解注解下的类中使用,使用时创建一个函数,返回一个想要交给Spring管理的对象即可:

@Controller
public class UserController {
    public void say() {
        System.out.println("Hello @Controller");
    }
    @Bean
    public User getUser1() {
        return new User();
    }
    @Bean
    public User getUser2() {
        return new User();
    }
}

注意:使用@Bean注解交给Spring管理的对象的名称和方法名一致,获取Bean时,如果对应类的对象在Spring中存在多个,则不能只通过类对象获取:

@SpringBootApplication
public class J20240323SpringIocApplication {
    public static void main(String[] args) {
        User user1 = (User) context.getBean("getUser1", User.class);
        user1.say();
        User user2 = (User) context.getBean("getUser2", User.class);
        user2.say();
        System.out.println(user1 == user2);
    }
}

3.2 扫描路径

一个对象要被Spring管理,不仅需要加上注解,还需要被Spring扫描到才行,Spring的默认扫描路径是启动类所在的目录及子目录,我们可以通过@ComponentScan注解修改扫描路径:

@ComponentScan(basePackages = "com.example")
@SpringBootApplication
public class J20240323SpringIocApplication {
    public static void main(String[] args) {
        SpringApplication.run(J20240323SpringIocApplication.class, args);
    }
}

4. DI 详解

4.1 属性注入

属性注入就是通过@Autowired注解,注入,前面我们简单的演示过,只需把@Autowired写在对应的引用上即可获取对应对像的引用

@Service
public class User {
    public void say() {
        System.out.println("helle");
    }
}
@Controller
public class UserController {
    @Autowired
    private User user;
    public void say() {
        user.say();
    }
}

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

        UserController userController = context.getBean(UserController.class);
        userController.say();
    }
}

4.2 构造方法注入

我只需在需要引入依赖的类中添加一个初始化的构造方法,即可完成依赖注入

@Controller
public class UserController {
    private User user;
    public UserController(User user) {
        this.user = user;
    }
    public void say() {
        user.say();
    }
}

运行代码后同样能打印hello

注意:交给Spring管理的类,Spring会使用反射获取到对应类的构造方法来创建对象,如果使用构造方法注入,并且本类中存在多个构造函数,需要使用@Autowired指定使用哪个,否者可能会出错:

@Controller
public class UserController {
    private User user;
    public UserController() {

    }
    @Autowired
    public UserController(User user) {
        this.user = user;
    }
    public void say() {
        user.say();
    }
}

4.3 Setter 方法注入

给需要注入依赖的类添加带@Autowired注解的set方法也可以注入依赖:

@Controller//将对象储存到Spring中
public class UserController {
    @Autowired
    public UserController(User user) {
        this.user = user;
    }
    public void say() {
        user.say();
    }
}

4.4 三种注入方式的优缺点 

4.4.1 属性注入
  • 优点:

    • 简洁,使用方便。
  • 缺点:

    • 只能用于IoC容器,非IoC容器不可用,并且只有在使用的时候才会出现空指针异常(NPE)。
    • 不能注入一个Final修饰的属性。
4.4.2 构造函数注入
  • 优点:

    • 可以注入final修饰的属性。
    • 注入的对象不会被修改。
    • 依赖对象在使用前一定会被完全初始化,因为依赖是在类的构造方法中执行的,而构造方法是在类加载阶段就会执行的方法。
    • 通用性好,构造方法是JDK支持的,所以更换任何框架,它都是适用的。
  • 缺点:

    • 注入多个对象时,代码会比较繁琐。
4.4.3 Setter注入
  • 优点:

    • 方便在类实例化之后,重新对该对象进行配置或者注入。
  • 缺点:

    • 不能注入一个Final修饰的属性。
    • 注入对象可能会被改变,因为setter方法可能会被多次调用,就有被修改的风险。

根据具体情况和需求,可以选择适合的注入方式来管理对象之间的依赖关系。

4.5 @Autowired存在的问题

当同一类型存在多个Bean时,使用@Autowired 会存在问题:

public class User {
    String name;
    public User() {

    }
    public User(String name) {
        this.name = name;
    }
    public void say() {
        System.out.println(name);
    }
}
@Configuration
public class UserConfig {
    @Bean
    public User User1() {
        return new User("a");
    }
    @Bean
    public User User2() {
        return new User("b");
    }
    @Bean
    public User User3() {
        return new User("c");
    }
}
@Controller//将对象储存到Spring中
public class UserController {
    @Autowired
    public void say() {
        user.say();
    }
}

项目启动失败,我们在描述这里看到,Spring找到了三个对象 。使用@Autowired注入时,Spring会先根据对象的类型去寻找,如果只找到一个就直接用,如果找到多个,再根据名称(这里我们是user)来寻找,如果没有对应名称就会抛异常。

我们可以使用 @Primary ,@Qualifier,@Resource 这三个注解来解决。

@Primary:指定默认使用哪一个Bean

@Configuration
public class UserConfig {
    @Primary
    @Bean
    public User User1() {
        return new User("a");
    }
    @Bean
    public User User2() {
        return new User("b");
    }
    @Bean
    public User User3() {
        return new User("c");
    }
}

@Qualifier:指定使用Bean的名称

@Controller//将对象储存到Spring中
public class UserController {
    @Qualifier("User2")
    @Autowired
    private User user;
    public void say() {
        user.say();
    }
}

使用@Resource注入依赖:

@Controller//将对象储存到Spring中
public class UserController {
    @Resource(name = "User3")
    private User user;
    public void say() {
        user.say();
    }
}

5. 总结

  1. Spring: Spring 是一个开源的Java框架,它提供了丰富的功能和组件,用于简化企业级应用程序的开发。Spring 框架提供了依赖注入、面向切面编程、事务管理、数据访问等功能,是一个全面的企业应用开发解决方案。
  2. Spring MVC: Spring MVC 是 Spring 框架中的一个模块,即Spring功能的一部分,用于构建 Web 应用程序的 MVC(Model-View-Controller)架构。Spring MVC 提供了基于注解的方式来定义控制器、处理请求和渲染视图,使得开发 Web 应用程序更加简单和灵活。
  3. Spring Boot: Spring Boot 是基于 Spring 框架的快速开发框架,它简化了 Spring 应用程序的搭建和部署过程。Spring Boot 提供了自动配置、约定优于配置、快速启动等特性,使得开发者可以更快速地搭建和运行 Spring 应用程序,而不需要进行繁琐的配置。简单来说Spring Boot 就是对Spring的使用做了一些简化使我们开发效率更高,但其所有的功能都是基于Spring实现的。

 

本文含有隐藏内容,请 开通VIP 后查看