SpringIOC和DI的代码实现,Spring如何存取对象?@Controller、@Service、@Repository、@Component、@Configuration、@Bean DI详解

简介: 本文详细讲解了Spring框架中IOC容器如何存储和取出Bean对象,包括五大类注解(@Controller、@Service、@Repository、@Component、@Configuration)和方法注解@Bean的用法,以及DI(依赖注入)的三种注入方式:属性注入、构造方法注入和Setter注入,并分析了它们的优缺点。
    通过上篇博客(也就是文章开头推荐的博客,不看上篇博客的话,只要知道Spring是什么,Spring的本质也可以)我们知道了什么是Spring,那这篇博客就带大家**用代码实际使用Spring的存和取的功能**,以及**相关注解的使用**

既然Spring是一个IOC(控制反转)容器,作为容器,那么它就具备两个最基础的功能

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

一、Bean的存储和取用

    前⾯我们提到IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对 象。 也就是**bean的存储**

要把某个对象交给IOC容器管理,需要添加相对应的注解

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

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

1、存 —— 五大类注解

@Controller(控制器存储)

使用@Controller存储以及相关取用

@Controller
public class UserController {
    public void doUserController(){
        System.out.println("do Controller...");
    }
}

接下来我们学习如何从Spring容器中获取对象

此处先跟着走,不要太过理解,存对象讲完后下面就是详细讲如何取对象

@SpringBootApplication
public class SpringBoot2Application {

    public static void main(String[] args) {
        // 获取Spring上下文对象
        ApplicationContext context = SpringApplication.run(SpringBoot2Application.class, args);
        // 从context(Spring上下文)中获取bean
        // 通过类型拿bean
        UserController userController = context.getBean(UserController.class);
        userController.doUserController();
}

ApplicationContext本质就是Spring的运行环境封装成的对象

Spring上下文的理解

    ApplicationContext 翻译过来就是: Spring 上下⽂

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

关于上下⽂的概念

    上学时, 阅读理解经常会这样问: 根据上下⽂, 说⼀下你对XX的理解

    在计算机领域, 上下⽂这个概念, 咱们最早是在学习线程时了解到过, ⽐如我们应⽤进⾏线程切换的时 候,切换前都会把线程的状态信息暂时储存起来,这⾥的上下⽂就包括了当前线程的信息,等下次该 线程⼜得到CPU时间的时候, 从上下⽂中拿到线程上次运⾏的信息

    **这个上下⽂, 就是指当前的运⾏环境**, **也可以看作是⼀个容器**, 容器⾥存了很多内容, 这些内容是当前运⾏的环境

观察运⾏结果, 发现成功从Spring中获取到Controller对象, 并执⾏Controller的doController⽅法

如果把@Controller删掉,再观察运行结果

报错信息显⽰: 找不到类型是: com.example.springboot2.controller.UserController的bean

其他四个类注解操作类似

  • @Service(服务存储)
  • @Repository(仓库存储)
  • @Component(组件存储)
  • @Configuration(配置存储)

UserService、UserRepository等文件就不在这里放源码了,模仿我上面的UserController源码写一下即可

@SpringBootApplication
public class SpringBoot2Application {

    public static void main(String[] args) {
        // 获取Spring上下文对象
        ApplicationContext context = SpringApplication.run(SpringBoot2Application.class, args);
        // 从context(Spring上下文)中获取bean
        // 通过类型拿bean
        UserController userController = context.getBean(UserController.class);
        userController.doUserController();

        UserService userService = context.getBean(UserService.class);
        userService.doService();

        UserRepository userRepository = context.getBean(UserRepository.class);
        userRepository.doRepository();

        UserComponent userComponent = context.getBean(UserComponent.class);
        userComponent.doComponent();

        UserConfig userConfig = context.getBean(UserConfig.class);
        userConfig.doConfig();
}

观察运⾏结果, 发现全部取出成功

2、取 —— Spring取Bean对象(详细)

拿上面的Controller在这里作为例子讲解

@SpringBootApplication
public class SpringBoot2Application {

