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构建中。
-
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观察结果:
思考:
- starter是什么?我们如何去使用这些starter?
- 为什么包扫描只会扫描核心启动类所在的包及其子包
- 在springBoot启动的过程中,是如何完成自动装配的?
- 内嵌Tomcat是如何被创建及启动的?
- 使用了web场景对应的starter,springmvc是如何自动装配?
Spring Boot热部署
在修改代码后需要验证时,需要重启项目。Spring开发团队提供了一个插件:spring-boot-devtools,解决启动缓慢问题。(开发阶段提高效率,不建议线上使用)
热部署演示
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
配置idea自动编译:
image
ctrl+alt+shift+/调出窗口:
image
选择registry,在新弹出的窗口中找到并勾选下面的选项:
重启项目,调用请求http://localhost:8080/demo查看
image
修改Controller:
@RestController
public class DemoController {
@RequestMapping("/demo")
public String demo() {
return "热部署 demo";
}
}
稍等一会,再次调用http://localhost:8080/demo
到此就实现了应用热部署演示。
热部署原理分析
Spring官网对自动部署的介绍:
image
自动部署插件会监听classpath下面的文件,一旦文件有变化,就会重启。重启的唯一方式就是更新classpath。
所以它的触发条件是classpath下面的文件变化,因此需要配合idea的自动编译功能。自动编译后,会将编译后的class文件更新到classpath下面,触发spring-boot-devtools的重启功能,完成应用更新。
spring-boot-devtools的热部署功能是怎么实现的?
官方描述:
工具是通过两个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:
并且这个类里面有一个内部类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依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
application.yml配置文件
- yml与xml比,少了结构性代码,数据更直接
- 相比properties文件更简洁
- 扩展名为yml或yaml
- yml文件使用“key:(空格)”格式配置属性,使用缩进控制层级关系。
1)普通类型配置
person:
id: 1
name: tom
2)list/数组类型配置
person:
hobby:
- 吃饭
- 睡觉
- 打豆豆
或
person:
hobby:
吃饭
睡觉
打豆豆
或
person:
hobby: [吃饭,睡觉,打豆豆]
- 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依赖
<dependency>
<groupId>com.github.drtrang</groupId>
<artifactId>druid-spring-boot2-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.24</version>
</dependency>
自定义配置属性:
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依赖注入:
<dependency>
<groupId>com.github.drtrang</groupId>
<artifactId>druid-spring-boot2-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.24</version>
</dependency>
自定义配置属性:
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.pngSpring 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中,将三方框架的其他日志实现框架进行了排除。
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如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>