JavaEE--SpringIoC

目录

一、IoC概述

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

[2. 问题分析](#2. 问题分析)

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

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

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

二、IoC详解

[1. Bean的存储](#1. Bean的存储)

[1.1 @Controller(控制器存储)](#1.1 @Controller(控制器存储))

[1.2 @Service(服务存储)](#1.2 @Service(服务存储))

[1.3 @Repository(仓库存储)](#1.3 @Repository(仓库存储))

[1.4 @Component(组件存储)](#1.4 @Component(组件存储))

[1.5 @Configuration(配置存储)](#1.5 @Configuration(配置存储))

[2. 为什么需要这么多注解?](#2. 为什么需要这么多注解?)

[3. 方法注解@Bean](#3. 方法注解@Bean)

[3.1 方法注解要配合类注解使用](#3.1 方法注解要配合类注解使用)

[3.2 定义多个对象](#3.2 定义多个对象)

[3.3 重命名Bean](#3.3 重命名Bean)

[3.4 扫描路径](#3.4 扫描路径)


一、IoC概述

控制反转(Inversion of Control,IoC)是一种设计原则,用于解耦组件之间的依赖关系。传统编程中,调用者主动创建和管理依赖对象;而IoC将控制权交给外部容器或框架,由容器负责创建和注入依赖对象。

接下来我们通过案例来了解一下什么是IoC。

需求:造一辆车

1. 传统程序开发

我们的设计思路是这样的:

先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据底盘设计车身(Framework),最后根据车身设计好整个汽车(Car)。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。

最终程序的实现代码如下:

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

    /**
     * 汽车对象
     */
    static class Car {
        private Framework framework;

        public Car() {
            framework = new Framework();
            System.out.println("Car init...");
        }

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

    /**
     * 车身类
     */
    static class Framework {
        private Bottom bottom;

        public Framework() {
            bottom = new Bottom();
            System.out.println("Framework init...");
        }
    }

    /**
     * 底盘类
     */
    static class Bottom {
        private Tire tire;

        public Bottom() {
            this.tire = new Tire();
            System.out.println("Bottom init...");
        }
    }

    /**
     * 轮胎类
     */
    static class Tire {
        // 尺寸
        private int size;

        public Tire() {
            this.size = 17;
            System.out.println("轮胎尺寸: " + size);
        }
    }
}

2. 问题分析

这样的设计看起来没问题,但是可维护性却很低。

如果接下来需求有了变更:随着对车的需求量越来越大,个性化需求也会越来越多,我们需要加工多种尺寸的轮胎。

那这个时候就需要对上面的程序进行修改,修改后的代码如下所示:

修改之后,其他调用程序也会报错,我们需要继续修改:

完整代码如下:

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

    /**
     * 汽车对象
     */
    static class Car {
        private Framework framework;

        public Car(int size) {
            framework = new Framework(size);
            System.out.println("Car init...");
        }

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

    /**
     * 车身类
     */
    static class Framework {
        private Bottom bottom;

        public Framework(int size) {
            bottom = new Bottom(size);
            System.out.println("Framework init...");
        }
    }

    /**
     * 底盘类
     */
    static class Bottom {
        private Tire tire;

        public Bottom(int size) {
            this.tire = new Tire(size);
            System.out.println("Bottom init...");
        }
    }

    /**
     * 轮胎类
     */
    static class Tire {
        // 尺寸
        private int size;

        public Tire(int size) {
            this.size = size;
            System.out.println("轮胎尺寸: " + size);
        }
    }
}

从以上代码可以看出:当最底层代码改动之后,整个调用链上的所有代码都需要修改(程序的耦合度非常高)。

3. 解决方案

我们尝试换一种思路,我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘,底盘依赖车身,车身依赖汽车。

如何来实现呢:

我们可以尝试不在每个类中自己创建下级类,如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改。此时,我们只需要将原来由自己创建的下级类,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。

4. IoC程序开发

基于以上思路,我们把调用汽车的程序示例改造一下,把创建子类的方式,改为注入传递的方式。

具体实现代码如下:

java 复制代码
public class IocCarExample {
    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();
    }

    static 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...");
        }
    }

    static class Framework {
        private Bottom bottom;

        public Framework(Bottom bottom) {
            this.bottom = bottom;
            System.out.println("Framework init...");
        }
    }

    static class Bottom {
        private Tire tire;

        public Bottom(Tire tire) {
            this.tire = tire;
            System.out.println("Bottom init...");
        }
    }

    static class Tire {
        private int size;

        public Tire(int size) {
            this.size = size;
            System.out.println("轮胎尺寸: " + size);
        }
    }
}

代码经过以上调整,无论底层类如何变化,整个调用链是不用做任何改变的,这样就完成了代码之间的解耦,从而实现了更加灵活、通用的程序设计了。

5. IoC优势

  • 降低耦合度:组件不直接依赖具体实现。
  • 增强可测试性:便于模拟依赖进行单元测试。
  • 提高可维护性:依赖关系集中管理,修改时影响小。
  • 灵活性:运行时动态替换依赖实现。

Spring就是一种IoC容器,帮助我们来做了这些资源管理。

二、IoC详解

1. Bean的存储

Spring框架为了更好的服务web应用程序,提供了更丰富的注解。

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

  1. 类注解:@Controller、@Service、@Repository、@Component、@Configuration
  2. 方法注解:@Bean
1.1 @Controller(控制器存储)

使用@Controller存储bean的代码如下所示:

java 复制代码
import org.springframework.stereotype.Controller;

@Controller
public class HelloController {
    public void print() {
        System.out.println("Hello control");
    }
}

从Spring容器中获取这个对象:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        //获取Spring上下文对象
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        //从Spring上下文中获取对象
        HelloController bean = context.getBean(HelloController.class);
        //使用对象
        bean.print();
    }
}

ApplicationContext:Spring上下文

因为对象都交给Spring管理了,所以获取对象要从Spring中获取,那么就得下得到Spring的上下文。

观察运行结果,发现成功从Spring中获取到Controller对象,并执行Controller的print方法。

若把@Controller删掉,观察运行结果:

报错信息显示:找不到bean。

ApplicationContext也提供了其他获取bean的方式,ApplicationContext获取bean对象的功能,是父类BeanFactory提供的功能。

java 复制代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.beans.factory;

import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";
    char FACTORY_BEAN_PREFIX_CHAR = '&';
    //1
    Object getBean(String name) throws BeansException;
    //2
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;
    //3
    Object getBean(String name, Object... args) throws BeansException;
    //4
    <T> T getBean(Class<T> requiredType) throws BeansException;
    //5
    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
    //6
    <T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
    //7
    <T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
    
    boolean containsBean(String name);

    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String name) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;

    String[] getAliases(String name);
}

常用的是上述1,2,4种,这三种方式获取到的bean是一样的。其中1,2种都涉及到根据名称来获取对象,Bean的名称可以参考官方文档

程序开发人员不需要为bean指定名称(Beanld),如果没有显式的提供名称(Beanld),Spring容器将为该bean生成唯一的名称。

命名约定使用Java标准约定作为实例字段名。也就是说,bean名称以小写字母开头,然后使用驼峰式大小写。

比如

类名:UserController,Bean的名称为:userController

类名:AccountManager,Bean的名称为:accountManager

类名:AccountService,Bean的名称为:accountService

也有一些特殊情况,当有多个字符并且第一个和第二个字符都是大写时,将保留原始的大小写。这些规则与java.beans.Introspector.decapitalize(Spring在这里使用的)定义的规则相同。

比如

类名:UController,Bean的名称为:UController

类名:AManager,Bean的名称为:AManager

根据这个命名规则,我们来获取bean。

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        
        HelloController bean = context.getBean(HelloController.class);
        bean.print();
        
        HelloController bean2 = (HelloController) context.getBean("helloController");
        bean2.print();

        HelloController bean3 = context.getBean("helloController", HelloController.class);
        bean3.print();

        System.out.println(bean);
        System.out.println(bean2);
        System.out.println(bean3);
    }
}

三种方法都成功从Spring中获取到Controller对象,并执行Controller的print方法。并且地址一样,说明是同一个对象。

ApplicationContext VS BeanFactory

  • 从继承关系和功能方面来说:Spring容器有两个顶级的接口:BeanFactory和ApplicationContext。其中BeanFactory提供了基础的访问容器的能力,而ApplicationContext属于BeanFactory的子类,它除了继承了BeanFactory的所有功能之外,它还拥有独特的特性,还添加了对国际化支持、资源访问支持、以及事件传播等方面的支持。
  • 从性能方面来说:ApplicationContext是一次性加载并初始化所有的Bean对象,而BeanFactory是需要那个才去加载那个,因此更加轻量。(空间换时间)
1.2 @Service(服务存储)

使用@Service存储的代码如下所示:

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void print() {
        System.out.println("Hello service");
    }
}

读取bean的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        UserService bean = context.getBean(UserService.class);
        bean.print();
    }
}
1.3 @Repository(仓库存储)

使用@Repository存储的代码如下所示:

java 复制代码
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    public void print() {
        System.out.println("Hello Repository");
    }
}

