本文将对 Spring Boot 进行介绍,包括基本使用、高级特性等。

对于 Spring、Spring MVC、Spring Boot 等生态环境,可以直接在官网进行了解,这里将不在给出。

Demo

至于 Demo 的编写,可以按照 Getting Started 一步一步的完成。

需要说明的一点,如下:

1
2
3
4
5
6
7
8
9
@Controller
@ResponseBody
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "Hello World!";
    }
}

当浏览器请求localhost:8080/hello时,由于hello()方法是向浏览器返回一个字符串,因此需要使用@ResponseBody注解,这个注解放在类上或方法上都可以,用于表明向浏览器返回信息。

当然,也可以直接使用@RestController注解代替@Controller@ResponseBody注解,效果都是相同的。如下所示:

1
2
3
4
5
6
7
8
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "Hello World!";
    }
}

配置文件

Spring Boot 可以简化配置,即它会使用application.properties配置文件,自动读取相应的配置。例如,将服务器的端口号改为8888,如下所示:

1
server.port=8888

那么在启动 Spring Boot 的时候,就以8888的端口号来启动服务了。具体可以配置哪些参数,可以在官网中进行查找。

简化部署

可以直接在 pom 文件中引入 Maven 插件,进行打包时,会将当前应用打成 jar 包,以供外部服务使用。这里可以在官网进行查看。

依赖管理

默认情况下,Spring Boot 会依赖 pom 文件中 parent 标签下的版本,其依赖路径为spring-boot-starter-parent->spring-boot-dependencies。如果其使用的版本号与我们的不同,那么可以直接在 pom 文件中的<properties>标签下指定想要使用的版本号即可,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<properties>
    <mysql.version>5.1.3</mysql.version>
</properties>

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
    </dependency>
</dependencies>

以上具体内容如何写,还需要参照spring-boot-dependencies中是如何配置的。

你可以看到,在 pom 文件中,有形如spring-boot-starter-web的依赖。它表明:Spring Boot 会通过这种依赖关系,直接导入与web开发所需环境的包,也就是将与web相关的包都进行了依赖传递。具体细节,可以查看官方文档

容器功能

自动配置

之前在使用 Spring MVC 的时候,需要在 xml 文件中配置 bean,提供一些组件的支持。而我们在 Demo 中其实没有进行相应的配置,那么 Spring Boot 是如何进行自动配置的呢?

在 Spring Boot 的启动类SpringBootDemoApplication中,其SpringApplication.run会返回 IoC 容器,我们将其打印,看看有哪些组件被配置了,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@SpringBootApplication
public class SpringBootDemoApplication {

    public static void main(String[] args) {
        // 返回 IoC 容器
        ConfigurableApplicationContext beans = SpringApplication.run(SpringBootDemoApplication.class, args);
        // 查看容器中的组件
        String[] names = beans.getBeanDefinitionNames();
        for (int i = 0; i < names.length; i++) {
            System.out.println(i + "-name: " + names[i]);
        }
    }
}

输出结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
0-name: org.springframework.context.annotation.internalConfigurationAnnotationProcessor
1-name: org.springframework.context.annotation.internalAutowiredAnnotationProcessor
2-name: org.springframework.context.annotation.internalCommonAnnotationProcessor
3-name: org.springframework.context.event.internalEventListenerProcessor
4-name: org.springframework.context.event.internalEventListenerFactory
5-name: springBootDemoApplication
6-name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
7-name: helloController
8-name: org.springframework.boot.autoconfigure.AutoConfigurationPackages
9-name: org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
// 以下省略

此外,Spring Boot 在扫描包时,会自动扫描主程序启动类所在的包及其子包下的组件。如果想要扫描到其他位置的组件,则可以在主程序启动类中,添加@SpringBootApplication(scanBasePackages="com.example")即可,也就是将扫描包的范围扩大。当然,也可以使用@ComponentScan执行扫描路径。

其实,这里的@SpringBootApplication包含以下三种注解:

1
2
3
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.example")

我们直接在主程序启动类中,添加如上三种注解也是可以的。

在配置文件中,所配置的值其实是与特定的类进行绑定的,各种配置都有默认值。

@Configuration

我们给一个类添加@Configuration注解,以此来代替以前使用beans.xml的方式。如下所示:

1
2
3
4
5
6
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Pet {
    private String name;
}
1
2
3
4
5
6
7
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class MyConfig {

    /**
     * 给容器中添加组件
     * <p>
     * 以方法名作为组件 id,返回类型作为组件类型,方法返回的值就是组件在容器中的实例
     *
     * @return
     */
    @Bean
    public User user() {
        return new User("zhangsan", 25);
    }

    @Bean("carol")
    public Pet pet() {
        return new Pet("Tom");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@SpringBootApplication
public class SpringBootDemoApplication {

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

        String[] names = context.getBeanDefinitionNames();
        for(String name : names) {
            System.out.println(name);
        }
    }
}

输出结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
springBootDemoApplication
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
myConfig
helloController
user
carol
// 以下省略

可以看到,组件的名称也被打印了出来,说明组件的确是被 Spring Boot 加载了。

需要注意的是,通过@Configuration注解默认配置的组件是单实例的,并且配置类本身也是组件。该注解中有一个proxyBeanMethods属性,默认为true,表示不管调用多少次这个 bean 的某个方法,其返回的对象都是一样的。也就是说,Spring Boot 总会检查该组件是否在容器中。如果已经在容器中了,则直接取容器中的组件。如果将其置为false,那么每次都是不同的,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Configuration(proxyBeanMethods = false)
public class MyConfig {

    /**
     * 给容器中添加组件
     * <p>
     * 以方法名作为组件 id,返回类型作为组件类型,方法返回的值就是组件在容器中的实例
     *
     * @return
     */
    @Bean
    public User user() {
        return new User("zhangsan", 25);
    }

    @Bean("carol")
    public Pet pet() {
        return new Pet("Tom");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@SpringBootApplication
public class SpringBootDemoApplication {

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

        MyConfig bean = context.getBean(MyConfig.class);

        User user1 = bean.user();
        User user2 = bean.user();

        System.out.println(user1 == user2);
    }
}

输出结果如下:

1
false

针对该属性,在 Spring Boot V1 版本和 Spring Boot V2 版本中,最大的差别就在于此。也就是说,如果配置类组件之间无依赖关系,则使用 Lite 模式(proxyBeanMethods = false)会加速容器的启动过程,减少判断;如果配置类组件之间有依赖关系,则使用 Full 模式(proxyBeanMethods = true),表明方法会得到单实例组件。

综上,在 Full 模式下,能够保证每个被@Bean注解修饰的方法被调用多次所返回的组件都是相同的;而在 Lite 模式下,能够保证每个被@Bean注解修饰的方法被调用多次所返回的组件都是新创建的。

其实,Lite 模式相当于不在该类上添加@Configuration注解,可以将其换成@Component注解,其效果与 Lite 模式是一样的。

@Import

使用@Import可以获取容器中的其他组件,如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Import({User.class, DBHelper.class})
@Configuration
public class MyConfig {

    /**
     * 给容器中添加组件
     * <p>
     * 以方法名作为组件 id,返回类型作为组件类型,方法返回的值就是组件在容器中的实例
     *
     * @return
     */
    @Bean
    public User user() {
        return new User("zhangsan", 25);
    }

    @Bean("carol")
    public Pet pet() {
        return new Pet("Tom");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@SpringBootApplication
public class SpringBootDemoApplication {

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

        String[] userNames = context.getBeanNamesForType(User.class);
        for(String name : userNames) {
            System.out.println(name);
        }

        String[] dBHelperNames = context.getBeanNamesForType(DBHelper.class);
        for(String name : dBHelperNames) {
            System.out.println(name);
        }
    }
}

输出结果如下:

1
2
3
com.example.springbootdemo.boot.bean.User
user
ch.qos.logback.core.db.DBHelper

其中,全类名所表示的就是通过@Import注解导入的组件,而user是我们之前通过@Bean注解注册到容器中的组件。

@Conditional

该注解表示按照条件装配,即满足@Conditional指定的条件时,才会进行组件的注入。我们在这里演示@ConditionalOnBean注解的作用。如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Configuration
public class MyConfig {

    @ConditionalOnBean(name = "carol")
    @Bean
    public User user() {
         User user = new User("zhangsan", 25, new Pet("Jack"));
         return user;
    }

    //@Bean("carol")
    public Pet pet() {
        return new Pet("Tom");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@SpringBootApplication
public class SpringBootDemoApplication {

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

        boolean carol = context.containsBean("carol");
        boolean user = context.containsBean("user");
        System.out.println(user);
        System.out.println(carol);
    }
}

MyConfig类中,user()方法使用到了Pet对象,然而Pet对象没有添加@Bean注解,即没有被注入到容器中。那么此时User也不会被注入到容器中。因为这里使用了@ConditionalOnBean(name = "carol"),即只有声明为carol的 Bean 在容器中,那么User才会被注册到容器中。输出结果如下:

1
2
false
false

@ImportResource

如果你的项目比较老,还是使用类似于beans.xml配置文件进行 bean 的注入,那么如果想将该文件的所有配置通过注解的方式注入到容器中,那么可以使用@ImportResource组件,如下所示:

1
2
3
4
@ImportResource("classpath:beans.xml")
public class MyConfig() {
    ...
}

配置绑定

配置绑定可以将 properties 文件中的内容绑定到 JavaBean 中。

@ConfigurationProperties

将配置文件中的属性值绑定到 Car 中的属性中,如下所示:

1
2
mycar.brand=BMW
mycar.price=100000.01
1
2
3
4
5
6
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
    private String brand;
    private Double price;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@RestController
public class HelloController {

    @Autowired
    private Car car;

    @RequestMapping("/car")
    public Car testCar() {
        return car;
    }
}

在浏览器中访问localhost:8080/car,输出结果如下:

1
2
3
4
{
  "brand": "BMW",
  "price": 100000.01
}

当然,也可以结合使用@ConfigurationProperties(prefix = "mycar")@EnableConfigurationProperties(Car.class)实现上述功能,需要注意的是,这里要将后者添加在配置类中。对于后者的作用,一方面可以开启配置绑定功能,另一方面会将Car这个组件自动注入到容器中。

自动配置原理

整理 P13、P14、P15

此外,如果想查看 Spring Boot 在初始化时哪些配置生效了,哪些配置没有生效,可以在application.properties配置文件中声明debug=true,在启动项目时,即可看到输出信息。

dev-tools

可以在 pom 文件中添加 dev-tools 依赖,当我们对代码进行修改后,不用重启 Spring Boot 环境,可以直接通过Build Projet(Command + F9)来进行热更新。

1
2
3
4
5
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

yaml

我们可以使用application.yaml来代替之前的application.properties文件。其语法格式有如下规定:

  • key: value(key 和 value 之间有空格);
  • 大小写敏感;
  • 使用缩进表示层级关系;
  • 缩进不允许使用 Tab,只允许空格;
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可;
  • #表示注释;
  • 字符串无需加引号,如果要加,''表示字符串的内容不会被转义,""表示字符串的内容会被转义。
    • 例如,'zhangsan\nlisi'会在控制台输出zhangsan\nlisi,而在浏览器的页面中会输出zhangsan\\nlisi
    • 例如"zhangsan\nlisi"会在控制台换行,而在浏览器页面中会输出zhangsan\nlisi

对于 yaml 的数据类型,其可以表示字面量、对象、数组等,如下所示:

字面量:单个的、不可再分的值。例如 date、boolean、string、number、null,如下所示:

1
k: v

对象:键值对的集合。例如 map、hash、set、object,如下所示:

1
2
3
4
5
6
行内写法:k: {k1:v1,k2:v2,k3:v3}
# 或者
k: 
  k1: v1
  k2: v2
  k3: v3

数组:一组按次序排列的值。例如 array、list、queue,如下所示:

1
2
3
4
5
6
行内写法:k: [v1,v2,v3]
#或者
k:
 - v1
 - v2
 - v3

以下为代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Data
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String userName;
    private Boolean boss;
    private Date birth;
    private Integer age;
    private Pet pet;
    private String[] interests;
    private List<String> animal;
    private Map<String, Object> score;
    private Set<Double> salary;
    private Map<String, List<Pet>> allPets;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
person:
  userName: zhangsan
  boss: true
  birth: 2020/12/12
  age: 25
  pet:
    name: Dog
  interests: [篮球,乒乓球,Play Game]
  animal:
    - Tom
    - Jack
  score:
    english:
      first: 60
      second: 89
      third: 99
    math: [131,141,111]
    chinese: {first: 128,second: 145}
  salary: [18000,1900.11,2000.21]
  allPets:
    sick:
      - {name: tom}
      - {name: jerry}
    health: [{name: mario}]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@RestController
public class HelloController {

    @Autowired
    private Person person;

    @RequestMapping("/person")
    public Person testApplicationYaml() {
        System.out.println(person.getUserName());
        return person;
    }
}

如果想要在 yaml 文件中有代码提示的话,则可以引入 processor 到 pom 文件中,如下所示:

1
2
3
4
5
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>