SpringBoot 的两大核心

  1. IoC:控制反转
  2. 站内文章AOP:面向切面编程

基本概念

控制反转 Inversion of Control

控制反转(Inversion of Control,IoC) 即控制反转/反转控制。它是一种设计思想,而不是一个技术实现。它描述的是 Java 开发领域对象的创建以及管理的问题。这种设计思想可以用来指导框架层面的设计。

  • 控制 :指的是对象创建(实例化、管理)的权力,对程序执行流程的控制。
  • 反转 :控制权交给外部环境(IoC 容器)。在没有反转之前,程序员自己控制整个程序的执行。在使用某个控制反转框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员「反转」到了框架。

依赖注入 Dependency Injection

IoC 最常见以及最合理的实现方式叫做 依赖注入(Dependency Injection,DI)。它是一种具体的编码技巧:不通过 new 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

下面通过简单例子展示 DI:手机类 Phone 存在内部成员 SIMCard 卡。插入 SIMCard 的手机才能使用,调用 SIMCard 类中的方法。

下面展示 Phone 非依赖注入实现的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 非依赖注入实现方式
public class Phone{
private SIMCard card;
public Phone(){
this.card = new SIMCard(); // 构造函数创建了SIM卡,有点硬编码
}
public void sendMsg(String text){
this.card.send(text);
}
}

public class SIMCard{
public void send(String text){/* ... */}
}

// 使用 Phone 时
Phone phone = new Phone()

依赖注入的实现过程:

1
2
3
4
5
6
7
8
9
10
11
12
// 依赖注入实现方式
public class Phone{
private SIMCard card;
public Phone(SIMCard card){
this.card = card;
}
public void sendMsg(String text){/* ... */}
}

// 使用 Phone
SIMCard card = new SIMCard();
Phone phone = new Phone(card); //通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类。

依赖注入框架 DI Framework

在上面的手机和 SIM 的例子中,依赖注入的实现过程虽然不存在硬编码在 Phone 内部创建 SIMCard 类的方式,但是 SIMCard 类的创建、组装(注入)Phone 的工作仅仅是被移动到了更加上层的代码而已,还是需要我们程序员自己来实现。

在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。这个框架就是「依赖注入框架」。

现成的依赖注入框架有很多,比如 Google Guice、Java Spring、Pico Container、Butterfly Container 等。

Spring 是依赖注入框架,而它自称是控制反转容器(Inversion Of Control Container)。这两种说法都没错。控制反转容器这种表述是一种非常宽泛的描述,DI 依赖注入框架的表述更具体、更有针对性。控制反转的方式有很多,除了依赖注入,还有模板模式等,而 Spring 框架的控制反转主要是通过依赖注入来实现的。

依赖反转原则 Dependency Inversion Principle

详见:站内文章设计原则

Spring 中 IoC/DI 的使用

Bean 对象:IoC 容器中创建、管理的对象,称之为 bean。

基础使用:

  • @Component:将当前类(Service 层、Dao 层)交给 IoC 容器管理,成为 IoC 容器中的 bean。实现控制反转。
  • @Autowired:(为 Controller 和 Service 注入)运行时,IoC 容器会提供该类型的 bean 对象,并赋值给该变量。实现依赖注入。

所以说,切换业务实现类时,直接把需要用的实现类上加上 @Component 就行。

SpringBoot 中 Bean 的声明:

注解 说明 位置
@Component 声明 bean 的基础注解 不属于以下三类时,用此注解(比如一些工具类)
@Controller @Component 的衍生注解 标注在控制器类上
@Service 标注在业务类上
@Repository 标注在数据访问类上(由于与 mybatis 整合,用的少)

声明 bean 的时候,可以通过 value 属性指定 bean 的名字,如果没有指定,默认为类名首字母小写。

使用以上四个注解都可以声明 bean,但是在 Springboot 集成 web 开发中,声明控制器 bean 只能用 @Controller

@RestController 中包含 @Controller 注解。

Bean 组件扫描:

  • 前面声明 bean 的四大注解,要想生效,还需要被组件扫描注解 @Componentscan 扫描。@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中,默认扫描的范围是启动类所在包及其子包。
  • 【不推荐】我们可以在启动类上增加 @ComponentScan 注解,手动设置扫描包的范围。注意检查有没有覆盖掉默认的薮猫范围,可以自己手动添加上。

Bean 注入:

  • @Autowired 注解,默认是按照类型进行。也就是在 IoC 容器中找这个类型的 Bean 对象然后再注入。
  • 如果存在多个相同类型的 bean,将会报错,可以使用以下方案解决:
    • @Primary。在 Bean 上面再加上这个注解,可以让该 Bean 优先生效。
    • @Autowired+@Qualifier("<beanName>")。使该名字的 Bean 生效。
    • @Resource(name="<beanName>")。使该名字的 Bean 生效。
【高频面试题】@Resource@Autowired 区别:

  • @Autowired 是 Spring 框架提供的注解,而 @Resource 是 JDK 提供的注解,
  • @Autowired 默认是按照类型注入,而 @Resource 默认是按照名称注入。