读取bean的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        UserRepository bean = context.getBean(UserRepository.class);
        bean.print();
    }
}
1.4 @Component(组件存储)

使用@Component存储bean的代码如下所示:

java 复制代码
import org.springframework.stereotype.Component;

@Component
public class UserComponent {
    public void print() {
        System.out.println("Hello component");
    }
}

读取bean的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        UserComponent bean = context.getBean(UserComponent.class);
        bean.print();
    }
}
1.5 @Configuration(配置存储)

使用@Configuration存储bean的代码如下所示:

java 复制代码
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfig {
    public void print() {
        System.out.println("Hello configuration");
    }
}

读取bean的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        UserConfig bean = context.getBean(UserConfig.class);
        bean.print();
    }
}

2. 为什么需要这么多注解?

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

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

程序的应用分层,调用流程如下:

查看@Controller /@Service /@Repository /@configuration等注解的源码发现:

其实这些注解里面都有一个注解@Component,说明它们本身就是属于@Component 的"子类"。@Component是一个元注解,也就是说可以注解其他类注解,如@Controller,@Service,@Repository等。这些注解被称为@Component的衍生注解。

@Controller,@Service和@Repository用于更具体的用例(分别在控制层,业务逻辑层,持久化层),在开发过程中,如果你要在业务逻辑层使用@Component或@Service,显然@Service是更好的选择。

