SpringBoot简单应用及小知识点

本文开始搭建简单的SpringBoot应用,并就涉及的部分小知识点进行描述,本文主要内容为:

参考SpringBoot官网搭建一个简单的父子工程,父工程和子工程pom依赖及版本管理,SpringBoot默认包扫描,yml文件解析和配置修改,配置文件加载顺序,外部配置文件的加载,jar或war打包运行原理;@SpringBootApplication注解和@EnableAutoConfiguration注解及@ConditionXXXX条件注解意义。

(1)SpringBoot官网搭建一个简单的父子工程:https://docs.spring.io/spring-boot/docs/2.1.14.RELEASE/reference/html/getting-started.html文档对不同版本的SpringBoot所支持的JDK,Maven,Gradle,Servlet容器的版本都做了介绍,下面我们以maven构建为例进行搭建:

a)首先创建一个maven父工程,同时新建一个maven子工程,项目结构如下:

SpringBoot简单应用及小知识点SpringBoot简单应用及小知识点

父pom.xml中引入所需要的的jar依赖,主要为spring-boot-starter-parent以及spring-boot-starter-web,如果使用内置Tomcat进行项目打包发布运行,则还需要引入相应的plugin插件;同时声明包含的子工程:

<?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.example</groupId>
    <artifactId>myspringboot</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>springboot_1</module>
    </modules>

    <!-- Inherit defaults from Spring Boot -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
    </parent>

    <!-- dependencies会被子工程完全引用 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <!-- 子工程引用时只会继承版本号,不需要在子工程声明版本号 -->
    <dependencyManagement>
    </dependencyManagement>

    <!-- 以可执行jar运行所需插件 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

b)在新建的子工程pom.xml中,引入父工程的pom,同时加入子工程自己所需的依赖,注意子工程里的依赖不需要加版本号,因为已经在父工程中统一管理了(这是一个父子工程pom管理的知识点,后面会说明):

<?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>
    <artifactId>springboot_1</artifactId>

    <!-- 引入父工程 -->
    <parent>
        <artifactId>myspringboot</artifactId>
        <groupId>com.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- Add typical dependencies for a web application -->
    <!-- 子工程不用加版本号,因为springboot的父pom内用dependencyManagement已经管理维护了对应适配的版本号 -->
    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
    </dependencies>
</project>

c)在子工程的代码创建相应包结构,如本例com.springboot1包,新建处理请求的控制器类(@RestController),提供服务方法:

package com.springboot1;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Example {

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

}

d)新建SpringBoot启动类(App.class),添加@SpringBootApplication声明为启动配置类,然后运行main方法即可使用内置默认容器Tomcat发布这个应用了:

package com;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 启动类:放在最外层包,可默认扫描当前包及子目录下的文件,可省略ComponentScan注解
 * 声明为SpringBoot工程,并开启自动配置
 */
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}

e)可以在resources/application.yml中修改相关配置如修改tomcat默认端口为9090,发布成功后在浏览器可直接访问对应服务:

SpringBoot简单应用及小知识点

SpringBoot简单应用及小知识点

SpringBoot简单应用及小知识点

以上一个最简单的SpringBoot应用就搭建完成并发布,展示了最基本的配置过程。总结就是创建maven工程-->引入SpringBoot依赖和Starter依赖,插件依赖,子工程自身需要依赖-->创建启动类-->创建各个服务发布所需的业务Controller-->按需修改yml配置文件-->启动服务并访问。

(2)父工程和子工程pom依赖及版本管理:

上面提供,子工程在引入相关自己需要的依赖如javax.servlet-api时,是不需要另外声明版本号的,这又是为什么呢?因为子工程所需依赖的版本号都已经在父工程spring-boot-starter-parent当中管理了,可以点开查看内部有哪些管理依赖:

SpringBoot简单应用及小知识点

SpringBoot简单应用及小知识点

SpringBoot简单应用及小知识点

SpringBoot简单应用及小知识点

可以看到在上面父工程引入的springboot2.0.2版本中,管理的javax.servlet-api是3.1.0版本,在子工程maven引入的这个依赖自然也就是3.1.0版本。在父pom.xml是通过<dependencyManagement>标签来管理版本号,那么和<dependencies>标签有何区别?

<dependencies>标签:父工程使用这个标签管理的依赖,那么在子工程中也会自动引入这个依赖,存在强依赖关系;

<dependencyManagement>标签:父工程使用这个标签管理的依赖,在子工程引用时只会继承对应依赖的版本号,故不需要在子工程声明版本号。

(3)SpringBoot默认包扫描:

对于SpringBoot包扫描,存在一个默认的扫描路径,那就是用@SpringBootApplication修饰的启动配置类所在包及其子包,这个是在@SpringBootApplication注解内@ComponentScan注解定义的:

@ComponentScan(excludeFilters = {
      @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
      @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

SpringBoot简单应用及小知识点

这也就是说,为什么一般要将SpringBoot启动类默认都放在代码工程的根目录上,因为这样就可以默认扫描全部的代码目录了,而不需要在App.class启动类上再添加@ComponentScan注解来显示声明包扫描。

(4)yml文件解析和配置修改,配置文件加载顺序,外部配置文件的加载:

关于yml配置文件,有多种语法规则,例如字符,K-V键值对,json对象,数组,集合,对象,复合对象,函数嵌入转换等。解析时K-V之间以空格分开,多个K-V之间用逗号分隔:

server:
  port: 9090

#字符
myserver: server1

#json
user: {name: 123,password: 123}

#json集合
users:
  -  {name: 123,password: 123}
  -  {name: 456,password: 456}

#对象
student:
  name: yang
  age: 20

#对象集合
dogs:
  - dog1
  - dog2

#嵌入日期转换函数
date: 2020-06-10
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss

那么怎么使用配置文件中定义的配置信息?

a)可使用@ConfigurationProperties(prefix = "xxxx")注解修饰在指定类上,这个类的属性就将配置文件中xxxx下的yml配置属性自动绑定到这个类中:

package com.testyml;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * 自定义配置类获取配置文件内定义的配置信息
 */
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

b)也可使用@Value注解获取:

package com.testyml;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestYmlController {

    @Value("${myserver}") private String serverName;

    @Autowired
    private Student student;

    @RequestMapping("/yml")
    void test() {
        System.out.println("student="+student);
        System.out.println("serverName="+serverName);
    }

}
访问该localhost:9090/yml会输出:
student=Student{name='yang', age=20}
serverName=server1

其中@ConfigurationProperties支持松散写法(即属性名称不强制驼峰命令,大小写,下划线等),同时还支持JSR303数据校验:JSR303是一个校验规范,用于专门检查值是否满足指定条件,包括数值,日期,邮箱,字符串格式等等校验,这些可以在实体类上通过@Validated注解声明支持JSR303规范,然后在类的属性中就可以使用这些JSR303所提供的各种校验注解如@Email,@NotNull等

SpringBoot简单应用及小知识点

SpringBoot简单应用及小知识点

那么如果属性不通过校验,就会在启动时出现对应报错信息:

SpringBoot简单应用及小知识点

@Value支持SpEL表达式写法,如@Value{#{${b}+${c}}}等,${XXX}是去配置文件找XXX属性的值,而#{}是去识别一个SpEL表达式(如加减乘除运算,三元表达式等)。

对于外部yml配置文件,要怎么加载呢?

对于外部yml配置文件,可以通过外部配置类用@PropertySource("classpath:外部.yml") 注解进行加载,然后在这个外部配置类的属性内通过@Value获取外部配置文件对应的配置信息:

SpringBoot简单应用及小知识点

SpringBoot简单应用及小知识点

配置文件加载顺序?

在SpringBoot项目中,yml 和 properties 文件是一样的原理,且一个项目一般上要么 yml 或者 properties,推荐使用 yml,更简洁。

一、bootstrap ,yml ,properties加载顺序上的区别

SpringBoot中有以下两种配置文件bootstrap (.yml 或者 .properties),application (.yml 或者 .properties)
(1)bootstrap.yml(bootstrap.properties)先加载
(2)application.yml(application.properties)后加载
(3)同一种类型的.yml的配置文件加载顺序优先于.properties的配置文件

bootstrap.yml 用于应用程序上下文的引导阶段。

bootstrap.yml 由父 Spring ApplicationContext加载。

父 ApplicationContext 在 application.yml 之前被加载。

bootstrap.yml 和 application.yml 都可以用来配置参数。

  • bootstrap.yml 可以理解成系统级别的一些参数配置,这些参数一般是不会变动的。

  • application.yml 可以用来定义应用级别的,如果搭配 spring-cloud-config 使用 application.yml 里面定义的文件可以实现动态替换。

总结:bootstrap.yml 文件里面放系统级别的配置,基本不会发生改变,并且在使用 spring cloud 的时候,一定要把 config server 的属性配置到 bootstrap.yml 文件里面,因为 bootstrap.yml 加载顺序优先于 application.yml 文件,这样的话,就可以在 application.yml 加载之前加载远程的配置,然后去覆盖 application.yml (如果有)中的配置;然后 application.yml 配置文件中的配置就是应用的自动化配置了;

所以,如果项目中的配置比较多的时候建议 bootstrap.yml 和 application.yml 这两个配置文件一起使用最好;

如果项目中的配置比较少,就建议直接使用 application.yml 一个配置文件就够了;

(5)jar或war打包运行原理:

在工程内运行,可使用mvn spring-boot:run命令运行,但是一般是打包成可运行jar或war的方式进行运行。

对于打成可运行jar,则需要引入下面spring-boot-maven-plugin的插件依赖协助打成jar:

<plugin>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

生成的jar文件后,如果要在外部运行可以通过jar -cvf xxx.jar运行;

对于打成war运行方式,则是在启动类App.class继承SpringBootServletInitializer,并重写configure方法:

@SpringBootApplication
public class App extends SpringBootServletInitializer {

    //以war包启动时,启动类继承SpringBootServletInitializer并重写configure,
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return super.configure(builder).sources(App.class);
    }
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}

(6)@SpringBootApplication注解和@EnableAutoConfiguration注解及@ConditionXXXX条件注解意义:

