Spring Boot源码剖析之Spring Boot应用回顾

Spring Boot应用回顾

约定由于配置

约定优于配置:按约定编程,是一种软件设计规范。

image

什么是Spring Boot

Spring boot官网

image

使用Spring Boot可以简单的创建一个基于Spring 应用的独立的产品级的应用。
Spring Boot 的目的是简化Spring应用的开发,尽可能的减少配置,尽快的让你的Spring应用跑起来。

  • Spring Boot 是Pivotal团队研发
  • SpringBoot是基于Spring 4.0设计的。
  • Spring Boot集成了大量的框架,使得依赖包的版本冲突和引用不稳定性得到了很好的解决。

Spring Boot 就是一种快速使用Spring的方式,并且可以省去繁琐的配置。

Spring Boot主要特性

  • Spring Boot Starter(起步依赖):将常用依赖分组整合,将其合并到一个依赖中,这样就可以一次性添加到Maven或Gradle构建中。
image.png
  • JavaConfig方式配置
    Spring发展史:


    image

Spring Boot是基于Spring4.0设计的,当时的Spring4.0已经支持了JavaConfig方式。

  • 自动配置:利用Spring对条件化配置的支持,合理地推测应用所需的bean并自动化配置他们。


    image.png
  • SpringBoot内置Servlet容器,部署简单。

Spring Boot案例实现

目标:使用Spring initializr创建springboot工程,编写Controller,并成功请求Controller,返回响应。
1)创建Spring Boot工程:


image

填写项目基本信息


image

选择依赖
image

创建完成之后的目录结构:
image

2)创建 Controller

package com.xdf.springbootdemo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author xdf
 * @version 1.0
 * @date Create in 9:32 2021/6/23
 * @description 测试Controller
 * @modifiedBy
 */

@RestController
public class DemoController {

    @RequestMapping("/demo")
    public String demo() {
        return "demo";
    }
}

注意:controller必须放在SpringBootDemoApplication启动类下面的包中,也就是说controller的包路径必须必启动了深。
因为SpringBoot包扫描的根目录是启动类所在包。

3)启动测试
通过SpringBootDemoApplication启动类中的main方法启动应用。
访问http://localhost:8080/demo观察结果:

image

思考:

  1. starter是什么?我们如何去使用这些starter?
  2. 为什么包扫描只会扫描核心启动类所在的包及其子包
  3. 在springBoot启动的过程中,是如何完成自动装配的?
  4. 内嵌Tomcat是如何被创建及启动的?
  5. 使用了web场景对应的starter,springmvc是如何自动装配?

Spring Boot热部署

在修改代码后需要验证时,需要重启项目。Spring开发团队提供了一个插件:spring-boot-devtools,解决启动缓慢问题。(开发阶段提高效率,不建议线上使用)

热部署演示

引入依赖:


    org.springframework.boot
    spring-boot-devtools
    true

配置idea自动编译:


image

ctrl+alt+shift+/调出窗口:


image

选择registry,在新弹出的窗口中找到并勾选下面的选项:

image

重启项目,调用请求http://localhost:8080/demo查看
image

修改Controller:

@RestController
public class DemoController {

    @RequestMapping("/demo")
    public String demo() {
        return "热部署 demo";
    }
}

稍等一会,再次调用http://localhost:8080/demo

image

到此就实现了应用热部署演示。

热部署原理分析

Spring官网对自动部署的介绍:


image

自动部署插件会监听classpath下面的文件,一旦文件有变化,就会重启。重启的唯一方式就是更新classpath。

所以它的触发条件是classpath下面的文件变化,因此需要配合idea的自动编译功能。自动编译后,会将编译后的class文件更新到classpath下面,触发spring-boot-devtools的重启功能,完成应用更新。

spring-boot-devtools的热部署功能是怎么实现的?
官方描述:

image

工具是通过两个classloader来实现的,一个classloader加载不变的类,一个classloader是加载我们自己写的class。

image.png

两个类加载器分别加载机制验证:
编写测试代码

package com.xdf.springbootdemo.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.DispatcherServlet;


/**
 * @author xdf
 * @version 1.0
 * @date Create in 10:12 2021/6/23
 * @description spring boot devtools类加载验证
 * @modifiedBy
 */
@Component
public class DevtoolTest implements InitializingBean {
    private static final Logger log = LoggerFactory.getLogger(DevtoolTest.class);

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("guava jar classlodaer:${}" , DispatcherServlet.class.getClassLoader());
        log.info("DevtoolsTest classlodaer:${}" , this.getClass().getClassLoader());
    }
}
image