3. 方法注解@Bean

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

  1. 使用外部包里的类,没办法添加类注解
  2. 一个类,需要多个对象,比如多个数据源

这种场景,我们就需要使用方法注解@Bean。

下来看方法注解如何使用,创建一个Student类:

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Student {
    private String name;
    private Integer age;
}
java 复制代码
public class StudentComponent {
    @Bean
    public Student student() {
        return new Student("zhangsan", 18);
    }
}

尝试获取Bean对象中的student:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        Student student = context.getBean(Student.class);
        System.out.println(student);
    }
}

但是未能成功获取。

3.1 方法注解要配合类注解使用

在Spring框架的设计中,方法注解@Bean要配合类注解才能将对象正常的存储到Spring容器中,

如下代码所示:

java 复制代码
@Component
public class StudentComponent {
    @Bean
    public Student student() {
        return new Student("zhangsan", 18);
    }
}

再次运行:

3.2 定义多个对象

对于同一个类,如何定义多个对象呢?(比如多数据源的场景,类是同一个,但是配置不同,志向不同的数据源)

@Bean的使用:

java 复制代码
@Component
public class StudentComponent {
    @Bean
    public Student student1() {
        return new Student("zhangsan", 18);
    }

    @Bean
    public Student student2 () {
        return new Student("lisi", 20);
    }
}

根据类型获取到的是哪个对象呢?

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        Student student = context.getBean(Student.class);
        System.out.println(student);
    }
}

报错信息显示:期望只有一个匹配,结果发现了两个。

从报错信息中,可以看出@Bean注解的bean,bean的名称就是它的方法名。

接下来我们根据名称来获取bean对象:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        //根据Bean名称,从Spring上下文获取对象
        Student student1 = (Student) context.getBean("student1");
        Student student2 = (Student) context.getBean("student2");
        System.out.println(student1);
        System.out.println(student2);
    }
}
3.3 重命名Bean

可以通过设置name属性给Bean对象进行重命名操作,如下代码所示:

java 复制代码
@Bean(name = {"s","stu"})
public Student student() {
    return new Student("zhangsan", 18);
}

此时我们使用s或stu就可以获取到Student对象了:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        Student student1 = (Student) context.getBean("s");
        Student student2 = (Student) context.getBean("stu");
        System.out.println(student1);
        System.out.println(student2);
    }
}

"name="可以省略:

java 复制代码
@Bean({"s","stu"})
public Student student() {
    return new Student("zhangsan", 18);
}

只有一个名称时,"{}"也可以省略:

java 复制代码
@Bean("s")
public Student student() {
    return new Student("zhangsan", 18);
}
3.4 扫描路径

下面我们先修改启动类的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

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

        Student bean = (Student) context.getBean("s");
        System.out.println(bean);
    }
}

再修改项目工程的目录结构,测试bean对象是否生效:

使用五大注解声明的bean,要想生效,还需要配置扫描路径,让Spring扫描到这些注解,也就是通过@ComponentScan来配置扫描路径。

java 复制代码
@ComponentScan("com.bite.springiocdemo")
@SpringBootApplication
public class SpringIocDemoApplication {

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

        Student bean = (Student) context.getBean("s");
        System.out.println(bean);
    }
}

在之前的代码中,@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中了,默认扫描的范围是SpringBoot启动类所在包及其子包。在配置类上添加@ComponentScan注解,该注解默认会扫描该类所在的包下所有的配置类。


创作不易,给个三连支持一下吧

相关推荐
滑水滑成滑头2 小时前
**点云处理:发散创新,探索前沿技术**随着科技的飞速发展,点云处理技术在计算机视觉、自动驾驶、虚拟现实等领域的应用愈发广
java·python·科技·计算机视觉·自动驾驶
newxtc2 小时前
【猿辅导-注册安全分析报告-无验证方式导致安全隐患】
开发语言·selenium·安全·yolo·安全爆破
张人玉2 小时前
c#WPF基础知识
开发语言·c#·wpf
千里马-horse2 小时前
fastddsgen.jar 简介
java·jar·fast dds·fastddsgen
TT哇2 小时前
【Maven】Maven设置国内源
java·maven
dyj0953 小时前
【Devops-Jenkins自动将Java Maven工程编译成jar、并打成Docker镜像,并上传Harbor】
java·jenkins·devops
std78793 小时前
Rust 与 Go – 比较以及每个如何满足您的需求
开发语言·golang·rust
gordon~93 小时前
Spring 的bean是安全的吗
java·安全·spring·bean
报错小能手3 小时前
python(入门)map内置函数及import模块导入,as别名
开发语言·人工智能·python