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/> <!-- lookup parent from repository -->
</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>
<!-- 这里进行打包时,可以考虑为jar包,jar包是类似于war包中的运行方式 jar包与war包的区别: 启动: jar包是操作main启动所形成的处理 war包则是根据web容器(如tomcat)来启动的,但是本质上也是main来处理的 所以jar和war都是main来处理,只不过处理方式不同,jar是自身main,而war是容器main 是否具有前端页面: jar包也存在,通常指使用WebJars来完成类似于web容器中对页面的访问,集成了main与页面的联系 war则是直接考虑web中与页面的联系,也就是使用tomcat来联系 所以jar使用WebJars来联系,而war使用tomcat来联系 虽然上面说WebJars,但是实际上他只是一个补充,真实情况下,jar包一般是war和tomcat的集合体,所以启动jar相当于启动了tomcat和war,也就不用配置服务器了(即,spring boot通常内嵌tomcat,前提是有对应的mvc依赖(其整合的spring-boot-starter-web)) -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
<!-- 所有的springboot项目,都会直接或者间接的继承spring-boot-starter-parent 作用:对项目依赖的版本进行管理,当前项目再引入其他常用的依赖时就不需要再指定版本号,避免版本冲突的问题,存在默认的资源过滤和插件管理 -->
对应的项目:
在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 {
//你只是定义注解并可以操作找到启动类的路径,但是springboot并没有真正的执行
//也就是说,该注解并没有起作用
//需要使用这个方法才可,并传入启动类的字节码文件,及其对应main方法的参数
//执行后springboot会找到帮助@SpringBootApplication注解
//并根据该注解进行扫描他的包及其子包
//那么springboot就会使得帮我们准备所有的环境,包括server,监听器,装配spring的上下文等等
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
/* SpringBoot的启动类,通常放在二级包中,比如这里的:com.boot下 (但必须在某个包里面,而不能直接的在资源文件的下层目录,否则启动报错,这是规定,底层的原因,为什么会这样,你可以试着将一个类放在java资源文件夹下,然后在一个包里,来导入该类,你会发现,不能导入,为什么:因为他没有包指定,那么就应该直接的import 类名,但是他的意思也包括在当前包下导入,所以冲突了,一般来说,以当前包为主,所以你是导入不了该类的,即这里虽然说是规定,但又何尝不是因为导入不了包而形成的错误呢,可能某些操作会导入该包吧,比如,通过注解得到包名称,创建一个文件,里面加上该导入,然后通过java操作命令行执行,因为java本身好像操作这样的是执行不了的,可以使用网上的某些工具类,当然这只是想象中而已,可能实际上是某些判断,导致的) 因为SpringBoot在做包扫描时,会扫描启动类所在的包,及其子包下的所有内容 实际情况:一般我们会在com.boot下创建对应的dao包,service包等等,如果启动类不在com.boot下 而在dao包里面,那么service也就扫描不到了 所以这时可能会出现问题(如扫描的注解不进行操作,使得对应的环境没有) @SpringBootApplication该注解标识当前类为SpringBoot的启动类 这时,springboot扫描项目时(先进行扫描,后进行启动,这个扫描只找@SpringBootApplication该注解) 找到该注解,并可以得到对应的包路径 就会扫描其包及其子包下的其他内容(这个扫描是扫描spring注解的) 这个扫描一般在其父依赖中,也就是: <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> 没有这个依赖,也就没有这个注解 */
我们直接的启动上面的启动类,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>
<!--使得可以进行测试spring boot,这个@SpringBootTest需要这个依赖-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<!--一般需要他来操作测试,如@RunWith(JUnit4.class)-->
<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); //Hello Spring Boot
}
}
启动,若有结果说明操作成功
热部署:
在开发过程中,通常会对一段业务代码不断地修改测试,在修改之后往往需要重启服务,有些服务需要加载很久才能启动成功,这种不必要的重复操作极大的降低了程序开发效率,为此, 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>
<!-- 如果自己写版本,则使用自己的,但是因为spring boot的版本一般是经过测试的,也就基本没有版本冲突 自己写的版本,可能会出现冲突,所以也最好不要自己写(除非你确定不会发生冲突) -->
</dependency>
还需要如下的包:
<dependency>
<!--通常spring boot加上就会在内部使用数据库,所以当没有配置数据库时,他可能会使得启动报错-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.7.1</version>
<!-- 虽然spring boot一般是进行测试的,但也有可能对应的包突然不存在(不开放了),所以这时需要指定版本 虽然基本不会,但这里使用我写的版本,实际上使用默认的也可 -->
</dependency>
<!--一般有对应的spring-jdbc包,使用连接池的,可以被注入-->
至此我们可以操作如下:
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; //可能会报红,但这是idea检查的操作,运行时不会出错的
@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 //记得要被spring boot扫描到
//将配置文件中所有以person开头的配置信息注入到当前类中
//前提1:必须保证配置文件中person.xxx的xxx要与Person类的setxxx中的xxx一致(首字母可以忽略大小写)
//前提2:必须保证当前Person中的属性都具有set方法,因为是使用setxxx方法进行注入的
//若没有,如没有满足对应的存在(首字母可以忽略大小写,则代表没有,那么会报错)
@ConfigurationProperties(prefix = "person")
public class Person {
private int id; //id
private String name; //名称
private List hobby; //爱好
private String[] family; //家庭成员
private Map map;
private Pet pet; //宠物
/* 当然有类似于这样的如下: @Value("${person.id}") private int id; //id @Value("${person.name}") private String name; //名称 @Value("${person.hobby}") private List hobby; //爱好 @Value("${person.family}") private String[] family; //家庭成员 private Map map; private Pet pet; //宠物 但是一般却不能直接操作对应的map集合和类,因为这里只给出了参数(因为参数只能是一个) 具体的解决方案可以百度,一般来说会使得配置文件里面参数进行包括起来,只包含一个参数 */
@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);
/* Person{id=100, name='哈哈', hobby=[喝水,吃早餐], family=[儿子,老婆], map={k1=v1, k2=v2}, pet=Pet{type='狗', name='旺财'}} */
}
若返回数据则代表注入成功,即操作成功
具体的编码问题可以参考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的三种配置文件是可以共存的,也就是说,可以写这三个配置文件,都会进行读取:
<!-- 点击spring-boot-starter-parent(使用ctrl+鼠标左键),往下找可以找到 当然修改这个里面的值,不会操作当前项目,因为他不是项目里面的 他只是一个投影而已(可以试着全部删除,然后启动运行,发现并没有影响) -->
<includes>
<!--谁在前面,谁基本先读取,当然他可能并不绝对,也受版本影响(他这里可能还是不变,但顺序不同)-->
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
<!-- 一般的读取顺序是,yaml,yml,properties(可能不同版本yml在yaml前面,现在通常是yml在前面了,比如上面的顺序,但properties基本在后面) 后读取的那么相关参数会进行覆盖,没有的不会 如properties设置端口为8888端口 那么前面两个无论怎么设置端口,都是8888端口了,当然若有其他的操作,自然不会覆盖,因为properties并没有设置 假设都没有对应的配置,那么使用默认值,如都没有设置端口,那么就是8080端口(默认值) -->
所以在某种程度上,我们建议使用一个配置文件,通常简单点,也就是yml,我们删除我们的properties的所有内容,修改后缀为yml
然后删除pojo包,再在controller对应的类中和测试类中,去掉关于之前数据库配置的代码,然后再删除数据库相关依赖,再在yml中添加如下:
案例:
server:
port: 8080
#设置起始路径,需要在项目前面加上hello才可,即访问http://localhost:8081/hello/hello/boot
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") // 指定自定义配置文件位置和名称,该文件的类型好像并不做要求
//好像只要对应的文件名称对应即可,到那时,一般是
//在扫描时,一般会先操作该@PropertySource注解,然后再操作其他的注解,使得可以操作属性值
@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);
//MyProperties{id=110, name='test'}
}
使用@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 // 将返回值对象作为组件添加到Spring容器中,该组件id默认为方法名
//当然也可以自己指定,如@Bean("myService2")
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); //com.lagou.service.MyService@23564dd2
System.out.println(myConfig);
//com.lagou.config.MyConfig$$EnhancerBySpringCGLIB$$d53e0fdf@54895681
}
若返回数据,则代表注入成功,当然也可以操作如下:
@Autowired
//包记得要对应,import org.springframework.context.ApplicationContext;
private ApplicationContext applicationContext;
//使用测试的注解,即测试的读取配置文件或者配置类(这里实际上也是),一般会将IOC容器本身放入自己的IOC容器中
//那么也就可以得到对应的ApplicationContext了(本身,即自己)
@Test
public void Test() {
System.out.println(applicationContext.getBean("myService2"));
System.out.println(applicationContext.containsBean("myService2")); //查看是否有该key
//记得是:@Bean("myService2")
/* com.lagou.service.MyService@200d1a3d true */
}
本质上是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]}
#int给String时,会自动的转换的
测试类:
@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;
}
//修改toString
@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}
#${test.id}和上面的值通常不同,但是后的${test.ui}是8,也就是说,这个调用是重新的赋值原来的处理,所以如果是固定值,自然赋值为8,如果是随机的,那么赋值随机的自然也会随机一次,所以${test.id}和上面的值通常不同
SpringBoot原理深入及源码剖析:
在前面88章博客中,这里的说明比较粗糙,那么这里我们来进行更加细节的说明
依赖管理:
为什么导入dependency时不需要指定版本,这是因为项目pom.xml文件有两个核心依赖,这里以前面为主:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</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>
...
<!-- 定义了依赖的版本,所以我们引入其他相关依赖时可以不指定版本号,因为spring-boot-dependencies帮我们定义好了(这是maven自身的作用,因为是建立在maven上的) 但也要记住,他是有限的,也就是说,大多数的帮我们定义好了版本,但有些没有,但基本不会出现 因为这些版本都是Spring官方经过兼容性测试的,基本不会出现版本冲突或者兼容性的问题 -->
这是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在什么时候操作扫描,我们直接的进入他:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
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}) //注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Retention(RetentionPolicy.RUNTIME) //表示注解的生命周期,Runtime(RUNTIME)表示运行时
@Documented //表示注解可以记录在javadoc中
@Inherited //表示可以被子类继承该注解
@SpringBootConfiguration // 标明该类为配置类
@EnableAutoConfiguration // 启动自动配置功能
@ComponentScan(
excludeFilters = {
@Filter(
type = FilterType.CUSTOM,
classes = {
TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {
AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
// 根据class来排除特定的类,使其不能加入spring容器,传入参数value类型是class类型
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {
};
@AliasFor(
// 根据classname 来排除特定的类,使其不能加入spring容器,传入参数value类型是class的全类名字符串数组
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {
};
// 指定扫描包,参数是包名的字符串数组
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {
};
// 扫描特定的包,参数类似是Class类型数组
@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注解源码,核心代码具体如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
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能实现自动配置的关键
同样,查看该注解内部查看源码信息,核心代码具体如下 :
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
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 // 自动配置包
// Spring的底层注解@Import,给容器中导入一个组件,在spring中他是一个导入配置类的操作,相当于在配置类中引入配置类
// 导入的组件是AutoConfigurationPackages.Registrar.class(上面的)
@Import({
AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
// 返回不会被导入到 Spring 容器中的类
Class<?>[] exclude() default {
};
// 返回不会被导入到 Spring 容器中的类名
String[] excludeName() default {
};
}
可以发现它是一个组合注解, Spring 中有很多以Enable开头的注解,上面还有个注解:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
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}) // 导入Registrar中注册的组件(实例)
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() {
}
/** * 实现ImportBeanDefinitionRegistrar接口中的registerBeanDefinitions方法 * 这个方法在Spring识别导入注解时,如果对应的类实现了对应的接口就会被调用,比如实现ImportBeanDefinitionRegistrar接口,主要用于注册bean定义 * @param metadata 当前注解的元数据 * @param registry Bean定义注册器,用于注册bean */
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 将包名注册到AutoConfigurationPackages
// AutoConfigurationPackages.register方法用于将指定的包名注册到Spring的自动配置包列表中
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
/** * 实现DeterminableImports接口中的determineImports方法 * 这个方法用于确定当前配置类的导入集合 * @param metadata 当前注解的元数据 * @return 返回导入的包名集合 */
public Set<Object> determineImports(AnnotationMetadata metadata) {
// 返回一个包含PackageImports实例的集合
// PackageImports构造方法会从注解元数据中提取包名,并封装在PackageImports对象中
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 {
//..
//AutoConfigurationImportSelector实现了DeferredImportSelector接口,该接口允许在@Configuration类处理之后,延迟选择和导入其他配置类,selectImports方法就是用来确定这些配置类的,所以在被注解导入后,若他实现了这个接口,就会操作这个方法
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的结果,他应该有这个方法来得到的,具体怎么得到可以百度(全局搜索):
//得到自动配置元信息,需要传入beanClassLoader这个类加载器
this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
final class AutoConfigurationMetadataLoader {
//..
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
//默认情况下,它从 META-INF/spring-autoconfigure-metadata.properties 文件中加载元数据
return loadMetadata(classLoader, "META-INF/spring-autoconfigure-metadata.properties");
}
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
try {
// 获取指定路径下的所有资源URL
Enumeration<URL> urls = classLoader != null ? classLoader.getResources(path) : ClassLoader.getSystemResources(path);
// 创建一个 Properties 对象,用于存储加载到的属性
Properties properties = new Properties();
// 遍历所有的资源URL
while(urls.hasMoreElements()) {
// 加载并合并所有资源的属性到 Properties 对象中
properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource((URL)urls.nextElement())));
}
// 调用下面的方法加载元数据并返回
return loadMetadata(properties);
} catch (IOException var4) {
// 如果发生IO异常,则抛出 IllegalArgumentException
throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", var4);
}
}
static AutoConfigurationMetadata loadMetadata(Properties properties) {
// 创建一个 PropertiesAutoConfigurationMetadata 实例,用加载到的属性初始化它
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);
//根据pom文件中加入的依赖文件筛选中最终符合当前项目运行环境对应的自动配置类(比较)
//传入了条件(autoConfigurationMetadata),进行比较
//根据配置类找到相同的信息,若满足条件,则自动配置,从而操作默认的配置类
//但基本只能操作他里面规定过的(基本是常用的,所以,并不是所有的依赖,都会自动配置)
//好像一般会有自动的扩展(满足条件的),使得可以操作,所以配置类基本都可以,就如扫描一样
//他们两个的作用基本是一样的
//this.getConfigurationClassFilter()里面可以得到autoConfigurationMetadata数据,从而进行比较
// 过滤配置类列表
configurations = this.getConfigurationClassFilter().filter(configurations);
// 触发自动配置导入事件
this.fireAutoConfigurationImportEvents(configurations, exclusions);
// 返回新的 AutoConfigurationEntry 实例,其中包含配置类列表和排除的类(这些会在spring容器中进行创建实例的),实现接口的操作也称为配置类
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
深入getCandidateConfigurations方法:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//loadFactoryNames方法传入了两个参数
//this.getSpringFactoriesLoaderFactoryClass()返回的是EnableAutoConfiguration.class
//this.getBeanClassLoader()返回的是beanClassLoader(类加载器)
//使用了内部的工具类SpringFactoriesLoader操作方法进行读取文件信息
// 从spring.factories文件中加载所有自动配置类名到configurations列表中
List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
// 通常从spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中加载更多的自动配置类
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) {
// 如果传入的类加载器为空,则使用SpringFactoriesLoader类的类加载器
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
//获取出入的键,获取工厂类型的名称,即传入类的全限定名,之前的就是:EnableAutoConfiguration
String factoryTypeName = factoryType.getName();
// 加载Spring工厂配置信息,返回工厂类型名称对应的列表,如果没有找到则返回空列表
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 {
// 初始化一个新的结果Map
HashMap result = new HashMap();
try {
//如果类加载器不为null,则加载类路径下spring.factories文件
//将其中设置的配置类的全路径信息封装 为Enumeration类对象
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
//循环Enumeration类对象,根据相应的节点信息生成Properties对象
//通过传入的键获取值,在将值切割为一个个小的字符串转化为Array,方法result集合中
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);
}
}
}
//读取后,会进行过滤,也就是只拿取this.getSpringFactoriesLoaderFactoryClass()名称的里面的值,也就是EnableAutoConfiguration里面的内容
/* 过滤是:return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList()); */
会去读取一个 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})
/* 第一个是操作扫描spring boot中的手动或者自身存在的实例信息,保存了对应的信息,以便让spring启动时进行的操作 第二个则是扫描引入与spring boot整合的依赖的,当然,如果按照专有名词的话,那么这里因为是扫描配置类 也就是说,配置类存在三种,注解,实现的一些接口,和上面的文件(两个) 他的主要功能是,根据两个文件,其中一个是条件,另外一个是实际创建的bean,当条件满足就创建对应的bean 即使完成了扫描,以及自动配置(对应的第二个注解才能算是真的自动配置) */
//自动配置原理总结:
/* 首先找到类的条件,在最终找到实例文件的对应部分时,他们变成列表时,看看里面的类中对应是否存在注解来操作条件,如果没有或者不需要,那么不需要条件直接进行创建实例(一般都有注解,但是可以设置为不需要条件),如果有,那么查看里面的条件是否在类的条件中存在,若存在则进行创建实例,否则不进行创建实例,默认创建实例的部分是EnableAutoConfiguration对应的列表,也就是对应的注解 */
在项目中加入了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 个注解的组合注解:
/* @SpringBootConfiguration @Configuration //通过javaConfig的方式来添加组件到IOC容器中(当前为配置类) 使得可以被获取(底层获取执行),从而调用main方法执行 @EnableAutoConfiguration //最核心的注解 @AutoConfigurationPackage //自动配置包,与@ComponentScan扫描到的添加到IOC @Import(AutoConfigurationImportSelector.class) //到META-INF/spring.factories中定义的bean添加到IOC容器中(一般都是配置类) @ComponentScan //包扫描,得到地址,给@AutoConfigurationPackage 然后信息也会给@Import(AutoConfigurationImportSelector.class) 操作(进行比较)
那么注解怎么操作的呢,自然是由于启动时考虑的初始化呗,了解即可,以后说明源码时会知道的
前面的基本回顾完毕(可能在原来的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>
<!-- spring-boot-autoconfigure 模块是 Spring Boot 框架中的一个核心模块 它提供了自动配置机制,根据应用程序的类路径和依赖,自动配置和加载各种 Spring Bean 这些自动配置的 Bean 包括但不限于数据库连接、事务管理、Web 容器配置等,大大简化了 Spring 应用程序的配置过程 在前面说明原理时,你可以看看对应的文件,好像是不是有autoconfigure单词,且是对应的spring-boot-autoconfigure的jar里面的,自己看看就知道了 -->
<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;
//让ConfigurationProperties起作用,并顺便注入容器,如果是考虑在spring boot中的话,那么建议加在启动类上
//否则通常只能的当前的了,而不是全部(注意:这个当前指的是当前类所在的包和子包,否则通常会报错,也就是数据上不会得到的报错,因为没有到容器中)
//当然了为了保证正确的配置,spring boot他不会直接扫描这个来,也就是说,单纯的在spring中考虑包和子包,那么在spring boot中,则必须加载启动类上,否则不会起作用,也就不会使得放入容器,即会导致注入失败
@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 //无需设置任何的类,相当于没有条件
//@ConditionalOnClass:当类路径classpath下有指定的类的情况下进行自动配置
//如果是@ConditionalOnClass(SimpleBean.class),那么就需要项目中存在这个类才可以创建MyAutoConfiguration的bean
//这是考虑其中在操作时,对应的SimpleBean类没有的可能
public class MyAutoConfiguration {
static {
System.out.println("MyAutoConfiguration init....");
}
/* @Bean public SimpleBean simpleBean() { return new SimpleBean(); } 不能多个相同实例的,否则就算名称不会报错,也会导致注入自然会报错 */
}
然后在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
编写测试方法:
既然他会自动的配置,那么我们只需要注入即可:
//测试自定义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;
// 允许命令行参数注入到Spring环境中
this.addCommandLineProperties = true;
// 添加转换服务
this.addConversionService = true;
// 设置图形界面模式为无界面(头less模式)
this.headless = true;
// 注册关闭钩子,在JVM退出时会调用
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));
// 从类路径中推断应用类型(如是否为Web应用)
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 从Spring Factories加载BootstrapRegistryInitializer实例并初始化
this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
// 从Spring Factories加载ApplicationContextInitializer实例并设置
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 从Spring Factories加载ApplicationListener实例并设置
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
// 推断主启动类
this.mainApplicationClass = this.deduceMainApplicationClass();
}
前面都是一些默认的,那么我们看这个:
// 从类路径中推断应用类型(如是否为Web应用)
this.webApplicationType = WebApplicationType.deduceFromClasspath();
public enum WebApplicationType {
//..
static WebApplicationType deduceFromClasspath() {
// 检查是否存在 Spring Web Reactive 的核心类,并且不存在 Servlet 和 Jersey 的核心类
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 {
//private static final String[] SERVLET_INDICATOR_CLASSES = new String[]{"javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext"};
// 遍历 SERVLET_INDICATOR_CLASSES 数组,检查每个类是否存在于类路径中
String[] var0 = SERVLET_INDICATOR_CLASSES;
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
String className = var0[var2];
// 如果有任何一个类不存在,返回 NONE 类型
if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
return NONE;
}
}
// 如果所有的 SERVLET_INDICATOR_CLASSES 都存在,返回 SERVLET 类型,表示这是一个传统的 Servlet Web 应用程序
return SERVLET;
}
}
}
//上面是判断是否为web的,如果是,则会考虑后续的启动内部的tomcat
//在对应的如下:
public enum WebApplicationType {
//没有Web环境
NONE,
//Servlet环境,通常用于传统的Servlet和Spring MVC应用,上面的就是这个
SERVLET,
//反应式Web环境,通常用于Spring WebFlux应用
REACTIVE;
//..
}
当然,依赖最终都会和项目路径一起的,但是这些由构建工具来完成,反正都会编译当前项目的,这里我们了解即可
所以的确检查是否是web程序,检查完进行赋值,为后面的一些判断进行处理,通常来说,如果不是web程序,那么jar包就相当于普通的main方法了,自然就是普通的java程序
继续看后面的代码:
// 从Spring Factories加载ApplicationContextInitializer实例并设置
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();
// 从Spring工厂加载器中获取指定类型的工厂类名,并存储在LinkedHashSet中以去除重复
//其中type是之前的ApplicationContextInitializer.class
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 使用前面获取的工厂类名来创建工厂实例
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 使用AnnotationAwareOrderComparator对实例进行排序
AnnotationAwareOrderComparator.sort(instances);
// 返回排序后的实例集合
return instances;
}
上面操作了这个:
//到这里
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// 定义将要使用的类加载器
ClassLoader classLoaderToUse = classLoader;
// 如果传入的类加载器为空,则使用SpringFactoriesLoader类的类加载器
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// 获取工厂类型的名称
String factoryTypeName = factoryType.getName();
// 从Spring工厂加载器中加载工厂类,并获取指定工厂类型的类名列表,如果没有找到则返回空列表
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
//上面操作了这个:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
//..
}
//这里是之前读取spring.factories文件信息的地方,眼熟吧
继续看后面:
// 从Spring工厂加载器中获取指定类型的工厂类名,并存储在LinkedHashSet中以去除重复
//其中type是之前的ApplicationContextInitializer.class
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 使用前面获取的工厂类名来创建工厂实例
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 使用AnnotationAwareOrderComparator对实例进行排序
AnnotationAwareOrderComparator.sort(instances);
// 返回排序后的实例集合
return instances;
//很明显,对ApplicationContextInitializer列表创建实例,而且没有处理条件,我们可以看他对应的列表,发现,的确都没有对应的注解(通常操作条件的,都会有注解,当然了,没有注解也就不会操作条件,自然也会,只不过基本保证顺序,操作条件的都会操作注解的,虽然可以没有)
//还有,官方写在对应文件的,基本都会存在某种联系,没有联系的很少,比如ApplicationContextInitializer列表对应的类基本都会实现ApplicationContextInitializer接口,而且一般来说,对应的前面的指定列表基本都是接口,比如:org.springframework.context.ApplicationContextInitializer,当然,这只是规范,可以选择不是,但是建议不要,因为可能在某些地方有判断的,当然,如果可以,你可以修改判断,来完成自身的自动配置
/* # Initializers org.springframework.context.ApplicationContextInitializer=\ org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\ org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener */
他是根据ApplicationContextInitializer.class来得到列表的,而不是之前自动配置中的列表,那么很明显:
// 从Spring Factories加载ApplicationContextInitializer实例并设置
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
//他们拿取的就是ApplicationContextInitializer对应的列表实例,并且操作了排序,先进行保存,看后续的处理了(这里了解即可)
/* public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) { this.initializers = new ArrayList(initializers); } 保存好对应实例 */
同样的,我们看后面:
// 从Spring Factories加载ApplicationContextInitializer实例并设置
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
//这里是操作ApplicationListener.class然后保存的
// 从Spring Factories加载ApplicationListener实例并设置
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
其他的初始化就不多说了,我们直接到run里面,看如下:
public class SpringApplication {
//..
// 主运行方法,启动 Spring 应用程序
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);
// 配置忽略 BeanInfo 的属性,进行执行环境
this.configureIgnoreBeanInfo(environment);
// 打印横幅(Banner)
Banner printedBanner = this.printBanner(environment);
// 创建应用上下文,也就是创建spring容器
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 准备应用上下文,或者说spring容器的前置处理
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);
// 执行所有应用程序的 runners
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};
//看到这个没有:getSpringFactoriesInstances
//很明显也是得到SpringApplicationRunListener.class的对应实例
//但是这里的参数与之前说明的是不同的
/* 之前初始化时是: private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) { return this.getSpringFactoriesInstances(type, new Class[0]); } 这里存在了两个new Class[]{SpringApplication.class, String[].class};,并且第三个可变长参数也是两个this, args */
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));
//在创建实例时需要对应的parameterTypes和args
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);
//这个是创建ApplicationArguments对象,初始化默认的应用参数类
//其中args作为spring应用的命令行参数可以在spring应用中访问到(jar启动时,是可以选择命令行启动的,并且可以选择一些参数,比如:--server.port=9000,等等)
//这里是建立在jar命令情况下,并且建议在java命令行参数的情况下的处理,使得得到的数据自然被识别的,jar命令负责启动jar包,而java命令行负责参数
//我们继续看这个:
// 准备环境(如系统环境变量和配置文件),主要是识别springboot的配置文件的
//放入的参数就有前面的监听器listeners,applicationArguments,还有:
/* // 创建一个默认的引导上下文 DefaultBootstrapContext bootstrapContext = this.createBootstrapContext(); */
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
我们进入上面的this.prepareEnvironment方法:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// 获取或创建一个ConfigurableEnvironment实例
ConfigurableEnvironment environment = this.getOrCreateEnvironment();
// 配置环境,包括设置默认的属性和配置文件
this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
// 附加配置属性源到环境
ConfigurationPropertySources.attach((Environment)environment);
// 通知所有监听器环境已经准备好
listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment);
// 将默认属性源移到环境属性源列表的末尾
DefaultPropertiesPropertySource.moveToEnd((ConfigurableEnvironment)environment);
// 断言环境中没有设置 "spring.main.environment-prefix" 属性的处理
Assert.state(!((ConfigurableEnvironment)environment).containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");
// 绑定SpringApplication的属性到环境
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);
// 配置忽略 BeanInfo 的属性,进行执行环境
this.configureIgnoreBeanInfo(environment);
// 打印横幅(Banner),也就是启动spring boot时,会打印在console上的艺术字体(ascii)
//这个字体是啥,就是启动spring boot时,第一次出现的,也就是如" :: Spring Boot :: (v2.7.2)"上面的怪图案
Banner printedBanner = this.printBanner(environment);
//我们继续看后面:
// 创建应用上下文,也就是创建spring容器
context = this.createApplicationContext();
//设置一些东西,就不看了
context.setApplicationStartup(this.applicationStartup);
// 准备应用上下文,或者说spring容器的前置处理
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
既然前面创建了监听器并且启动了他,然后解析了命令行参数和配置文件并保存到对象,并且打印横幅,那么我们进入this.createApplicationContext(),看看他怎么创建spring容器的,然后再看看保存的对象在哪里进行了操作:
protected ConfigurableApplicationContext createApplicationContext() {
//this.applicationContextFactory = ApplicationContextFactory.DEFAULT;是前面构造方法时进行的处理
return this.applicationContextFactory.create(this.webApplicationType);
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
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 {
//到这里,参数是:SERVLET
ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
// 从 Spring 工厂加载器中加载所有 ApplicationContextFactory 实例
Iterator var1 = SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class, ApplicationContextFactory.class.getClassLoader()).iterator();
ConfigurableApplicationContext context;
// 如果没有更多的工厂实例,返回一个默认的 AnnotationConfigApplicationContext 实例
do {
if (!var1.hasNext()) {
return new AnnotationConfigApplicationContext();
}
// 获取下一个工厂实例
ApplicationContextFactory candidate = (ApplicationContextFactory)var1.next();
// 使用工厂实例创建 ApplicationContext
context = candidate.create(webApplicationType);
} while(context == null); // 如果 context 为空,继续尝试下一个工厂实例
// 返回创建的 ApplicationContext 实例
return context;
} catch (Exception var4) {
// 如果发生异常,抛出一个非法状态异常,提示可能需要一个自定义的 ApplicationContextFactory
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) {
//创建了AnnotationConfigServletWebServerApplicationContext对象,因为参数是:SERVLET,这里为false(!=),所以进行创建
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 {
//..
}
//其中ConfigurableApplicationContext在spring中是容器的实现接口:
/* //spring的容器:ApplicationContext public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable { 那么很明显,对应的AnnotationConfigServletWebServerApplicationContext是spring容器的在boot中的实现,也就是说,是补充的,而不是spring自带的 */
public interface ConfigurableWebServerApplicationContext extends ConfigurableApplicationContext, WebServerApplicationContext {
void setServerNamespace(String serverNamespace);
}
//上面的:ConfigurableApplicationContext
//为什么是补充,而不是写一个,因为他是建立在已有spring容器的基础上的,所以这里只是进行了扩展,最终可以找到AbstractApplicationContext类,这个类在spring中就是一系列初始化操作的,也就是refresh()方法,所以
public class GenericWebApplicationContext extends GenericApplicationContext implements ConfigurableWebApplicationContext, ThemeSource {
//..
}
//AbstractApplicationContext;类
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
//..
}
至此context = this.createApplicationContext();得到的就是AnnotationConfigServletWebServerApplicationContext对象,并且是ConfigurableApplicationContext context = null;的类型,我们继续:
// 准备应用上下文,或者说spring容器的前置处理
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//这里是在容器刷新之前的准备动作,包含一个关键的操作,将启动类注入容器,因为是配置类,所以自然是放在了容器
//这里需要的参数有:
/* 1:bootstrapContext: // 创建一个默认的引导上下文 DefaultBootstrapContext bootstrapContext = this.createBootstrapContext(); 2:context:容器,也就是new AnnotationConfigServletWebServerApplicationContext(); 3:environment: // 准备环境(如系统环境变量和配置文件) ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments); 4:listeners: // 获取运行监听器并通知它们应用程序开始启动 SpringApplicationRunListeners listeners = this.getRunListeners(args); listeners.starting(bootstrapContext, this.mainApplicationClass); 5:applicationArguments,命令行参数 6:printedBanner,打印横幅 很明显,这一步,基本都将前面的操作都作为了参数,那么前面的设置,如environment,还有context这种设置的基本会在这里进行操作,而已经处理的,或者说也是准备处理的,应该也在这里进行处理,如printedBanner(一般是处理完的),applicationArguments 其他的,就是进一步的补充了,如listeners,bootstrapContext */
那么我们进入,看看他做了什么:
//bootstrapContext,上下文
//context:容器
//environment:环境
//listeners:监听器
//applicationArguments:命令行参数
//printedBanner:打印横幅
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置应用上下文的环境,设置容器变量,包括各种变量
context.setEnvironment(environment);
// 对应用上下文进行后处理(可以自定义修改上下文),设置bean生成器和资源加载器
this.postProcessApplicationContext(context);
// 应用初始化器,对上下文进行进一步初始化,执行容器中的ApplicationContextInitializer(包括spring.factories和自定义实例)
this.applyInitializers(context);
// 通知监听器上下文已准备好
listeners.contextPrepared(context);
// 关闭引导上下文
bootstrapContext.close(context);
// 如果配置为记录启动信息,则记录启动信息和启动的配置文件信息
if (this.logStartupInfo) {
this.logStartupInfo(context.getParent() == null);
this.logStartupProfileInfo(context);
}
// 获取应用上下文的 Bean 工厂
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 注册单例 Bean "springApplicationArguments",该 Bean 保存了应用程序的参数
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
// 如果打印了横幅(Banner),则将其注册为单例 Bean "springBootBanner"
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 如果 Bean 工厂是 AbstractAutowireCapableBeanFactory 的实例,则设置是否允许循环依赖
if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
((AbstractAutowireCapableBeanFactory)beanFactory).setAllowCircularReferences(this.allowCircularReferences);
// 如果 Bean 工厂是 DefaultListableBeanFactory 的实例,则设置是否允许覆盖 Bean 定义
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
}
// 如果启用了延迟初始化,则添加延迟初始化的 Bean 工厂后处理器
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// 添加属性源排序的 Bean 工厂后处理器
context.addBeanFactoryPostProcessor(new SpringApplication.PropertySourceOrderingBeanFactoryPostProcessor(context));
// 获取所有的源,其中得到的有启动类名称:com.boot.BootApplication
Set<Object> sources = this.getAllSources();
// 确保 sources 不为空
Assert.notEmpty(sources, "Sources must not be empty");
//前面的操作了
//设置环境:context.setEnvironment(environment);,容器的设置:this.postProcessApplicationContext(context);,当然还有其他的一些需要的处理,就不多说,我们主要看这里
//这里是主要操作:将启动类注入容器,为后续开启自动化配置作为基础的
// 加载应用上下文的 Bean 定义
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) {
// 如果日志级别是 DEBUG,则记录加载的资源列表
if (logger.isDebugEnabled()) {
logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
}
//this.getBeanDefinitionRegistry(context)得到的就是context容器,也就是:new AnnotationConfigServletWebServerApplicationContext();
// 创建 BeanDefinitionLoader 对象,用于加载 Bean 定义
BeanDefinitionLoader loader = this.createBeanDefinitionLoader(this.getBeanDefinitionRegistry(context), sources); //设置启动类名称所在对象
// 如果配置了 beanNameGenerator,则设置 Bean 名称生成器
if (this.beanNameGenerator != null) {
loader.setBeanNameGenerator(this.beanNameGenerator);
}
// 如果配置了 resourceLoader,则设置资源加载器
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
// 如果配置了 environment,则设置环境对象
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
// 执行加载操作,加载资源中定义的 Bean 定义
loader.load();
}
//BeanDefinitionLoader类的
//到这里
void load() {
Object[] var1 = this.sources;
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
Object source = var1[var3];
this.load(source); //启动类名称所在对象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) {
// 检查是否存在Groovy的支持,并且source是GroovyBeanDefinitionSource的子类或实现类
if (this.isGroovyPresent() && BeanDefinitionLoader.GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
// 实例化GroovyBeanDefinitionSource类型的source对象,并获取其定义的Bean信息
BeanDefinitionLoader.GroovyBeanDefinitionSource loader = (BeanDefinitionLoader.GroovyBeanDefinitionSource)BeanUtils.instantiateClass(source, BeanDefinitionLoader.GroovyBeanDefinitionSource.class);
((GroovyBeanDefinitionReader)this.groovyReader).beans(loader.getBeans());
}
// 如果source符合条件,则注册其上的注解类到annotatedReader中
if (this.isEligible(source)) {
//this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
this.annotatedReader.register(new Class[]{
source});
}
}
//我们进入this.annotatedReader.register(new Class[]{source});
//AnnotatedBeanDefinitionReader类的
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) {
// 创建一个基于注解的通用Bean定义
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
// 检查是否应跳过当前Bean定义,根据条件决定是否跳过
if (!this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
// 设置Bean的实例提供者
abd.setInstanceSupplier(supplier);
// 解析Bean的作用域元数据并设置作用域
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
abd.setScope(scopeMetadata.getScopeName());
// 生成Bean的名称,如果未提供名称则由beanNameGenerator生成
String beanName = name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry);
// 处理通用的定义注解
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
int var10;
int var11;
// 处理限定符(Qualifiers),例如Primary和Lazy
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));
}
}
}
// 应用Bean定义定制器
if (customizers != null) {
BeanDefinitionCustomizer[] var13 = customizers;
var10 = customizers.length;
for(var11 = 0; var11 < var10; ++var11) {
BeanDefinitionCustomizer customizer = var13[var11];
customizer.customize(abd);
}
}
// 创建Bean定义的持有者
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
// 应用作用域代理模式并注册Bean定义
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
//进入这里
//this.registry就是之前的容器,也就是new AnnotationConfigServletWebServerApplicationContext();
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}
}
public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {
// 获取Bean的名称和定义
String beanName = definitionHolder.getBeanName();
// 将Bean定义注册到Bean注册表中
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
// 处理Bean的别名
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); // 注册别名
}
}
}
//最终的进入:registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
//并且由容器来执行的:
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
//真正的注册,里面最终会存在对应的put方法,即放入map中了,一个名称,一个beanDefinition
this.beanFactory.registerBeanDefinition(beanName, beanDefinition);
}
//这里再之前的spring原理中也存在的
//至此,他的确被进行了注册,但是他也只是注册,最终的实例还是由spring来完成,并且他的注册的地方
他的加载本质上是调用创建的容器中的方法,来保存注册信息,所以前面的将启动类注入容器,本质上是注入信息,而非将实例进行放入,最终调用spring的核心方法来进行创建实例,其中是进行整合的,所以这里换句话说,是利用spring的某些处理先进行保存,如保存beanDefinition,然后调用spring的核心创建实例方法,在操作自身创建实例时,并且顺序也读取这个信息来创建实例,只需要传递这个信息即可,一般在spring中,只需要将spring boot保存注册信息的容器信息给入到spring对应的容器即可,然后让spring自身创建实例,这就需要操作spring的某些通知了(如实现了BeanFactoryPostProcessor接口的Bean),这里先了解,一般在spring中存在准备工作也就是postProcessBeanFactory方法(默认情况下,spring自身并不会进行任何处理),且是AbstractApplicationContext的,但是具体是不是这样,我们继续看后面:
// 准备应用上下文,或者说spring容器的前置处理
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) {
//注册ShutdownHook钩子
if (this.registerShutdownHook) {
//向JVM注册一个关机钩子,在JVM关机时关闭这个上下文,除非他已经关闭
//即这个钩子在JVM关闭时会被调用,用于关闭Spring的应用上下文(ApplicationContext),确保资源正确释放
shutdownHook.registerApplicationContext(context);
}
//刷新容器,对整个IOC容器初始化,包括资源定位,解析,注册等等
/* 刷新操作包括: 资源定位:找到配置文件或注解等资源 资源解析:解析配置文件或注解,得到Bean定义 Bean注册:将解析得到的Bean定义注册到Spring容器中 其他初始化工作:例如初始化单例Bean等 */
this.refresh(context);
}
//最终由容器调用,而容器来调用的话,那么自然,最终调用其父类的刷新方法,而父类的刷新方法就是spring的刷新,在spring中这个就是初始化的处理,也就是spring的真正处理
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的说明进行了讲解,并没有考虑扫描的处理)
适当的总体总结:
/* 准备: 1:bootstrapContext:默认的引导上下文 2:listeners:获取运行监听器并通知它们应用程序开始启动 3:applicationArguments,命令行参数 4:environment:准备环境(如系统环境变量和配置文件) 5:printedBanner,打印横幅 6:context:容器,也就是new AnnotationConfigServletWebServerApplicationContext(); 准备他们之后 7:然后进行注册:this.prepareContext() 8:注册后,调用容器的刷新方法,进行创建bean,操作扫描处理(配置类是xml的注解形式):this.refreshContext(context); 在这个过程中,通过@SpringBootApplication注解,以及他里面的导入操作,以及对应的spring的具体实现方法的操作,完成所有自动配置需要的实例bean 至此,容器里面就存在了对应的实例 */
我们继续看后面的操作,那么很明显,后面的操作是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);
//找到这里:注意:super并不会改变this指向,所以这个this还是对应的new AnnotationConfigServletWebServerApplicationContext();,所以这也是为什么会指向tomcat的根本原因,因为它里面的方法可能执行的并不是spring的或者被增强了,而且,这是在初始化完毕(this.finishBeanFactoryInitialization(beanFactory);)后的处理,所以tomcat启动是正常的
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();
}
}
}
//..
}
进入后:
//还是AbstractApplicationContext
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
//..
protected void finishRefresh() {
// 清理资源缓存,释放不再需要的资源
this.clearResourceCaches();
// 初始化生命周期处理器,确保在上下文刷新后,生命周期管理机制能够正常工作
this.initLifecycleProcessor();
// 调用生命周期处理器的 onRefresh 方法,通知所有注册的 Bean 生命周期处理器上下文已经刷新
this.getLifecycleProcessor().onRefresh();
// 发布一个 ContextRefreshedEvent 事件,通知所有的监听器上下文已经刷新完毕
this.publishEvent((ApplicationEvent)(new ContextRefreshedEvent(this)));
// 检查当前是否在原生镜像环境中,如果不是,则注册当前应用上下文到 LiveBeansView 以便于监控和管理
if (!NativeDetector.inNativeImage()) {
LiveBeansView.registerApplicationContext(this);
}
}
//..
}
public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware {
//..
public void onRefresh() {
//进入
this.startBeans(true); //this是DefaultLifecycleProcessor了
this.running = true;
}
//..
}
初始化生命周期处理器,确保在上下文刷新后,生命周期管理机制能够正常工作
this.initLifecycleProcessor();
// 调用生命周期处理器的 onRefresh 方法,通知所有注册的 Bean 生命周期处理器上下文已经刷新
this.getLifecycleProcessor().onRefresh();
//上面的
//AbstractApplicationContext
protected void initLifecycleProcessor() {
ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();
//为true,则是spring boot的
if (beanFactory.containsLocalBean("lifecycleProcessor")) {
//spring boot的
this.lifecycleProcessor = (LifecycleProcessor)beanFactory.getBean("lifecycleProcessor", LifecycleProcessor.class);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]");
}
} else {
//否则通常是spring的
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) {
// 获取所有实现了 Lifecycle 接口的 Bean
Map<String, Lifecycle> lifecycleBeans = this.getLifecycleBeans();
// 使用 TreeMap 以保证启动顺序,根据 phase 阶段来存储 LifecycleGroup
Map<Integer, DefaultLifecycleProcessor.LifecycleGroup> phases = new TreeMap();
// 遍历每一个生命周期 Bean
lifecycleBeans.forEach((beanName, bean) -> {
// 如果 autoStartupOnly 为 true,只启动那些实现了 SmartLifecycle 且 isAutoStartup 方法返回 true 的 Bean
if (!autoStartupOnly || bean instanceof SmartLifecycle && ((SmartLifecycle)bean).isAutoStartup()) {
// 获取 Bean 的启动阶段(phase)
int phase = this.getPhase(bean);
// 根据 phase 阶段,获取或创建相应的 LifecycleGroup,并将当前 Bean 添加到其中
((DefaultLifecycleProcessor.LifecycleGroup)phases.computeIfAbsent(phase, (p) -> {
return new DefaultLifecycleProcessor.LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly);
})).add(beanName, bean);
}
});
// 如果有生命周期组,按照阶段顺序启动每一个组中的所有 Bean
if (!phases.isEmpty()) {
//最终到这里:很明显,默认是调用这个phases.values()里面的每个实例的start方法
//因为spring boot的原因,所以这里有两个,也受其bean影响,具体情况了解即可,可以百度
phases.values().forEach(DefaultLifecycleProcessor.LifecycleGroup::start);
}
}
//..
}
一般情况下,spring的phases.values()是没有值的,自然是进入不到这里的,所以在前面中phases.values()值受spring boot的影响,在某个部分进行处理,一般是lifecycleBeans.forEach,且由于this不同,自然这里考虑的东西不同,了解即可
phases.values()一般有两个,我们先看看第一个:
//还是DefaultLifecycleProcessor
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();
// 调用生命周期处理器的 doStart 方法执行启动操作
DefaultLifecycleProcessor.this.doStart(this.lifecycleBeans, member.name, this.autoStartupOnly);
}
}
}
//..
}
我们进入这里:
DefaultLifecycleProcessor.this.doStart(this.lifecycleBeans, member.name, this.autoStartupOnly);
//还是DefaultLifecycleProcessor类的
private void doStart(Map<String, ? extends Lifecycle> lifecycleBeans, String beanName, boolean autoStartupOnly) {
// 从生命周期 beans 的映射中移除指定名称的 bean
Lifecycle bean = (Lifecycle)lifecycleBeans.remove(beanName);
// 如果 bean 不为 null 且不是当前对象本身
if (bean != null && bean != this) {
// 获取 bean 的依赖列表
String[] dependenciesForBean = this.getBeanFactory().getDependenciesForBean(beanName);
// 递归调用 doStart 方法,启动 bean 的所有依赖项
String[] var6 = dependenciesForBean;
int var7 = dependenciesForBean.length;
for(int var8 = 0; var8 < var7; ++var8) {
String dependency = var6[var8];
this.doStart(lifecycleBeans, dependency, autoStartupOnly);
}
// 如果 bean 尚未运行,并且满足启动条件(非仅自动启动或者是 SmartLifecycle 且设置为自动启动)
if (!bean.isRunning() && (!autoStartupOnly || !(bean instanceof SmartLifecycle) || ((SmartLifecycle)bean).isAutoStartup())) {
// 如果日志记录器的跟踪级别是启用的,记录 bean 启动的跟踪信息
if (this.logger.isTraceEnabled()) {
this.logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]");
}
try {
// 启动 bean,很明显,是存在spring boot给的bean,是给的第一个(与bean有关),在前面操作不是默认的,这个bean专门用来启动tomcat的
//那么这个专门操作tomcat的实例是什么:是WebServerStartStopLifecycle,而他的变量中private final ServletWebServerApplicationContext applicationContext;中则有出现:org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext,是不是眼熟,是的,就是之前的容器,并且这个类所在的包是:package org.springframework.boot.web.servlet.context;,且在spring boot中,所以spring boot在spring的基础上,又操作了tomcat的启动,我们进入
bean.start();
} catch (Throwable var10) {
// 如果启动 bean 时发生异常,抛出应用程序上下文异常
throw new ApplicationContextException("Failed to start bean '" + beanName + "'", var10);
}
// 如果日志记录器的调试级别是启用的,记录 bean 成功启动的调试信息
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() {
//webServer是:TomcatWebServer
//我们进入:
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();
// 获取 Tomcat 实例的连接器,并在自动启动模式下进行延迟启动处理
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) {
// 捕获其他异常,判断是否是端口绑定异常,如果是,则抛出端口占用异常;否则抛出 WebServerException
PortInUseException.throwIfPortBindingException(var12, () -> {
return this.tomcat.getConnector().getPort();
});
throw new WebServerException("Unable to start embedded Tomcat server", var12);
} finally {
if (var10) {
// 如果在 try 块中设置了 var10 为 true,执行上下文类加载器解绑操作
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;前面,我们先看这里:
// 获取 Tomcat 实例的连接器,并在自动启动模式下进行延迟启动处理
Connector var2 = this.tomcat.getConnector();
if (var2 != null && this.autoStart) {
this.performDeferredLoadOnStartup();
}
/* 获取 Tomcat 实例的连接器,这一步确保 Tomcat 已经配置好连接器(如 HTTP 连接器) */
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); //默认的是protected int port = 8080;,所以spring boot在没有设置时,默认的就是8080,并且这个this是this.tomcat,而this.tomcat,而this.tomcat的this,是this.webServer.start();的this.webServer,而他的this则是spring boot操作的bean,这个bean与:
/* //spring boot的 this.lifecycleProcessor = (LifecycleProcessor)beanFactory.getBean("lifecycleProcessor", LifecycleProcessor.class); 有关系,考虑到读取yml文件后才创建实例到工厂,那么这个bean与他的关系自然是有的,所以yml文件设置的内容也会影响到这里 */
service.addConnector(connector);
return connector;
}
}
//..
}
得到连接器后,主要操作这里了:
this.performDeferredLoadOnStartup();
//进入:
public class TomcatWebServer implements WebServer {
//..
private void performDeferredLoadOnStartup() {
try {
// 获取所有子容器(Context),这些容器是部署在 Tomcat 的 Web 应用
Container[] var1 = this.tomcat.getHost().findChildren();
int var2 = var1.length;
// 遍历每个子容器
for(int var3 = 0; var3 < var2; ++var3) {
Container child = var1[var3];
// 如果子容器是 TomcatEmbeddedContext 的实例,则执行延迟加载操作
if (child instanceof TomcatEmbeddedContext) {
((TomcatEmbeddedContext)child).deferredLoadOnStartup();
}
}
} catch (Exception var5) {
// 如果捕获到异常并且异常是 WebServerException 类型,则重新抛出该异常
if (var5 instanceof WebServerException) {
throw (WebServerException)var5;
} else {
// 否则,将异常包装为 WebServerException 并抛出
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的启动在哪里,我们继续观察前面的代码可以在这个地方发现:
//performDeferredLoadOnStartup
// 遍历每个子容器
for(int var3 = 0; var3 < var2; ++var3) {
Container child = var1[var3];
// 如果子容器是 TomcatEmbeddedContext 的实例,则执行延迟加载操作
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 ),所以是有的(也就是存在启动的地方,在tomcat中,对应的命令可以适当执行一些操作,比如执行java的字节码,或者解压jar包后再执行,然后对应的代码考虑路径,至此也就是可以启动的处理了,只不过这种处理,我们这里使用代码来完成,而非对应的启动命令),但是并非不同语言之间不能调用,比如java可以调用本地方法的C,所以这些实现我们并不说明,只管用即可
首先我们先思考流程,自然tomcat是需要启动的以及他的其他设置,但是加载的web程序怎么处理呢,mvc是怎么给他加载的,这肯定与打包后,其他tomcat是类似的,打包的是放在对应的目录自动加载,那么这里我们应该是手动的加载,只不过打包这个过程在代码中如何体现,自然就需要考虑tomcat加载web程序的原理了,一般他的原理很简单,就是通过网络编程来通信servlet,这里可以考虑27章博客最后的小型tomcat,所以tomcat对mvc,只是拿到mvc唯一的servlet而已,然后进行处理的,也就是说tomcat负责拿到请求数据和响应数据,中间的处理由servlet来完成(或者说只是将中间的处理封装成了servlet),那么加载原理就比较简单了,就是拿取servlet,这里我们测试使用tomcat的依赖来完成,就不考虑网络编程方面了
那么我们现在开始完成,先引入依赖:
<dependencies>
<dependency>
<!--这是嵌入式 Tomcat 的核心库,包含了启动和运行嵌入式 Tomcat 所需的基本类和功能-->
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.65</version>
</dependency>
<dependency>
<!--手动操作时,这个是需要的(前提是没有禁止jsp支持,如果禁止了可以不需要)-->
<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 tomcat = new Tomcat();
tomcat.setHostname("localhost"); //设置主机名称
//上面相当于访问的起始,如localhost:8080,默认情况下是如localhost,所以可以不用设置
//设置协议,接受这样的协议(也就是http),否则访问会失败的
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setPort(8081);//设置端口
//禁用JSP支持,默认情况下,spring boot是这样的,否则需要引入依赖,不引入启动就会报错,如:
/* <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>9.0.65</version> <!-- 按实际情况调整版本号 --> </dependency> */
tomcat.getConnector().setAttribute("enableNaming", false);
//添加进入
tomcat.getService().addConnector(connector);
// 创建上下文
//addWebapp(String contextPath, String docBase):在嵌入式 Tomcat 中添加一个 web 应用
/* contextPath:web 应用的上下文路径 这里表示 web 应用的上下文路径,"" 表示根上下文路径,也就是默认的 web 应用路径,通常对应 URL 路径 /,即将 web 应用部署在根路径上 docBase:web 应用的文档根目录 这里代表获取系统临时目录的路径,作为 web 应用的文档根目录 这段代码在嵌入式 Tomcat 中添加了一个 web 应用,应用的上下文路径是根路径(即 "/") 文档根目录是系统的临时目录,通过这种方式,你可以在嵌入式 Tomcat 中部署一个 web 应用 并指定它的根目录和上下文路径 换句话说,""类似于将web应用程序放在tomcat的默认目录下,而不是创建文件(如webapps,否则这里面的值相当于在这里创建文件) 虽然这只是一个指定名称,因为是代码完成的(默认情况下,我们希望是/) 而后面的临时目录是固定的操作(默认情况下System.getProperty("java.io.tmpdir")代表当前项目下面),总体代表是tomcat的临时目录,如果学习过tomcat的人会知道的tomcat他的一些启动通常会在临时目录中进行处理 具体可以看111章博客,全局搜索"很明显,带有exploded的结果与配置文件一致"即可 启动后,会在当前项目下面创建临时文件的 */
Context ctx = tomcat.addWebapp("", System.getProperty("java.io.tmpdir"));
//上面可以说是tomcat自身的处理
//现在我们来完成给tomcat加web应用
//tomcat内部存在HttpServlet相关的操作的,相当于有对应jar包的处理或者jar包
//所以可以不用引入对应的servlet的操作包,所以可以操作HttpServlet
//内部使用的是自身的,因为参数的原因,所以不会使用其他的依赖的
// 添加 Servlet,传递上面的处理,然后指定servlet名称为helloServlet,给servlet来操作
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>");
}
});
//加好后,进行配置
/* /hello相当于在tomcat配置中设置了/hello(如果你在idea中配置过tomcat应该知道的),所以只需要访问如http://localhost:8080/hello即可 后面的是需要操作的名称 */
ctx.addServletMappingDecoded("/hello", "helloServlet");
// 启动 Tomcat
tomcat.start();
//至此如果访问了http://localhost:8080/hello,那么会将请求信息和响应信息操作这个helloServlet
System.out.println(2);
tomcat.getServer().await();
System.out.println(3);
/* tomcat.getServer() 返回 Tomcat 实例中的 Server 对象,这个对象代表了 Tomcat 服务器本身 await() 方法是 Server 类中的一个方法,它会使当前线程进入等待状态,直到 Tomcat 服务器停止运行 因为你不可能让tomcat停止吧,那么怎么操作访问呢 */
}
}
直接启动,然后(浏览器)访问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/> <!-- lookup parent from repository -->
</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
回到前面,虽然我们说刷新后,后面的代码无关紧要,但是这里还是需要提一下这个:
// 执行所有应用程序的 runners
this.callRunners(context, applicationArguments);
/* 他用于调用项目中的自定义的类,在启动后执行一些程序,因为刷新后,tomcat就已经启动了,这个时候可以操作如定时等等的任务,他们自然之后执行一次 */
//我们进入:
private void callRunners(ApplicationContext context, ApplicationArguments args) {
// 获取 Spring 应用程序上下文中所有实现了 ApplicationRunner 接口的 bean,并将它们加入到 runners 列表中
// 同样地,获取所有实现了 CommandLineRunner 接口的 bean,并加入到 runners 列表中
List<Object> runners = new ArrayList();
//从容器获取的
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
// 对 runners 列表中的 bean 进行排序,排序依据是 @Order 注解或 Ordered 接口的顺序
AnnotationAwareOrderComparator.sort(runners);
// 使用 LinkedHashSet 去重并保持顺序,创建迭代器来遍历所有的 runner
Iterator var4 = (new LinkedHashSet(runners)).iterator();
// 遍历迭代器,依次调用每个 runner 的 run 方法
while(var4.hasNext()) {
Object runner = var4.next();
// 如果 runner 是 ApplicationRunner 类型的实例,调用 callRunner 方法执行其 run 方法
if (runner instanceof ApplicationRunner) {
this.callRunner((ApplicationRunner)runner, args);
}
// 如果 runner 是 CommandLineRunner 类型的实例,调用 callRunner 方法执行其 run 方法
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;
-- 创建表t_article并插入相关数据
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基础入门', '从入门到精通讲解...');
-- 创建表t_comment并插入相关数据
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/> <!-- lookup parent from repository -->
</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>
<!--引入spring boot mybatis的启动器-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--mysql驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--如果出现了 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</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>
创建启动类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文件(其他没有的目录或者文件自然不会起作用,所以不写是没有问题的):
# MySQL数据库连接配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: 123456
到测试的java资源文件创建com包,然后创建test类测试(测试类与资源类都是项目文件夹下的,所以路径也需要考虑启动类的,之前可能没有说明过,这里提一下):
下面需要加上依赖:
<dependency>
<!--一般需要他来操作测试,如@RunWith(JUnit4.class)-->
<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
#将数据库中如first_name会被映射到Java对象的属性firstName
继续执行,看看结果,至此对应的信息就匹配了
配置文件的方式整合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_article
where 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
#配置MyBatis的xml配置文件路径,就使得该mapper文件夹下的所有文件进行加载(换句话说,对比扫描,又多出了配置)
mapper-locations: classpath:mapper/*.xml
#配置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:
# MySQL数据库连接配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: 123456
然后再编写实体类,在pojo下创建:
package com.pojo;
import javax.persistence.*;
@Entity(name = "t_comment") // 设置ORM实体类,并指定映射的表名
public class Comment {
@Id // 表明映射对应的主键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的对应知识:
/* 对应的映射关系,通常并没有依靠set来完成,而是看变量名称的赋值,也就是说,你可以选择把get和set都去掉(这里与mp是不同的,本质是mybatis(sql)的区别,或者是像Hibernate(接口,上面的接口是增强的,或者这里他只是操作注解)的类似的区别) 他们都需要mysql的配置的 还有,相关的注解是import javax.persistence.*;,在jpa是存在这个注解,但是一些其他框架也会存在jpa的规范,比如 MyBatis-Plus,它也可以使用这个注解,只不过spring boot jpa更加的增强而已(相当于之前的Hibernate 也是在注解层面的,而并没有具体的接口,如:JpaRepository,是不是有点类似于:mybatis->MyBatis-Plus,和Hibernate到spring boot 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") // 指定操作实体类对象在Redis数据库中的存储空间
public class Person {
@Id // 标识实体类主键
private String id;
@Indexed // 标识对应属性在Redis数据库中生成二级索引
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的配置了,通常是自动连接的,也就是说,启动redis,会连接上的(循环的)
redis:
#自己redis的地址(默认的服务器密码为空)
host: 192.168.136.128 #redis注解配置
port: 6379 #端口号
测试类里面:
@Autowired
private PersonRepository repository; //这个注入是存在的
//那么有个问题,如果存在多个整合(如jpa,因为接口是一样的),那么他操作哪个实例,多个实例不会报错吗
//答:会报错,也就是说明jpa也加上的话,对应的注入实例存在多个,也就会报错了,除非可以显示的指定,当然了,如果出现这样,可以选择看看他们的实例名称,这里可以百度就不多说
//但是这个报错的前提是操作的不是CrudRepository,而是jpa的,如JpaRepository,由于CrudRepository是redis的,且是JpaRepository的父类,所以如果引入这两个依赖,并且操作jpa的,自然是多个实例,所以当他们都存在时,建议redis操作CrudRepository就不会报错了,如果操作的是JpaRepository,就会报错,原因如下:
/* 由于JpaRepository的父类是CrudRepository,所以操作JpaRepository时,代理类会操作两个,如果只是CrudRepository代理类自然只有一个,而当存在其他的接口来处理时,这个时候代理类由于赋值的类型不同,自然互不影响,这里是java基础,了解即可,因为需要看类型的,如PersonRepository */
@Test
public void savePerson() {
Person person = new Person();
person.setFirstname("张");
person.setLastname("三");
Address address = new Address();
address.setCity("北京");
address.setCountry("中国");
person.setAddress(address);
// 向Redis数据库添加数据
Person save = repository.save(person);
}
注意:记得设置可以访问redis,在80章博客中,可以全局搜索"可以自己设置成127.0.0.1,然后测试"
上面执行后,在redis中进行查看:
/* 127.0.0.1:6379> keys * 1) "persons:lastname:\xe4\xb8\x89" 2) "persons:address.city:\xe5\x8c\x97\xe4\xba\xac" 3) "persons" 4) "persons:524f697f-eadd-43bc-8cc8-14b7cef21116:idx" 5) "persons:firstname:\xe5\xbc\xa0" 6) "persons:address.country:\xe4\xb8\xad\xe5\x9b\xbd" 7) "persons:524f697f-eadd-43bc-8cc8-14b7cef21116" 127.0.0.1:6379> */
这里不用看,底层肯定操作了设置
但是有序列化问题,首先解决序列化问题后,再考虑其类型,那么我们先进行分析其序列化的出现原因:
再分析原因之前,我们先操作一些如下的代码:
首先,再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;
/* 读取缓存(对于redis来说,就是缓存) */
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",现在我们来知晓所有的细节:
/* 首先对应的注入也存在: @Autowired private StringRedisTemplate redisTemplate;,他是专门只用来操作String的,也就让 public boolean set(String key, Object value) {中的Object变成String,但是RedisTemplate基本都可以操作,所以我们使用RedisTemplate,还有StringRedisTemplate extends RedisTemplate,一般情况下他们却有个特点,当单独注入时,另外一个的实例不会给他(或者说没有创建),但当他们一起时,那么都创建了,而正是因为RedisTemplate是父类,则他会得到两个实例,从而报错,当然我们通常都只会使用RedisTemplate,所以这样的情况我们可以忽略 我们通过上面的序列化来进行分析 RedisTemplate他给redis中set设置的key或者value基本是操作序列化或者编码的(set的时候,其他的操作好像并不会) 但对应的get还是可以得到(因为反过来得到的结果刚好就是对应的结果) 因为同样的操作,只是我们在服务器里查询时,是序列化问题,这是显示的问题 上面的说明并不好,假设我们使用这个传入key是44,value是100的key 那么一般情况下,服务器的显示会出现"\xac\xed\x00\x05t\x00\x0244" 我们也可以使用get "\xac\xed\x00\x05t\x00\x0244"要加上"" 大概是因为解析显示的原因,因为真实的数据可能就是""\xac\xed\x00\x05t\x00\x0244""(序列化的问题) 而不是"\xac\xed\x00\x05t\x00\x0244"(一般的会默认加上""的) 所以get \xac\xed\x00\x05t\x00\x0244得不到 从而得到类似的如:"\xac\xed\x00\x05t\x00\x03100"(有趣的是,后缀一般是真的值,如这里的100) 我们会发现,显示出来的是序列化问题,为什么44变成上面的序列化问题呢,原因是我们传入的44中是经过了序列化数据 然而存在序列化,自然也会有编码问题 下面给出一个图形(只是说明):(假设为a,b,c编码,注意:这只是假设,是为了更好的理解) a(java程序,使用a编码) b(使用b编码,redis数据库的数据) c(服务器的查看,出现c编码的显示,出现乱码) 乱码可以认为是识别不了出现的数据,只要不是原来的数据都可以称为乱码,然而真正意义上的乱码是操作系统自定义的乱码,否则只能称为其他乱码 若要解决他的序列化的显示,那么需要如下的设置代码: //设置序列化Key的实例化对象,主要是赋值new StringRedisSerializer()属性,后面的也是这样 redisTemplate.setKeySerializer(new StringRedisSerializer()); //设置序列化Value的实例化对象 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); 设置序列化Value的实例化对象,若没有加上这个,那么当value是类对象时,对应的类且是没有实现Serializable接口的类,那么就会报错,否则一般不会,当然需要看版本,如果他没有操作对象的处理自然也会报错 当然,他们也可以只操作一个,即可以是单独的,只是另外一个不会操作了而已 注意:上面的new StringRedisSerializer()和new GenericJackson2JsonRedisSerializer()这两个参数,实际上是一种方式,后面的解释是以上面的为主,因为new GenericJackson2JsonRedisSerializer()基本可以操作任何类型,而new StringRedisSerializer()只能操作String类型 */
我们修改代码,在这里:
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了,但是值却变成了这样:
/* 其中new GenericJackson2JsonRedisSerializer()会看整体 所以会出现类似的"\"100\"",而不是"100",当然,正是因为这样 所以实际上对于key来说,"\"100\""不是"100",可以修改上面的两个参数后,添加就知道了 出现了两个key,因为本来就是不同(特别是显示) 但是为了更好的观察,以及可以操作类的类型 所以通常value需要设置new GenericJackson2JsonRedisSerializer() 而key我们只需要观察即可,所以通常key需要设置new StringRedisSerializer() 还有,并不是说\"是不好的数据,他是因为json的存在造成的,因为需要json的显示,否则json格式并不好进行区分,本质上他们的显示是一样的,所以以后操作数据时,我们建议使用GenericJackson2JsonRedisSerializer或者其他json的操作来处理value,因为json格式的数据是主流,主要是因为redis中不能保存""a""这样的数据 */
这里我们总结一下具体操作的细节:
/* 首先,set操作参数 redisUtils.set("1", "1"); redisUtils.set(1, "1"); //修改Object(第一个参数) redisUtils.set("\"1\"", "1"); 第一:不操作序列化: 在redis中查询得到如下: "\xac\xed\x00\x05t\x00\x011" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01" "\xac\xed\x00\x05t\x00\x03\"1\"" redis得到值,都需要如get "\xac\xed\x00\x05t\x00\x011"才可以得到,与单纯的在redis中操作set 1 1,get 1是不同的,也就是说,他默认保存的数据是加上""的,然而这个结论并不能说明,我们继续看后面,但是有个结论是可以说明的,也就是redis查询的数据,必然是""包括的 通过get得到,不操作序列化: redisUtils.get("1"); redisUtils.get(1); redisUtils.get("\"1\""); 都得到了值 具体序列化和编码解释: 以redisUtils.set("1", "1");为例子 key的1通过a编码和序列化,变成一个数据a,然后a通过redis数据库编码,变成数据c,然后c通过显示编码变 value的1通过a编码和序列化,变成一个数据a,然后a通过redis数据库编码,变成数据c,然后c通过显示编码变成"\xac\xed\x00\x05t\x00\x011" 以redisUtils.get("1");为例子 key的1通过a编码和序列化,变成一个数据a,然后a通过redis数据库编码,变成数据c,读取c对应的数据value,自然就是"\xac\xed\x00\x05t\x00\x011",然后反过来编码和序列化,自然也就是1 get是操作key的,所以我们单纯的给get操作序列化(注意:如果需要,建议给value也操作,因为value是得到然后返回的): redisTemplate.setKeySerializer(new StringRedisSerializer()); //注意:发送的数据是String的 我们继续上面的操作,会发现 都没有得到数据 也就是说,编码或者序列化需要一致的 具体编码和序列化解释: key的1通过a编码和序列化,变成一个数据aa,然后aa通过redis数据库编码,变成数据cc,读取cc对应的数据value,什么都没有,自然得不到,因为是cc,而不是c 所以我们操作序列化,无非就是改变编码和序列化 只不过key的序列化没有任何副作用,但是value那个会多加一个""号(实际上不是副作用,只是json而已),使得他们最终操作redis的显示编码或者序列化时出现的问题(多加的是显示编码或者序列化的地方) 然而编码和序列化虽然作为一种必然处理,但是数据的显示可能也会由操作来完成的,因为编码中乱码的处理,在一定程度上可以通过手动操作来显示,所以考虑到编码的情况,我们也需要考虑手动处理的情况,也就是序列化本身 前面我们可能说类似于\xac\xed\x00\x05t\x00\x011的格式是可能编码问题,但是更加细节的说,是序列化的问题,编码对他的影响几乎是没有的,这里我们将编码问题认为是序列化和编码的结合,所以统称为编码问题,因为他们几乎是一起的,再后面学习时记得知道还有序列化问题即可 */
设置好后,就不会出现对应的序列化问题了(相对于普通的数据来说,如数字和字母等等)
其中他们两个StringRedisTemplate和RedisTemplate操作中文的数据时,会出现乱码的,但他是数据的乱码还是显示的乱码呢
/* 这里就有两个概念,显示的乱码和数据的乱码 显示的乱码的说明很简单,随便在一个文件里面,修改文件的编码格式,会发现 对应的数据的显示基本不同,因为二进制都是一样的,只是显示的数据不同而已 数据的乱码,由于编码不同,那么对应传递的数据的结果一般不同 所以实际上是显示的乱码,也就是说b编码可以操作中文,只是使用了c编码的显示,所以中文就是乱码,我们可以假设你加上了key是"还会",value是"还会"的数据,一般服务器显示"\xe8\xbf\x98\xe4\xbc\x9a" 使用get "\xe8\xbf\x98\xe4\xbc\x9a"会得到"\xe8\xbf\x98\xe4\xbc\x9a" 也就是说对应的值是得到的,只是因为服务器的显示有问题,因为是c编码,那么如何变成b编码呢 主要是改变对应的客户端的编码,就如这里的手动设置一样 那么使用redis-cli --raw执行客户端,那么就使用的是b编码了,那么就可以看到中文了 至此,redis的中文问题及其java的设置和获取问题的操作中文都解决完毕 这个时候,我们会发现,保存的是还会(使用上面的默认redis去掉""),而不是"还会",并且操作get 还会和get "还会"都是一样的,那么可以知道,在redis中""里面只是一种展示,本质上操作是否加上""都可以,而之前的"\xac\xed\x00\x05t\x00\x0244"必须加上""是需要保证是一个整体,那么里面的内容\xac\xed\x00\x05t\x00\x0244应该存在什么歧义的,这里了解即可,所以以后我们建议在redis中操作加上"" */
编码原理:
为什么加上对应的序列化操作,就可以解决呢:
/* 实际上对应的设置编码,最终还是new StringRedisSerializer()和new GenericJackson2JsonRedisSerializer()里面的(他们需要实现RedisSerializer接口)serialize方法的结果,其中该方法的参数就是我们传递的值,返回值就是给redis的数据,很明显,new StringRedisSerializer()操作UTF-8,那么说明redis默认也是UTF-8(b编码),那么对应的new GenericJackson2JsonRedisSerializer()是操作UTF-8吗,实际上也是操作的,只是由于他的value需要像对象看齐,所以会使得添加某些东西(json)导致显示不对,那么由于这样,我们也可以定义自己的显示,只需要对应给redis的数据是正确的即可(操作),比如说(测试的结果): @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } //import com.alibaba.fastjson2.JSON; return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(CHARSET); } 他的作用与new GenericJackson2JsonRedisSerializer()的结果基本类似 那么很明显,默认操作到redis的编码必然不是UTF-8(一般是jdk的默认序列化编码,通常其序列化会造成HEX格式存储,然而,可能会随着时间的改变而可能也操作UTF-8,这里我们就认为不操作),所以通常需要我们这样的进行设置,注意:编码的设置无关紧要,但是key最好设置(redis好显示的),而value需要考虑类,所以一般我们都会使用一种方便的写法,由于默认的new GenericJackson2JsonRedisSerializer()在redis中不好观察(实际上是不好拿取,虽然都有显示,但是有些数据并不需要),所以我们最好使用上面测试的结果 但也要注意一点,上面的serialize只是代表我们数据的过去,并不代表得到,如果要操作得到,那么会执行对应设置的deserialize方法,当然,不同于new GenericJackson2JsonRedisSerializer()的结果,看对应的redis的value的值就知道了,而这样的出现也就说明我们客户端于redis服务端直接的连接的数据是按照字节来的,而既然按照字节,那么就没有什么编码可言,因为编码只是将字节进行显示的,而不是操作字节数据的传递(看对应的方法参数和返回值时,也的确都是字节数组) 最后:由于我们客户端必然需要将key或者value交给对应的redis,所以对应的key和value都会操作编码和序列化(是任何对应的key和value的操作哦,也就是说,删除,修改等等会操作key和value的,都会经过对应的方法) 而且,大多数序列化类中,他们在操作key或者value的set和get时,基本是一样的序列化,或者说编码,也就是说,set和get并不会出现不一致的情况 还要注意一点:但凡涉及到远程数据的操作或者数据的移动,都会操作编码的,因为数据只是二进制,需要编码来显示 */
自定义序列化格式:既然我们知道了对应的结果是怎么来的了,我们也可以自定义:
首先,无论是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 {
//这里是发送的数据,由于参数是String,那么需要考虑类型转换问题,在之前的StringRedisSerializer就没有考虑
//所以这里使用Object来进行处理
@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();
}
}
//不为String的情况
//有很多,那么我们这里只考虑Integer的情况吧(Object也会考虑自动装箱,所以只需要考虑string instanceof Integer即可,int不用管的)
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"是一样的)
回到之前:
/* 127.0.0.1:6379> keys * 1) "persons:lastname:\xe4\xb8\x89" 2) "persons:address.city:\xe5\x8c\x97\xe4\xba\xac" 3) "persons" 4) "persons:524f697f-eadd-43bc-8cc8-14b7cef21116:idx" 5) "persons:firstname:\xe5\xbc\xa0" 6) "persons:address.country:\xe4\xb8\xad\xe5\x9b\xbd" 7) "persons:524f697f-eadd-43bc-8cc8-14b7cef21116" 127.0.0.1:6379> */
/* 顺序通常也受序列化或者编码影响,但是不需要了解,意义不大 127.0.0.1:6379> keys * persons:82e20802-ccb4-43ef-8bd5-a789872b01fe persons:82e20802-ccb4-43ef-8bd5-a789872b01fe:idx persons:address.city:北京 persons:address.country:中国 persons persons:firstname:张 persons:lastname:三 127.0.0.1:6379> */
那么不用看,对应的应该需要访问他们的整体,一个一个用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索引查询索引值为"北京"的数据信息,如果没有设置对应属性的二级索引,那么通过属性索引查询数据结果将会为空(前提是没有保存过,否则也会的查询到的)
这里我们可以测验:
首先,去掉之前实体类的所有注解,清空数据库,执行保存方法,看如下:
/* 没有注解,那么报错 这个时候,我们加上@RedisHash("persons"),来指定key,继续执行看结果: 127.0.0.1:6379> keys * persons persons:158b6c6b-857d-48b7-a374-a6fad5b55756 //id的数据 127.0.0.1:6379> 添加@Id,没有任何作用,那么为什么加上,前面说明了,是自动的处理,在代码中,我们也没有设置id,那么这个操作是什么,我们在如下代码加上这个: Person person = new Person(); person.setFirstname("张"); person.setLastname("三"); Address address = new Address(); address.setCity("北京"); address.setCountry("中国"); person.setAddress(address); // 向Redis数据库添加数据 Person save = repository.save(person); //返回得到的对象数据,System.out.println(person==save);是返回true的 //加上这个 System.out.println(person.getId()); 也就是说,自动生成(前提是没有手动设置),与mp类似,前提这里需要设置为@Id 然而,这个@Id,不加也没有问题,因为默认存在操作的,可以自行测试 我们可以查看persons和persons:158b6c6b-857d-48b7-a374-a6fad5b55756的值,我们继续测试,删除,然后执行,然后查看如下: 127.0.0.1:6379> hgetall persons:fcff82f7-2359-4b97-89e7-01d41b8ccf6b _class com.pojo.Person address.city 北京 address.country 中国 firstname 张 id fcff82f7-2359-4b97-89e7-01d41b8ccf6b lastname 三 127.0.0.1:6379> smembers persons fcff82f7-2359-4b97-89e7-01d41b8ccf6b 127.0.0.1:6379> 很明显,persons:fcff82f7-2359-4b97-89e7-01d41b8ccf6b里面存放各种信息,现在我们先给firstname加上@Indexed注解: 127.0.0.1:6379> keys * persons persons:d7a60ffa-ce02-4442-961d-7d48144d32ce:idx //多出 persons:firstname:张 //多出 persons:d7a60ffa-ce02-4442-961d-7d48144d32ce 127.0.0.1:6379> 127.0.0.1:6379> type persons:firstname:张 set 127.0.0.1:6379> type persons:d7a60ffa-ce02-4442-961d-7d48144d32ce:idx set 127.0.0.1:6379> smembers persons:d7a60ffa-ce02-4442-961d-7d48144d32ce:idx persons:firstname:张 127.0.0.1:6379> smembers persons:firstname:张 d7a60ffa-ce02-4442-961d-7d48144d32ce 很明显,直接可以看到他的具体数据了,也就是说,可以通过直接找persons:d7a60ffa-ce02-4442-961d-7d48144d32ce:idx得到数据,也就是索引值,而不用进行处理取词: firstname 张 也就是索引,而恢复之前的,所有注解: 127.0.0.1:6379> keys * persons:763b372b-3cc9-47a6-969a-ac2f43c46c0b:idx persons:763b372b-3cc9-47a6-969a-ac2f43c46c0b persons:address.city:北京 persons:address.country:中国 persons persons:firstname:张 persons:lastname:三 //相当于更加详细的索引,所以需要: repository.findByAddress_City("北京")通过address.city索引查询索引值为"北京"的数据信息 由于这些是索引,所以重复执行,只会出现对应的如下: 127.0.0.1:6379> keys * persons persons:address.city:北京 persons:dae8cab2-57c2-49d7-a318-d8c41eb7a215 persons:763b372b-3cc9-47a6-969a-ac2f43c46c0b:idx persons:763b372b-3cc9-47a6-969a-ac2f43c46c0b persons:address.country:中国 persons:firstname:张 persons:dae8cab2-57c2-49d7-a318-d8c41eb7a215:idx persons:lastname:三 127.0.0.1:6379> 其中persons里面存在两个: 127.0.0.1:6379> smembers persons dae8cab2-57c2-49d7-a318-d8c41eb7a215 763b372b-3cc9-47a6-969a-ac2f43c46c0b 127.0.0.1:6379> 但是外面的索引不变,因为是同样的 当然,你得到这些数据,你自然是明白怎么处理的,因为数据都非常关键,这里就不多说了,具体的get和set底层操作无非就是读取类信息来操作的,了解即可 */
文章评论