输出结果验证,发现DispatchorServlet是AppClassLoader加载的。而我们自己写的DevtoolsTest类的加载器是RestartClassloader。

如果不引入Devtools,他们的类加载器都是AppClassloader。

Spring Boot 热部署排除资源

某些资源在更改后不一定需要触发重新启动。例如,Thymeleaf模板可以就地编辑。默认情况下,改变资源 /META-INF/maven , /META-INF/resources ,/resources , /static , /public , 或 /templates 不触发重新启动,但确会触发现场重装。如果要自定义这些排除项,则可以使用该 spring.devtools.restart.exclude 属性。例如,仅排除 /static , /public 您将设置以下属性:

找到devtools jar包,打开spring.factories文件

image

找到LocalDevToolsAutoConfiguration类


image

点击进去:


image

找到DevToolsProperties类,点击进入
在这个类中默认排除了:
image

如果我们想更改默认值
我们现在找到prefix:

image

并且这个类里面有一个内部类Restart,Restart方法中的exclude是我们需要修改的值。


image

并且这个属性提供了setter方法:


image

因此,我们需要在配置文件中配置的属性为:
spring.devtools.restart.exclude

因此我们可以在application.propterties中这样配置:

spring.devtools.restart.exclude=static/**,templates/**

Spring Boot 全局配置文件

全局配置文件概述及优先级

Spring Boot使用一个application.properties和application.yml文件作为全局配置文件。

配置文件的加载目录:

image

分别对应:

–file:./config/ 
–file:./ 
–classpath:/config/ 
–classpath:/
image

同时,序号也是配置文件的加载顺序。
其中,序号越大的配置文件的属性优先级越低。(后面加载的配置文件不会覆盖前面配置的配置文件)

不同配置文件的不同属性,会相互互补

properties和yml文件在相同目录下,谁的配置优先级高?
springboot在2.4.0之前默认properties文件的优先级更高,在2.4.0之后是yml文件优先级更高。
如果在2.4.0z之后的版本还是希望是properties优于yml,可以配置:

spring.config.use-legacy-processing = true

自定义配置文件名:

$ java -jar myproject.jar --spring.config.name=myproject 

指定配置文件:

java -jar run-0.0.1-SNAPSHOT.jar --spring.config.location=D:/application.properties 

和其他配置文件互补。

application.properties配置文件

Spring boot在启动的时候会加载application.properties文件。除了框架的配置信息之外。我们还可以自定义配置属性注入。

Person:

package com.xdf.springbootdemo.pojo;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * @author xdf
 * @date Create in 13:45 2021/6/23
 * @description 人
 * @modifiedBy
 */
@Component
@ConfigurationProperties(prefix = "person")
public class Person {

    private int id;
    /**
     * 名字
     */
    private String name;

    /**
     * 爱好
     */
    private List hobby;

    /**
     * 家庭成员
     */
    private String[] family;

    /**
     *
     */
    private Map map;

    /**
     * 宠物
     */
    private Pet pet;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List getHobby() {
        return hobby;
    }

    public void setHobby(List hobby) {
        this.hobby = hobby;
    }

    public String[] getFamily() {
        return family;
    }

    public void setFamily(String[] family) {
        this.family = family;
    }

    public Map getMap() {
        return map;
    }

    public void setMap(Map map) {
        this.map = map;
    }

    public Pet getPet() {
        return pet;
    }

    public void setPet(Pet pet) {
        this.pet = pet;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", hobby=" + hobby +
                ", family=" + Arrays.toString(family) +
                ", map=" + map +
                ", pet=" + pet +
                '}';
    }
}

Pet:

package com.xdf.springbootdemo.pojo;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author xdf
 * @version 1.0
 * @date Create in 13:45 2021/6/23
 * @description 宠物
 * @modifiedBy
 */

@Component
@ConfigurationProperties(prefix = "pet")
public class Pet {
    private String type;
    private String name;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Pet{" +
                "type='" + type + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

application.properties

person.id=1
person.name=tom
person.hobby=吃饭,睡觉,打豆豆
person.family=爸爸
person.map.ley1=v1
person.map.ley2=v2
person.pet.type=dog
person.pet.name=大黄

为了idea能给自定义属性注入提示,需要引入maven依赖:


    org.springframework.boot
    spring-boot-configuration-processor

application.yml配置文件

  • yml与xml比,少了结构性代码,数据更直接
  • 相比properties文件更简洁
  • 扩展名为yml或yaml
  • yml文件使用“key:(空格)”格式配置属性,使用缩进控制层级关系。

1)普通类型配置

person:
  id: 1
  name: tom

2)list/数组类型配置

