Spring Boot的底层原理
之前学习过了Spring Boot(在88章博客),但是并没有很深入的了解,这里致力于在原来的基础上学习更多Spring Boot知识
回顾(注意:只是回顾,所以更多细节在88章博客):
约定优于配置:
Spring Boot 是所有基于 Spring 开发的项目的起点,Spring Boot 的设计是为了让你尽可能快的跑起来,Spring 应用程序并且尽可能减少你的配置文件
约定优于配置(Convention over Configuration),又称按约定编程,是一种软件设计范式
本质上是说,系统、类库或框架应该假定合理的默认值,而非要求提供不必要的配置,比如说模型中有一个名为User的类,那么数据库中对应的表就应该默认命名为user,只有在偏离这一个约定的时候,例如想要将该表命名为person,才需要写有关这个名字的配置
比如平时架构师搭建项目就是限制软件开发随便写代码,制定出一套规范,让开发人员按统一的要求进行开发编码测试之类的,这样就加强了开发效率与审查代码效率,所以说写代码的时候就需要按要求命名,这样统一规范的代码就有良好的可读性与维护性了
约定优于配置简单来理解,就是遵循约定,比如,现在有一个依赖,我Spring需要他,那么Spring定义一部分与他连接,形成一个新的依赖,让这个依赖使用,这个时候Spring可以直接使用他,再考虑自动的配置类等等,于是乎,由于这样的约定越来越多,就形成了对应的起始依赖(通用的,或者某些考虑自动的配置,这里再后面会说明,通常在自动配置的原理那里,一般是一开始的父依赖),并且再后续进行维护,且许多框架都来进行整合,这样的整体,就是Spring Boot了,所以说Spring Boot不只是一个思想(约定),也是具体实现(依赖),只不过是各个框架整体的实现
SpringBoot概念:
Spring优缺点:
优点: spring是Java企业版(Java Enterprise Edition,JEE,也称J2EE)的轻量级代替品,无需开发重量级的 Enterprise JavaBean(EJB),Spring为企业级Java开发提供了一种相对简单的方法,通过依赖注入和面向切面编程,用简单的Java对象(Plain Old Java Object,POJO)实现了EJB的功能
缺点: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的,一开始,Spring用XML配置,而且是很多XML配置,Spring 2.5引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式XML 配置,Spring 3.0引入 了基于Java的配置,这是一种类型安全的可重构配置方式,可以代替XML,所有这些配置都代表了开发时的损耗,因为在思考Spring特性配置和解决业务问题之间需要进行思维切换,所以编写配置挤占了编写应用程序逻辑的时间,和所有框架一样,Spring实用,但与此同时它要求的回报也不少,除此之外,项目的依赖管理也是一件耗时耗力的事情,在环境搭建时,需要分析要导入哪些库的坐标, 而且还需要分析导入与之有依赖关系的其他库的坐标,一旦选错了依赖的版本,随之而来的不兼容问题 就会严重阻碍项目的开发进度,还有,虽然在一定程度上简化了配置处理,但是其配置在一定量的代码下,比xml需要更多的性能,只不过大多数是忽略的,当然,这是建立在扫描很多包的情况下(前面博客很多情况下,说明的都是这个),通常来说xml由于需要操作IO,所以性能会更加的大,也就是说,平常注解的开销就是比xml要小,所以建议使用注解,当然了,为了更加的明确,这里就给出具体的情况吧,考虑任何情况下注解和xml的区别:
注解和xml的区别和优缺点:
注解:通过反射来完成的配置,在代码里面进行处理
xml:通过配置文件来完成的配置,在配置文件中进行处理
在读取方面的区别:一个是反射,一个是IO
在运行后方面的区别:在代码中不可修改,在配置文件中可以修改
在开发方便的区别:注解简单,xml编写困难
这样要考虑性能,就看反射开销和IO开销谁大,看维护性,就看是否需要动态的修改(前提是可以读取)
那么由于通常情况下IO的开销是比较大的,所以呢,在不考虑其他因素,那么注解的启动效率基本都高于xml(读取后就固定了,所以只会考虑启动),但是由于注解绝对的运行时不可改变,所以在后续维护中也必然小于xml
从上面讲,那么需要考虑如下的问题:
考虑启动的快慢:那么使用注解
考虑后续的维护:那么使用xml
考虑开发的便捷:那么使用注解
很明显,在现在的环境下,我们通常需要启动快,便捷的方式,所以注解的操作现在非常流行
SpringBoot解决上述spring的问题:
SpringBoot对上述Spring的缺点进行的改善和优化,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短 了项目周期
起步依赖 :
起步依赖本质上是一个Maven项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能,简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能,通常用来解决依赖多的情况(一个依赖包含多个依赖)
自动配置:
springboot的自动配置,指的是springboot,会自动将一些配置类的bean注册进ioc容器(有些通常需要指定),我们可以需要的地方使用@autowired或者@resource等注解来使用它,"自动"的表现形式就是我们只需要引我们想用功能的包,相关的配置我们完全不用管,springboot会自 动注入这些配置bean,我们直接使用这些bean即可,所以springboot可以简单、快速、方便地搭建项目,对主流开发框架的无配置集成,极大提高了开发、部署效率(具体之所以可以配置是因为他依赖整合的),解决需要手动扫描的问题(虽然也是一个注解造成的全扫描(这个扫描是任何可以的框架的扫描,而不是单独的,所以解决了手动扫描的问题),但是我只需要一个注解即可)
现在看一下案例吧,创建项目(不会,到88章博客学习吧,也记得配置好maven哦):
对应的依赖:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd" > < modelVersion> 4.0.0</ modelVersion> < parent> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-parent</ artifactId> < version> 2.7.2</ version> < relativePath/> </ parent> < groupId> com</ groupId> < artifactId> boot</ artifactId> < version> 0.0.1-SNAPSHOT</ version> < name> boot</ name> < description> boot</ description> < properties> < java.version> 11</ java.version> </ properties> < dependencies> < dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-web</ artifactId> </ dependency> < dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-test</ artifactId> < scope> test</ scope> </ dependency> </ dependencies> < build> < plugins> < plugin> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-maven-plugin</ artifactId> </ plugin> </ plugins> </ build> </ project>
对应的项目:
在boot包下,创建controller包,然后创建HelloController类:
package com. boot. controller ; import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. RestController ; @RestController
@RequestMapping ( "hello" )
public class HelloController { @RequestMapping ( "/boot" ) public String helloBoot ( ) { return "Hello Spring Boot" ; } }
启动类:
package com. boot ; import org. springframework. boot. SpringApplication ;
import org. springframework. boot. autoconfigure. SpringBootApplication ; @SpringBootApplication
public class BootApplication { public static void main ( String [ ] args) { SpringApplication . run ( BootApplication . class , args) ; } }
我们直接的启动上面的启动类,http://localhost:8080/hello/boot,上面的依赖版本和jdk版本是需要适配的,否则可能启动不了,还有,在104章博客中,还有关于一些项目版本的说明,可以看一看
如果访问后出现了数据,那么我们操作成功了
单元测试与热部署 :
单元测试 :
开发中,每当完成一个功能接口或业务方法的编写后,通常都会借助单元测试验证该功能是否正确,Spring Boot对项目的单元测试提供了很好的支持,在使用时,需要提前在项目的pom.xml文件中添加spring-boot-starter-test测试依赖启动器,快速构建springboot项目一般会加上该依赖,即使用Spring Initializr方式搭建的Spring Boot项目,会自动加入spring-boot-starter-test测试依赖启动器,无需再手动添加,然后可以通过相关注解实现单元测试
依赖:
< dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-test</ artifactId> < scope> test</ scope> </ dependency> < dependency> < groupId> junit</ groupId> < artifactId> junit</ artifactId> < scope> test</ scope> </ dependency>
在测试类中BootApplicationTests中加上如下:
package com. boot ; import com. boot. controller. HelloController ;
import org. junit. jupiter. api. Test ;
import org. junit. runner. RunWith ;
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. boot. test. context. SpringBootTest ;
import org. springframework. test. context. junit4. SpringRunner ; @RunWith ( SpringRunner . class )
@SpringBootTest
class BootApplicationTests { @Autowired private HelloController helloController; @Test public void contextLoads ( ) { String s = helloController. helloBoot ( ) ; System . out. println ( s) ; } }
启动,若有结果说明操作成功
热部署:
在开发过程中,通常会对一段业务代码不断地修改测试,在修改之后往往需要重启服务,有些服务需要加载很久才能启动成功,这种不必要的重复操作极大的降低了程序开发效率,为此, Spring Boot框架专门提供了进行热部署的依赖启动器,用于进行项目热部署,而无需手动重启项目(也就是重新执行启动类,或者说重新执行启动类的main方法)
简单来说,热部署就是:在修改完代码之后(无论是配置文件还是类,基本只要是项目的进行了修改就会更新)
不需要重新启动容器,就可以实现更新,但是需要等待他更新
等日志出现,那么才会真正的部署,中途再次改变,会影响日志的出现
一般会迟一点,因为有缓冲等待(大概等几秒才更新),防止你频繁的更新(每次的改变,基本会重置该等待)
使用步骤:
1:添加SpringBoot的热部署依赖启动器
2:开启Idea的自动编译
3:开启Idea的在项目运行中自动编译的功能
添加spring-boot-devtools热部署依赖启动器:
在Spring Boot项目进行热部署测试之前,需要先在项目的pom.xml文件中添加spring-boot-devtools热部署依赖启动器:
< dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-devtools</ artifactId> </ dependency>
由于使用的是IDEA开发工具,添加热部署依赖后可能没有任何效果,接下来还需要针对IDEA开发工具进行热部署相关的功能设置
IDEA工具热部署设置:
选择IDEA工具界面的【File】 ->【Settings】选项,打开Compiler面板设置页面
然后启动项目(不是测试的),访问:http://localhost:8080/hello/boot,然后修改打印信息,直接看看结果吧
全局配置文件 :
全局配置文件能够对一些默认配置值进行修改
Spring Boot使用一个application.properties或者application.yaml的文件作为全局配置文件
该文件存放在src/main/resource目录或者类路径的/config,一般会选择resource目录
接下来,将针对这两种全局配置文件进行讲解 :
Spring Boot配置文件的命名及其格式:
application.properties,application.yaml,application.yml(前面一个的简写,相当于是一样的)
application.properties配置文件 :
使用Spring Initializr方式构建Spring Boot项目时,会在resource目录下自动生成一个空的application.properties文件(通常是空的)
Spring Boot项目启动时会自动加载application.properties文件
我们可以在application.properties文件中定义Spring Boot项目的相关属性
当然,这些相关属性可以是系统属性、环境变量、命令参数等等信息,也可以是自定义配置文件名称和位置
比如:
#修改tomcat的端口号(有些时候也可以说是版本号)
server.port=8888
现在我们重新启动,就需要访问http://localhost:8888/hello/boot了
还需要注意一点:IDEA 通常会自动保存管理的项目的文件的(是管理的项目哦,通常指该文件夹下的所有,也可以认为是.idea所在文件夹下面的所有)
比如:
1:窗口切换:当你切换到另一个应用程序或窗口时,IDEA 会自动保存当前的更改
2:运行/调试:当你运行或调试项目时,IDEA 会自动保存所有未保存的文件
3:版本控制操作:在执行与版本控制相关的操作(如提交、推送、拉取)时,IDEA 会自动保存所有更改
4:特定时间间隔:如果启用了相应的设置,IDEA 会在一段时间的空闲之后自动保存文件(通常来说是默认启动的)
这些都会使得保存文件,所以在idea中不用担心文件没有保存哦,如果不放心可以ctrl+s保存一下的
继续说明全局配置文件,我们可以给全局配置文件加上如下:
#注意:
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver中
#他后面不能有空格,否则报错,这是一个特殊的地方,当然可以不写,会根据驱动默认加上的(这一般是spring boot自身的处理,其他的如单纯的spring可能还是需要指定)
#其他的基本都可以有空格(且空格无影响,相当于没有什么,基本不会参与到数值里面去)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ceshi
spring.datasource.username=root
spring.datasource.password=123456
操作了对应的数据库,那么一般需要对应的驱动包
spring boot的依赖中并不是所有的包都有传递,有些需要自己加上,如这个驱动包:
< dependency> < groupId> mysql</ groupId> < artifactId> mysql-connector-java</ artifactId> < version> 6.0.6</ version> </ dependency>
还需要如下的包:
< dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-jdbc</ artifactId> < version> 2.7.1</ version> </ dependency>
至此我们可以操作如下:
package com. boot. controller ; import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. jdbc. core. JdbcTemplate ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. RestController ; @RestController
@RequestMapping ( "hello" )
public class HelloController { @RequestMapping ( "/boot" ) public String helloBoot ( ) { return "Hello Spring Boot" ; } @Autowired private JdbcTemplate jdbcTemplate; @RequestMapping ( "/jdbc" ) public String jdbc ( ) { return jdbcTemplate. toString ( ) ; }
}
访问一下吧,http://localhost:8888/hello/jdbc
为了进一步的说明配置文件,我们在boot包下创建pojo包,然后创建如下的类:
package com. boot. pojo ; public class Pet { private String type; private String name; @Override public String toString ( ) { return "Pet{" + "type='" + type + '\'' + ", name='" + 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; }
}
package com. boot. pojo ; import org. springframework. boot. context. properties. ConfigurationProperties ;
import org. springframework. stereotype. Component ; import java. util. Arrays ;
import java. util. List ;
import java. util. Map ; @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; @Override public String toString ( ) { return "Person{" + "id=" + id + ", name='" + name + '\'' + ", hobby=" + hobby + ", family=" + Arrays . toString ( family) + ", map=" + map + ", 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; }
}
@ConfigurationProperties(prefix = “person”)注解的作用:
将配置文件中以person开头的属性值通过setXX()方法注入到实体类对应属性中
@Component注解的作用是将当前注入属性值的Person类对象作为Bean组件放到Spring容器中
只有这样才能被@ConfigurationProperties注解进行赋值,就如@Value一样需要对应的实例
但是若@ConfigurationProperties注解和@Value共存,那么@ConfigurationProperties注解会覆盖@Value注解的操作
若@Value注解报错,那么启动基本就会报错,使得访问不了
打开项目的resources目录下的application.properties配置文件,在该配置文件中编写需要对Person类设置的配置属性
#自定义配置信息注入到Person对象中
person.id=100
person.name=哈哈
#list
person.hobby=喝水,吃早餐
#String[]
person.family=儿子,老婆
#集合和对象,一般都需要多一个层(多了.)
#Map
person.map.k1=v1
person.map.k2=v2
#Pet对象
person.pet.type=狗
person.pet.name=旺财
#他们可以使用@Value注解来进行注入(对应的参数获取),因为他们是全局的
在测试类这加上如下:
@Autowired private Person person; @Test public void configurationTest ( ) { System . out. println ( person) ; }
若返回数据则代表注入成功,即操作成功
具体的编码问题可以参考88章博客
application.yaml(yml)配置文件:
YAML文件格式是Spring Boot支持的一种JSON文件格式,相较于传统的Properties配置文件, YAML文件以数据为核心,是一种更为直观且容易被电脑识别的数据序列化格式
application.yaml配置文件的工作原理和application.properties是一样的,只不过yaml格式配置文件看起来更简洁一些,他们的区别主要在于基本上springboot的配置他们都有联系,但是并非都有编写,所以大多数情况下,可以通过properties来得到yml的编写,但是有些只有yml才可以或者只有properties才可以(少部分),相当于整合的依赖中,都存在他们的配置,只不过有些只操作yml,有些只操作properties,现在我们基本考虑yml,所以后面也基本以这个为主的(yaml与yml是一样的,解析也是一样,当然,可能某些只会看后缀名称,但是这里非常少,所以这里不考虑)
YAML文件的扩展名可以使用.yml或者.yaml,application.yml文件使用 "key:(空格) value"格式配置属性,使用缩进控制层级关系
SpringBoot的三种配置文件是可以共存的,也就是说,可以写这三个配置文件,都会进行读取:
< includes> < include> **/application*.yml</ include> < include> **/application*.yaml</ include> < include> **/application*.properties</ include> </ includes>
所以在某种程度上,我们建议使用一个配置文件,通常简单点,也就是yml,我们删除我们的properties的所有内容,修改后缀为yml
然后删除pojo包,再在controller对应的类中和测试类中,去掉关于之前数据库配置的代码,然后再删除数据库相关依赖,再在yml中添加如下:
案例:
server : port : 8080 servlet : context-path : /hello
启动项目,访问http://localhost:8080/hello/hello/boot,就有数据了
还有一些在yml中关于属性编写的介绍,但是这些细节太多,请到88章博客查看
配置文件属性值的注入:
使用Spring Boot全局配置文件设置属性时:
如果配置属性是Spring Boot已有属性,例如服务端口server.port,那么Spring Boot内部自动扫描并读取这些配置文件时,对应的属性值覆盖默认属性,如果配置的属性是用户自定义属性,例如刚刚自定义的Person实体类属性,则不会自动的覆盖,因为没有,那么他只是定义,并没有操作,需要我们在程序中手动注入这些配置属性方可操作,而不是自动的使用(因为定义)
那么实际上也可以这样的操作:如@Value(“${server.port}”),那么可以注入对应的端口值,因为虽然他覆盖了对应的默认属性,但他任然是定义的,既然是定义的,就可以使用
总体而言:该配置在spring boot中多了一个已有属性进行覆盖,其余的与普通的配置存放信息文件是一样的,需要被使用(如properties文件,以前有操作数据库的信息,那时就是被使用),然后使用注解注入对应的实例,当然若没有对应的注入配置属性,那么对应的实例自然是使用默认的值的,当然他们注入的方式基本都是扫描时进行操作的,只有扫描时,对应的注解操作才会进行
而对应的配置文件信息(写的信息)实际上也是使用后的再扫描的(在spring中也有注解和配置文件操作他,他基本是全局的)
Spring Boot支持多种注入配置文件属性的方式,下面来介绍如何使用注解@ConfigurationProperties和@Value注入属性
使用@ConfigurationProperties注入属性:
Spring Boot提供的@ConfigurationProperties注解
用来快速、方便地将配置文件中的自定义属性值批量注入到某个Bean对象的多个对应属性中
前面的操作中,我们就使用了这个注解并说明了,所以这里就不作说明
实际上上面的注解方式不够灵活,要想要更加的灵活(也就是不够更加的设置具体的细节),一般使用如下方式进行注入属性值
使用@Value注入属性:
@Value注解是Spring框架提供的,用来读取配置文件中的属性值并逐个注入到Bean对象的对应属性中
Spring Boot框架从Spring框架中对@Value注解进行了默认继承
所以在Spring Boot框架中还可以使用该注解读取和注入配置文件属性值,使用@Value注入属性的示例代码如下
@Value ( "${person.id}" )
private int id;
上述代码中,使用@Component和@Value注入Person实体类的id属性
其中,@Value不仅可以将配置文件的属性注入Person的id属性,还可以直接给id属性直接的赋值,如@Value(“1”),直接赋值为1
这点是@ConfigurationProperties不支持的,因为他只能去配置文件里加上对应的属性及其值才可,不够灵活,且使得配置文件信息变多
具体操作这里就不说明了
自定义配置:
spring Boot免除了项目中大部分的手动配置,对于一些特定情况,我们可以通过修改全局配置文件以适应具体生产环境,可以说,几乎所有的配置都可以写在application.yml文件中
Spring Boot会自动加载全局配置文件从而免除我们手动加载的烦恼(因为设置好的三个)
但是,如果我们自定义配置文件,Spring Boot是无法识别这些配置文件的(因为只有那三个可以),此时就需要我们手动加载
接下来,将针对Spring Boot的自定义配置文件及其加载方式进行讲解
使用@PropertySource加载配置文件:
对于这种加载自定义配置文件的需求,可以使用@PropertySource注解来实现,@PropertySource注解用于指定自定义配置文件的具体位置和名称,当然,如果需要将自定义配置文件中的属性值注入到对应类的属性中,可以使用@ConfigurationProperties或者@Value注解进行属性值注入,因为自定义配置文件与其他三个配置文件一样,都被读取操作了,自然结果是一样的,只是不会自动读取操作该自定义的配置文件而已,需要手动读取操作
现在我们演示:
打开Spring Boot项目的resources目录,在项目的类路径下新建一个test.properties自定义配置文件,在该配置文件中编写需要设置的配置属性
#对实体类对象MyProperties进行属性配置
test.id=110
test.name=test
在com.boot包下创建pojo包,然后创建一个配置类MyProperties
提供test.properties自定义配置文件中对应的属性,并根据@PropertySource注解的使用进行相关配置
package com. boot. pojo ; import org. springframework. boot. context. properties. ConfigurationProperties ;
import org. springframework. context. annotation. PropertySource ;
import org. springframework. stereotype. Component ; @Component
@PropertySource ( "classpath:test.properties" )
@ConfigurationProperties ( prefix = "test" )
public class MyProperties { private int id; private String name; @Override public String toString ( ) { return "MyProperties{" + "id=" + id + ", name='" + name + '\'' + '}' ; } 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; }
}
进行测试:
@Autowired private MyProperties myProperties; @Test public void myPropertiesTest ( ) { System . out. println ( myProperties) ; }
使用@Configuration编写自定义配置类:
在Spring Boot框架中,推荐使用配置类的方式向容器中添加和配置组件
在Spring Boot框架中,通常使用@Configuration注解定义一个配置类
Spring Boot会自动扫描和识别配置类,从而替换传统Spring框架中的XML配置文件
当定义一个配置类后,还需要在类中的方法上使用@Bean注解进行组件配置,将方法的返回对象注入到Spring容器中
并且组件名称默认使用的是方法名,当然也可以使用@Bean注解的name或value属性自定义组件的名称
演示:
在项目下新建一个com.boot.config包,并在该包下新创建一个类MyConfig,该类中不需要编写任何代码
而该类目前没有添加任何配置和注解,因此还无法正常被Spring Boot扫描和识别
创建了一个com.boot.service包,里面创建空的MyService类,用来操作
接下来使用@Configuration注解将该MyConfig类声明一个配置类,内容如下:
@Configuration
public class MyConfig { @Bean public MyService myService ( ) { return new MyService ( ) ; } }
MyConfig是@Configuration注解声明的配置类(类似于声明了一个XML配置文件)
该配置类会被Spring Boot自动扫描识别,使用@Bean注解的myService()方法
其返回值对象会作为组件添加到了Spring容器中(类似于XML配置文件中的标签配置),并且该组件的id默认是方法名myService
测试类:
@Autowired private MyService myService; @Autowired private MyConfig myConfig; @Test public void iocTest ( ) { System . out. println ( myService) ; System . out. println ( myConfig) ; }
若返回数据,则代表注入成功,当然也可以操作如下:
@Autowired
private ApplicationContext applicationContext;
@Test public void Test ( ) { System . out. println ( applicationContext. getBean ( "myService2" ) ) ; System . out. println ( applicationContext. containsBean ( "myService2" ) ) ; }
本质上是spring的知识,只不过spring boot整合了而已(spring boot只是对应注解使得操作了而已,也就是扫描)
如果硬要说的话,spring boot本身基本没有任何注解,就算有,也非常少,他的注解主要是他引入的依赖中spring造成的,他自身的具体作用就是依赖管理相关,和自动配置相关,当然了启动类的注解还是他的,只不过内部基本操作了spring的,当然,如果看包含关系,那么也可以说成spring注解是spring boot的
随机数设置及参数间引用:
在Spring Boot配置文件中设置属性时,除了可以像前面示例中显示的配置属性值外,还可以使用 随机值和参数间引用对属性值进行设置,下面,针对配置文件中这两种属性值的设置方式进行讲解
随机值设置:
在Spring Boot配置文件中,随机值设置使用到了Spring Boot内嵌的 RandomValuePropertySource类,对一些隐秘属性值或者测试用例属性值进行随机值注入,随机值设置的语法格式为${random.xx},xx表示需要指定生成的随机数类型和范围,它可以生成随机的整数、uuid或字符串,示例代码如下:
my.secret=${random.value} // 配置随机值
my.number=${random.int} // 配置随机整数
my.bignumber=${random.long} // 配置随机long类型数
my.uuid=${random.uuid} // 配置随机uuid类型数
my.number.less.than.ten=${random.int(10)} // 配置小于10的随机整数
my.number.in.range=${random.int[1024,65536]} // 配置范围在[1024,65536]之间的随机整数
上述代码中,使用RandomValuePropertySource类中random提供的随机数类型,分别展示了不同类型随机值的设置示例 ,本质上是读取配置时,识别形成的
我们来测试:
首先在pojo包下,创建MyTest类:
package com. boot. pojo ; import org. springframework. boot. context. properties. ConfigurationProperties ;
import org. springframework. stereotype. Component ; @Component
@ConfigurationProperties ( prefix = "test" )
public class MyTest { private int id; private String name; @Override public String toString ( ) { return "MyTest{" + "id=" + id + ", name='" + name + '\'' + '}' ; } 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; }
}
我们可以测试,在yml(只有少部分与properties是不同的识别,这里肯定类似的)中加上如下:
test : id : ${ random.int} name : ${ random.int[ 1024 , 65536 ] }
测试类:
@Autowired private MyTest myTest; @Test public void fa ( ) { System . out. println ( myTest) ; }
多次的执行看看打印结果吧
参数间引用:
在Spring Boot配置文件中,配置文件的属性值还可以进行参数间的引用,也就是在后一个配置的属性值中直接引用先前已经定义过的属性,这样可以直接解析其中的属性值了, 使用参数间引用的好处就是,在多个具有相互关联的配置属性中,只需要对其中一处属性预先配置,其他地方都可以引用,省去了后续多处修改的麻烦,参数间引用的语法格式为${xx},xx表示先前在配置文件中已经配置过的属性名,示例代码如下:
app.name=MyApp
app.description=${app.name} is a Spring Boot application
我们继续修改上面的操作,在前面的yml中加上如下:
test : ui : 8 id : ${ random.int} name : ${ random.int[ 1024 , 65536 ] } de : ${ test.id} is ${ test.ui}
在对应的MyTest类中加上如下:
private String de; public String getDe ( ) { return de; } public void setDe ( String de) { this . de = de; } @Override public String toString ( ) { return "MyTest{" + "id=" + id + ", name='" + name + '\'' + ", de='" + de + '\'' + '}' ; }
继续访问测试类看看结果吧,这个时候你会发现一个问题,在上面的配置中:
test : ui : 8 id : ${ random.int} name : ${ random.int[ 1024 , 65536 ] } de : ${ test.id} is ${ test.ui}
SpringBoot原理深入及源码剖析:
在前面88章博客中,这里的说明比较粗糙,那么这里我们来进行更加细节的说明
依赖管理:
为什么导入dependency时不需要指定版本,这是因为项目pom.xml文件有两个核心依赖,这里以前面为主:
< parent> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-parent</ artifactId> < version> 2.7.2</ version> < relativePath/> </ parent> < dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-web</ artifactId> </ dependency>
首先我们先说明spring-boot-starter-parent
进入他,他里面配置一些基本的处理,通常有对应的三个配置文件,但是这不是我们需要的,我们进入他的:
< parent> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-dependencies</ artifactId> < version> 2.7.2</ version> </ parent>
核心代码具体如下:
< properties> < activemq.version> 5.16.5</ activemq.version> < antlr2.version> 2.7.7</ antlr2.version> < appengine-sdk.version> 1.9.98</ appengine-sdk.version> < artemis.version> 2.19.1</ artemis.version> < aspectj.version> 1.9.7</ aspectj.version> ...
这是pom.xml引入依赖文件不需要标注依赖文件版本号的原因,这是依赖管理
spring-boot-starter-parent父依赖启动器的主要作用是进行版本统一管理,那么项目运行依赖的JAR包是从何而来的,很明显,就是对应的spring-boot-starter-web,在上面的spring-boot-dependencies后面是有对应的
所以在结合依赖管理以及其他自身存在的依赖操作,也就形成了我们的起步依赖,当然了,这里的起步并不是指spring boot原始依赖,是指功能加上后的,单纯来说spring boot对依赖的只有管理和部分起步的依赖
自动配置:
如果说依赖管理不算是spring boot的,那么这里绝对是,这是最重要的
在这里需要提一点,一般不同的版本的boot,对应的代码显示是不同的(上面的依赖管理的内容可能也会不同)
但大致底层原理是一样的,后面有时会说明一下,所以注意即可
概念:能够在我们添加jar包依赖的时候,自动为我们配置一些组件的相关配置
我们无需配置或者只需要少量配置就能运行编写的项目
Spring Boot到底是如何进行自动配置的,都把哪些组件进行了自动配置,这些的思考离不开我们补充的依赖,由于没有配置文件,也就是具体的xml,那么基本上都是操作配置类,而我们加上的依赖自然是存在配置类的,且由于是整合了spring boot,所以在满足约定的情况下,扫描的可以扫描到的,那么我们就只需要直到他是怎么扫描的即可
Spring Boot应用的启动入口是@SpringBootApplication注解标注的类中的main()方法
package com. boot ; import org. springframework. boot. SpringApplication ;
import org. springframework. boot. autoconfigure. SpringBootApplication ; @SpringBootApplication
public class BootApplication { public static void main ( String [ ] args) { SpringApplication . run ( BootApplication . class , args) ; } }
配置类自身是作为bean的,所以只需要考虑配置类的扫描处理,那么上面的注解是最重要的,我们看看他到底怎么操作扫描,或者他使得spring在什么时候操作扫描,我们直接的进入他:
package org. springframework. boot. autoconfigure ; import java. lang. annotation. Documented ;
import java. lang. annotation. ElementType ;
import java. lang. annotation. Inherited ;
import java. lang. annotation. Retention ;
import java. lang. annotation. RetentionPolicy ;
import java. lang. annotation. Target ;
import org. springframework. beans. factory. support. BeanNameGenerator ;
import org. springframework. boot. SpringBootConfiguration ;
import org. springframework. boot. context. TypeExcludeFilter ;
import org. springframework. context. annotation. ComponentScan ;
import org. springframework. context. annotation. Configuration ;
import org. springframework. context. annotation. FilterType ;
import org. springframework. context. annotation. ComponentScan . Filter ;
import org. springframework. core. annotation. AliasFor ; @Target ( { ElementType . TYPE } )
@Retention ( RetentionPolicy . RUNTIME )
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan ( excludeFilters = { @Filter ( type = FilterType . CUSTOM , classes = { TypeExcludeFilter . class }
) , @Filter ( type = FilterType . CUSTOM , classes = { AutoConfigurationExcludeFilter . class }
) }
)
public @interface SpringBootApplication { @AliasFor ( annotation = EnableAutoConfiguration . class ) Class < ? > [ ] exclude ( ) default { } ; @AliasFor ( annotation = EnableAutoConfiguration . class ) String [ ] excludeName ( ) default { } ; @AliasFor ( annotation = ComponentScan . class , attribute = "basePackages" ) String [ ] scanBasePackages ( ) default { } ; @AliasFor ( annotation = ComponentScan . class , attribute = "basePackageClasses" ) Class < ? > [ ] scanBasePackageClasses ( ) default { } ; @AliasFor ( annotation = ComponentScan . class , attribute = "nameGenerator" ) Class < ? extends BeanNameGenerator > nameGenerator ( ) default BeanNameGenerator . class ; @AliasFor ( annotation = Configuration . class ) boolean proxyBeanMethods ( ) default true ;
}
从上述源码可以看出,@SpringBootApplication注解是一个组合注解,前面 4 个是注解的元数据信息
我们主要看后面 3 个注解:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan三个核心注解
关于这三个核心注解的相关说明具体如下(第三个:
@SpringBootConfiguration注解:
@SpringBootConfiguration:SpringBoot的配置类,标注在某个类上,表示这是一个SpringBoot的配置类
查看@SpringBootConfiguration注解源码,核心代码具体如下:
package org. springframework. boot ; import java. lang. annotation. Documented ;
import java. lang. annotation. ElementType ;
import java. lang. annotation. Retention ;
import java. lang. annotation. RetentionPolicy ;
import java. lang. annotation. Target ;
import org. springframework. context. annotation. Configuration ;
import org. springframework. core. annotation. AliasFor ;
import org. springframework. stereotype. Indexed ; @Target ( { ElementType . TYPE } )
@Retention ( RetentionPolicy . RUNTIME )
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration { @AliasFor ( annotation = Configuration . class ) boolean proxyBeanMethods ( ) default true ;
}
从上述源码可以看出,@SpringBootConfiguration注解内部有一个核心注解@Configuration
该注解是Spring框架提供的,表示当前类为一个配置类(XML配置文件的注解表现形式),并可以被组件扫描器扫描,其中xml可以引入xml和扫描,配置类也可以引入配置类和扫描,而扫描(如:ComponentScan)也是可以扫描配置类的,当然,最终是谁是最开始的扫描,大概率是spring boot底层中进行处理的
由此可见,@SpringBootConfiguration注解的作用与@Configuration注解相同,都是标识一个可以被组件扫描器扫描的配置类
只不过@SpringBootConfiguration是被Spring Boot进行了重新封装命名而已,这样会使得该类可以被获取调用(底层获取执行),从而执行main方法
虽然我们也可以再次进行获取,但也要注意,同一个类里面,多个不同的变量(通常考虑同类型),基本是能注入同一个对象的
@EnableAutoConfiguration注解:
@EnableAutoConfiguration:开启自动配置功能,以前由我们需要配置的东西,现在由SpringBoot帮我们自动配置
这个注解就是Springboot能实现自动配置的关键
同样,查看该注解内部查看源码信息,核心代码具体如下 :
package org. springframework. boot. autoconfigure ; import java. lang. annotation. Documented ;
import java. lang. annotation. ElementType ;
import java. lang. annotation. Inherited ;
import java. lang. annotation. Retention ;
import java. lang. annotation. RetentionPolicy ;
import java. lang. annotation. Target ;
import org. springframework. context. annotation. Import ; @Target ( { ElementType . TYPE } )
@Retention ( RetentionPolicy . RUNTIME )
@Documented
@Inherited
@AutoConfigurationPackage
@Import ( { AutoConfigurationImportSelector . class } )
public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration" ; Class < ? > [ ] exclude ( ) default { } ; String [ ] excludeName ( ) default { } ;
}
可以发现它是一个组合注解, Spring 中有很多以Enable开头的注解,上面还有个注解:
package org. springframework. boot. autoconfigure ; import java. lang. annotation. Documented ;
import java. lang. annotation. ElementType ;
import java. lang. annotation. Inherited ;
import java. lang. annotation. Retention ;
import java. lang. annotation. RetentionPolicy ;
import java. lang. annotation. Target ;
import org. springframework. boot. autoconfigure. AutoConfigurationPackages . Registrar ;
import org. springframework. context. annotation. Import ; @Target ( { ElementType . TYPE } )
@Retention ( RetentionPolicy . RUNTIME )
@Documented
@Inherited
@Import ( { Registrar . class } )
public @interface AutoConfigurationPackage { String [ ] basePackages ( ) default { } ; Class < ? > [ ] basePackageClasses ( ) default { } ;
}
很明显EnableAutoConfiguration注解存在两个导入,也就是:
@Import ( { Registrar . class } )
@Import ( { AutoConfigurationImportSelector . class } )
可查看Registrar类中registerBeanDefinitions方法:
这个方法就是导入组件类的具体实现(ctrl+鼠标左键点击Registrar.class中的Registrar):
static class Registrar implements ImportBeanDefinitionRegistrar , DeterminableImports { Registrar ( ) { } public void registerBeanDefinitions ( AnnotationMetadata metadata, BeanDefinitionRegistry registry) { AutoConfigurationPackages . register ( registry, ( String [ ] ) ( new AutoConfigurationPackages. PackageImports ( metadata) ) . getPackageNames ( ) . toArray ( new String [ 0 ] ) ) ; } public Set < Object > determineImports ( AnnotationMetadata metadata) { return Collections . singleton ( new AutoConfigurationPackages. PackageImports ( metadata) ) ; } }
从上述源码可以看出,在Registrar类中有一个registerBeanDefinitions()方法,他最终会执行的,那么我们进入AutoConfigurationPackages.register,首先我们需要看看他给了什么数据,我们给他调试:
在这之前,我们需要说明一下:
扫描,配置类等等的说明:首先扫描通常是注解的操作,也就是说配置类的注解也属于扫描范围,当然,配置类存在三种,一个是注解,一个是导入时实现接口的操作(通常与文件一起,所以不考虑说明他),一个是文件的读取操作,他们都是考虑创建bean的
也就是说对应的元数据是启动类名称
我们发现,通过注解,的确得到了当前类的对应的包com.boot(实际上是结合了@ComponentScan注解,而得到的地址信息,扫描,因为启动类是配置类,要么扫描导入,要么进行扫描)
也就是说,@AutoConfigurationPackage注解的主要作用就是将主程序类所在包及所有子包下的组件扫描到spring容器中(这里注意了,他只是将信息放入对应的列表中)
因为他操作了@Import注解,一般是将实例放入到ioc容器中的操作或者一些其他的方法处理(上面的)
该注解在这里一般会操作执行参数类的方法,好像是固定的几个
如selectImports方法包括内部类的,可能也有registerBeanDefinitions方法
因此 在定义项目包结构时,要求定义的包结构非常规范,项目主程序启动类要定义在最外层的根目录位置(他自身所在就是)
然后在根目录位置内部建立子包和类进行业务开发,这样才能够保证定义的类能够被组件扫描器扫描
我们继续看后面的@Import({AutoConfigurationImportSelector.class})注解:
将AutoConfigurationImportSelector这个类导入到Spring容器中,AutoConfigurationImportSelector可以帮助Springboot应用
将所有符合条件的@Configuration配置(配置类)都加载到当前SpringBoot创建并使用的IOC容器(ApplicationContext)中
如果说@AutoConfigurationPackage注解是得到扫描的实例信息(可能包括配置类),但却不能操作其他依赖的配置类和扫描信息(指其他引入的依赖)
那么这个@Import({AutoConfigurationImportSelector.class})注解是操作配置类或者扫描得到实例,包括完成@AutoConfigurationPackage注解定义信息后续的自动扫描处理(当然,这个处理可能由其他操作来完成,或者spring自身上下文存在的处理),但一般是需要先进行扫描,他们一起,使得扫描得到实例,好像spring中扫描时,他们是一起操作的,而不是分开,spring boot却是分开,但总体是一起
他们两个都需要操作完才会真正的启动,简单来说第一个注解定义扫描信息,另外一个是操作自动的配置,然后处理文件的配置类创建bean(一般没有扫描,所以前面是"或")
继续研究AutoConfigurationImportSelector这个类
通过源码分析这个类中是通过selectImports这个方法告诉springboot都需要导入那些组件:
public class AutoConfigurationImportSelector implements DeferredImportSelector , BeanClassLoaderAware , ResourceLoaderAware , BeanFactoryAware , EnvironmentAware , Ordered { public String [ ] selectImports ( AnnotationMetadata annotationMetadata) { if ( ! this . isEnabled ( annotationMetadata) ) { return NO_IMPORTS ; } else { AutoConfigurationImportSelector. AutoConfigurationEntry autoConfigurationEntry = this . getAutoConfigurationEntry ( annotationMetadata) ; return StringUtils . toStringArray ( autoConfigurationEntry. getConfigurations ( ) ) ; } }
}
一般的annotationMetadata的结果,他应该有这个方法来得到的,具体怎么得到可以百度(全局搜索):
this . autoConfigurationMetadata = AutoConfigurationMetadataLoader . loadMetadata ( this . beanClassLoader) ;
final class AutoConfigurationMetadataLoader { static AutoConfigurationMetadata loadMetadata ( ClassLoader classLoader) { return loadMetadata ( classLoader, "META-INF/spring-autoconfigure-metadata.properties" ) ; } static AutoConfigurationMetadata loadMetadata ( ClassLoader classLoader, String path) { try { Enumeration < URL> urls = classLoader != null ? classLoader. getResources ( path) : ClassLoader . getSystemResources ( path) ; Properties properties = new Properties ( ) ; while ( urls. hasMoreElements ( ) ) { properties. putAll ( PropertiesLoaderUtils . loadProperties ( new UrlResource ( ( URL ) urls. nextElement ( ) ) ) ) ; } return loadMetadata ( properties) ; } catch ( IOException var4) { throw new IllegalArgumentException ( "Unable to load @ConditionalOnClass location [" + path + "]" , var4) ; } } static AutoConfigurationMetadata loadMetadata ( Properties properties) { return new AutoConfigurationMetadataLoader. PropertiesAutoConfigurationMetadata ( properties) ; }
}
我们直接看对应的文件有什么:
org.springframework.boot.autoconfigure.AutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration=
org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.annotation.EnableRabbit
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration=
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration$MessagingTemplateConfiguration=
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration$MessagingTemplateConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.core.RabbitMessagingTemplate
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.ConditionalOnClass=com.rabbitmq.client.Channel,org.springframework.amqp.rabbit.core.RabbitTemplate
org.springframework.boot.autoconfigure.amqp.RabbitStreamConfiguration=
org.springframework.boot.autoconfigure.amqp.RabbitStreamConfiguration.ConditionalOnClass=org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration=
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration=
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration.ConditionalOnClass=org.aspectj.weaver.Advice
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration=
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration=
他写的是啥,有什么用:
#其中org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration
#第一个点,往前推
#代表进行自动配置的类,后面的ConditionalOnClass是一个注解,该注解一般表示的是条件(=后面的)
#如果要向ioc容器中注入我们自动配置的类需要满足=号后面的条件
#具体条件是
#当该注解里面出现了后面的org.springframework.amqp.rabbit.annotation.EnableRabbit(EnableRabbit这个类时,因为约定,所以存在某个地方统一获取的)
#就进行该RabbitAnnotationDrivenConfiguration类的自动注入
#自动注入:可以说成是自动的创建实例,放在ioc容器里面
#即使得可以被注入得到,所以我们导入对应的依赖,spring boot会帮我们生成实例,就是这样的原因,但并不是所有依赖,因为该文件的内容是有限的,这是肯定的#其他的基本都是这样的说明
上面的操作总得来说是得到所有的自动配置类及其需要的对应条件,如果没有,说明不需要条件,自然会进行注入的
也就是说,这里是自动注入的地方,那么就非常明白了,对应的两个注解:
@Import ( { Registrar . class } )
@Import ( { AutoConfigurationImportSelector . class } )
第一个是操作扫描spring boot中的手动或者自身存在的实例的信息,而第二个则是扫描引入与spring boot整合的依赖的(因为对应的可能不在boot包下面的(他们有自身的处理的,不可能都必须按照我们的路径来,而且,我们启动类路径我们可以随时改变,而他们不行,因为是编写好的依赖),我们可以试着打包看看路径就知道了,否则单纯的扫描就行了,也就没有必要需要第二个注解了)
所以真正的自动配置就是@Import({AutoConfigurationImportSelector.class}),当然了,上面说明他们两个时,只是看到定义的一些具体数据,而他们是扫描的数据,最终的扫描处理在更加的底层了,这里就不去看了
接下来我们接着看selectImports方法里面的getAutoConfigurationEntry,也就是前面的AutoConfigurationImportSelector:
public String [ ] selectImports ( AnnotationMetadata annotationMetadata) { if ( ! this . isEnabled ( annotationMetadata) ) { return NO_IMPORTS ; } else { AutoConfigurationImportSelector. AutoConfigurationEntry autoConfigurationEntry = this . getAutoConfigurationEntry ( annotationMetadata) ; return StringUtils . toStringArray ( autoConfigurationEntry. getConfigurations ( ) ) ; } }
既然我们拿取了对应的元数据,那么我们进入getAutoConfigurationEntry方法:
protected AutoConfigurationImportSelector. AutoConfigurationEntry getAutoConfigurationEntry ( AnnotationMetadata annotationMetadata) { if ( ! this . isEnabled ( annotationMetadata) ) { return EMPTY_ENTRY ; } else { AnnotationAttributes attributes = this . getAttributes ( annotationMetadata) ; List < String > configurations = this . getCandidateConfigurations ( annotationMetadata, attributes) ; configurations = this . removeDuplicates ( configurations) ; Set < String > exclusions = this . getExclusions ( annotationMetadata, attributes) ; this . checkExcludedClasses ( configurations, exclusions) ; configurations. removeAll ( exclusions) ; configurations = this . getConfigurationClassFilter ( ) . filter ( configurations) ; this . fireAutoConfigurationImportEvents ( configurations, exclusions) ; return new AutoConfigurationImportSelector. AutoConfigurationEntry ( configurations, exclusions) ; } }
深入getCandidateConfigurations方法:
protected List < String > getCandidateConfigurations ( AnnotationMetadata metadata, AnnotationAttributes attributes) { List < String > configurations = new ArrayList ( SpringFactoriesLoader . loadFactoryNames ( this . getSpringFactoriesLoaderFactoryClass ( ) , this . getBeanClassLoader ( ) ) ) ; ImportCandidates . load ( AutoConfiguration . class , this . getBeanClassLoader ( ) ) . forEach ( configurations:: add ) ; Assert . notEmpty ( configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct." ) ; return configurations; }
继续点开loadFactoryNames方法:
public final class SpringFactoriesLoader { public static List < String > loadFactoryNames ( Class < ? > factoryType, @Nullable ClassLoader classLoader) { ClassLoader classLoaderToUse = classLoader; if ( classLoader == null ) { classLoaderToUse = SpringFactoriesLoader . class . getClassLoader ( ) ; } String factoryTypeName = factoryType. getName ( ) ; return ( List ) loadSpringFactories ( classLoaderToUse) . getOrDefault ( factoryTypeName, Collections . emptyList ( ) ) ; } }
我们再次点开loadSpringFactories方法:
private static Map < String , List < String > > loadSpringFactories ( ClassLoader classLoader) { Map < String , List < String > > result = ( Map ) cache. get ( classLoader) ; if ( result != null ) { return result; } else { HashMap result = new HashMap ( ) ; try { Enumeration urls = classLoader. getResources ( "META-INF/spring.factories" ) ; while ( urls. hasMoreElements ( ) ) { URL url = ( URL ) urls. nextElement ( ) ; UrlResource resource = new UrlResource ( url) ; Properties properties = PropertiesLoaderUtils . loadProperties ( resource) ; Iterator var6 = properties. entrySet ( ) . iterator ( ) ; while ( var6. hasNext ( ) ) { Entry < ? , ? > entry = ( Entry ) var6. next ( ) ; String factoryTypeName = ( ( String ) entry. getKey ( ) ) . trim ( ) ; String [ ] factoryImplementationNames = StringUtils . commaDelimitedListToStringArray ( ( String ) entry. getValue ( ) ) ; String [ ] var10 = factoryImplementationNames; int var11 = factoryImplementationNames. length; for ( int var12 = 0 ; var12 < var11; ++ var12) { String factoryImplementationName = var10[ var12] ; ( ( List ) result. computeIfAbsent ( factoryTypeName, ( key) -> { return new ArrayList ( ) ; } ) ) . add ( factoryImplementationName. trim ( ) ) ; } } } result. replaceAll ( ( factoryType, implementations) -> { return ( List ) implementations. stream ( ) . distinct ( ) . collect ( Collectors . collectingAndThen ( Collectors . toList ( ) , Collections :: unmodifiableList ) ) ; } ) ; cache. put ( classLoader, result) ; return result; } catch ( IOException var14) { throw new IllegalArgumentException ( "Unable to load factories from location [META-INF/spring.factories]" , var14) ; } } }
会去读取一个 spring.factories 的文件
读取不到会表示对应的这个错误,我们根据类变量会看到,最终路径的长这样
public final class SpringFactoriesLoader { public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories" ;
上面的方法,总体来说是去加载一个外部的文件,而这文件是在如下
与前面的META-INF/spring-autoconfigure-metadata.properties在同一个目录下
他们两个基本是有对照的,因为都是操作实例,只是一个扫描操作,一个配置类操作而已
至此得到了默认的支持的自动配置类列表,而基本不用去比较条件触发(有的话)
后面的操作自然是操作配置类,使得条件成立的放入ioc容器,这也使得我们只需要导入对应的依赖即可自动的配置好,而不用扫描放入IOC容器了
这里就需要进行一下总结了:
@Import ( { Registrar . class } )
@Import ( { AutoConfigurationImportSelector . class } )
在项目中加入了Web环境依赖启动器,对应的WebMvcAutoConfiguration自动配置类就会生效(有依赖的话,基本会满足条件),打开该自动配置类会发现
在该配置类中通过全注解配置类的方式对Spring MVC运行所需环境进行了默认配置
包括默认前缀、默认后缀、视图解析器、MVC校验器等
而这些自动配置类的本质是传统Spring MVC框架中对应的XML配置文件
只不过在Spring Boot中以自动配置类的形式进行了预先配置
因此,在Spring Boot项目中加入相关依赖启动器后,基本上不需要任何配置就可以运行程序
当然,我们也可以对这些自动配置类中默认的配置进行更改
总结
因此springboot底层实现自动配置的步骤是:
1: springboot应用启动;
2:@SpringBootApplication起作用;
3:@EnableAutoConfiguration:
@AutoConfigurationPackage:这个组合注解主要是@Import(AutoConfigurationPackages.Registrar.class),也就是:@Import({Registrar.class})
它通过将Registrar类导入到容器中
而Registrar类作用是扫描主配置类同级目录以及子包,并将相应的组件导入到springboot创建管理的容器中(也就是扫描得到实例),虽然只是信息
@Import(AutoConfigurationImportSelector.class):它通过将AutoConfigurationImportSelector类导入到容器中
AutoConfigurationImportSelector类作用是通过selectImports方法执行的过程中
会使用内部工具类SpringFactoriesLoader,查找classpath上所有jar包中的META-INF/spring.factories进行加载
实现将配置类信息交给SpringFactory加载器进行一系列的容器创建过程(操作配置类得到实例)
至此,包地址信息使得扫描和操作配置类得到实例的操作完毕,即自动配置完成
最后说明一下最后一个核心注解,@ComponentScan注解
@ComponentScan注解使得具体扫描的包的根路径由Spring Boot项目主程序启动类所在包位置决定
在扫描过程中由前面介绍的@AutoConfigurationPackage注解进行操作解析
从而得到Spring Boot项目主程序启动类所在包的具体位置
也就是说他使得对应的地址,是当前所在的包进行扫描的最终处理(是前面说明的,对应的第一个注解是信息,操作由spring来处理),但参数一般由@AutoConfigurationPackage解析,而不是直接的定义
至此@SpringBootApplication 的注解的功能就分析差不多了, 简单来说就是 3 个注解的组合注解:
那么注解怎么操作的呢,自然是由于启动时考虑的初始化呗,了解即可,以后说明源码时会知道的
前面的基本回顾完毕(可能在原来的88章博客中有部分的补充,稍微看看吧),现在我们来学习一下后面的补充的知识:
自定义Starter:
SpringBoot starter机制:
SpringBoot由众多Starter组成(一系列的自动化配置的starter插件),SpringBoot之所以流行,也是因为starter,starter是SpringBoot非常重要的一部分,可以理解为一个可拔插式的插件,正是这些starter使得使用某个功能的开发者不需要关注各种依赖库的处理,不需要具体的配置信息,由Spring Boot自动通过 classpath路径下的类发现需要的Bean,并织入相应的Bean,例如,你想使用Reids插件,那么可以使用spring-boot-starter-redis,如果想使用MongoDB,可以使 用spring-boot-starter-data-mongodb
为什么要自定义starter:
开发过程中,经常会有一些独立于业务之外的配置模块,如果我们将这些可独立于业务代码之外的功能配置模块封装成一个个starter,复用的时候只需要将其在pom中引用依赖即可,SpringBoot为我们完成自动装配
自定义starter的命名规则:
SpringBoot提供的starter以 spring-boot-starter-xxx 的方式命名的,官方建议自定义的starter使用 xxx-spring-boot-starter 命名规则,以区分SpringBoot生态提供的starter(名称基本可以随便)
整个过程分为两部分:自定义starter,使用starter
首先,先完成自定义starter:
那么我们首先创建工程,工程名为zdy-spring-boot-starter,导入依赖:
<?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> org.example</ groupId> < artifactId> zdy-spring-boot-starter</ artifactId> < version> 1.0-SNAPSHOT</ version> < properties> < maven.compiler.source> 11</ maven.compiler.source> < maven.compiler.target> 11</ maven.compiler.target> </ properties> < dependencies> < dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-autoconfigure</ artifactId> < version> 2.2.2.RELEASE</ version> </ dependency> </ dependencies> </ project>
还要考虑一点,在spring boot中或者说在maven中,如果不同的jar包存在相同路径的文件,他们是怎么操作的,答:合并,所有的都是合并吗,答:不是,只是有些固定的会进行合并(如这里,但是呢,他可能也不是,只是利用类加载来完成(命名空间)),像类或者资源文件通常应该是不会的,所以可以发现,很多jar包中基本都会存在spring.factories,也是为什么前面会存在这样的操作:
configurations = this . removeDuplicates ( configurations) ;
使得避免重复加载或者某些不必要的冲突,那么不合并的怎么共存:通常指类加载器之间的处理,这里我们了解就行了,每个工程都有其加载的命名的,通常来说,每个框架都有其特有的包,所以一般是不会相同的,所以我们也不用考虑类的合并,并且大多数情况下,idea默认是使用当前项目的配置的,所以我们编写时考虑名称也是需要的,防止配置不操作
所以java在后面版本中,基本上是不会操作合并的,只有手动处理编写程序才能操作,基本上都是使用类加载器来解决会合并的问题(如这里)
那么步骤其实还是明显的,首先我们需要一个类,该类的信息是yml中设置的,且他需要为bean,这里我们就不操作使用@Component来使得他加入容器了,而是使用@EnableConfigurationProperties注解,这个注解相当于可以手动的指定给操作了@ConfigurationProperties注解的配置的类进行加入bean,就不需要加上@Component了,并且可以指定多个
然后由于前面是得到对应spring.factories的信息的,且存在条件,其条件是根据注解ConditionalOnClass来的,所以我们需要设置条件,这个时候我们还需要创建一个类来作为类来操作,当然,通常我们会考虑为配置类(特别的,在高版本下,明确需要是配置类了,因为存在判断了,并且配置类可以更加的方便处理一些注解,而不是反射再考虑后面的问题,当然了具体还是需要测试的),那么我们先看如下吧:
先创建com.pojo包,然后创建如下的类:
package com. pojo ; import org. springframework. boot. context. properties. ConfigurationProperties ;
import org. springframework. boot. context. properties. EnableConfigurationProperties ;
@EnableConfigurationProperties ( SimpleBean . class )
@ConfigurationProperties ( prefix = "simplebean" )
public class SimpleBean { private int id; private String name; 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; } @Override public String toString ( ) { return "SimpleBean{" + "id=" + id + ", name='" + name + '\'' + '}' ; }
}
然后再com包下,创建config包,然后创建如下的类:
package com. config ; import com. pojo. SimpleBean ;
import org. springframework. boot. autoconfigure. condition. ConditionalOnClass ;
import org. springframework. context. annotation. Bean ;
import org. springframework. context. annotation. Configuration ; @Configuration
@ConditionalOnClass
public class MyAutoConfiguration { static { System . out. println ( "MyAutoConfiguration init...." ) ; }
}
然后在resources下创建/META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.config.MyAutoConfiguration#之所以是EnableAutoConfiguration:是因为对应的启动类注解中,默认创建实例的部分是EnableAutoConfiguration对应的列表
EnableAutoConfiguration代表操作自动的配置,前面我们没有具体的说明是因为有很多情况,有些可能只是初始化的操作
一般在条件满足后,会将各种满足的进入spring.factories中进行处理,有些可能处理多次,比如初始化,然后放入bean中
为什么不用写spring-autoconfigure-metadata.properties,这是因为他也是条件,一般来说对应的类里面都存在@ConditionalOnClass条件,而他是进一步的进行判断的条件,所以对应的条件过滤是考虑多个条件的,而这里我们不加,那么只要考虑@ConditionalOnClass是否写即可(不写上条件,也不只是他)
那么现在我们来测试吧,既然条件绝对满足,并且在是自动配置的直接放入容器,那么我们就可以这样操作了:
直接进行打包,安装(虽然在对应的工程下面,通常可以直接拿取,而不用打包,安装),然后再之前的spring boot项目中操作如下(由于是引入的,所以要记得写上yml里面的内容满足前面的前缀哦):
先加入依赖:
< dependency> < groupId> org.example</ groupId> < artifactId> zdy-spring-boot-starter</ artifactId> < version> 1.0-SNAPSHOT</ version> </ dependency>
对应的yaml:
simplebean : id : 1 name : 自定义starter
编写测试方法:
既然他会自动的配置,那么我们只需要注入即可:
@Autowired private SimpleBean simpleBean; @Test public void zdyStarterTest ( ) { System . out. println ( simpleBean) ; }
然后修改@EnableConfigurationProperties(SimpleBean.class)的注解位置,删除在类上的注解,放在启动类上,然后测试,如果发现,存在数据,说明他已经自动配置成功了,至此,手写的Starter基本成功(记得将启动类的位置移动到com包下,保证处理到他们)
但是你可能会说,这是扫描造成的,而不是自动配置,那么你就忽略了一点,扫描是有路径的,而大多数情况下,是扫描不到对应的注解的,所以,你现在将@EnableConfigurationProperties(SimpleBean.class)在启动类上的注解去掉,然后移动位置,放在boot包下,形成扫描的路径在MyAutoConfiguration所在包之下,然后操作该类,在里面操作:
@Bean public SimpleBean simpleBean ( ) { return new SimpleBean ( ) ; }
继续测试,可以发现,就算不在对应的扫描路径下,也可以得到实例,测试去掉对应配置类的@Configuration,发现也可以,证明与是否配置类无关,证明虽然不是配置类,但是会考虑其为配置类,操作反射,然后操作@Bean,只不过这样耗费性能(所以我们建议设置为配置类,这样性能会提高)
所以也正如前面所说,其文件也是操作配置类的一种(接口不做说明,因为是结合文件的)(直接创建bean,你可以到之前的对应文件看看,发现,他们通常也不是配置类的)
执行原理:
每个Spring Boot项目都有一个主程序启动类,在主程序启动类中有一个启动项目的main()方法, 在该方法中通过执行SpringApplication.run()即可启动整个Spring Boot程序,问题:那么SpringApplication.run()方法到底是如何做到启动Spring Boot项目的呢,下面我们查看run()方法内部的源码,核心代码具体如下:
package com ; import org. springframework. boot. SpringApplication ;
import org. springframework. boot. autoconfigure. SpringBootApplication ; @SpringBootApplication
public class BootApplication { public static void main ( String [ ] args) { SpringApplication . run ( BootApplication . class , args) ; } }
我们进入这个run:
前面说明自动配置时,以及之前博客中,基本上方法都写在一起,这样并不好观察,所以从这里开始,尽量分开写,那么就只需要一个类前缀的标识了,如:
public class xxx{ xxx
}
后面相同的就不继续这样了,在前面操作自动配置原理时或者某些博客中就是这样了,那么后面就这样处理吧,当然了怎么舒服怎么来,没有必要限制的:
public class SpringApplication { public static ConfigurableApplicationContext run ( Class < ? > primarySource, String . . . args) { return run ( new Class [ ] { primarySource} , args) ; } public static ConfigurableApplicationContext run ( Class < ? > [ ] primarySources, String [ ] args) { return ( new SpringApplication ( primarySources) ) . run ( args) ; }
} public SpringApplication ( Class < ? > . . . primarySources) { this ( ( ResourceLoader ) null , primarySources) ; } public SpringApplication ( ResourceLoader resourceLoader, Class < ? > . . . primarySources) { this . sources = new LinkedHashSet ( ) ; this . bannerMode = Mode . CONSOLE ; this . logStartupInfo = true ; this . addCommandLineProperties = true ; this . addConversionService = true ; this . headless = true ; this . registerShutdownHook = true ; this . additionalProfiles = Collections . emptySet ( ) ; this . isCustomEnvironment = false ; this . lazyInitialization = false ; this . applicationContextFactory = ApplicationContextFactory . DEFAULT ; this . applicationStartup = ApplicationStartup . DEFAULT ; this . resourceLoader = resourceLoader; Assert . notNull ( primarySources, "PrimarySources must not be null" ) ; this . primarySources = new LinkedHashSet ( Arrays . asList ( primarySources) ) ; this . webApplicationType = WebApplicationType . deduceFromClasspath ( ) ; this . bootstrapRegistryInitializers = new ArrayList ( this . getSpringFactoriesInstances ( BootstrapRegistryInitializer . class ) ) ; this . setInitializers ( this . getSpringFactoriesInstances ( ApplicationContextInitializer . class ) ) ; this . setListeners ( this . getSpringFactoriesInstances ( ApplicationListener . class ) ) ; this . mainApplicationClass = this . deduceMainApplicationClass ( ) ; }
前面都是一些默认的,那么我们看这个:
this . webApplicationType = WebApplicationType . deduceFromClasspath ( ) ; public enum WebApplicationType { static WebApplicationType deduceFromClasspath ( ) { if ( ClassUtils . isPresent ( "org.springframework.web.reactive.DispatcherHandler" , ( ClassLoader ) null ) && ! ClassUtils . isPresent ( "org.springframework.web.servlet.DispatcherServlet" , ( ClassLoader ) null ) && ! ClassUtils . isPresent ( "org.glassfish.jersey.servlet.ServletContainer" , ( ClassLoader ) null ) ) { return REACTIVE ; } else { String [ ] var0 = SERVLET_INDICATOR_CLASSES ; int var1 = var0. length; for ( int var2 = 0 ; var2 < var1; ++ var2) { String className = var0[ var2] ; if ( ! ClassUtils . isPresent ( className, ( ClassLoader ) null ) ) { return NONE ; } } return SERVLET ; } } }
public enum WebApplicationType { NONE , SERVLET , REACTIVE ;
}
当然,依赖最终都会和项目路径一起的,但是这些由构建工具来完成,反正都会编译当前项目的,这里我们了解即可
所以的确检查是否是web程序,检查完进行赋值,为后面的一些判断进行处理,通常来说,如果不是web程序,那么jar包就相当于普通的main方法了,自然就是普通的java程序
继续看后面的代码:
this . setInitializers ( this . getSpringFactoriesInstances ( ApplicationContextInitializer . class ) ) ; private < T > Collection < T > getSpringFactoriesInstances ( Class < T > type) { return this . getSpringFactoriesInstances ( type, new Class [ 0 ] ) ; } private < T > Collection < T > getSpringFactoriesInstances ( Class < T > type, Class < ? > [ ] parameterTypes, Object . . . args) { ClassLoader classLoader = this . getClassLoader ( ) ; Set < String > names = new LinkedHashSet ( SpringFactoriesLoader . loadFactoryNames ( type, classLoader) ) ; List < T > instances = this . createSpringFactoriesInstances ( type, parameterTypes, classLoader, args, names) ; AnnotationAwareOrderComparator . sort ( instances) ; return instances; }
上面操作了这个:
public static List < String > loadFactoryNames ( Class < ? > factoryType, @Nullable ClassLoader classLoader) { ClassLoader classLoaderToUse = classLoader; if ( classLoader == null ) { classLoaderToUse = SpringFactoriesLoader . class . getClassLoader ( ) ; } String factoryTypeName = factoryType. getName ( ) ; return ( List ) loadSpringFactories ( classLoaderToUse) . getOrDefault ( factoryTypeName, Collections . emptyList ( ) ) ; }
private static Map < String , List < String > > loadSpringFactories ( ClassLoader classLoader) {
}
继续看后面:
Set < String > names = new LinkedHashSet ( SpringFactoriesLoader . loadFactoryNames ( type, classLoader) ) ; List < T > instances = this . createSpringFactoriesInstances ( type, parameterTypes, classLoader, args, names) ; AnnotationAwareOrderComparator . sort ( instances) ; return instances;
他是根据ApplicationContextInitializer.class来得到列表的,而不是之前自动配置中的列表,那么很明显:
this . setInitializers ( this . getSpringFactoriesInstances ( ApplicationContextInitializer . class ) ) ;
同样的,我们看后面:
this . setInitializers ( this . getSpringFactoriesInstances ( ApplicationContextInitializer . class ) ) ; this . setListeners ( this . getSpringFactoriesInstances ( ApplicationListener . class ) ) ;
其他的初始化就不多说了,我们直接到run里面,看如下:
public class SpringApplication {
public ConfigurableApplicationContext run ( String . . . args) { long startTime = System . nanoTime ( ) ; DefaultBootstrapContext bootstrapContext = this . createBootstrapContext ( ) ; ConfigurableApplicationContext context = null ; this . configureHeadlessProperty ( ) ; SpringApplicationRunListeners listeners = this . getRunListeners ( args) ; listeners. starting ( bootstrapContext, this . mainApplicationClass) ; try { ApplicationArguments applicationArguments = new DefaultApplicationArguments ( args) ; ConfigurableEnvironment environment = this . prepareEnvironment ( listeners, bootstrapContext, applicationArguments) ; this . configureIgnoreBeanInfo ( environment) ; Banner printedBanner = this . printBanner ( environment) ; context = this . createApplicationContext ( ) ; context. setApplicationStartup ( this . applicationStartup) ; this . prepareContext ( bootstrapContext, context, environment, listeners, applicationArguments, printedBanner) ; this . refreshContext ( context) ; this . afterRefresh ( context, applicationArguments) ; Duration timeTakenToStartup = Duration . ofNanos ( System . nanoTime ( ) - startTime) ; if ( this . logStartupInfo) { ( new StartupInfoLogger ( this . mainApplicationClass) ) . logStarted ( this . getApplicationLog ( ) , timeTakenToStartup) ; } listeners. started ( context, timeTakenToStartup) ; this . callRunners ( context, applicationArguments) ; } catch ( Throwable var12) { this . handleRunFailure ( context, var12, listeners) ; throw new IllegalStateException ( var12) ; } try { Duration timeTakenToReady = Duration . ofNanos ( System . nanoTime ( ) - startTime) ; listeners. ready ( context, timeTakenToReady) ; return context; } catch ( Throwable var11) { this . handleRunFailure ( context, var11, ( SpringApplicationRunListeners ) null ) ; throw new IllegalStateException ( var11) ; } }
}
这里就完成了jar包的启动方式,相当于在tomcat中处理服务器,然后初始化,然后spring启动(考虑扫描,注入等等),最终形成远程访问
前面我们说过了,jar包是操作main启动所形成的处理,他使用WebJars来完成类似于web容器中对页面的访问,本质上是war和tomcat的集合体,所以上面的初始化,然后spring启动是正确的,所以我们继续看上面代码怎么做的:
SpringApplicationRunListeners listeners = this . getRunListeners ( args) ; listeners. starting ( bootstrapContext, this . mainApplicationClass) ;
进入:
public class SpringApplication { private SpringApplicationRunListeners getRunListeners ( String [ ] args) { Class < ? > [ ] types = new Class [ ] { SpringApplication . class , String [ ] . class } ; return new SpringApplicationRunListeners ( logger, this . getSpringFactoriesInstances ( SpringApplicationRunListener . class , types, this , args) , this . applicationStartup) ; }
} private < T > Collection < T > getSpringFactoriesInstances ( Class < T > type, Class < ? > [ ] parameterTypes, Object . . . args) { ClassLoader classLoader = this . getClassLoader ( ) ; Set < String > names = new LinkedHashSet ( SpringFactoriesLoader . loadFactoryNames ( type, classLoader) ) ; List < T > instances = this . createSpringFactoriesInstances ( type, parameterTypes, classLoader, args, names) ; AnnotationAwareOrderComparator . sort ( instances) ; return instances; }
这样 SpringApplicationRunListeners listeners = this.getRunListeners(args);相当于拿到SpringApplicationRunListener.class的实例,并且保存在了如下:
class SpringApplicationRunListeners { SpringApplicationRunListeners ( Log log, Collection < ? extends SpringApplicationRunListener > listeners, ApplicationStartup applicationStartup) { this . log = log; this . listeners = new ArrayList ( listeners) ; this . applicationStartup = applicationStartup; } }
我们进行查看:
SpringApplicationRunListeners listeners = this . getRunListeners ( args) ;
listeners. starting ( bootstrapContext, this . mainApplicationClass) ; ApplicationArguments applicationArguments = new DefaultApplicationArguments ( args) ;
ConfigurableEnvironment environment = this . prepareEnvironment ( listeners, bootstrapContext, applicationArguments) ;
我们进入上面的this.prepareEnvironment方法:
private ConfigurableEnvironment prepareEnvironment ( SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) { ConfigurableEnvironment environment = this . getOrCreateEnvironment ( ) ; this . configureEnvironment ( ( ConfigurableEnvironment ) environment, applicationArguments. getSourceArgs ( ) ) ; ConfigurationPropertySources . attach ( ( Environment ) environment) ; listeners. environmentPrepared ( bootstrapContext, ( ConfigurableEnvironment ) environment) ; DefaultPropertiesPropertySource . moveToEnd ( ( ConfigurableEnvironment ) environment) ; Assert . state ( ! ( ( ConfigurableEnvironment ) environment) . containsProperty ( "spring.main.environment-prefix" ) , "Environment prefix cannot be set via properties." ) ; this . bindToSpringApplication ( ( ConfigurableEnvironment ) environment) ; if ( ! this . isCustomEnvironment) { EnvironmentConverter environmentConverter = new EnvironmentConverter ( this . getClassLoader ( ) ) ; environment = environmentConverter. convertEnvironmentIfNecessary ( ( ConfigurableEnvironment ) environment, this . deduceEnvironmentClass ( ) ) ; } ConfigurationPropertySources . attach ( ( Environment ) environment) ; return ( ConfigurableEnvironment ) environment; }
至此拿到环境对象,我们继续看后面:
ConfigurableEnvironment environment = this . prepareEnvironment ( listeners, bootstrapContext, applicationArguments) ; this . configureIgnoreBeanInfo ( environment) ;
Banner printedBanner = this . printBanner ( environment) ; context = this . createApplicationContext ( ) ;
context. setApplicationStartup ( this . applicationStartup) ; this . prepareContext ( bootstrapContext, context, environment, listeners, applicationArguments, printedBanner) ;
既然前面创建了监听器并且启动了他,然后解析了命令行参数和配置文件并保存到对象,并且打印横幅,那么我们进入this.createApplicationContext(),看看他怎么创建spring容器的,然后再看看保存的对象在哪里进行了操作:
protected ConfigurableApplicationContext createApplicationContext ( ) { return this . applicationContextFactory. create ( this . webApplicationType) ; }
package org. springframework. boot ; import java. util. Iterator ;
import java. util. function. Supplier ;
import org. springframework. beans. BeanUtils ;
import org. springframework. context. ConfigurableApplicationContext ;
import org. springframework. context. annotation. AnnotationConfigApplicationContext ;
import org. springframework. core. io. support. SpringFactoriesLoader ; @FunctionalInterface
public interface ApplicationContextFactory { ApplicationContextFactory DEFAULT = ( webApplicationType) -> { try { Iterator var1 = SpringFactoriesLoader . loadFactories ( ApplicationContextFactory . class , ApplicationContextFactory . class . getClassLoader ( ) ) . iterator ( ) ; ConfigurableApplicationContext context; do { if ( ! var1. hasNext ( ) ) { return new AnnotationConfigApplicationContext ( ) ; } ApplicationContextFactory candidate = ( ApplicationContextFactory ) var1. next ( ) ; context = candidate. create ( webApplicationType) ; } while ( context == null ) ; return context; } catch ( Exception var4) { throw new IllegalStateException ( "Unable create a default ApplicationContext instance, you may need a custom ApplicationContextFactory" , var4) ; } } ; ConfigurableApplicationContext create ( WebApplicationType webApplicationType) ; static ApplicationContextFactory ofContextClass ( Class < ? extends ConfigurableApplicationContext > contextClass) { return of ( ( ) -> { return ( ConfigurableApplicationContext ) BeanUtils . instantiateClass ( contextClass) ; } ) ; } static ApplicationContextFactory of ( Supplier < ConfigurableApplicationContext > supplier) { return ( webApplicationType) -> { return ( ConfigurableApplicationContext ) supplier. get ( ) ; } ; }
}
最终通过调试,context = candidate.create(webApplicationType);最终会操作如下:
public class AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext implements AnnotationConfigRegistry { public ConfigurableApplicationContext create ( WebApplicationType webApplicationType) { return webApplicationType != WebApplicationType . SERVLET ? null : new AnnotationConfigServletWebServerApplicationContext ( ) ; }
} public class ServletWebServerApplicationContext extends GenericWebApplicationContext implements ConfigurableWebServerApplicationContext {
} public class GenericWebApplicationContext extends GenericApplicationContext implements ConfigurableWebApplicationContext , ThemeSource {
} public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
}
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
}
public interface ConfigurableApplicationContext extends ApplicationContext , Lifecycle , Closeable {
}
public interface ConfigurableWebServerApplicationContext extends ConfigurableApplicationContext , WebServerApplicationContext { void setServerNamespace ( String serverNamespace) ;
}
public class GenericWebApplicationContext extends GenericApplicationContext implements ConfigurableWebApplicationContext , ThemeSource {
}
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
}
至此context = this.createApplicationContext();得到的就是AnnotationConfigServletWebServerApplicationContext对象,并且是ConfigurableApplicationContext context = null;的类型,我们继续:
this . prepareContext ( bootstrapContext, context, environment, listeners, applicationArguments, printedBanner) ;
那么我们进入,看看他做了什么:
private void prepareContext ( DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { context. setEnvironment ( environment) ; this . postProcessApplicationContext ( context) ; this . applyInitializers ( context) ; listeners. contextPrepared ( context) ; bootstrapContext. close ( context) ; if ( this . logStartupInfo) { this . logStartupInfo ( context. getParent ( ) == null ) ; this . logStartupProfileInfo ( context) ; } ConfigurableListableBeanFactory beanFactory = context. getBeanFactory ( ) ; beanFactory. registerSingleton ( "springApplicationArguments" , applicationArguments) ; if ( printedBanner != null ) { beanFactory. registerSingleton ( "springBootBanner" , printedBanner) ; } if ( beanFactory instanceof AbstractAutowireCapableBeanFactory ) { ( ( AbstractAutowireCapableBeanFactory ) beanFactory) . setAllowCircularReferences ( this . allowCircularReferences) ; if ( beanFactory instanceof DefaultListableBeanFactory ) { ( ( DefaultListableBeanFactory ) beanFactory) . setAllowBeanDefinitionOverriding ( this . allowBeanDefinitionOverriding) ; } } if ( this . lazyInitialization) { context. addBeanFactoryPostProcessor ( new LazyInitializationBeanFactoryPostProcessor ( ) ) ; } context. addBeanFactoryPostProcessor ( new SpringApplication. PropertySourceOrderingBeanFactoryPostProcessor ( context) ) ; Set < Object > sources = this . getAllSources ( ) ; Assert . notEmpty ( sources, "Sources must not be empty" ) ; this . load ( context, sources. toArray ( new Object [ 0 ] ) ) ; listeners. contextLoaded ( context) ; }
我们进入this.load(context, sources.toArray(new Object[0]));,sources存在启动类名称:
protected void load ( ApplicationContext context, Object [ ] sources) { if ( logger. isDebugEnabled ( ) ) { logger. debug ( "Loading source " + StringUtils . arrayToCommaDelimitedString ( sources) ) ; } BeanDefinitionLoader loader = this . createBeanDefinitionLoader ( this . getBeanDefinitionRegistry ( context) , sources) ; if ( this . beanNameGenerator != null ) { loader. setBeanNameGenerator ( this . beanNameGenerator) ; } if ( this . resourceLoader != null ) { loader. setResourceLoader ( this . resourceLoader) ; } if ( this . environment != null ) { loader. setEnvironment ( this . environment) ; } loader. load ( ) ; }
void load ( ) { Object [ ] var1 = this . sources; int var2 = var1. length; for ( int var3 = 0 ; var3 < var2; ++ var3) { Object source = var1[ var3] ; this . load ( source) ; } } private void load ( Object source) { Assert . notNull ( source, "Source must not be null" ) ; if ( source instanceof Class ) { this . load ( ( Class ) source) ; } else if ( source instanceof Resource ) { this . load ( ( Resource ) source) ; } else if ( source instanceof Package ) { this . load ( ( Package ) source) ; } else if ( source instanceof CharSequence ) { this . load ( ( CharSequence ) source) ; } else { throw new IllegalArgumentException ( "Invalid source type " + source. getClass ( ) ) ; } } private void load ( Class < ? > source) { if ( this . isGroovyPresent ( ) && BeanDefinitionLoader. GroovyBeanDefinitionSource . class . isAssignableFrom ( source) ) { BeanDefinitionLoader. GroovyBeanDefinitionSource loader = ( BeanDefinitionLoader. GroovyBeanDefinitionSource ) BeanUtils . instantiateClass ( source, BeanDefinitionLoader. GroovyBeanDefinitionSource . class ) ; ( ( GroovyBeanDefinitionReader ) this . groovyReader) . beans ( loader. getBeans ( ) ) ; } if ( this . isEligible ( source) ) { this . annotatedReader. register ( new Class [ ] { source} ) ; } }
public void register ( Class < ? > . . . componentClasses) { Class [ ] var2 = componentClasses; int var3 = componentClasses. length; for ( int var4 = 0 ; var4 < var3; ++ var4) { Class < ? > componentClass = var2[ var4] ; this . registerBean ( componentClass) ; } } public void registerBean ( Class < ? > beanClass) { this . doRegisterBean ( beanClass, ( String ) null , ( Class [ ] ) null , ( Supplier ) null , ( BeanDefinitionCustomizer [ ] ) null ) ; }
private < T > void doRegisterBean ( Class < T > beanClass, @Nullable String name, @Nullable Class < ? extends Annotation > [ ] qualifiers, @Nullable Supplier < T > supplier, @Nullable BeanDefinitionCustomizer [ ] customizers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition ( beanClass) ; if ( ! this . conditionEvaluator. shouldSkip ( abd. getMetadata ( ) ) ) { abd. setInstanceSupplier ( supplier) ; ScopeMetadata scopeMetadata = this . scopeMetadataResolver. resolveScopeMetadata ( abd) ; abd. setScope ( scopeMetadata. getScopeName ( ) ) ; String beanName = name != null ? name : this . beanNameGenerator. generateBeanName ( abd, this . registry) ; AnnotationConfigUtils . processCommonDefinitionAnnotations ( abd) ; int var10; int var11; if ( qualifiers != null ) { Class [ ] var9 = qualifiers; var10 = qualifiers. length; for ( var11 = 0 ; var11 < var10; ++ var11) { Class < ? extends Annotation > qualifier = var9[ var11] ; if ( Primary . class == qualifier) { abd. setPrimary ( true ) ; } else if ( Lazy . class == qualifier) { abd. setLazyInit ( true ) ; } else { abd. addQualifier ( new AutowireCandidateQualifier ( qualifier) ) ; } } } if ( customizers != null ) { BeanDefinitionCustomizer [ ] var13 = customizers; var10 = customizers. length; for ( var11 = 0 ; var11 < var10; ++ var11) { BeanDefinitionCustomizer customizer = var13[ var11] ; customizer. customize ( abd) ; } } BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder ( abd, beanName) ; definitionHolder = AnnotationConfigUtils . applyScopedProxyMode ( scopeMetadata, definitionHolder, this . registry) ; BeanDefinitionReaderUtils . registerBeanDefinition ( definitionHolder, this . registry) ; } } public static void registerBeanDefinition ( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { String beanName = definitionHolder. getBeanName ( ) ; registry. registerBeanDefinition ( beanName, definitionHolder. getBeanDefinition ( ) ) ; String [ ] aliases = definitionHolder. getAliases ( ) ; if ( aliases != null ) { String [ ] var4 = aliases; int var5 = aliases. length; for ( int var6 = 0 ; var6 < var5; ++ var6) { String alias = var4[ var6] ; registry. registerAlias ( beanName, alias) ; } } }
public void registerBeanDefinition ( String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException { this . beanFactory. registerBeanDefinition ( beanName, beanDefinition) ; }
他的加载本质上是调用创建的容器中的方法,来保存注册信息,所以前面的将启动类注入容器,本质上是注入信息,而非将实例进行放入,最终调用spring的核心方法来进行创建实例,其中是进行整合的,所以这里换句话说,是利用spring的某些处理先进行保存,如保存beanDefinition,然后调用spring的核心创建实例方法,在操作自身创建实例时,并且顺序也读取这个信息来创建实例,只需要传递这个信息即可,一般在spring中,只需要将spring boot保存注册信息的容器信息给入到spring对应的容器即可,然后让spring自身创建实例,这就需要操作spring的某些通知了(如实现了BeanFactoryPostProcessor接口的Bean),这里先了解,一般在spring中存在准备工作也就是postProcessBeanFactory方法(默认情况下,spring自身并不会进行任何处理),且是AbstractApplicationContext的,但是具体是不是这样,我们继续看后面:
this . prepareContext ( bootstrapContext, context, environment, listeners, applicationArguments, printedBanner) ; this . refreshContext ( context) ; this . afterRefresh ( context, applicationArguments) ; Duration timeTakenToStartup = Duration . ofNanos ( System . nanoTime ( ) - startTime) ;
既然我们知道了对应的注册好,最后交给spring进行处理的,并且调用容器的方法来保存(因为是boot的自定义spring容器(子类啊),所以存在方法,自然存在对应的map),那么我们看看后面的操作:
this . refreshContext ( context) ;
进入:
private void refreshContext ( ConfigurableApplicationContext context) { if ( this . registerShutdownHook) { shutdownHook. registerApplicationContext ( context) ; } this . refresh ( context) ; } protected void refresh ( ConfigurableApplicationContext applicationContext) { applicationContext. refresh ( ) ; }
至此,我们可以明白,之前说明的通知,并不对,因为spring boot,就是使用spring的操作来完成的,那么放入到的map自然是同一个,所以自然会进行创建bean,相当于我们在spring中手动的读取xml的那个实例一样了(如:ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext(“applicationContext.xml”);)
所以我们进行总结:
在spring中,比如ClassPathXmlApplicationContext本质上其实只是调用AbstractApplicationContext的refresh方法来完成的,那么根据这一点spring boot自然也可以
其中spring boot先创建一个spring容器(只需要继承或者实现(操作:AbstractApplicationContext),然后调用对应的refresh方法即可,所以我们自定义也未尝不可,只是可能需要考虑其他的东西,这里了解),将需要的先进行注册到对应的容器的map,然后调用容器的refresh进行初始化(AbstractApplicationContext是父类,最终调用他的),由于是同一个map,所以之前的启动类自然会变成bean,自然也就会考虑他上面的注册处理,并且,在spring源码中,会考虑复合注解的判断,而非之前我们手写的例子中的单纯判断,所以@SpringBootApplication里面的注解最终都会进行处理的,因为无论是xml还是注解的操作,都是经过refresh方法来完成(虽然之前说明源码时,只是对xml的说明进行了讲解,并没有考虑扫描的处理)
适当的总体总结:
我们继续看后面的操作,那么很明显,后面的操作是spring boot的又一个补充操作了,前置操作是先保存启动类的注册,我们看后置(注意这个前置和后置是针对spring整个容器来说的,刷新处理就是spring的操作了,至于其他的,基本都是spring boot自身的操作,所以是补充):
this . refreshContext ( context) ; this . afterRefresh ( context, applicationArguments) ;
直接进入:
protected void afterRefresh ( ConfigurableApplicationContext context, ApplicationArguments args) { }
我们发现上面都没有,说明这个是我们进行的自定义补充,通常指在框架中补充(并且由于run是静态的,所以重写没有意义,都会调用父类版本)
至于后面的代码,就无关紧要要,那么tomcat在哪里,一般的spring并没有全部操作后的后置,只是每个bean的后置,所以这个处理应该是spring boot的进一步的处理,一般在前的run方法里面,经过调试,还是在this.refreshContext(context);中,只不过是完全初始化后的,也就是spring最后的this.finishRefresh();方法,但是spring怎么可能存在tomcat的处理呢,他为什么可以呢,所以我们需要看看,首先进入:
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { public void refresh ( ) throws BeansException , IllegalStateException { synchronized ( this . startupShutdownMonitor) { StartupStep contextRefresh = this . applicationStartup. start ( "spring.context.refresh" ) ; this . prepareRefresh ( ) ; ConfigurableListableBeanFactory beanFactory = this . obtainFreshBeanFactory ( ) ; this . prepareBeanFactory ( beanFactory) ; try { this . postProcessBeanFactory ( beanFactory) ; StartupStep beanPostProcess = this . applicationStartup. start ( "spring.context.beans.post-process" ) ; this . invokeBeanFactoryPostProcessors ( beanFactory) ; this . registerBeanPostProcessors ( beanFactory) ; beanPostProcess. end ( ) ; this . initMessageSource ( ) ; this . initApplicationEventMulticaster ( ) ; this . onRefresh ( ) ; this . registerListeners ( ) ; this . finishBeanFactoryInitialization ( beanFactory) ; this . finishRefresh ( ) ; } catch ( BeansException var10) { if ( this . logger. isWarnEnabled ( ) ) { this . logger. warn ( "Exception encountered during context initialization - cancelling refresh attempt: " + var10) ; } this . destroyBeans ( ) ; this . cancelRefresh ( var10) ; throw var10; } finally { this . resetCommonCaches ( ) ; contextRefresh. end ( ) ; } } }
}
进入后:
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
protected void finishRefresh ( ) { this . clearResourceCaches ( ) ; this . initLifecycleProcessor ( ) ; this . getLifecycleProcessor ( ) . onRefresh ( ) ; this . publishEvent ( ( ApplicationEvent ) ( new ContextRefreshedEvent ( this ) ) ) ; if ( ! NativeDetector . inNativeImage ( ) ) { LiveBeansView . registerApplicationContext ( this ) ; } }
} public class DefaultLifecycleProcessor implements LifecycleProcessor , BeanFactoryAware { public void onRefresh ( ) { this . startBeans ( true ) ; this . running = true ; }
} this . initLifecycleProcessor ( ) ; this . getLifecycleProcessor ( ) . onRefresh ( ) ;
protected void initLifecycleProcessor ( ) { ConfigurableListableBeanFactory beanFactory = this . getBeanFactory ( ) ; if ( beanFactory. containsLocalBean ( "lifecycleProcessor" ) ) { this . lifecycleProcessor = ( LifecycleProcessor ) beanFactory. getBean ( "lifecycleProcessor" , LifecycleProcessor . class ) ; if ( this . logger. isTraceEnabled ( ) ) { this . logger. trace ( "Using LifecycleProcessor [" + this . lifecycleProcessor + "]" ) ; } } else { DefaultLifecycleProcessor defaultProcessor = new DefaultLifecycleProcessor ( ) ; defaultProcessor. setBeanFactory ( beanFactory) ; this . lifecycleProcessor = defaultProcessor; beanFactory. registerSingleton ( "lifecycleProcessor" , this . lifecycleProcessor) ; if ( this . logger. isTraceEnabled ( ) ) { this . logger. trace ( "No 'lifecycleProcessor' bean, using [" + this . lifecycleProcessor. getClass ( ) . getSimpleName ( ) + "]" ) ; } } }
进入,注意了上的this的内容与spring是不同的(虽然是同一个对象),所以这里肯定操作了tomcat,主要是因为设置的this.getLifecycleProcessor(),其中lifecycleProcessor的值在spring boot中不是操作默认的(所以存在spring boot中的某些操作,比如可能有专门操作tomcat的依赖或者bean),而spring则是,我们继续看:
public class DefaultLifecycleProcessor implements LifecycleProcessor , BeanFactoryAware {
private void startBeans ( boolean autoStartupOnly) { Map < String , Lifecycle > lifecycleBeans = this . getLifecycleBeans ( ) ; Map < Integer , DefaultLifecycleProcessor. LifecycleGroup > phases = new TreeMap ( ) ; lifecycleBeans. forEach ( ( beanName, bean) -> { if ( ! autoStartupOnly || bean instanceof SmartLifecycle && ( ( SmartLifecycle ) bean) . isAutoStartup ( ) ) { int phase = this . getPhase ( bean) ; ( ( DefaultLifecycleProcessor. LifecycleGroup ) phases. computeIfAbsent ( phase, ( p) -> { return new DefaultLifecycleProcessor. LifecycleGroup ( phase, this . timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) ; } ) ) . add ( beanName, bean) ; } } ) ; if ( ! phases. isEmpty ( ) ) { phases. values ( ) . forEach ( DefaultLifecycleProcessor. LifecycleGroup :: start ) ; } }
}
一般情况下,spring的phases.values()是没有值的,自然是进入不到这里的,所以在前面中phases.values()值受spring boot的影响,在某个部分进行处理,一般是lifecycleBeans.forEach,且由于this不同,自然这里考虑的东西不同,了解即可
phases.values()一般有两个,我们先看看第一个:
public class DefaultLifecycleProcessor implements LifecycleProcessor , BeanFactoryAware { public void start ( ) { if ( ! this . members. isEmpty ( ) ) { if ( DefaultLifecycleProcessor . this . logger. isDebugEnabled ( ) ) { DefaultLifecycleProcessor . this . logger. debug ( "Starting beans in phase " + this . phase) ; } Collections . sort ( this . members) ; Iterator var1 = this . members. iterator ( ) ; while ( var1. hasNext ( ) ) { DefaultLifecycleProcessor. LifecycleGroupMember member = ( DefaultLifecycleProcessor. LifecycleGroupMember ) var1. next ( ) ; DefaultLifecycleProcessor . this . doStart ( this . lifecycleBeans, member. name, this . autoStartupOnly) ; } } }
}
我们进入这里:
DefaultLifecycleProcessor . this . doStart ( this . lifecycleBeans, member. name, this . autoStartupOnly) ;
private void doStart ( Map < String , ? extends Lifecycle > lifecycleBeans, String beanName, boolean autoStartupOnly) { Lifecycle bean = ( Lifecycle ) lifecycleBeans. remove ( beanName) ; if ( bean != null && bean != this ) { String [ ] dependenciesForBean = this . getBeanFactory ( ) . getDependenciesForBean ( beanName) ; String [ ] var6 = dependenciesForBean; int var7 = dependenciesForBean. length; for ( int var8 = 0 ; var8 < var7; ++ var8) { String dependency = var6[ var8] ; this . doStart ( lifecycleBeans, dependency, autoStartupOnly) ; } if ( ! bean. isRunning ( ) && ( ! autoStartupOnly || ! ( bean instanceof SmartLifecycle ) || ( ( SmartLifecycle ) bean) . isAutoStartup ( ) ) ) { if ( this . logger. isTraceEnabled ( ) ) { this . logger. trace ( "Starting bean '" + beanName + "' of type [" + bean. getClass ( ) . getName ( ) + "]" ) ; } try { bean. start ( ) ; } catch ( Throwable var10) { throw new ApplicationContextException ( "Failed to start bean '" + beanName + "'" , var10) ; } if ( this . logger. isDebugEnabled ( ) ) { this . logger. debug ( "Successfully started bean '" + beanName + "'" ) ; } } } }
当你调试执行bean.start();后,就会打印如下:
Tomcat started on port ( s) : 8080 ( http) with context path '/hello'
很明显,他的确是启动tomcat的,我们进入(第一个列表的,有两个类,是第一个类完成的):
public class AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext implements AnnotationConfigRegistry {
}
class WebServerStartStopLifecycle implements SmartLifecycle { public void start ( ) { this . webServer. start ( ) ; this . running = true ; this . applicationContext. publishEvent ( new ServletWebServerInitializedEvent ( this . webServer, this . applicationContext) ) ; } }
进入后的代码:
public class TomcatWebServer implements WebServer {
public void start ( ) throws WebServerException { synchronized ( this . monitor) { if ( ! this . started) { boolean var10 = false ; try { var10 = true ; this . addPreviouslyRemovedConnectors ( ) ; Connector var2 = this . tomcat. getConnector ( ) ; if ( var2 != null && this . autoStart) { this . performDeferredLoadOnStartup ( ) ; } this . checkThatConnectorsHaveStarted ( ) ; this . started = true ; logger. info ( "Tomcat started on port(s): " + this . getPortsDescription ( true ) + " with context path '" + this . getContextPath ( ) + "'" ) ; var10 = false ; } catch ( ConnectorStartFailedException var11) { this . stopSilently ( ) ; throw var11; } catch ( Exception var12) { PortInUseException . throwIfPortBindingException ( var12, ( ) -> { return this . tomcat. getConnector ( ) . getPort ( ) ; } ) ; throw new WebServerException ( "Unable to start embedded Tomcat server" , var12) ; } finally { if ( var10) { Context context = this . findContext ( ) ; ContextBindings . unbindClassLoader ( context, context. getNamingToken ( ) , this . getClass ( ) . getClassLoader ( ) ) ; } } Context context = this . findContext ( ) ; ContextBindings . unbindClassLoader ( context, context. getNamingToken ( ) , this . getClass ( ) . getClassLoader ( ) ) ; } } }
}
很明显,真正启动tomcat的应该是this.started = true;前面,我们先看这里:
Connector var2 = this . tomcat. getConnector ( ) ; if ( var2 != null && this . autoStart) { this . performDeferredLoadOnStartup ( ) ; }
public class Tomcat { public Connector getConnector ( ) { Service service = this . getService ( ) ; if ( service. findConnectors ( ) . length > 0 ) { return service. findConnectors ( ) [ 0 ] ; } else { Connector connector = new Connector ( "HTTP/1.1" ) ; connector. setPort ( this . port) ; service. addConnector ( connector) ; return connector; } }
}
得到连接器后,主要操作这里了:
this . performDeferredLoadOnStartup ( ) ;
public class TomcatWebServer implements WebServer { private void performDeferredLoadOnStartup ( ) { try { Container [ ] var1 = this . tomcat. getHost ( ) . findChildren ( ) ; int var2 = var1. length; for ( int var3 = 0 ; var3 < var2; ++ var3) { Container child = var1[ var3] ; if ( child instanceof TomcatEmbeddedContext ) { ( ( TomcatEmbeddedContext ) child) . deferredLoadOnStartup ( ) ; } } } catch ( Exception var5) { if ( var5 instanceof WebServerException ) { throw ( WebServerException ) var5; } else { throw new WebServerException ( "Unable to start embedded Tomcat connectors" , var5) ; } } }
}
很明显他不是,并且我经过查看,我们并没有发现启动方法,那么在哪里,我们可以在构造方法中找到这个:
public TomcatWebServer ( Tomcat tomcat, boolean autoStart, Shutdown shutdown) { this . monitor = new Object ( ) ; this . serviceConnectors = new HashMap ( ) ; Assert . notNull ( tomcat, "Tomcat Server must not be null" ) ; this . tomcat = tomcat; this . autoStart = autoStart; this . gracefulShutdown = shutdown == Shutdown . GRACEFUL ? new GracefulShutdown ( tomcat) : null ; this . initialize ( ) ; } private void initialize ( ) throws WebServerException { logger. info ( "Tomcat initialized with port(s): " + this . getPortsDescription ( false ) ) ; synchronized ( this . monitor) { try { this . addInstanceIdToEngineName ( ) ; Context context = this . findContext ( ) ; context. addLifecycleListener ( ( event) -> { if ( context. equals ( event. getSource ( ) ) && "start" . equals ( event. getType ( ) ) ) { this . removeServiceConnectors ( ) ; } } ) ; this . tomcat. start ( ) ; this . rethrowDeferredStartupExceptions ( ) ; try { ContextBindings . bindClassLoader ( context, context. getNamingToken ( ) , this . getClass ( ) . getClassLoader ( ) ) ; } catch ( NamingException var5) { } this . startDaemonAwaitThread ( ) ; } catch ( Exception var6) { this . stopSilently ( ) ; this . destroySilently ( ) ; throw new WebServerException ( "Unable to start embedded Tomcat" , var6) ; } } }
上面进行了启动,也就是说,TomcatWebServer在出现实例时就已经启动了,而该实例出现在于bean.start();中的bean,也就是WebServerStartStopLifecycle,而他则是前面第一个的处理,而他们受spring boot的bean影响,所以在初始化时进行处理,通过调试,也的确如此,但是准备工作呢,他一定是操作的吗,所以我们需要继续看最后一个方法:
this . checkThatConnectorsHaveStarted ( ) ;
进入:
private void checkThatConnectorsHaveStarted ( ) { this . checkConnectorHasStarted ( this . tomcat. getConnector ( ) ) ; Connector [ ] var1 = this . tomcat. getService ( ) . findConnectors ( ) ; int var2 = var1. length; for ( int var3 = 0 ; var3 < var2; ++ var3) { Connector connector = var1[ var3] ; this . checkConnectorHasStarted ( connector) ; } }
很明显他只是用来检查启动的,那么tomcat的启动在哪里,我们继续观察前面的代码可以在这个地方发现:
for ( int var3 = 0 ; var3 < var2; ++ var3) { Container child = var1[ var3] ; if ( child instanceof TomcatEmbeddedContext ) { ( ( TomcatEmbeddedContext ) child) . deferredLoadOnStartup ( ) ; } }
上面的deferredLoadOnStartup应该是启动的,但是进去后,本质也并没有,那么奇怪了,tomcat启动与上面的启动有啥区别,这里就需要注意了:
对应的tomcat的启动,如this.tomcat.start();,他的确是启动的,但是他只是让tomcat处于启动状态,其他的操作需要代码来处理,也就是说,this.tomcat.start()方法只是触发 Tomcat 服务器的启动过程,但具体的启动操作可能涉及到许多复杂的异步操作,如初始化连接器、加载 Web 应用程序、启动线程池等,因此,虽然调用了 start()方法,但并不意味着整个 Tomcat 服务器的所有部分都已经完全启动和准备就绪
当上面都操作完毕后,才会进行启动,tomcat才会真正的起作用,所以最终操作:
this . started = true ;
那么很明显,上面的performDeferredLoadOnStartup里面是完成tomcat识别web应用的,最终使得tomcat操作我们的web应用
至此,tomcat的启动说明完毕,至此spring boot的run执行过程也说明完毕了
现在我们来自行操作tomcat启动web应用:
前面我们知道,spring boot内嵌了tomcat,那么这个tomcat是怎么来的,答:依赖而来,具体在如下这个依赖:
< dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-web</ artifactId> </ dependency> < dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-tomcat</ artifactId> < version> 2.7.2</ version> < scope> compile</ scope> </ dependency>
回到前面启动的时候,对应的是this.tomcat.start();,而this.tomcat是:
public class TomcatWebServer implements WebServer { private final Tomcat tomcat;
} package org. apache. catalina. startup ;
public class Tomcat {
}
而上面的包org.apache.catalina.startup就在spring-boot-starter-tomcat(整合的)里面的tomcat-embed-core(我这里是9.0.65版本)里面,所以要手动的处理tomcat,那么需要引入这些需要的包,那么还有一个问题,tomcat在程序中为什么可以启动,如果学习过tomcat的使用,那么应该知道,我们操作过他的命令启动,而这些自然由其语言来决定,而上面的整合的,说明tomcat自身与spring boot整合了,并且有一套java语言的启动,所以自然存在,所以自然也存在通过代码完成tomcat的启动和他对web项目的对应访问和配置,如加载 Web 应用程序等等
现在我们创建一个项目:
先说明为什么tomcat可以有java依赖,这是因为tomcat基本由java所编写(因为Servlet ),所以是有的,但是并非不同语言之间不能调用,比如java可以调用本地方法的C,所以这些实现我们并不说明,只管用即可
首先我们先思考流程,自然tomcat是需要启动的以及他的其他设置,但是加载的web程序怎么处理呢,mvc是怎么给他加载的,这肯定与打包后,其他tomcat是类似的,打包的是放在对应的目录自动加载,那么这里我们应该是手动的加载,只不过打包这个过程在代码中如何体现,自然就需要考虑tomcat加载web程序的原理了,一般他的原理很简单,就是通过网络编程来通信servlet,这里可以考虑27章博客最后的小型tomcat,所以tomcat对mvc,只是拿到mvc唯一的servlet而已,然后进行处理的,也就是说tomcat负责拿到请求数据和响应数据,中间的处理由servlet来完成(或者说只是将中间的处理封装成了servlet),那么加载原理就比较简单了,就是拿取servlet,这里我们测试使用tomcat的依赖来完成,就不考虑网络编程方面了
那么我们现在开始完成,先引入依赖:
< dependencies> < dependency> < groupId> org.apache.tomcat.embed</ groupId> < artifactId> tomcat-embed-core</ artifactId> < version> 9.0.65</ version> </ dependency> < dependency> < groupId> org.apache.tomcat.embed</ groupId> < artifactId> tomcat-embed-jasper</ artifactId> < version> 9.0.65</ version> </ dependency> </ dependencies>
在com包下,创建EmbeddedTomcat类:
package com ; import org. apache. catalina. Context ;
import org. apache. catalina. connector. Connector ;
import org. apache. catalina. startup. Tomcat ; import javax. servlet. ServletException ;
import javax. servlet. http. HttpServlet ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. IOException ; public class EmbeddedTomcat { public static void main ( String [ ] args) throws Exception { Tomcat tomcat = new Tomcat ( ) ; tomcat. setHostname ( "localhost" ) ; Connector connector = new Connector ( "org.apache.coyote.http11.Http11NioProtocol" ) ; connector. setPort ( 8081 ) ; tomcat. getConnector ( ) . setAttribute ( "enableNaming" , false ) ; tomcat. getService ( ) . addConnector ( connector) ; Context ctx = tomcat. addWebapp ( "" , System . getProperty ( "java.io.tmpdir" ) ) ; Tomcat . addServlet ( ctx, "helloServlet" , new HttpServlet ( ) { @Override protected void doGet ( HttpServletRequest req, HttpServletResponse resp) throws ServletException , IOException { System . out. println ( 1 ) ; resp. setContentType ( "text/html" ) ; resp. getWriter ( ) . println ( "<h1>Hello, World!</h1>" ) ; } } ) ; ctx. addServletMappingDecoded ( "/hello" , "helloServlet" ) ; tomcat. start ( ) ; System . out. println ( 2 ) ; tomcat. getServer ( ) . await ( ) ; System . out. println ( 3 ) ; }
}
直接启动,然后(浏览器)访问http://localhost:8080/hello,自然将请求信息和响应信息响应给我们,如果你看到页面出现Hello, World!,说明我们手动操作tomcat完成,但是这里还会出现一个问题,就算禁止了jsp支持,还是需要引入对应的依赖,为什么,这应该受版本影响,还有可能需要某些其他的设置,这里我们了解即可,没有必要关注(所以我们建议加上对应的依赖吧)
tomcat默认线程池和jdk默认线程池的区别:
tomcat自然也会存在用户访问的瓶颈,并且由于线程池也是代码完成的,所以tomcat的线程池可能与单纯的jdk的线程池(jdk的线程池就是并发编程中或者平常所说的线程池)不同
具体体现如下:
先创建一个项目,我们来压测一下:
依赖如下:
<?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> < parent> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-parent</ artifactId> < version> 2.7.2</ version> < relativePath/> </ parent> < groupId> org.example</ groupId> < artifactId> te</ artifactId> < version> 1.0-SNAPSHOT</ version> < properties> < maven.compiler.source> 11</ maven.compiler.source> < maven.compiler.target> 11</ maven.compiler.target> </ properties> < dependencies> < dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-web</ artifactId> </ dependency> </ dependencies>
</ project>
创建com包,里面创建启动类start:
package com ; import org. springframework. boot. SpringApplication ;
import org. springframework. boot. autoconfigure. SpringBootApplication ; @SpringBootApplication
public class BootApplication { public static void main ( String [ ] args) { SpringApplication . run ( BootApplication . class , args) ; }
}
然后在com包下创建controller包,然后创建TestController类:
package com. boot. controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. RestController ; @RestController
public class HelloController { @RequestMapping ( "/boot" ) public void helloBoot ( int num) throws InterruptedException { System . out. println ( Thread . currentThread ( ) . getName ( ) + "位:" + num) ; Thread . sleep ( 1000 ) ; } }
启动项目访问http://localhost:8080/boot?num=1,若出现数据,说明操作成功,然后我们在com包下创建test包,然后创建测试类url:
package com. test ; public class url { public static void main ( String [ ] args) { for ( int i = 0 ; i < 1000 ; i++ ) { int finalI = i; new Thread ( ( ) -> { HttpUtil . get ( "http://localhost:8080/boot?num=" + finalI) ; } ) . start ( ) ; } Thread . yield ( ) ; }
}
上面补充依赖:
< dependency> < groupId> com.squareup.okhttp3</ groupId> < artifactId> okhttp</ artifactId> < version> 4.9.3</ version>
</ dependency>
然后在test包下补充这个类:
package com. test ; import okhttp3. OkHttpClient ;
import okhttp3. Request ;
import okhttp3. Response ; import java. io. IOException ; public class HttpUtil { private static final OkHttpClient httpClient = new OkHttpClient ( ) ; public static String get ( String url) { Request request = new Request. Builder ( ) . url ( url) . build ( ) ; try ( Response response = httpClient. newCall ( request) . execute ( ) ) { return response. body ( ) . string ( ) ; } catch ( IOException e) { e. printStackTrace ( ) ; return null ; } }
}
访问执行url,可以得到结论,通常tomcat可以最大线程为200,默认最小是10,并且他的线程池是先占满最大线程,才会操作队列(一般是:Integer.MAX_VALUE大小),与jdk的先占满队列然后操作最大线程是不同的,具体参照:https://www.cnblogs.com/thisiswhy/p/17559808.html
回到前面,虽然我们说刷新后,后面的代码无关紧要,但是这里还是需要提一下这个:
this . callRunners ( context, applicationArguments) ;
private void callRunners ( ApplicationContext context, ApplicationArguments args) {
List < Object > runners = new ArrayList ( ) ; runners. addAll ( context. getBeansOfType ( ApplicationRunner . class ) . values ( ) ) ; runners. addAll ( context. getBeansOfType ( CommandLineRunner . class ) . values ( ) ) ; AnnotationAwareOrderComparator . sort ( runners) ; Iterator var4 = ( new LinkedHashSet ( runners) ) . iterator ( ) ; while ( var4. hasNext ( ) ) { Object runner = var4. next ( ) ; if ( runner instanceof ApplicationRunner ) { this . callRunner ( ( ApplicationRunner ) runner, args) ; } if ( runner instanceof CommandLineRunner ) { this . callRunner ( ( CommandLineRunner ) runner, args) ; } } }
这里我们看看即可
注意:代码内容也受版本影响的,但是总体并不会出现问题
至此,spring boot的启动我们全部说明完毕,也就是说,spring boot的底层原理说明完毕,现在我们稍微复习一下其他的知识吧
SpringBoot数据访问:
SpringData是Spring提供的一个用于简化数据库访问、支持云服务的开源框架,它是一个伞形项目,包含了大量关系型数据库及非关系型数据库的数据访问解决方案,其设计目的是使我们可以快速且简单地使用各种数据访问技术,Spring Boot默认采用整合SpringData的方式统一处理数据访问层,通过添加大量自动配置,引入各种数据访问模板xxxTemplate以及统一的Repository接口,从而达到简化数据访问层的操作
Spring Data提供了多种类型数据库支持,对支持的的数据库进行了整合管理,提供了各种依赖启动器,接下来,通过一张表罗列提供的常见数据库依赖启动器,如表所示
除此之外,还有一些框架技术,Spring Data项目并没有进行统一管理, Spring Boot官方也没有提供对应的依赖启动器,但是为了迎合市场开发需求、这些框架技术开发团队自己适配了对应的依赖启动器,例如,mybatis-spring-boot-starter支持MyBatis的使用
Spring Boot整合MyBatis:
对应的sql语句:
CREATE DATABASE springbootdata;
USE springbootdata;
DROP TABLE IF EXISTS t_article;
CREATE TABLE t_article ( id int ( 20 ) NOT NULL AUTO_INCREMENT COMMENT '文章id' , title varchar ( 200 ) DEFAULT NULL COMMENT '文章标题' , content longtext COMMENT '文章内容' , PRIMARY KEY ( id)
) ENGINE = InnoDB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8; INSERT INTO t_article VALUES ( '1' , 'Spring Boot基础入门' , '从入门到精通讲解...' ) ;
INSERT INTO t_article VALUES ( '2' , 'Spring Cloud基础入门' , '从入门到精通讲解...' ) ;
DROP TABLE IF EXISTS t_comment;
CREATE TABLE t_comment ( id int ( 20 ) NOT NULL AUTO_INCREMENT COMMENT '评论id' , content longtext COMMENT '评论内容' , author varchar ( 200 ) DEFAULT NULL COMMENT '评论作者' , a_id int ( 20 ) DEFAULT NULL COMMENT '关联的文章id' , PRIMARY KEY ( id)
) ENGINE = InnoDB AUTO_INCREMENT = 3 DEFAULT CHARSET = utf8; INSERT INTO t_comment VALUES ( '1' , '很全、很详细' , 'lucy' , '1' ) ;
INSERT INTO t_comment VALUES ( '2' , '赞一个' , 'tom' , '1' ) ;
INSERT INTO t_comment VALUES ( '3' , '很详细' , 'eric' , '1' ) ;
INSERT INTO t_comment VALUES ( '4' , '很好,非常详细' , '张三' , '1' ) ;
INSERT INTO t_comment VALUES ( '5' , '很不错' , '李四' , '2' ) ;
随便创建项目,引入相应的启动器,这里我们直接操作依赖即可:
<?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> < parent> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-parent</ artifactId> < version> 2.7.2</ version> < relativePath/> </ parent> < groupId> org.example</ groupId> < artifactId> bootmyba</ artifactId> < version> 1.0-SNAPSHOT</ version> < properties> < maven.compiler.source> 11</ maven.compiler.source> < maven.compiler.target> 11</ maven.compiler.target> </ properties> < dependencies> < dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-web</ artifactId> </ dependency> < dependency> < groupId> org.mybatis.spring.boot</ groupId> < artifactId> mybatis-spring-boot-starter</ artifactId> < version> 2.2.2</ version> </ dependency> < dependency> < groupId> mysql</ groupId> < artifactId> mysql-connector-java</ artifactId> < scope> runtime</ scope> </ dependency> < dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-test</ artifactId> < scope> test</ scope> </ dependency> </ dependencies> < build> < plugins> < plugin> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-maven-plugin</ artifactId> </ plugin> </ plugins> </ build> </ project>
创建启动类com.boot:
package com ; import org. springframework. boot. SpringApplication ;
import org. springframework. boot. autoconfigure. SpringBootApplication ; @SpringBootApplication
public class boot { public static void main ( String [ ] args) { SpringApplication . run ( boot. class , args) ; }
}
创建com.pojo包,然后创建下面的类:
package com. pojo ; public class Comment { private Integer id; private String content; private String author; private Integer aId; @Override public String toString ( ) { return "Comment{" + "id=" + id + ", content='" + content + '\'' + ", author='" + author + '\'' + ", aId=" + aId + '}' ; } public Integer getId ( ) { return id; } public void setId ( Integer id) { this . id = id; } public String getContent ( ) { return content; } public void setContent ( String content) { this . content = content; } public String getAuthor ( ) { return author; } public void setAuthor ( String author) { this . author = author; } public Integer getaId ( ) { return aId; } public void setaId ( Integer aId) { this . aId = aId; }
}
package com. pojo ; public class Article { private Integer id; private String title; private String content; @Override public String toString ( ) { return "Article{" + "id=" + id + ", title='" + title + '\'' + ", content='" + content + '\'' + '}' ; } public Integer getId ( ) { return id; } public void setId ( Integer id) { this . id = id; } public String getTitle ( ) { return title; } public void setTitle ( String title) { this . title = title; } public String getContent ( ) { return content; } public void setContent ( String content) { this . content = content; }
}
编写配置文件,现在资源文件夹下创建application.yml文件(其他没有的目录或者文件自然不会起作用,所以不写是没有问题的):
spring : datasource : url : jdbc: mysql: //localhost: 3306/springbootdata? serverTimezone=UTC&characterEncoding=UTF- 8 username : rootpassword : 123456
到测试的java资源文件创建com包,然后创建test类测试(测试类与资源类都是项目文件夹下的,所以路径也需要考虑启动类的,之前可能没有说明过,这里提一下):
下面需要加上依赖:
< dependency> < groupId> junit</ groupId> < artifactId> junit</ artifactId> < scope> test</ scope> </ dependency>
package com ; import org. junit. jupiter. api. Test ;
import org. junit. runner. RunWith ;
import org. springframework. boot. test. context. SpringBootTest ;
import org. springframework. test. context. junit4. SpringRunner ; @RunWith ( SpringRunner . class )
@SpringBootTest
class test { @Test public void contextLoads ( ) { System . out. println ( 1 ) ; }
}
先启动看看,如果有打印1,说明操作完毕
注解方式整合Mybatis:
在com包下创建mapper包,然后创建如下的接口:
package com. mapper ; import com. pojo. Comment ;
import org. apache. ibatis. annotations. Select ; public interface CommentMapper { @Select ( "select * from t_comment where id = #{id}" ) public Comment findById ( Integer id) ; }
在启动类上加上mapper的扫描:
package com ; import org. mybatis. spring. annotation. MapperScan ;
import org. springframework. boot. SpringApplication ;
import org. springframework. boot. autoconfigure. SpringBootApplication ; @SpringBootApplication
@MapperScan ( "com.mapper" )
public class boot { public static void main ( String [ ] args) { SpringApplication . run ( boot. class , args) ; }
}
在测试类中,加上如下:
@Autowired private CommentMapper commentMapper; @Test public void mtbatis ( ) { Comment byId = commentMapper. findById ( 1 ) ; System . out. println ( byId) ; }
执行看看结果吧,有结果,说明操作完毕
但在这之前,我们需要解决对应的数据库的下划线,防止没有得到数据
因为这时控制台中查询的Comment的aId属性值为null,没有映射成功
这是因为编写的实体类Comment中使用了驼峰命名方式将t_comment表中的a_id字段设计成了aId属性,所以无法正确映射查询结果了
解决上述由于驼峰命名方式造成的表字段值无法正确映射到类属性的情况
可以在Spring Boot全局配置文件application.yml中添加开启驼峰命名匹配映射配置,示例代码如下
mybatis : configuration : map-underscore-to-camel-case : true
继续执行,看看结果,至此对应的信息就匹配了
配置文件的方式整合MyBatis:
继续创建接口,在mapper中创建ArticleMapper:
package com. mapper ; import com. pojo. Article ; public interface ArticleMapper { public Article selectArticle ( Integer id) ;
}
resources目录下创建一个统一管理映射文件的包mapper(创建com包,然后创建mapper包),并在该包下编写与ArticleMapper接口方应的映射文件ArticleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<! DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
< mapper namespace = " com.mapper.ArticleMapper" > < select id = " selectArticle" resultType = " com.pojo.Article" > select *from t_articlewhere id = #{id}</ select>
</ mapper>
然后在测试类中操作如下:
@Autowired private ArticleMapper articleMapper; @Test public void artice ( ) { Article byId = articleMapper. selectArticle ( 1 ) ; System . out. println ( byId) ; }
看看结果吧
配置XML映射文件路径:
在项目中编写的XML映射文件,Spring Boot并无从知晓,所以无法扫描到该自定义(注意是自定义的)编写的XML配置文件
还必须在全局配置文件application.yml中添加MyBatis映射文件路径的配置
同时需要添加实体类别名映射路径,所以我们现在将之前在资源文件夹下的com/mapper包变成mapper包(这里是扫描读取的,会mybatis自然知道),继续测试,发现上面的代码报错了,所以我们需要修改如下:
mybatis : configuration : map-underscore-to-camel-case : true mapper-locations : classpath: mapper/*.xml type-aliases-package : com.pojo
修改xml中的resultType=“com.pojo.Article”,变成Article(因为别名)
继续测试吧
Spring Boot整合JPA:
添加Spring Data JPA依赖启动器,在项目的pom.xml文件中添加Spring Data JPA依赖启动器,示例代码如下
< dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-data-jpa</ artifactId>
</ dependency>
编写ORM实体类,但是在这之前,将前面mybatis的相关类和xml和配置都删除:
yml:
spring : datasource : url : jdbc: mysql: //localhost: 3306/springbootdata? serverTimezone=UTC&characterEncoding=UTF- 8 username : rootpassword : 123456
然后再编写实体类,在pojo下创建:
package com. pojo ; import javax. persistence. * ; @Entity ( name = "t_comment" )
public class Comment { @Id @GeneratedValue ( strategy = GenerationType . IDENTITY ) private Integer id; private String content; private String author; @Column ( name = "a_id" ) private Integer aId; public Integer getId ( ) { return id; } public void setId ( Integer id) { this . id = id; } public String getContent ( ) { return content; } public void setContent ( String content) { this . content = content; } public String getAuthor ( ) { return author; } public void setAuthor ( String author) { this . author = author; } public Integer getaId ( ) { return aId; } public void setaId ( Integer aId) { this . aId = aId; } @Override public String toString ( ) { return "Comment{" + "id=" + id + ", content='" + content + '\'' + ", author='" + author + '\'' + ", aId=" + aId + '}' ; }
}
在mapper包下编写接口:
package com. mapper ; import com. pojo. Comment ;
import org. springframework. data. jpa. repository. JpaRepository ; public interface CommentRepository extends JpaRepository < Comment , Integer > {
}
测试:
@Autowired private CommentRepository repository; @Test public void selectComment ( ) { Optional < Comment > optional = repository. findById ( 1 ) ; if ( optional. isPresent ( ) ) { System . out. println ( optional. get ( ) ) ; } System . out. println ( ) ; }
执行看看结果吧,这里需要补充一下JPA的对应知识:
Spring Boot整合Redis:
继续去掉上面所操作的代码(依赖什么的也去掉,如jpa,mybatis等等),然后引入依赖:
除了对关系型数据库的整合支持外,Spring Boot对非关系型数据库也提供了非常好的支持,Spring Boot与非关系型数据库Redis的整合使用
< dependency> < groupId> org.springframework.boot</ groupId> < artifactId> spring-boot-starter-data-redis</ artifactId> </ dependency> < dependency> < groupId> jakarta.persistence</ groupId> < artifactId> jakarta.persistence-api</ artifactId> </ dependency>
编写实体类,为了演示Spring Boot与Redis数据库的整合使用,在项目的com.pojo 包下编写几个对应的实体类
package com. pojo ; import org. springframework. data. redis. core. RedisHash ;
import org. springframework. data. redis. core. index. Indexed ; import javax. persistence. Id ; @RedisHash ( "persons" )
public class Person { @Id private String id; @Indexed private String firstname; @Indexed private String lastname; private Address address; public Person ( String id, String firstname, String lastname, Address address) { this . id = id; this . firstname = firstname; this . lastname = lastname; this . address = address; } public Person ( ) { } public String getId ( ) { return id; } public void setId ( String id) { this . id = id; } public String getFirstname ( ) { return firstname; } public void setFirstname ( String firstname) { this . firstname = firstname; } public String getLastname ( ) { return lastname; } public void setLastname ( String lastname) { this . lastname = lastname; } public Address getAddress ( ) { return address; } public void setAddress ( Address address) { this . address = address; }
}
package com. pojo ; import org. springframework. data. redis. core. index. Indexed ; public class Address { @Indexed private String city; @Indexed private String country; public Address ( String city, String country) { this . city = city; this . country = country; } public Address ( ) { } public String getCity ( ) { return city; } public void setCity ( String city) { this . city = city; } public String getCountry ( ) { return country; } public void setCountry ( String country) { this . country = country; }
}
实体类示例中,针对面向Redis数据库的数据操作设置了几个主要注解,这几个注解的说明如下:
@RedisHash(“persons”):用于指定操作实体类对象在Redis数据库中的存储空间,此处表示针对 Person实体类的数据操作都存储在Redis数据库中名为persons的存储空间下,在redis中默认从0数据库开始的,在这个数据库中的名称为persons的值作为key,其值作为表数据,他是操作序列化的,但是这个序列化通常并不是io流那样的,只是一种手动的格式处理,但是呢,序列化本质上就是格式的处理,只不过这里redis与io是不同的(io需要考虑序号)
@Id:用于标识实体类主键,在Redis数据库中(整合的会操作的)会默认生成字符串形式的HashKey表示唯一的实体对象id,当然也可以在数据存储时手动指定id(也就是说,redis会考虑自动的处理这个id,或者说整合的处理)
@Indexed:用于标识对应属性在Redis数据库中生成二级索引,使用该注解后会在Redis数据库中生成属性对应的二级索引,索引名称就是属性名,可以方便的进行数据条件查询,相当于mysql中添加索引时产生的B+树结构,可以快速的定位的
编写Repository接口,Spring Boot针对包括Redis在内的一些常用数据库提供了自动化配置,可以通过实现Repository接口简化对数据库中的数据进行增删改查操作,我们在mapper中写上:
package com. mapper ; import com. pojo. Person ;
import org. springframework. data. repository. CrudRepository ; import java. util. List ; public interface PersonRepository extends CrudRepository < Person , String > { List < Person > findByAddress_City ( String name) ;
}
相当于增强(操作这样的基本都是),其实可以不用,单纯的操作注入也行的
需要说明的是,在操作Redis数据库时编写的Repository接口文件需要继承最底层的 CrudRepository接口,而不是继承JpaRepository(他是CrudRepository的子类),这是因为JpaRepository是Spring Boot整合 JPA特有的,当然,也可以在项目pom.xml文件中同时导入Spring Boot整合的JPA依赖和Redis依赖,这样就可以编写一个继承JpaRepository的接口操作Redis数据库(前提是多个实例不报错的情况)
在项目的全局配置文件yml中添加Redis数据库的连接配置,但是在这个配置之前,我们需要在虚拟机中进行配置redis,具体参考80章博客,具体虚拟机的处理可以参照54章博客和55章博客
配置好虚拟机,启动redis后,操作如下的配置:
spring : redis : host : 192.168.136.128 port : 6379
测试类里面:
@Autowired private PersonRepository repository;
@Test public void savePerson ( ) { Person person = new Person ( ) ; person. setFirstname ( "张" ) ; person. setLastname ( "三" ) ; Address address = new Address ( ) ; address. setCity ( "北京" ) ; address. setCountry ( "中国" ) ; person. setAddress ( address) ; Person save = repository. save ( person) ; }
注意:记得设置可以访问redis,在80章博客中,可以全局搜索"可以自己设置成127.0.0.1,然后测试"
上面执行后,在redis中进行查看:
这里不用看,底层肯定操作了设置
但是有序列化问题,首先解决序列化问题后,再考虑其类型,那么我们先进行分析其序列化的出现原因:
再分析原因之前,我们先操作一些如下的代码:
首先,再com包下创建util包,然后创建RedisUtils类:
package com. util ; import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. data. redis. core. RedisTemplate ;
import org. springframework. stereotype. Component ; import java. util. concurrent. TimeUnit ; @Component
public class RedisUtils { @Autowired private RedisTemplate redisTemplate; public Object get ( final String key) { return redisTemplate. opsForValue ( ) . get ( key) ; } public boolean set ( String key, Object value) { boolean result = false ; try { redisTemplate. opsForValue ( ) . set ( key, value, 1 , TimeUnit . DAYS ) ; result = true ; } catch ( Exception e) { e. printStackTrace ( ) ; } return result; } public boolean getAndSet ( final String key, String value) { boolean result = false ; try { redisTemplate. opsForValue ( ) . getAndSet ( key, value) ; result = true ; } catch ( Exception e) { e. printStackTrace ( ) ; } return result; } public boolean delete ( final String key) { boolean result = false ; try { redisTemplate. delete ( key) ; result = true ; } catch ( Exception e) { e. printStackTrace ( ) ; } return result; }
}
然后再测试类里面操作如下:
@Autowired private RedisUtils redisUtils; @Test public void writeRedis ( ) { boolean set = redisUtils. set ( "1" , "1" ) ; System . out. println ( set) ; } @Test public void readRedis ( ) { Object o = redisUtils. get ( "1" ) ; System . out. println ( o) ; }
执行这两个方法,其中保存在redis中的数据是"\xac\xed\x00\x05t\x00\x011",现在我们来知晓所有的细节:
我们修改代码,在这里:
public boolean set ( String key, Object value) { boolean result = false ; try { redisTemplate. setKeySerializer ( new StringRedisSerializer ( ) ) ; redisTemplate. setValueSerializer ( new GenericJackson2JsonRedisSerializer ( ) ) ; redisTemplate. opsForValue ( ) . set ( key, value, 1 , TimeUnit . DAYS ) ; result = true ; } catch ( Exception e) { e. printStackTrace ( ) ; } return result; }
继续操作(清空redis数据库flushdb):
前提应该需要加上这个依赖(一些整合依赖里面存在,如web):
< dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-databind</ artifactId> < version> 2.13.4</ version>
</ dependency>
可以发现,对应的key是1了,但是值却变成了这样:
这里我们总结一下具体操作的细节:
设置好后,就不会出现对应的序列化问题了(相对于普通的数据来说,如数字和字母等等)
其中他们两个StringRedisTemplate和RedisTemplate操作中文的数据时,会出现乱码的,但他是数据的乱码还是显示的乱码呢
编码原理:
为什么加上对应的序列化操作,就可以解决呢:
自定义序列化格式:既然我们知道了对应的结果是怎么来的了,我们也可以自定义:
首先,无论是StringRedisSerializer还是GenericJackson2JsonRedisSerializer都是实现了RedisSerializer接口,并且都存在操作key和value的结果,也就是说,实际上前面我们可以都操作StringRedisSerializer来免除在redis中的显示问题,前面是为了保证差异操作json的(GenericJackson2JsonRedisSerializer的显示)
在com包下,创建config包,然后创建一个类:
package com. config ; import org. springframework. data. redis. serializer. RedisSerializer ;
import org. springframework. data. redis. serializer. SerializationException ; import java. io. UnsupportedEncodingException ; public class keyValueConfig implements RedisSerializer { @Override public byte [ ] serialize ( Object string) throws SerializationException { if ( string instanceof String ) { try { return string == null ? null : ( ( String ) string) . getBytes ( "UTF-8" ) ; } catch ( UnsupportedEncodingException e) { e. printStackTrace ( ) ; } } if ( string instanceof Integer ) { try { return string == null ? null : ( String . valueOf ( string) ) . getBytes ( "UTF-8" ) ; } catch ( UnsupportedEncodingException e) { e. printStackTrace ( ) ; } } return null ; } @Override public Object deserialize ( byte [ ] bytes) throws SerializationException { try { return bytes == null ? null : new String ( bytes, "UTF-8" ) ; } catch ( UnsupportedEncodingException e) { e. printStackTrace ( ) ; } return null ; }
}
之后操作如下:
public Object get ( final Object key) { redisTemplate. setKeySerializer ( new keyValueConfig ( ) ) ; return redisTemplate. opsForValue ( ) . get ( key) ; } public boolean set ( Object key, Object value) { boolean result = false ; try { redisTemplate. setKeySerializer ( new keyValueConfig ( ) ) ; redisTemplate. setValueSerializer ( new keyValueConfig ( ) ) ; redisTemplate. opsForValue ( ) . set ( key, value, 1 , TimeUnit . DAYS ) ; result = true ; } catch ( Exception e) { e. printStackTrace ( ) ; } return result; }
继续执行前面的测试方法,可以看到,结果都是正常的,并且也可以操作int类型了,那么也不用看,如果出现错误,基本上是他们的序列化方法所造成的,或者默认的所造成的,并且由于key和value无论是否操作set和get,基本都会操作这些数据编码或者序列化,所以我建议,都进行设置序列化(上面的序列化1和"1"是一样的)
回到之前:
那么不用看,对应的应该需要访问他们的整体,一个一个用type测试就行了,这里就不多说
我们继续操作测试方法:
@Test public void selectPerson ( ) { List < Person > list = ( List < Person > ) repository. findByAddress_City ( "北 京" ) ; for ( Person person : list) { System . out. println ( person) ; } }
相当于找到对象里面中Address里面的City的值为北京的这个对象
其中@Indexed相当于在redis中添加一个key,并且存在一个key里面保存了他们的数据,并且这个key信息关于对象的,所以相当于索引了,具体操作也就是:如repository.findByAddress_City(“北京”)通过address.city索引查询索引值为"北京"的数据信息,如果没有设置对应属性的二级索引,那么通过属性索引查询数据结果将会为空(前提是没有保存过,否则也会的查询到的)
这里我们可以测验:
首先,去掉之前实体类的所有注解,清空数据库,执行保存方法,看如下:
至此,整合redis也说明完毕
由于博客字数限制,其他内容到下一章博客查看(115章博客)