@SpringBootApplication和@EnableAutoConfiguration两个注解分别是声明为配置启动类和自动配置的核心注解,其中@SpringBootApplication包括了@EnableAutoConfiguration的功能,本节不做讨论放在自动配置原理中讨论;

此外SpringBoot内部提供了特有的注解:条件注解(Conditional Annotation)。

比如@ConditionalOnBean、@ConditionalOnClass、@ConditionalOnExpression、@ConditionalOnMissingBean等。

条件注解存在的意义在于动态识别(也可以说是代码自动化执行)。比如@ConditionalOnClass会检查类加载器中是否存在对应的类,如果有的话被注解修饰的类就有资格被Spring容器注册,否则会被忽略不注册。

基于Class的条件注解

SpringBoot提供了两个基于Class的条件注解,对应的条件类是OnClassCondition:

@ConditionalOnClass(类加载器中存在指明的类)或者@ConditionalOnMissingClass(类加载器中不存在指明的类)。

被这两个注解修饰的类,就要先判断指定的类是不是满足对应条件(存在,或不存在),当成立时这个修饰类才会被Spring容器加载注册。

基于Bean的条件注解

基于Bean的条件注解,它们对应的条件类是ConditionOnBean:

@ConditionalOnBean(Spring容器中存在指明的bean)

@ConditionalOnMissingBean(Spring容器中不存在指明的bean)

@ConditionalOnSingleCandidate(Spring容器中存在且只存在一个指明的bean)

SpringBoot还提供了其他比如ConditionalOnJava、ConditionalOnNotWebApplication、ConditionalOnWebApplication、ConditionalOnResource、ConditionalOnProperty、ConditionalOnExpression等条件注解。

各种条件注解的总结

条件注解 对应的Condition处理类 处理逻辑
@ConditionalOnBean OnBeanCondition Spring容器中是否存在对应的实例。可以通过实例的类型、类名、注解、昵称去容器中查找(可以配置从当前容器中查找或者父容器中查找或者两者一起查找)这些属性都是数组,通过”与”的关系进行查找
@ConditionalOnClass OnClassCondition 类加载器中是否存在对应的类。可以通过Class指定(value属性)或者Class的全名指定(name属性)。如果是多个类或者多个类名的话,关系是”与”关系,也就是说这些类或者类名都必须同时在类加载器中存在
@ConditionalOnExpression OnExpressionCondition 判断SpEL 表达式是否成立
@ConditionalOnJava OnJavaCondition 指定Java版本是否符合要求。内部有2个属性value和range。value表示一个枚举的Java版本,range表示比这个老或者新于等于指定的Java版本(默认是新于等于)。内部会基于某些jdk版本特有的类去类加载器中查询,比如如果是jdk9,类加载器中需要存在java.security.cert.URICertStoreParameters;如果是jdk8,类加载器中需要存在java.util.function.Function;如果是jdk7,类加载器中需要存在java.nio.file.Files;如果是jdk6,类加载器中需要存在java.util.ServiceLoader
@ConditionalOnMissingBean OnBeanCondition Spring容器中是否缺少对应的实例。可以通过实例的类型、类名、注解、昵称去容器中查找(可以配置从当前容器中查找或者父容器中查找或者两者一起查找)这些属性都是数组,通过”与”的关系进行查找。还多了2个属性ignored(类名)和ignoredType(类名),匹配的过程中会忽略这些bean
@ConditionalOnMissingClass OnClassCondition 跟ConditionalOnClass的处理逻辑一样,只是条件相反,在类加载器中不存在对应的类
@ConditionalOnNotWebApplication OnWebApplicationCondition 应用程序是否是非Web程序,没有提供属性,只是一个标识。会从判断Web程序特有的类是否存在,环境是否是Servlet环境,容器是否是Web容器等
@ConditionalOnProperty OnPropertyCondition 应用环境中的屬性是否存在。提供prefix、name、havingValue以及matchIfMissing属性。prefix表示属性名的前缀,name是属性名,havingValue是具体的属性值,matchIfMissing是个boolean值,如果属性不存在,这个matchIfMissing为true的话,会继续验证下去,否则属性不存在的话直接就相当于匹配不成功
@ConditionalOnResource OnResourceCondition 是否存在指定的资源文件。只有一个属性resources,是个String数组。会从类加载器中去查询对应的资源文件是否存在
@ConditionalOnSingleCandidate OnBeanCondition Spring容器中是否存在且只存在一个对应的实例。只有3个属性value、type、search。跟ConditionalOnBean中的这3种属性值意义一样
@ConditionalOnWebApplication OnWebApplicationCondition 应用程序是否是Web程序,没有提供属性,只是一个标识。会从判断Web程序特有的类是否存在,环境是否是Servlet环境,容器是否是Web容器等

这个概念非常重要,因为不管在实际应用还是在源码解析中这个概念有大量使用,因此必须掌握,关于详细说明和条件注解的源码,**机制,可以参考下面作者的语雀链接(本篇关于这个条件注解的内容也是转自下面语雀链接):

https://fangjian0423.github.io/2017/05/16/springboot-condition-annotation/