person:
  hobby:
    - 吃饭
    - 睡觉
    - 打豆豆

person:
  hobby:
    吃饭
    睡觉
    打豆豆

person:
  hobby: [吃饭,睡觉,打豆豆]
  1. map/对象类型配置
person:
  map:
    k1: v1
    k2: v2

person:
  map: {k1: v1, k2: v2}

属性注入

使用Spring Boot全局配置文件时:
如果配置的是Spring Boot的已有属性,Spring Boot会自动扫描读取这些配置并覆盖默认配置。
如果是自定义属性,必须在程序中注入这些信息才能生效。

属性注入常用注解

  • @Configration:声明一个类作为配置类
  • @Bean:将方法返回值作为bean放入IoC容器
  • @Value:属性注入
  • @ConfigurationProperties:批量属性注入
  • @PropertySource:指定外部属性文件。

@Value属性值注入

数据库数据源配置示例:
引入maven依赖


    com.github.drtrang
    druid-spring-boot2-starter
    1.1.10



    mysql
    mysql-connector-java
    8.0.24

自定义配置属性:

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring
jdbc.username=root
jdbc.password=123456

自定义配置类:

package com.xdf.springbootdemo.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/**
 * @author xdf
 * @version 1.0
 * @date Create in 14:34 2021/6/23
 * @description jdbc配置类
 * @modifiedBy
 */
@Configuration
public class JdbcConfig {

    /**
     * jdbc驱动
     */
    @Value("${jdbc.driverClassName}")
    private String driverClassName;
    /**
     * 数据库url
     */
    @Value("${jdbc.url}")
    private String url;

    /**
     * 用户名
     */
    @Value("${jdbc.username}")
    private String username;

    /**
     * 密码
     */
    @Value("${jdbc.password}")
    private String password;


    @Bean
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();

        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "JdbcConfig{" +
                "driverClassName='" + driverClassName + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

通过@Configuration注解让Spring扫描配置类,使用@value注入属性值。使用@Bean注解,将dataSoure方法的返回值放到IoC容器。

@ConfigurationProperties批量注入

使用批量注入方式重新配置数据源:
maven依赖注入:


    com.github.drtrang
    druid-spring-boot2-starter
    1.1.10



    mysql
    mysql-connector-java
    8.0.24

自定义配置属性:

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring
jdbc.username=root
jdbc.password=123456

自定义配置类:

package com.xdf.springbootdemo.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/**
 * @author xdf
 * @version 1.0
 * @date Create in 14:57 2021/6/23
 * @description 批量注入方式配置jdbc数据源
 * @modifiedBy
 */
@Configuration
@ConfigurationProperties(prefix = "jdbc")
public class BatchJdbcConfig {

    /**
     * 驱动类名
     */
    private String driverClassName;

    /**
     * jdbc url
     */
    private String url;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;


    /**
     * 将数据源bean放到ioc容器
     * @return dataSource
     */
    @Bean
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }

    /**
     * 批量方式必须实现setter方法
     */




    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * 方便打印观察
     * @return
     */
    @Override
    public String toString() {
        return "BatchJdbcConfig{" +
                "driverClassName='" + driverClassName + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

三方配置

引入的三方jar包中的类是无法修改的,没办法在原本的类上面去做属性注入。但是我们也有办法做属性注入。下面看个例子:

三方类(假设不能修改):

package com.xdf.springbootdemo.pojo;

/**
 * @author xdf
 * @version 1.0
 * @date Create in 15:20 2021/6/23
 * @description 模拟三方jar包中的类
 * @modifiedBy
 */
public class AnotherComponent {

    private String name;

    private String address;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "AnotherComponent{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

我们需要对name和address进行属性注入。
编写配置类:

/**
 * @author xdf
 * @version 1.0
 * @date Create in 15:22 2021/6/23
 * @description 三方jar包中的类的属性注入
 * @modifiedBy
 */
@Configuration
public class AnotherConfig {

    @Bean
    @ConfigurationProperties(prefix = "another")
    public AnotherComponent anotherComponent() {
        return new AnotherComponent();
    }
}

三方类通过@Bean放入Ioc容器。
在@Bean方法上面使用@ConfigurationProperties注解标记

属性配置:

another.name=anothername
another.address=address

例如:

@Data 
@Component 
@ConfigurationProperties("acme.my-person.person") 
public class OwnerProperties { 
    private String firstName; 
}
acme: 
  my-person: 
    person: 
      first-name: 泰森

松散绑定

Spring Boot 支持使用一些宽松的规则将属性绑定到@ConfigurationProperties bean。


image.png

@ConfigurationProperties VS @Value

image.png

Spring Boot 日志框架

日志框架介绍

日志框架设计思想


image.png

市面上常见的日志框架:
JCL、SLF4J、Jboss-logging、log4j、log4j2、logback等。
其中细分为:


image.png
  • Jboss-logging 是应用在特殊场景的日志抽象层框架,我们一般不用
  • JCL是Commons包中提供的日志框架,spring默认在用,但不维护了
  • SLF4J是log4j、logback的抽象层,三个抽象层中,前两个不用,一般就用SLF4J
  • jul是jdk提供的日志框架,功能不够强大
  • log4j存在性能问题,作者开发了升级版logback
  • logback跟log4j都遵循SLF4J的抽象层框架规范,性能比log4j强大,推荐使用
  • log4j2是Apache开发的另外一个日志框架,但是应用得比较少,不推荐

Spring boot采用的是SLF4J+logback方案。

SLF4J的使用

slf4j官方示例:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}

下图是SLF4J的官方提供的示例图。SEF4J框架作为其他日志实现框架的门面直接应用到应用程序中。


image

如果不引入日志实现框架,日志无法输出。logbank,slf4j-simple,slf4j-nop是遵循slf4j规范的日志实现框架,只要引入jar包就能使用。
而log4j、jdk日志框架,在开发的时候还没有slf4j规范,因此需要适配层slf4j-log412、slf4j-jdk14来进行适配。

注意:由于每一个日志的实现框架都有自己的配置文件,所以在使用 SLF4j 之后,配置文件还是要使用实现日志框架的配置文件。

统一日志框架的使用

不同的三方框架使用不同的日志框架,在应用中进行整合的时候,怎么使用统一的日志框架?


image

如果引入的三方框架也引入了日志实现类,那么

  • 在引入三方框架的时候要排除三方框架引入的日志实现框架。
  • 引入一个转换层(jcl-over-slf4j、log4j-over-slf4j、jul-to-slf4j)
  • 引入我们选择的日志实现框架

Spring Boot中的日志关系

1)排除其他日志框架
在Spring Boot的spring-boot-dependencies中,将三方框架的其他日志实现框架进行了排除。

image

2)统一框架引入替换包
Spring boot 在spring-boot-starter-logging中引入了自己选择的日志实现框架
image

image

引入日志框架转换层
image

日志相关Maven依赖关系:
image

Spring Boot 的日志使用

Spring Boot日志级别测试:

@Test
void testLog() {
    Logger logger = LoggerFactory.getLogger(this.getClass());
    logger.trace("trace 日志");
    logger.debug("debug 日志");
    logger.info("info 日志");
    logger.warn("warn 日志");
    logger.error("error 日志");
}

打印


image

Spring Boot默认日志级别是info级别

Spring Boot的日志格式是

%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 
# %d{yyyy-MM-dd HH:mm:ss.SSS} 时间 
# %thread 线程名称 
# %-5level 日志级别从左显示5个字符宽度 
# %logger{50} 类名 
# %msg%n 日志信息加换行

至于为什么 Spring Boot 的默认日志输出格式是这样?
我们可以在 Spring Boot 的源码里找到答案。


image

自定义日志输出

可以直接在配置文件编写日志相关配置

# 日志配置 
# 指定具体包(com.xdf)的日志级别 
logging.level.com.xdf=debug 
# 控制台和日志文件输出格式 
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level 
%logger{50} - %msg%n 
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} 
- %msg%n 
# 日志输出路径,默认文件spring.log 
logging.file.path=spring.log 
#logging.file.name=log.log 

关于日志的输出路径,可以使用 logging.file 或者 logging.path 进行定义,两者存在关系如下表。


image.png

替换日志框架

因为 Log4j 日志框架已经年久失修,原作者都觉得写的不好,所以下面演示替换日志框架为 Log4j2 的 方式。根据官网我们 Log4j2 与 logging 需要二选一,因此修改 pom如下

 
    org.springframework.boot 
    spring-boot-starter-web 
     
         
            spring-boot-starter-logging 
            org.springframework.boot 
         
     
 
 
    org.springframework.boot 
    spring-boot-starter-log4j2 

你可能感兴趣的:(Spring Boot源码剖析之Spring Boot应用回顾)