文章目录
- 方式一:XML 方式声明 bean
- 方式二:XML + 注解方式声明 bean
- 方式三:注解方式声明配置类
- 扩展一:@Bean 返回的对象和真实 Bean 对象可能不是一个
- 扩展二:加载配置类的同时,加载配置文件(系统迁移)
- 扩展三:@ImportResource、@Bean、@Component 加载优先级
- 扩展四:@ImportResource 引入多个配置文件的优先级
- 扩展五:proxyBeanMethods=true 生成代理对象
- 方式四:@Import 注解注入
- 方式五:上下文对象在容器初始化完毕后注入
- 方式六:实现 ImportSelector 接口 ★
- 方式七:实现 ImportBeanDefinitionRegistrar 接口 ★
- 方式八:实现 BeanDefinitionRegistryPostProcessor 接口
方式一:XML 方式声明 bean
目录初始化:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.axy</groupId>
<artifactId>springboot_bean_init</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
</dependencies>
</project>
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--xml方式声明自己开发的bean-->
<bean id="cat" class="com.axy.bean.Cat"/>
<bean id="dog" class="com.axy.bean.Dog"/>
</beans>
待注入对象:
public class Dog {
...
}
public class Cat {
...
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name); // 打印所有 bean 的名称
}
}
}
运行结果如下:
注:如果 application.xml 的 bean 标签不指定 id 属性,那么默认 bean 的名称为 全限定类名#索引
的形式,运行结果如下:
xml 方式声明第三方开发的 bean:
pom 中添加如下坐标:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--xml方式声明自己开发的bean-->
<bean id="cat" class="com.axy.bean.Cat"/>
<bean id="dog" class="com.axy.bean.Dog"/>
<!--xml方式声明第三方开发的bean-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
</beans>
那么这样会使用 DruidDataSource 的默认构造函数来创建 Bean 对象。控制台打印结果如下:
相关链接:Spring 从入门到精通系列 05 —— Spring 依赖注入的三种方式
方式二:XML + 注解方式声明 bean
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!--指定加载bean的位置,component-->
<context:component-scan base-package="com.axy.bean"/>
</beans>
待注入对象:
@Component("dog") // 如果不写 value 属性,当前 value 默认为类名首字母小写
public class Dog {
...
}
@Component("cat")
public class Cat {
...
}
注:如果要注入controller、service 或者 dao 的 Bean 添加至 IOC 容器,要使用其衍生注解 @Controller 、@Service、@Repository
。
相关链接:Spring 从入门到精通系列 06 —— Spring 中的 IOC 常用注解
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name); // 打印所有 bean 的名称
}
}
}
控制台打印结果如下:
问题: 第三方 bean(如:druid),没有由 ioc 容器创建出来。
解决步骤:① 新建配置类,在其里面定义第三方 bean,并在该配置类上添加 @Component 或 @Configuration 注解使得该方法参与解析。
② 在 application.xml 中添加扫描当前配置类的路径信息。
配置类中添加返回第三方 Bean 对象的方法,并添加相应注解:
//@Component
@Configuration
public class DbConfig {
@Bean
public DruidDataSource dataSource(){ // 方法的名称代表了当前 bean 的名称
return new DruidDataSource();
}
}
<!--指定加载bean的位置,component-->
<context:component-scan base-package="com.axy.bean, com.axy.config"/>
控制台打印结果如下:
注:@Configuration
注解的定义上添加了 @Component
,因此配置类使用 @Component
也是没问题的,但推荐写 @Configuration
。
方式三:注解方式声明配置类
当前工程目录如下:
声明配置类,并使用 @ComponentScan
注解指定要扫描的包:
@ComponentScan(value = {"com.axy.bean", "com.axy.config"})
public class SpringConfig {
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}
控制台打印结果:
注:因为使用 AnnotationConfigApplicationContext
方法指定加载了 SpringConfig
这个类,那么该类会被加载成 bean 对象,并且类上面的 @Configuration
注解可不用添加了,并且里面也可添加创建第三方 bean 的方法(但一般不这么写)。
扩展一:@Bean 返回的对象和真实 Bean 对象可能不是一个
当前工程目录如下:
在 bean 包下新建 DogFactoyBean
类,并实现 FactoryBean 接口:
import org.springframework.beans.factory.FactoryBean;
public class DogFactoryBean implements FactoryBean<Dog> {
@Override
public Dog getObject() throws Exception {
return new Dog();
}
@Override
public Class<?> getObjectType() { // 返回工厂所生产对象的类型
// 如果泛型是接口类型,那么当前返回其实现类的字节码
return Dog.class;
}
@Override
public boolean isSingleton() { // 工厂构建的对象是否是单例
return true;
}
}
将 Dog 类上的注解去掉:
//@Component("dog")
public class Dog {
...
}
添加返回 DogFactoryBean
类的方法,并将其返回值生成 Bean 对象:
@ComponentScan(value = {"com.axy.bean"})
public class SpringConfig {
@Bean
public DogFactoryBean dog(){ // Bean的名称是当前方法名
return new DogFactoryBean(); // 返回对象的类型应该是 “DogFactoryBean”
}
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(SpringConfig.class);
Object bean = ctx.getBean("dog");
System.out.println(bean.getClass());
}
}
控制台打印结果:
从结果可以看出,public DogFactoryBean dog(){…}
要返回的类型是 DogFactoryBean
,但真实返回的类型是 Dog
。
结论:@Bean 返回的对象类型和真实 Bean 对象类型可能不是一个。
扩展二:加载配置类的同时,加载配置文件(系统迁移)
场景:目前需要做一个系统的二次开发,原有系统用的是配置文件的形式声明 Bean,现准备用注解的形式配置声明 Bean。如何在注解的声明中将原有的配置文件加载进来呢?
当前工程目录如下:
添加旧配置类 applicationContext2.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
">
<bean id="tiger" class="com.axy.bean.Tiger" />
</beans>
bean 包下添加 Tiger 类:
public class Tiger {
private Integer age;
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Tiger{" +
"age=" + age +
'}';
}
}
可以在配置类上使用 @ImportResource
注解将配置文件加载进来:
@ComponentScan("com.axy.bean")
@ImportResource("applicationContext2.xml") // 加载旧配置文件
public class SpringConfig2 {
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig2.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}
控制台打印结果:
扩展三:@ImportResource、@Bean、@Component 加载优先级
首先给出结论:@ImportResource、@Bean、@Component 要注入的 Bean 的名称相同时,优先级表现为:@ImportResource > @Bean > @Component。
当前工程目录如下:
修改 applicationContext2.xml,Tiger 类采用 set 方法注入,age 设置为 30:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
">
<!--set方法注入-->
<bean id="tiger" class="com.axy.bean.Tiger">
<property name="age" value="30"/>
</bean>
</beans>
新建配置类 SpringConfig3,添加生成 Bean 对象方法,其中 tiger 对象 age 设置为 20:
@ComponentScan(value = {"com.axy.bean"})
@ImportResource("applicationContext2.xml")
public class SpringConfig3 {
@Bean
public Tiger tiger(){
Tiger tiger = new Tiger();
tiger.setAge(20);
return tiger;
}
}
修改 Tiger 类,当被 @ComponentScan 扫描生成 bean 对象时,使其 age 初始化为 10:
@Component
public class Tiger {
private Integer age = 10;
...
}
测试类:
public class App3 {
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(SpringConfig3.class);
Tiger tiger = (Tiger) ctx.getBean("tiger");
System.out.println(tiger);
}
}
控制台打印结果:
当注释 @ImportResource(“applicationContext2.xml”)
,控制台打印结果如下:
结果表明:@ImportResource、@Bean、@Component 要注入的 Bean 的名称相同时,优先级表现为:@ImportResource > @Bean > @Component。
扩展四:@ImportResource 引入多个配置文件的优先级
首先给出结论:同名的 Bean 对象,后加载的配置会覆盖先加载的配置。
添加配置文件 applicationContext3.xml,Tiger 类采用 set 方法注入,age 设置为 40:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
">
<!--set方法注入-->
<bean id="tiger" class="com.axy.bean.Tiger">
<property name="age" value="40"/>
</bean>
</beans>
用 @ImportResource 同时引入两个配置文件:
@ImportResource(value = {"applicationContext2.xml", "applicationContext3.xml"})
public class SpringConfig31 {
}
控制台打印结果:
如果交换引入顺序:
@ImportResource(value = {"applicationContext3.xml", "applicationContext2.xml"})
public class SpringConfig31 {
}
控制台打印结果:
因此得出结论:同名的 Bean 对象,后加载的配置会覆盖先加载的配置。
扩展五:proxyBeanMethods=true 生成代理对象
首先给出结论:proxyBeanMethods=true 可以保障当前配置类在 Spring 容器中生成的是代理对象,其里面定义的 Bean 是从容器中获取的,而不是重新创建的。
@Configuration 注解里面有一个属性 proxyBeanMethods,默认值为 true。
新建配置类 SpringConfig32,设置 proxyBeanMethods 属性
@Configuration(proxyBeanMethods = true)
public class SpringConfig32 {
@Bean
public Cat cat(){
return new Cat();
}
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig32.class);
System.out.println(ctx.getBean("springConfig32"));
System.out.println("---------------------------");
System.out.println(ctx.getBean("cat"));
System.out.println(ctx.getBean("cat"));
System.out.println(ctx.getBean("cat"));
}
}
proxyBeanMethods=true | proxyBeanMethods=false | |
---|---|---|
打印结果 |
结果表明:① 设置 proxyBeanMethods=true
,生成的配置类对象是代理对象,通过其调用加载 Bean 的方法, 是从容器中获取的,而不是重新创建的。
② 设置 proxyBeanMethods=false
,生成的配置类对象是普通对象,每次执行定义 Bean 的方法都会创建一个新的对象。
总结:配置类中设置 proxyBeanMethods=true
,若其某一个方法可以得到对象,并且该对象被加载成 Bean。那么这个方法 在该配置类中 不论调用多少次,都是从容器中获取,即只会创建一次。
注:设置 proxyBeanMethods=true,里面的加载 Bean 的方法也要添加 @Bean 注解。
方式四:@Import 注解注入
当前工程目录如下:
添加配置类 SpringConfig4,使用 @Import 注解导入要注入的 bean 对应的字节码:
@Import(Dog.class) // 被导入的 bean (Dog类) 无需使用注解声明为 bean。
public class SpringConfig4 {
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig4.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}
控制台打印结果:
从结果可以看出与前文 Bean 名称不同的是,@Import 注解加载的 Bean 名称采用 全路径类名。
注:此形式可以有效的降低源代码与 Spring 技术的耦合度,在 spring 技术底层及诸多框架的整合中大量使用
使用 @Import 加载配置类:
使用 @Import 加载配置类 DbConfig:
@Import(value = {Dog.class, DbConfig.class})
public class SpringConfig4 {
}
@Configuration
public class DbConfig {
@Bean
public DruidDataSource dataSource(){
return new DruidDataSource();
}
}
控制台打印结果:
结果表明:DbConfig 被加载了,并且配置类里面 Bean 的声明也会被加载。
如果去掉配置类里面的 @Configuration 注解,打印结果同上。
结论:使用 @Import 加载配置类,配置类可不用添加 @Configuration 注解。
方式五:上下文对象在容器初始化完毕后注入
上下文对象的 registerBean 方法,是 AnnotationConfigApplicationContext 独有的方法:
测试类:
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(SpringConfig4.class);
// 上下文容器对象已经初始化完毕后,手工加载 bean
ctx.registerBean("monkey", Monkey.class, 1); // 参数三代表构造函数的参数
ctx.registerBean("monkey", Monkey.class, 2);
ctx.registerBean("monkey", Monkey.class, 3);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
System.out.println("-------------");
Monkey monkey = ctx.getBean("monkey", Monkey.class);
System.out.println(monkey);
}
}
public class Monkey {
private Integer age;
public Monkey() {
}
public Monkey(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Monkey{" +
"age=" + age +
'}';
}
}
打印结果:
结果表明:上下文容器初始化后,注册了三次 monkey 对象,但最终容器中保留的是最后一次注册的对象。
注:如果使用 registerBean 方法的时候,没有指明 bean 的名称,除非待注入的对象使用 @Component 或其衍生注解指名 bean 的名称,否则名称默认类名首字母小写。
方式六:实现 ImportSelector 接口 ★
实现 ImportSelector 接口的类,实现对导入源的编程式处理。
当前工程目录如下:
添加 MyImportSelector 类,实现 ImportSelector 接口,并重写 selectImports 方法:
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata metadata) {
return new String[]{"com.axy.bean.Dog", "com.axy.bean.Cat"}; // 数组元素为全路径类名
}
}
public class Dog {
}
public class Cat {
}
添加配置类 SpringConfig6,使用 @Import 引入 MyImportSelector 类:
@Import(MyImportSelector.class)
public class SpringConfig6 {
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig6.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}
控制台打印结果:
String[] selectImports(AnnotationMetadata metadata)
方法形参 metadata 代表元数据,描述的是使用该类 MyImportSelector 的对象。
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata metadata) {
System.out.println("------------------");
System.out.println("提示:" + metadata.getClassName()); // 获取使用当前该类的对象的类名
System.out.println(metadata.hasAnnotation("org.springframework.context.annotation.Configuration"));
System.out.println("------------------");
return new String[]{"com.axy.bean.Dog", "com.axy.bean.Cat"};
}
}
控制台打印结果:
结果表明:使用类 MyImportSelector 类的对象,其类名是 SpringConfig6,并且该对象没有使用 @Configuration 注解。
medata 还有很多方法可以用,如:获取 SpringConfig6 是否包含指定注解,该注解是否有某种属性等。
因此,可以用 medata 做各种条件的判定,以此来决定是否加载指定的 Bean,即动态加载 Bean。
如以下代码:
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata metadata) {
boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
if (flag) {
return new String[]{"com.axy.bean.Dog"};
}
return new String[]{"com.axy.bean.Cat"};
}
}
注:源码中大量使用!
方式七:实现 ImportBeanDefinitionRegistrar 接口 ★
当前工程目录如下:
新建 MyRegistrar 类,实现 ImportBeanDefinitionRegistrar 接口,并重写 public void registerBeanDefinitions(…){…}
方法。
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 1.使用元数据去做判定
// 2.返回值是 void,不同于方式六中直接加载 Bean 的形式
// 创建 beanDefinition 对象,并将该对象使用 registry 注册进容器当中
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Tiger.class).getBeanDefinition();
registry.registerBeanDefinition("tiger", beanDefinition);
// registerBeanDefinition 参数一:bean 名称,参数二:beanDefinition对象
}
}
registerBeanDefinitions(…)
方法:
① 参数一:元数据对象,可以对使用该类 MyRegistrar 的对象各种判定;
② 参数二: Bean 注册器对象,可以实现对容器中 bean的裁定。如:例设置 bean 的单例多例等…
新建 SpringConfig7 类,并通过 @Import 注解引入 MyRegistrar 类:
@Import(MyRegistrar.class)
public class SpringConfig7 {
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(SpringConfig7.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}
控制台打印结果:
总结: 相比于方式六中实现 ImportSelector 接口,方式七实现 ImportBeanDefinitionRegistrar 接口将 bean 的管理开放了出来。
如果配置类中引入多个 ImportSelector 接口的实现类,那么对 Bean 对象的裁定由顺序决定。如以下代码:
新建 BookService 接口和实现类:
public interface BookService {
public void check();
}
public class BookServiceImpl1 implements BookService {
@Override
public void check() {
System.out.println("book service 1...");
}
}
public class BookServiceImpl2 implements BookService {
@Override
public void check() {
System.out.println("book service 2...");
}
}
新建注册器,并分别注册成同名 bean:
public class MyRegistrar1 implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl1.class).getBeanDefinition();
registry.registerBeanDefinition("bookService", beanDefinition);
}
}
public class MyRegistrar2 implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
registry.registerBeanDefinition("bookService", beanDefinition);
}
}
配置类引入两个注册器实现类:
@Import({MyRegistrar1.class, MyRegistrar2.class})
public class SpringConfig71 {
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig71.class);
BookService bookService = ctx.getBean("bookService", BookService.class);
bookService.check();
}
}
控制台打印结果:
如果调换引用顺序,则 Bean 对象的最终裁定也会发生变化,即控制台将打印:book service 1…
方式八:实现 BeanDefinitionRegistryPostProcessor 接口
当前工程目录如下:
新建 MyPostProcessor 类,实现 BeanDefinitionRegistryPostProcessor 方法,并重写里面的方法:
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl3.class).getBeanDefinition();
beanDefinitionRegistry.registerBeanDefinition("bookService", beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
方法 public void postProcessBeanDefinitionRegistry(…){…}
, 后处理并定义注册 Bean。简而言之,通过 BeanDefinition 的注册器注册实名bean,实现对容器中 bean 的最终裁定。
引荐配置类,并引用两个注册器和后处理注册器 MyPostProcessor:
@Import({MyPostProcessor.class, MyRegistrar1.class, MyRegistrar2.class})
public class SpringConfig8 {
}
测试类:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig8.class);
BookService bookService = ctx.getBean("bookService", BookService.class);
bookService.check();
}
}
控制台打印结果:
结果表明:后处理器的实现类会在所有 Bean 都注册定义后再进行处理,如果有同名 Bean,则会覆盖前面注册的 Bean。
另外,如果配置类引用多个后处理器的实现类,则按照后处理器实现类的顺序对 Bean 进行最终的裁定。
参考链接:黑马程序员SpringBoot2全套视频教程,springboot零基础到项目实战(spring boot2完整版)