    public static void main(String[] args) {
        // 获取Spring上下文对象
        ApplicationContext context = SpringApplication.run(SpringBoot2Application.class, args);
        // 从context(Spring上下文)中获取bean
        // 通过类型拿bean
        UserController userController = context.getBean(UserController.class);
        userController.doUserController();
        // 通过Bean名(对象名)取对象
        UserController userController2 = (UserController) context.getBean("userController");
        userController2.doUserController();
        // 通过Bean名和类型加在一起取对象
        UserController userController3 = context.getBean("userController",UserController.class);
        userController3.doUserController();
}

可以看到上面有三种方式取到这个对象

下面我们针对这段代码来个逐句解析

ApplicationContext context = SpringApplication.run(SpringBoot2Application.class, args);

这句代码的后半部分是不是特别熟悉?对,后半部分的意思就是启动我们的SpringBoot项目,每个SpringBoot项目都有后半部分

而这里我们用了ApplicationContext类型来接收它的返回值,这里ApplicationContext可以简单理解将Spring环境封装而成的对象

我们将这一步叫做获取Spring上下文对象

然后后面三种方式,都是获取Bean对象

getBean是该对象的方法,专门用来取出对象,有三种传参方式可供选择

  • 第一种方式是通过对象字节码文件来取出对象的
  • 第二种方式是通过Bean名称来取出对象的
  • 第三种方式是通过Bean名称和对象字节码一起取出该对象的

其中2,3种都涉及到根据名称来获取对象. bean的名称是什么呢?

Spring bean是Spring框架在运⾏时管理的对象, Spring会给管理的对象起⼀个名字.

⽐如学校管理学⽣, 会给每个学⽣分配⼀个学号, 根据学号, 就可以找到对应的学⽣.

Spring也是如此, 给每个对象起⼀个名字, 根据Bean的名称(BeanId)就可以获取到对应的对象.

3、Bean命名约定

我们可以查阅官方文档:

Bean Overview :: Spring Framework

    程序开发⼈员不需要为bean指定名称(BeanId), 如果没有显式的提供名称(BeanId),Spring容器将为该 bean⽣成唯⼀的名称.(**Bean名称可以自己指定,若不指定,则由Spring生成唯一的名称**)
    命名约定使⽤Java标准约定作为实例字段名. 也就是说,**bean名称以⼩写字⺟开头,然后使⽤驼峰式⼤⼩写.**

⽐如

  • 类名: UserController, Bean的名称为: userController
  • 类名: AccountManager, Bean的名称为: accountManager
  • 类名: AccountService, Bean的名称为: accountService

也有⼀些特殊情况, 当有多个字符并且第⼀个和第⼆个字符都是⼤写时, 将保留原始的⼤⼩写. 这些规则 与java.beans.Introspector.decapitalize (Spring在这⾥使⽤的)定义的规则相同

⽐如

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

4、为什么需要这么多类注解?

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

  • @Controller:控制层, 接收请求, 对请求进⾏处理, 并进⾏响应.
  • @Servie:业务逻辑层, 处理具体的业务逻辑.
  • @Repository:数据访问层,也称为持久层. 负责数据访问操作
  • @Configuration:配置层. 处理项⽬中的⼀些配置信息
  • @Component:元注解,也就是说可以注解其他类注解,上面的四个注解中源码都包含@Component,当有的类用上面四个都不合适的时候,再来考虑用@Component

这和每个省/市都有⾃⼰的⻋牌号是⼀样的.

⻋牌号都是唯⼀的, 标识⼀个⻋辆的. 但是为什么还需要设置不同的⻋牌开头呢.

⽐如陕西的⻋牌号就是:陕X:XXXXXX,北京的⻋牌号:京X:XXXXXX,甚⾄⼀个省不同的县区也 是不同的,⽐如西安就是,陕A:XXXXX,咸阳:陕B:XXXXXX,宝鸡,陕C:XXXXXX,⼀样.

这样做的好处除了可以节约号码之外,更重要的作⽤是可以直观的标识⼀辆⻋的归属地.

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

5、类注解之间的关系

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

其实这些注解⾥⾯都有⼀个注解 @Component,说明它们本⾝就是属于 @Component 的“⼦类”. @Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service , @Repository 等. 这些注解被称为 @Component 的衍⽣注解.

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

⽐如杯⼦有喝⽔杯, 刷⽛杯等, 但是我们更倾向于在⽇常喝⽔时使⽤⽔杯, 洗漱时使⽤刷⽛杯.

6、存和取 —— 方法注解@Bean

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

