目录
- 一、IoC & DI入门
- 1.1、Spring
- 1.1.1、什么是容器
- 1.1.2、什么是IoC
- 1.2、IoC介绍
- 1.2.1、传统程序开发
- 1.2.2、问题分析
- 1.2.3、问题解决
- 1.2.4、 IoC优势
- 1.3、Bean的作用域
- 1.4、DI介绍
- 二、IoC详解
- 2.1、Bean的存储
- 2.1.1、类注解的使用
- 2.1.2、获取bean对象的其他方式
- 2.1.3、Bean命名约定
- 2.2、为什么要这么多类注解?
- 2.2.1、类注解之间的关系
- 2.3、方法注解@Bean
- 2.3.1、方法注解需要配合类注解使用
- 2.3.2、定义多个对象
- 2.3.3、重命名Bean
- 2.4、扫描路径
- 三、DI详解
- 3.1、属性注入
- 3.2、构造方法注入
- 3.3、Setter注入
- 3.4、@Autowired存在问题
- 四、总结
一、IoC & DI入门
1.1、Spring
通过前面的学习, 我们知道了Spring是一个开源框架, 它让我们的开发更加简单. 它支持广泛的应用场景, 有着活跃且庞大的社区, 这就是Spring能够长久不衰的原因.
但是这个概念还是比较抽象.
可以用更具体的话描述Spring, 那就是: Spring是包含了众多工具方法的IoC容器.
那问题来力, 什么是容器? 什么是IoC容器
?
1.1.1、什么是容器
容器是用来容纳某种物品的(基本)装置.
我们想想, 之前接触的容器有哪些?
List/Map
-> 数据存储容器
Tomcat
-> Web容器
1.1.2、什么是IoC
IoC是Spring的核心思想, 也是常见的面试题, 那什么是IoC呢?
IoC我们已经使用了, 我们在前面讲到, 在类上面添加@RestController
和@Controller
注解,就是把这个对象交给Spring管理, Spring框架启动时就会加载该类. 把对象交给Spring管理, 就是IoC思想.
IoC: Inversion of Control(控制反转), 也就是说Spring是一个"控制反转"的容器.
什么是控制反转呢?也就是控制权反转. 什么的控制权发生了反转?
获得依赖对象的过程被反转了
. 也就是说, 当需要某个对象时,
传统开发模式中只需要自己通过new创建对象, 现在不需要再进行创建, 把创建对象的任务交给容器, 程序中只需要依赖注入就可以了.
这个容器称为: IoC容器. Spring是一个IoC容器, 所以有时Spring也称为Spring容器.
控制反转是一种思想, 在生活中也处处体现.
当人们斗地主时, 如果手里只剩下王炸, 可以不用管了, 整个托管即可.
在自动驾驶中, 驾驶员可以掌握驾驶的控制权, 也可以将这个控制权交给自动化驾驶系统.
1.2、IoC介绍
接下来我们通过案例来了解一下什么是IoC.
需求: 造一辆车
1.2.1、传统程序开发
我们的实现思路是这样的:
先设计轮子(Tire), 然后根据轮子的大小设计出底盘(Bottom), 接着根据底盘的设计车身(Framework), 最后根据车身设计好整辆汽车(Car). 这里就出现了一个"依赖"关系: 汽车依赖车身, 车身依赖底盘, 底盘依赖轮子.
最终实现的代码如下:
/*** @author hanson* @date 2024/4/8 19:16*/
public class NewCarExample {public static void main(String[] args) {Car car = new Car();car.run();}/*** 汽车对象*/static class Car {private FrameWork frameWork;public Car() {this.frameWork = new FrameWork();System.out.println("Car init...");}public void run() {System.out.println("Car run...");}}/*** 车身类*/static class FrameWork {private Bottom bottom;public FrameWork() {this.bottom = new Bottom();System.out.println("Frame 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);}}
}
运行结果:
1.2.2、问题分析
这样的设计看起来没问题, 但是可维护性却很低.
接下来需求有了变更: 随着对车的需求量越来越大, 个性化需求也越来越多, 我们需要加工多种尺寸的轮胎.
那这个时候就要对上面的程序进行修改了, 修改后的代码如下:
/*** 轮胎类*/
static class Tire {// 尺寸private int size;// public Tire() {
// this.size = 17;
// System.out.println("轮胎的尺寸:" + size);
// }public Tire(int size) {this.size = size;System.out.println("轮胎的尺寸:" + size);}
}
修改之后, 其它调用程序也会报错, 我们需要修改继续修改(即每一个构造方法都要传一个size)
完整代码如下:
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) {this.bottom = new Bottom(size);System.out.println("Frame 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);}}
}
从以上代码可以看出, 以上程序的问题是: 当最底层代码改动之后, 整个调用链上的所有代码都需要修改.
程序的耦合度非常高(修改一处代码, 影响其它处代码的修改).
1.2.3、问题解决
在上面的程序当中, 我们是根据轮子的尺寸设计底盘, 轮子尺寸一改, 底盘的设计就得修改. 同样因为我们是根据底盘设计的车身, 那么车身也得修改, 同理汽车设计也得修改, 也就是整个设计都会改.
我们尝试换一种思路, 我们先设计汽车的大概样子, 然后根据汽车的样子来设计车身, 根据车身来设计底盘, 最后根据底盘来设计轮子, 这时, 依赖关系就倒置过来了: 轮子依赖底盘, 底盘依赖车身, 车身依赖汽车.
如何来实现呢?
我们可以尝试不在每个类中创建自己的下级类, 如果自己创建下级类就会出现下级类改变操作, 自己也要跟着修改.
此时, 我们只需要将原来由自己创建的下级类, 改为传递的方式(也就是注入的方式), 因为我们不需要在当前类中创建下级类了, 所以下级类即使发生变化(创建或者减少参数), 当前类不用再改变代码了, 这就实现了程序的解耦.
/*** @author hanson* @date 2024/4/8 19:16*/
public class NewCarExample1 {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("Frame 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() {
// this.size = 17;
// System.out.println("轮胎的尺寸:" + size);
// }public Tire(int size) {this.size = size;System.out.println("轮胎的尺寸:" + size);}}
}
代码通过以上调整, 无论底层类如何变化, 整个调用类是不用做任何改变的, 这样就实现了代码之间的解耦, 从而实现更加灵活, 通用的程序设计了.
1.2.4、 IoC优势
在传统的代码中对象的创建对象的顺序是: Car -> FrameWork -> Bottom -> Tire
改进之后解耦的代码的对象的创建顺序是: Tire -> Bottom -> FrameWork -> Car
我们发现一个规律, 通用程序的实现代码, 类的创建顺序是反的, 传统代码是Car控制并创建了FrameWork, 依次向下,
而改进之后的控制权发生了反转
, 不再是使用方对象创建并控制依赖对象了, 而是把依赖对象注入到当前对象中,
依赖对象的控制权不再由当前类控制了.因此即使依赖类发生任何改变, 当前类都是不受影响的, 这就是典型的控制反转, 也就是IoC的实现思想.
学到这里, 我们就大概知道什么是控制反转了, 那什么是控制反转容器呢,也就是IoC容器.
这部分代码就是IoC容器所做的工作.
从上面也可以看出, IoC具有以下优点:
资源不再由资源的双方管理, 而由不使用资源的第三方管理, 这可以带来很多好处.
第一: 资源的集中管理, 实现资源的可配置和易管理, 用的时候只需要从IoC容器中取即可.
第二:降低了使用资源双方的依赖程度, 也就是我们说的耦合度.
1.3、Bean的作用域
Spring Bean支持五种作用域,后三种在web环境下才生效:
作用域 | 说明 |
---|---|
singleton | 容器内同名称的bean只有一个实例(默认) |
prototype | 每次请求该bean时会创建新的实例(非单例) |
request | 每个请求范围内会创建新的实例(web环境中,了解) |
session | 每个会话范围内会创建新的实例(web环境中,了解) |
application | 每个应用范围内会创建新的实例(web环境中,了解) |
配置Bean的作用域需要加上下面这个注解
@Scope
首先测试单例模式
@Scope("singleton")
@Controller //将对象存储到Spring中
public class MyController1 {public void sayHi(){System.out.println("Hi, UserController...");}
}
@SpringBootApplication
public class SpringIocDiApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(SpringIocDiApplication.class, args);for (int i = 0; i < 10; i++) {MyController1 myController1 = context.getBean(MyController1.class);myController1.sayHi();System.out.println(myController1);}}
}
运行后发现
我们调用10次getBean方法,得到的始终是一个对象。
现在将singleton变成prototype再次测试
@Scope("prototype")
@Controller //将对象存储到Spring中
public class MyController1 {public void sayHi(){System.out.println("Hi, UserController...");}
}
我们调用10次getBean方法,得到了10个bean对象
🎈注意
- 默认singleton的bean,在容器启动的时候被创建,可以使用
@Lazy
注解来延迟初始化(延迟到第一次使用时) - prototype的bean,每一次使用该bean的时候都会创建一个新的实例
- 实际开发中,大多数Bean的单例的,也就是说大部分bean不需要配置scope属性
1.4、DI介绍
DI:Dependency Injection(依赖注入).
容器在运行期间, 动态的为应用程序提供运行时所依赖的资源, 称之为依赖注入.
程序运行时需要某个资源, 容器就可以提供这个资源.
从这点来看, IoC(控制反转)和DI(依赖注入)是从不同角度描述同一件事情, 就是指通过引入IoC容器, 利用依赖关系注入的方式,
实现对象之间的解耦.
之前的代码中, 就是通过构造函数的方式, 将依赖的对象注入到需使用对象中.
DI是IoC的一种实现.
二、IoC详解
通过上面的案例, 我们已经知道了IoC
和DI
的基本操作, 接下来我们来系统地学习Spring IoC和DI的操作.
前面我们提到的IoC控制反转, 就是将对象的控制权交给Spring的IoC容器, 由IoC容器创建及管理对象. (也就是Bean的存储).
2.1、Bean的存储
我们之前只讲到了@Component
注解来使得对象交给IoC容器管理. 而Spring为了更好地管理Web应用程序, 提供了更丰富的注解.
当前有两类注解:
1.类注解
@Controller(控制器存储)
,@Service(服务存储)
,@Reposity(仓库)
,@Component(组件)
,@Configuration(配置)
2.方法注解:@Bean
2.1.1、类注解的使用
由于这里五个类注解在功能上基本是一致的, 所以这里用@Controller
进行介绍.
使用@Controller
存储bean
的代码如下所示:
@Controller //将对象存储到Spring中
public class MyController{public void sayHi(){System.out.println("Hi, UserController...");}
}
如何观察这个对象已经存在Spring容器当中了呢?
接下来我们学习如何从Spring容器中获取对象.
@SpringBootApplication
public class SpringIocDiApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(SpringIocDiApplication.class, args);//从Spring上下文中获取对象MyController myController = context.getBean(MyController.class);//获取对象myController.sayHi();}
}
ApplicationContext 翻译过来就是: Spring上下文.
因为对象交给Spring管理, 所以获取对象要从Spring中获取, 那么就得先得到Spring的上下文.
关于上下文的概念
在计算机领域, 上下文这个概念, 它是指支持进程调度的重要属性.
等下次调度回CPU时,会把寄存器上的回复回来.这里的上下文, 就是指当前的运行环境, 也可以看作是一个容器, 容器里存很多内容, 这些内容是当前运行的环境.
观察运行结果, 发现成功从Spring中获取到Controller对象, 并执行Controller的SayHi方法.
2.1.2、获取bean对象的其他方式
上述代码是根据类型来查找对象, 如果Spring容器中, 同一个类型存在多个bean的话, 怎么获取呢?
ApplicationContext
也提供了其它获取bean
的方式, ApplicationContext
获取bean对象
的功能, 是父类BeanFactory
提供的功能.
可以发现, 我们获取bean共有五种方法, 而常用的是第1, 2, 4三种. 第一种是根据名称获取bean对象, 第二种是通过名称, 类型获取bean对象. 第三种是通过类型获取对象.
其中1,2种都涉及根据名称来获取对象, bean的名称是什么呢?
Spring bean是Spring框架在运行时管理的对象, Spring会给管理的对象起一个名字.
比如学校管理学生, 会给每个学生分配一个学号, 根据学号, 可以找到对应学生.
Spring也是如此, 给每个对象起一个名字, 根据Bean名称(BeanId)就可以获取到对应对象.
2.1.3、Bean命名约定
程序开发人员不需要为bean
指定名称(BeanId)
, 如果没有显式提供名称(BeanId)
, Spring容器将为该bean生成唯一的名称.
命名约定使用Java标准约定作为实例字段名, 也就是说bean名称以小写字母开头, 然后使用驼峰时大小写.
eg. MyController -> myController
也有一些特殊情况, 当有多个字符且第一个和第二个字符都大写时, 将保留原始的大小写.
eg. UController -> UController
根据这个规则, 我们来获取Bean.
@SpringBootApplication
public class SpringIocDiApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(SpringIocDiApplication.class, args);//根据bean类型, 从Spring上下文中获取对象.MyController myController1 = context.getBean(MyController.class);//根据bean名称, 从Spring上下文中获取对象MyController myController2 = (MyController) context.getBean("myController");//根据bean名称+对象, 从Spring上下文中获取对象MyController myController3 = context.getBean("myController", MyController.class);//使用对象System.out.println(myController1);System.out.println(myController2);System.out.println(myController3);}}
运行结果:
地址一样, 说明对象是一个.
获取bean对象, 是父类BeanFactory提供的功能.
ApplicationContext VS BeanFactory (面试题)
继承关系和功能来说: Spring有两个顶级的接口: ApplicationContext 和BeanFactory. 其中
BeanFactory提供了基础的访问容器的能力, 而Application属于BeanFactory的子类.
它除了继承BeanFactory的所有功能之外, 还有独特的特性: 对国际化的支持, 资源访问支持, 以及事件传播方面的支持.从性能来说:
ApplicationContext是一次性加载并初始化的所有Bean对象(可类似于饿汉模式),BeanFactory是需要哪个再去加载哪个,
因此更加轻量.(空间换时间) (一般建议用ApplicationContext, 因为现在机器的性能更高了).
2.2、为什么要这么多类注解?
这个也是和前面讲的应用分层相呼应. 让程序员看到注解之后, 就能知道这个类的用途.
@Controller: 控制层, 接收请求, 对请求进行处理, 并进行响应.
@Service: 业务逻辑层,处理具体的逻辑.
@Respository: 数据访问层, 也称为持久层. 负责数据的访问操作.
@Configuration: 配置层. 处理项目中的一些配置信息.
这个就类似于车牌号的功能, 一看开头, 不管后面的字符串是什么, 就能知道这个车是哪里的.
程序的应用分层, 调用逻辑如下:
2.2.1、类注解之间的关系
查看@Controller
, @Configuration
, @Repository
, @Service
的源码发现:
这些注解中其实都有一个元注解: @Component
, 说明它们本身就属于@Component
的"子类", 说明它们本身就是属于@Component的"子类". @Component是一个元注解, 也就是说可以注解其它类注解, 如@Controller, @Service, @Repository, 这些注解都可以说是@Component的衍生注解.
2.3、方法注解@Bean
类注解是添加到某个类上的, 但是存在两个问题:
1.使用外部包里的类, 没办法添加类注解
2.一个类, 需要多个对象, 比如多个数据源.
这种场景, 我们就需要注解@Bean
2.3.1、方法注解需要配合类注解使用
在Spring框架的设计中, 方法注解@Bean要配合类注解才能将对象正常地存储到Spring容器中.
举个栗子:
@Component
public class BeanConfig {@Beanpublic User user(){User user = new User();user.setName("Hanson");user.setAge(20);return user;}
}
运行下述代码:
@SpringBootApplication
public class SpringIocDiApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(SpringIocDiApplication.class, args);User user = context.getBean(User.class);System.out.println(user);}
}
得到运行结果:
2.3.2、定义多个对象
对于同一个类, 如何定义多个对象呢?
比如多数据源的场景, 类是同一个, 但是配置不同, 指向不同的数据源.
我们看下@Bean的使用
@Component
public class BeanConfig {@Beanpublic User user1(){User user = new User();user.setName("Hanson1");user.setAge(20);return user;} @Beanpublic User user2(){User user = new User();user.setName("Hanson2");user.setAge(20);return user;}
}
当定义到多个对象时, 我们继续使用上面的代码, 能获取到什么对象? 我们来运行一下:
报错信息显示:期望只有一个匹配, 结果却发现了两个: user1, user2.
从报错信息中, 可以看出来, @Bean
注解的bean, bean名称就是它的方法名.
接下来以正确的方式来获取Bean对象.
public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(SpringIocDiApplication.class, args);User user1 = (User) context.getBean("user1");User user2 = (User) context.getBean("user2");System.out.println(user1);System.out.println(user2);}
运行结果:
可以看到, @Bean
针对同一个类, 定义多个对象.
2.3.3、重命名Bean
@Bean(name = {“u1”, “user1”})
添加类似的注解仍可以运行成功, 这是将user1
重命名为u1
的一种方式. 类似地, 还有如下方式:
@Bean({“u1”, “user1”})
//只有一个名称时, 其它的内容可以省略.
@Bean(“u1”)
2.4、扫描路径
使用前面注解声明的bean, 一定会生效吗?
不一定(原因: bean想要生效, 还需要被Spring扫描).
当在同一个包内, 可以直接被Spring扫描到, 如果不在同一个包, 就可以通过@ComponentScan来配置扫描路径.
@ComponentScan(value = "com.hanson.ioc.controller")
@SpringBootApplication
public class SpringIocDiApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(SpringIocDiApplication.class, args);// 从上下文中获取对象User user = context.getBean("u1",User.class);System.out.println(user);}
}
也可以使用@ComponentScans
配置多个包路径.
这种做法仅作了解, 不做推荐使用.
三、DI详解
接下来学习一下依赖注入DI的细节.
依赖注入是一个过程, 是指IoC容器
在创建Bean时, 去提供运行时所依赖的资源, 而资源指的就是对象. 在之前的案例中, 使用了@Autowired
这个注解, 完成了依赖注入这个操作.
简单来说, 就是把对象取出来放到某个类的属性中.
在一些文章中, 依赖注入也称为"对象注入", “属性装配”, 具体含义需要结合文章的上下文理解.
关于依赖注入, Spring提供了三种方式:
1.属性注入(Field Injection)
2.构造方法注入(Constructor Injection)
3.Setter注入(Setter Injection).
3.1、属性注入
属性注入通过@Autowired
实现的, 这里将Service
类注入到Controller
类中.
@Service
public class MyService {public void sayHi() {System.out.println("Hi, MyService");}
}
@Controller //将对象存储到Spring中
public class MyController2 {//注入方法1: 属性注入@Autowired private MyService myService;public void sayHi() {System.out.println("Hi, UserController...");myService.sayHi();}
}
使用:
@SpringBootApplication
public class SpringbootDemoApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(SpringbootDemoApplication.class);MyController2 myController2 = context.getBean(MyController2.class);myController2.sayHi();}
}
最终运行结果如下:
3.2、构造方法注入
构造方法注入是在类的构造方法中实现注入, 如下所示:
@Controller //将对象存储到Spring中
public class MyController3 {private MyService myService;//注入方法2: 构造方法注入@Autowiredpublic MyController3(MyService myService) {this.myService = myService;}public void sayHi() {System.out.println("Hi, UserController...");myService.sayHi();}
}
注意事项: 如果类中只有一个构造方法, 那么@Autowired注解可以省略(在Spring中, 如果一个类只有一个构造方法,
并且该构造方法不包含任何参数, 那么Spring在实例化这个类的时候会自动将其作为一个Bean注入到容器中); 如果类中有多个构造方法,
那么需要添加上@Autowired来明确指明到底使用哪个构造方法.
3.3、Setter注入
Setter注入和属性的Setter方法实现类似, 只不过在设置set方法的时候需要加上@Autowired注解:
@Controller //将对象存储到Spring中
public class MyController4 {private MyService myService;//注入方法3: Setter方法注入@Autowiredpublic void setMyService(MyService myService) {this.myService = myService;}public void sayHi() {System.out.println("Hi, UserController...");myService.sayHi();}
}
这里注意, 对于Setter方法, 是一定要写
@Autowired
的.
3.4、@Autowired存在问题
当同一类型存在多个bean时, 使用@Autowired会存在问题.
@Component
public class BeanConfig {@Bean("u1")public User user1() {User user = new User();user.setName("Hanson1");user.setAge(20);return user;}@Beanpublic User user2() {User user = new User();user.setName("Hanson2");user.setAge(18);return user;}
}
@Controller
public class MyController5 {@Autowiredprivate User user;public void sayHi() {System.out.println("hi, UserController5...");System.out.println(user);}
}
报错的原因是, 非唯一的Bean对象.
如何解决上述问题呢? Spring提供了以下几种解决方案:
使用@Primary
注解: 当存在多个相同类型的Bean注入时, 加上@Primary
注解, 来确定默认的实现.
@Component
public class BeanConfig {@Primary // 指定该bean为默认的bean实现.@Bean("u1")public User user1() {User user = new User();user.setName("lisi");user.setAge(20);return user;}@Beanpublic User user2() {User user = new User();user.setName("zhangsan");user.setAge(18);return user;}
}
使用@Qualifier
注解: 指定要注入的bean对象. 在@Qualifier
的value属性
中,指定注入bean的名称
.
@Qualifier
注解不能单独使用, 必须配合@Autowired
使用.
@Controller
public class MyController6 {@Qualifier("user2") //指定bean的名称.@Autowiredprivate User user;public void sayHi() {System.out.println("hi, UserController6...");System.out.println(user);}
}
使用@Resource
注解: 是按照bean的方式注入
. 通过name属性
指定要注入的bean名称.
@Controller
public class MyController7 {@Resource(name = "user2")private User user;public void sayHi() {System.out.println("hi, UserController7...");System.out.println(user);}
}
常见面试题:
@Autowired和@Resource的区别
@Autowired是Spring框架提供的注解, 而@Resource是JDK提供的注解.(@Primary,@Qualifier是Spring提供的注解).
@Autowired默认是按照类型注入, 而@Resource是按名称注入. 相比于@Autowired来说,
@Resource支持更多的参数配置, 例如name设置, 通过name获取bean.
四、总结
在项目中,我们自定义一个类,如果我们想把这个类交给ioc容器管理,加上@Component衍生注解即可,
如果这个类不是我们自己自定义的,是我们引入第三方依赖中的,而且我们还想把这个类交给ioc容器管理,那么我们应该定义一个方法,在这个方法上加上@Bean
文章代码:GitHub