示例工程

一个工程目录示例:

image.png

示例 Mapper:

1
2
3
4
5
6
@Mapper
public interface DeptMapper{
// 基于注解方式编写SQL语句
@Select("select * from user")
List<dept> list();
}

示例 Service 接口类:

1
2
3
public interface DeptService{
List<Dept> list();
}

示例 Service 的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class DeptServiceImpl implements DeptService{
// 注入Mapper
@Autowired
private DeptMapper deptMapper;

// 实现接口
@Override
public List<Dept> list(){
return deptMapper.list();
}
}

示例 Controller 的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j // 使用日志
@RestController
public class DeptController{
// @Slf4j 将省略下面这一行
// private static Logger log = LoggerFactory.getLogger(DeptController.class);

// Service的注入
@Autowired
private DeptService deptService;

// @GetMapping 作用和下面这条相同
// @RequestMapping(vaue = "/depts",method = RequestMethod.GET)
@GetMapping("/depts")
public Result list(){
log.info("查询部门数据");
List<Dept> deptList = deptService.list();
return Result.success(deptList); // 结果类Result是自己定义的
}
}

Bean 管理

获取 Bean

1
2
3
// 拿到 IoC 容器的方法
@Autowired
private ApplicationContext applicationContext; // IoC容器对象

默认情况下,Spring 项目启动时,会把 bean 都创建好放在 IoC 容器中 [1],如果想要主动获取这些 bean,可以通过如下方式使用 ApplicationContext 的方法:

  • 根据 name 获取 bean:Object getBean(String name)
  • 根据类型获取 bean:<T> T getBean(Class<T> requiredType)
  • 根据 name 获取 bean(带类型转换):<T> T getBean(String name, Class<T> requiredType)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootTest
class ApplicationTests{

@Autowired
private ApplicationContext applicationContext; // IoC容器对象

@Test
public void testGetBean(){

//根据bean的名称获取
DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
System.out.println(bean1);

//根据bean的类型获取
DeptController bean2 = applicationContext.getBean(DeptController.class);
System.out.println(bean2);

//根据bean的名称及类型获取
DeptController bean3 = applicationContext.getBean("deptController",DeptController.class);
System.out.println(bean3);
}

输出结果发现三个 bean 对象都是同一个对象,说明此时 bean 是单例的。

Bean 的作用域

Spring 支持五种作用域,后三种在 web 环境才生效:

作用域 说明
singleton 容器内同名称的 bean 只有一个实例(单例)(默认)
prototype 每次使用该 bean 时会创建新的实例(非单例)
request 每个请求范围内会创建新的实例(web 环境中,了解即可)
session 每个会话范围内会创建新的实例(web 环境中,了解即可)
application 每个应用范围内会创建新的实例(web 环境中,了解即可)

可以通过 @Scope 注解来进行配置作用域:@Scope("prototype")

默认 singleton 的 bean,在容器启动时被创建,可以使用 @Lazy 注解来延迟初始化(延迟到第一次使用时),prototype 的 bean,每一次使用该 bean 的时候都会创建一个新的实例。

实际开发当中,绝大部分的 Bean 是单例的,也就是说绝大部分 Bean 不需要配置 scope 属性。

第三方 Bean

如果要管理的 bean 对象来自于第三方(不是自定义的),是无法用 @Component 及衍生注解声明 bean 的(第三方某些类的源码定义中我们无法改动),就需要用到 @Bean 注解。若要管理的第三方 bean 对象,建议对这些 bean 进行集中分类配置,可以通过 @Configuration 注解声明一个配置类。

@Configuration 底层也是 @Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方法一:【不推荐】定义在启动类中
// 这样导致启动类不纯粹
@SpringBootApplication
public class SpringbootWebConfig2Application{
@Bean //将方法返回值交给IoC容器管理,成为IoC容器的bean对象
public MyBean myBean(){
return new MyBean();
}
}

// 方法二:声明一个配置类
// 通过@Bean注解的name/value属性指定bean名称,如果未指定,默认是方法名
@Configuration
public class CommonConfig{
@Bean
public MyBean myBean(){
return new MyBean();
}
}

使用 Bean 时直接注入即可:

1
2
@Autowired
private MyBean mybean;

如果第三方 bean 需要依赖其它 bean 对象,直接在 bean 定义方法中设置形参即可,容器会根据类型自动装配。

1
2
3
4
5
6
7
8
9
10
@Configuration
public class CommonConfig{

// SpringBoot容器会根据参数类型MyService自动装配Bean对象
@Bean
public MyBean myBean(MyService myService){
// 省略一些使用myService的操作
return new MyBean();
}
}

@Component 及衍生注解与 @Bean 注解使用场景?

  • 项目中自定义的,使用 @Component 及其衍生注解
  • 项目中引入第三方的,使用 @Bean 注解

后记

下面你可以立即移步这篇文章加深上面所学知识的印象:站内文章SpringBoot 的原理以及写一个自定义 Starter

本文参考


  1. 这还会受到作用域及延迟初始化影响,这里主要针对于默认的单例非延迟加载的 bean 而言。 ↩︎