  • 1. 使⽤外部包⾥的类, 没办法添加类注解
  • 2. ⼀个类, 需要多个对象, ⽐如多个数据源

存 —— @Bean

这种场景, 我们就需要使⽤⽅法注解 @Bean

@Configuration
public class BeanConfig {
    @Bean
    public UserInfo userInfo1(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(1);
        userInfo.setName("zhangsan");
        userInfo.setAge(12);
        return userInfo;
    }

}

注:UserInfo只是随便定义的实体类,在此处并不重要,代码如下:

@Data
public class UserInfo {
    private Integer id;
    private String name;
    private Integer age;
}

注:文章从此处开始往下的取对象的操作就不放前面的Spring上下文对象了,只放不重复重要的代码,若想找完整的代码往上面到Controller那里复制一下,然后把取对象那两行代码换成下面这种就行啦,非常简单而且绝对正确

取 —— 从Bean取对象:

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

有细心的同学会发现,我们这个类不是UserConfig么,为什么返回值用UserInfo?

    请注意,咱这个是方法注解,是将该方法交给Spring托管,该方法的返回值是UserInfo,因此咱取出对象的返回值是UserInfo

观察运⾏结果, 发现取出成功

定义多个对象

对于同⼀个类, 如何定义多个对象呢?

⽐如多数据源的场景, 类是同⼀个, 但是配置不同, 指向不同的数据源

@Configuration
public class BeanConfig {

    @Bean
    public UserInfo userInfo1(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(1);
        userInfo.setName("zhangsan");
        userInfo.setAge(12);
        return userInfo;
    }

    @Bean
    public UserInfo userInfo2(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(2);
        userInfo.setName("lisi");
        userInfo.setAge(13);
        return userInfo;
    }
}

定义多个对象的话,此时我们再根据类型获取对象,获取的是哪个对象呢?

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

运行结果:

报错信息显⽰: 期望只有⼀个匹配, 结果发现了两个, UserInfo1,UserInfo2

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

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

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

观看运行结果,发现取出成功,输出正确

可以看到, @Bean 可以针对同⼀个类, 定义多个对象

7、重命名Bean

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

@Configuration
public class BeanConfig {

    @Bean(name = "u1")
    public UserInfo userInfo1(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(1);
        userInfo.setName("zhangsan");
        userInfo.setAge(12);
        return userInfo;
    }
}

此时我们就可以通过u1来获取到UserInfo对象了,如下代码所示:

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

8、扫描路径

  • Q: 使⽤前⾯学习的四个注解声明的bean,⼀定会⽣效吗?
  • A: 不⼀定(原因:bean想要⽣效,还需要被Spring扫描)

下面我们通过修改项目工程的目录结构,来测试bean对象是否生效:

再运行代码就报错

报的错误是没有找到bean对象

为什么没有找到bean对象呢?
使⽤五⼤注解声明的bean,要想⽣效,还需要配置扫描路径,让Spring扫描到这些注解,也就是通过@ComponentScan 来配置扫描路径.

他就会扫描这个目录及以下的所有文件

也可以用{}配置多个包路径

@ComponentScan({"com.example.springboot2"})

这种做法仅作了解,不推荐使用

那为什么前⾯没有配置 @ComponentScan注解也可以呢?

@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中了

默认扫描的范围是SpringBoot启动类所在包及其⼦包

在配置类上添加@ComponentScan 注解, 该注解默认会扫描该类所在的包下所有的配置类

二、DI详解

在上篇博客我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注⼊DI的细节

    依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象.

    简单来说,就是把对象取出来放到某个类的属性中

    在⼀些⽂章中, 依赖注⼊也被称之为 "对象注⼊", "属性装配", 具体含义需要结合⽂章的上下⽂来理解

在上篇博客(文章开头推荐的博客)中我们用到了IOC,如下图

图片上标注的代码用DI的方法实现的话其实就是

@Autowired
private Framework framework;

关于依赖注⼊, Spring也给我们提供了三种⽅式:

  1. 属性注⼊(Field Injection) -> 最常用
  2. 构造⽅法注⼊(Constructor Injection)
  3. Setter 注⼊(Setter Injection)

接下来,我们分别来看这三种方式的用法和区别

下⾯我们按照实际开发中的模式,将 Service 类注⼊到 Controller 类中

1、属性注入

属性注⼊是使⽤ @Autowired 实现的,将 Service 类注⼊到 Controller 类中。

Service 类的实现代码如下:

import org.springframework.stereotype.Service;
@Service
public class UserService {
    public void sayHi() {
        System.out.println("Hi,UserService");
    }
}

Controller 类的实现代码如下:

@Controller
public class UserController {
     //注⼊⽅法1: 属性注⼊
     @Autowired
     private UserService userService;

     public void sayHi(){
         System.out.println("hi,UserController...");
         userService.sayHi();
     }
}

获取Controller中的sayHi方法

@SpringBootApplication
public class SpringIocDemoApplication {
     public static void main(String[] args) {
         //获取Spring上下⽂对象
         ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class,args);
         //从Spring上下⽂中获取对象
         UserController userController = (UserController) context.getBean("userController")
         //使⽤对象
         userController.sayHi();
     }
}

最终结果如下:

去掉@Autowired , 再运⾏⼀下程序看看结果

2、构造方法注入:

构造⽅法注⼊是在类的构造⽅法中实现注⼊,如下代码所⽰:

@Controller
public class UserController2 {
     //注⼊⽅法2: 构造⽅法
     private UserService userService;
     @Autowired
     public UserController2(UserService userService) {
         this.userService = userService;
     }
     public void sayHi(){
         System.out.println("hi,UserController2...");
     userService.sayHi();
     }
}
    注意事项:如果类只有⼀个构造⽅法,那么 @Autowired 注解可以省略;如果类中有多个构造⽅法, 那么需要添加上 @Autowired 来明确指定到底使⽤哪个构造⽅法。

3、Setter注入

    Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注 解 ,如下代码所⽰:
@Controller
public class UserController3 {
     //注⼊⽅法3: Setter⽅法注⼊
     private UserService userService;
     @Autowired
     public void setUserService(UserService userService) {
         this.userService = userService;
     }
     public void sayHi(){
     System.out.println("hi,UserController3...");
         userService.sayHi();
     }
}

4、三种注⼊优缺点分析

实际使用基本还是属性注入(@Autowired)

1、属性注⼊

优点: 简洁,使⽤⽅便

缺点:

  • 只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指针异常)
  • 不能注⼊⼀个Final修饰的属性

2、构造函数注⼊(Spring 4.X推荐)

优点:

    1. 可以注⼊final修饰的属性

    2. 注⼊的对象不会被修改

    3. 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅ 法是在类加载阶段就会执⾏的⽅法。

    4. 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的

缺点:

     注⼊多个对象时, 代码会⽐较繁琐

3、Setter注⼊(Spring 3.X推荐)

优点: ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊

缺点:

    1. 不能注⼊⼀个Final修饰的属性

    2. 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险

更多DI相关内容参考:

Dependencies :: Spring Framework

5、@Autowired存在的问题

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

还拿这段代码举例

@Configuration
public class BeanConfig {

    @Bean
    public UserInfo userInfo1(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(1);
        userInfo.setName("zhangsan");
        userInfo.setAge(12);
        return userInfo;
    }

    @Bean
    public UserInfo userInfo2(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(2);
        userInfo.setName("lisi");
        userInfo.setAge(13);
        return userInfo;
    }
}

在UserController注入UserInfo

@Controller
public class UserController {

    // 属性注入
    @Autowired
    private UserService userService;

    @Autowired
    private UserInfo userInfo;


    public void doUserController(){
        userService.doService();
        System.out.println(userInfo);
        System.out.println("do Controller...");
    }
}

运行报错,报错原因,不是唯一的Bean对象

如何解决上述问题呢?Spring提供了以下⼏种解决⽅案:

  • @Primary
  • @Qualifier
  • @Resource

使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现

@Configuration
public class BeanConfig {

    @Primary
    @Bean
    public UserInfo userInfo1(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(1);
        userInfo.setName("zhangsan");
        userInfo.setAge(12);
        return userInfo;
    }

    @Bean
    public UserInfo userInfo2(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(2);
        userInfo.setName("lisi");
        userInfo.setAge(13);
        return userInfo;
    }
}

使⽤@Qualifier注解:指定当前要注⼊的bean对象。 在@Qualifier的value属性中,指定注⼊的bean 的名称。

@Qualifier注解不能单独使⽤,必须配合@Autowired使⽤

@Controller
public class UserController {

    // 属性注入
    @Autowired
    private UserService userService;

    @Qualifier("userInfo2")
    @Autowired
    private UserInfo userInfo;


    public void doUserController(){
        userService.doService();
        System.out.println(userInfo);
        System.out.println("do Controller...");
    }
}

使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称

@Controller
public class UserController {

    // 属性注入
    @Autowired
    private UserService userService;

    @Resource(name = "userInfo2")
    private UserInfo userInfo;


    public void doUserController(){
        userService.doService();
        System.out.println(userInfo);
        System.out.println("do Controller...");
    }
}

6、⾯试题 —— @Autowird 与 @Resource的区别

    @Autowired 是spring框架提供的注解,⽽@Resource是JDK提供的注解

    @Autowired 默认是按照类型注⼊,⽽@Resource是按照名称注⼊

    使⽤时设置的参数不同:相⽐于 @Autowired 来说,@Resource ⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean。
目录
相关文章
|
4月前
|
开发框架 Java C++
SpringIOC第二课,@Bean用法,DI详解,常见面试题Autowired VS Resource
SpringIOC第二课,@Bean用法,DI详解,常见面试题Autowired VS Resource
|
5月前
|
Java 数据库 开发者
Spring注解大揭秘:@Component、@Service、@Repository详解
Spring注解大揭秘:@Component、@Service、@Repository详解
275 0
|
5月前
|
Java 数据库连接 API
SpringBoot【问题 01】借助@PostConstruct解决使用@Component注解的类用@Resource注入Mapper接口为null的问题(原因解析+解决方法)
SpringBoot【问题 01】借助@PostConstruct解决使用@Component注解的类用@Resource注入Mapper接口为null的问题(原因解析+解决方法)
481 0
|
开发框架 Java Spring
Spring 中的 Service 有多个实现类,怎么注入?
Spring 中的 Service 有多个实现类,怎么注入?
465 0
|
Dubbo Java 应用服务中间件
compoment注解与Controller、Service、Repository注解的关系
Controller、Service、Repository这三个注解都是Spring提供的,他们三个有着相似的作用,即将被注解类的实例放入到Spring容器中,作用很相似那他们有什么区别呢?
150 0
compoment注解与Controller、Service、Repository注解的关系
|
Java API Spring
|
XML JSON Java
Spring - Bean管理之注解(@Component、@Controller、@RestController、@Service、@Repository)
Spring - Bean管理之注解(@Component、@Controller、@RestController、@Service、@Repository)
458 0
Spring - Bean管理之注解(@Component、@Controller、@RestController、@Service、@Repository)
|
容器
@Configuration和@Component注解的区别
1.@Configuration和@Component注解的源码如下 (1)Configuration注解源码如下:
187 0
|
XML 缓存 Java
Spring IoC源码学习:context:component-scan 节点详解
在 Spring IoC:parseCustomElement详解 中,我们介绍了自定义命名空间节点解析的大部分内容,但是还剩下节点解析的具体过程。本文将以<context:component-scan /> 节点为例子,介绍自定义命名空间 context 的 component-scan 节点的解析过程。
219 0
Spring IoC源码学习:context:component-scan 节点详解
|
设计模式 Java 数据库连接
Spring - @Service、@Repository注解(service类、dao类)(实现类 & 接口类)
Spring - @Service、@Repository注解(service类、dao类)(实现类 & 接口类)
577 0