导航:
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
目录
1.热部署
什么是热部署?
简单说就是你程序改了,现在要重新启动服务器,嫌麻烦?不用重启,服务器会自己悄悄的把更新后的程序给重新加载一遍,这就是热部署。
热部署的功能是如何实现的呢?这就要分两种情况来说了,非springboot工程和springboot工程的热部署实现方式完全不一样。
非springboot项目热部署实现原理
开发非springboot项目时,我们要制作一个web工程并通过tomcat启动,通常需要先安装tomcat服务器到磁盘中,开发的程序配置发布到安装的tomcat服务器上。如果想实现热部署的效果,这种情况其实有两种做法,一种是在tomcat服务器的配置文件中进行配置,这种做法与你使用什么IDE工具无关,不管你使用eclipse还是idea都行。还有一种做法是通过IDE工具进行配置,比如在idea工具中进行设置,这种形式需要依赖IDE工具,每款IDE工具不同,对应的配置也不太一样。但是核心思想是一样的,就是使用服务器去监控其中加载的应用,发现产生了变化就重新加载一次。
举例:
我给你个最简单的思路,但是实际设计要比这复杂一些。例如启动一个定时任务,任务启动时记录每个文件的大小,以后每5秒比对一下每个文件的大小是否有改变,或者是否有新文件。如果没有改变,放行,如果有改变,刷新当前记录的文件信息,然后重新启动服务器,这就可以实现热部署了。当然,这个过程肯定不能这么做,比如我把一个打印输出的字符串"abc"改成"cba",比对大小是没有变化的,但是内容缺实变了,所以这么做肯定不行,只是给大家打个比方,而且重启服务器这就是冷启动了,不能算热部署,领会精神吧。
springboot项目热部署实现原理
基于springboot开发的web工程其实有一个显著的特征,就是内置tomcat服务器,服务器是以一个对象的形式在spring容器中运行的。本来我们期望于tomcat服务器加载程序后由tomcat服务器盯着程序,你变化后我就重新启动重新加载,但是现在tomcat和我们的程序是平级的了,都是spring容器中的组件,这下就麻烦了,缺乏了一个直接的管理权,那该怎么做呢?简单,再搞一个程序X在spring容器中盯着你原始开发的程序A不就行了吗?确实,搞一个盯着程序A的程序X就行了,如果你自己开发的程序A变化了,那么程序X就命令tomcat容器重新加载程序A就OK了。并且这样做有一个好处,spring容器中东西不用全部重新加载一遍,只需要重新加载你开发的程序那一部分就可以了,这下效率又高了,挺好。
这个程序Xspringboot早就做好了,坐标导入即可。
1-1.手动启动热部署
步骤①:导入开发者工具对应的坐标spring-boot-devtools
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
步骤②:每次更新代码要构建项目,可以使用快捷键Ctrl+f9
重启与重载
热部署只包括重启,不包括重载过程。程序第一次启动时候既重启又重载。
一个springboot项目在运行时实际上是分两个过程进行的,根据加载的东西不同,划分成base类加载器与restart类加载器。
- 重启加载restart类加载器:用来加载开发者自己开发的类、配置文件、页面等信息,这一类文件受开发者影响
- 重载加载base类加载器:用来加载jar包中的类,jar包中的类和配置文件由于不会发生变化,因此不管加载多少次,加载的内容不会发生变化
当springboot项目启动时,base类加载器执行,加载jar包中的信息后,restart类加载器执行,加载开发者制作的内容。当执行构建项目后,由于jar中的信息不会变化,因此base类加载器无需再次执行,所以仅仅运行restart类加载即可,也就是将开发者自己制作的内容重新加载就行了,这就完成了一次热部署的过程,也可以说热部署的过程实际上是重新加载restart类加载器中的信息。
总结
- 使用开发者工具可以为当前项目开启热部署功能
- 使用构建项目操作对工程进行热部署
思考
上述过程每次进行热部署都需要开发者手工操作,不管是点击按钮还是快捷键都需要开发者手工执行。这种操作的应用场景主要是在开发调试期,并且调试的代码处于不同的文件中,比如服务器启动了,我需要改4个文件中的内容,然后重启,等4个文件都改完了再执行热部署,使用一个快捷键就OK了。但是如果现在开发者要修改的内容就只有一个文件中的少量代码,这个时候代码修改完毕如果能够让程序自己执行热部署功能,就可以减少开发者的操作,也就是自动进行热部署,能这么做吗?是可以的。咱们下一节再说。
1-2.脱离焦点5秒后自动启动热部署
自动热部署其实就是设计一个开关,打开这个开关后,IDE工具就可以自动热部署。因此这个操作和IDE工具有关,以下以idea为例设置idea中启动热部署
如果想全局配置,请先关闭项目再点击设置,防止设置仅在本项目有效,而不是全局有效。
步骤①:设置自动构建项目
打开【settings...】,在面板左侧的菜单中找到【Compile】选项,然后勾选【Build project automatically】,意思是自动构建项目
步骤②:允许在程序运行时进行自动构建
使用快捷键【Ctrl】+【Alt】+【Shit】+【/】打开维护面板,选择第1项【Registry...】
在选项中搜索comple,然后勾选对应项即可
这样程序在运行的时候就可以进行自动构建了,实现了热部署的效果。
关注:如果你每敲一个字母,服务器就重新构建一次,这未免有点太频繁了,所以idea设置当idea工具失去焦点5秒后进行热部署。其实就是你从idea工具中切换到其他工具时进行热部署,比如改完程序需要到浏览器上去调试,这个时候idea就自动进行热部署操作。
总结
- 自动热部署要开启自动构建项目
- 自动热部署要开启在程序运行时自动构建项目
思考
现在已经实现了热部署了,但是到企业开发的时候你会发现,为了便于管理,在你的程序目录中除了有代码,还有可能有文档,如果你修改了一下文档,这个时候会进行热部署吗?不管是否进行热部署,这个过程我们需要自己控制才比较合理,那这个东西能控制吗?咱们下一节再说。
1-3.参与热部署监控的文件范围配置
通过修改项目中的文件,你可以发现其实并不是所有的文件修改都会激活热部署的,原因在于在开发者工具中有一组配置,当满足了配置中的条件后,才会启动热部署。
默认不参与热部署的目录:
- /META-INF/maven
- /META-INF/resources
- /resources(里面yml是参与热部署的)
- /static
- /public
- /templates
以上目录中的文件如果发生变化,是不参与热部署的。如果想修改配置,可以通过application.yml文件进行设定哪些文件不参与热部署操作
spring: devtools: restart: #设置不参与热部署的文件或文件夹 exclude: static/**,public/**,config/application.yml
总结
- 通过配置可以修改不参与热部署的文件或目录
思考
热部署功能是一个典型的开发阶段使用的功能,到了线上环境运行程序时,这个功能就没有意义了。能否关闭热部署功能呢?咱们下一节再说。
1-4.关闭热部署
线上环境运行时是不可能使用热部署功能的,所以需要强制关闭此功能,通过配置可以关闭此功能。
spring: devtools: restart: enabled: false
如果当心配置文件层级过多导致相符覆盖最终引起配置失效,可以提高配置的层级,在更高层级中配置关闭热部署。例如在启动容器前通过系统属性设置关闭热部署功能。
@SpringBootApplication public class SSMPApplication { public static void main(String[] args) { System.setProperty("spring.devtools.restart.enabled","false"); SpringApplication.run(SSMPApplication.class); } }
其实上述担心略微有点多余,因为线上环境的维护是不可能出现修改代码的操作的,这么做唯一的作用是降低资源消耗,毕竟那双盯着你项目是不是产生变化的眼睛只要闭上了,就不具有热部署功能了,这个开关的作用就是禁用对应功能。
总结
- 通过配置可以关闭热部署功能降低线上程序的资源消耗
2.配置高级
2-1.@ConfigurationProperties
在基础篇学习了配置文件读取,一种是@Value,一种注入Environment 对象。
最常用的一种是自定义实体类bean读取yml,使用了@ConfigurationProperties注解,此注解的作用是用来为bean绑定属性的。
开发者可以在yml配置文件中以对象的格式添加若干属性
servers: ip-address: 192.168.0.1 port: 2345 timeout: -1
然后再在config包下开发一个用来封装数据的实体配置类ServerConfig,注意要提供属性对应的setter方法。使用@ConfigurationProperties注解就可以将配置中的属性值关联到开发的模型类上
@Component @Data @ConfigurationProperties(prefix = "servers") public class ServerConfig { private String ipAddress; private int port; private long timeout; }
在引导类获取实体配置类的bean:
使用@ConfigurationProperties注解也可以为第三方bean加载属性,格式特殊一点而已:
步骤①:使用@Bean注解定义第三方bean
@Bean public DruidDataSource datasource(){ DruidDataSource ds = new DruidDataSource(); return ds; }
步骤②:在yml中定义要绑定的属性,注意datasource此时全小写
datasource: driverClassName: com.mysql.jdbc.Driver
步骤③:使用@ConfigurationProperties注解为第三方bean进行属性绑定,注意前缀是全小写的datasource
@Bean @ConfigurationProperties(prefix = "datasource") public DruidDataSource datasource(){ DruidDataSource ds = new DruidDataSource(); return ds; }
操作方式完全一样,只不过@ConfigurationProperties注解不仅能添加到类上,还可以添加到方法上,添加到类上是为spring容器管理的当前类的对象绑定属性,添加到方法上是为spring容器管理的当前方法的返回值对象绑定属性,其实本质上都一样。
做到这其实就出现了一个新的问题,目前我们定义bean不是通过类注解定义就是通过@Bean定义,使用@ConfigurationProperties注解可以为bean进行属性绑定,那在一个业务系统中,哪些bean通过注解@ConfigurationProperties去绑定属性了呢?因为这个注解不仅可以写在类上,还可以写在方法上,所以找起来就比较麻烦了。为了解决这个问题,spring给我们提供了一个全新的注解@EnableConfigurationProperties,专门标注使用@ConfigurationProperties注解绑定属性的bean是哪些。
步骤①:在引导类上开启@EnableConfigurationProperties注解,并标注要使用@ConfigurationProperties注解绑定属性的类
@SpringBootApplication @EnableConfigurationProperties(ServerConfig.class) public class Springboot13ConfigurationApplication { }
步骤②:在对应的类上直接使用@ConfigurationProperties进行属性绑定
@Data @ConfigurationProperties(prefix = "servers") //这里不用加@component了 public class ServerConfig { private String ipAddress; private int port; private long timeout; }
注意:
- 开启了@EnableConfigurationProperties注解后,绑定属性的ServerConfig类就不能声明@Component注解,否则会被spring检测到两个bean。
当使用@EnableConfigurationProperties注解时,spring会默认将其标注的类定义为bean,因此不能再次声明@Component注解了。
@ConfigurationProperties的提示信息:
使用@ConfigurationProperties注解时,会出现一个提示信息
解决办法:出现这个提示后只需要按照提示添加对应坐标即可,具体原因在元数据里讲。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency>
总结
- 使用@ConfigurationProperties可以为使用@Bean声明的第三方bean绑定属性
- 当使用@EnableConfigurationProperties声明进行属性绑定的bean后,无需使用@Component注解再次进行bean声明
2-2.宽松绑定/松散绑定
结论: @ConfigurationProperties绑定属性时支持属性名宽松绑定,即不同命名法属性都能绑定。
问题演示:
在进行属性绑定时,可能会遇到如下情况,为了进行标准命名,开发者会将属性名严格按照驼峰命名法书写,在yml配置文件中将datasource修改为dataSource,如下:
dataSource: driverClassName: com.mysql.jdbc.Driver
此时程序可以正常运行,然后又将代码中的前缀datasource修改为dataSource,如下:
@Bean @ConfigurationProperties(prefix = "dataSource") public DruidDataSource datasource(){ DruidDataSource ds = new DruidDataSource(); return ds; }
此时就发生了编译错误,而且并不是idea工具导致的,运行后依然会出现问题,配置属性名dataSource是无效的
Configuration property name 'dataSource' is not valid: Invalid characters: 'S' Bean: datasource Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter Action: Modify 'dataSource' so that it conforms to the canonical names requirements.
为什么会出现这种问题,这就要来说一说springboot进行属性绑定时的一个重要知识点了,有关属性名称的宽松绑定,也可以称为宽松绑定。
报错原因: 前缀只支持烤肉串命名,而前缀的属性支持各种命名方法。
宽松绑定:
什么是宽松绑定?实际上是springboot进行编程时人性化设计的一种体现,即配置文件中的命名格式与变量名的命名格式可以进行格式上的最大化兼容。兼容到什么程度呢?几乎主流的命名格式都支持,例如:
在ServerConfig中的ipAddress属性名
@Component @Data @ConfigurationProperties(prefix = "servers") public class ServerConfig { private String ipAddress; }
可以与下面的配置属性名规则全兼容
servers: ipAddress: 192.168.0.2 驼峰模式 ip_address: 192.168.0.2 下划线模式 ip-address: 192.168.0.2 烤肉串模式 IP_ADDRESS: 192.168.0.2 常量模式
也可以说,以上4种模式最终都可以匹配到ipAddress这个属性名。为什么这样呢?原因就是在进行匹配时,配置中的名称要去掉中划线和下划线后,忽略大小写的情况下去与java代码中的属性名进行忽略大小写的等值匹配,以上4种命名去掉下划线中划线忽略大小写后都是一个词ipaddress,java代码中的属性名忽略大小写后也是ipaddress,这样就可以进行等值匹配了,这就是为什么这4种格式都能匹配成功的原因。不过springboot官方推荐使用烤肉串模式,也就是中划线模式。
到这里我们掌握了一个知识点,就是命名的规范问题。再来看开始出现的编程错误信息
Configuration property name 'dataSource' is not valid: Invalid characters: 'S' Bean: datasource Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter Action: Modify 'dataSource' so that it conforms to the canonical names requirements.
其中Reason描述了报错的原因,规范的名称应该是烤肉串(kebab)模式(case),即使用-分隔,使用小写字母数字作为标准字符,且必须以字母开头。然后再看我们写的名称dataSource,就不满足上述要求。闹了半天,在书写前缀时,这个词不是随意支持的,必须使用上述标准。编程写了这么久,基本上编程习惯都养成了,到这里又被springboot教育了,没辙,谁让人家东西好用呢,按照人家的要求写吧。
最后说一句,以上规则仅针对springboot中@ConfigurationProperties注解进行属性绑定时有效,对@Value注解进行属性映射无效。有人就说,那我不用你不就行了?不用,你小看springboot的推广能力了,到原理篇我们看源码时,你会发现内部全是这玩意儿,算了,拿人手短吃人嘴短,认怂吧。
总结
- @ConfigurationProperties绑定属性时支持属性名宽松绑定,这个宽松体现在属性名的命名规则上
- @Value注解不支持松散绑定规则
- 绑定前缀名推荐采用烤肉串命名规则,即使用中划线做分隔符
2-3.常用计量单位绑定
在前面的配置中,我们书写了如下配置值,其中第三项超时时间timeout描述了服务器操作超时时间,当前值 是-1表示永不超时。
servers: ip-address: 192.168.0.1 port: 2345 timeout: -1
但是每个人都这个值的理解会产生不同,比如线上服务器完成一次主从备份,配置超时时间240,这个240如果单位是秒就是超时时间4分钟,如果单位是分钟就是超时时间4小时。面对一次线上服务器的主从备份,设置4分钟,简直是开玩笑,别说拷贝过程,备份之前的压缩过程4分钟也搞不定,这个时候问题就来了,怎么解决这个误会?
除了加强约定之外,springboot充分利用了JDK8中提供的全新的用来表示计量单位的新数据类型,从根本上解决这个问题。以下模型类中添加了两个JDK8中新增的类,分别是Duration和DataSize
@Component @Data @ConfigurationProperties(prefix = "servers") public class ServerConfig { @DurationUnit(ChronoUnit.HOURS) private Duration serverTimeOut; @DataSizeUnit(DataUnit.MEGABYTES) private DataSize dataSize; }
Duration:表示时间间隔,可以通过@DurationUnit注解描述时间单位,例如上例中描述的单位为小时(ChronoUnit.HOURS)
DataSize:表示存储空间,可以通过@DataSizeUnit注解描述存储空间单位,例如上例中描述的单位为MB(DataUnit.MEGABYTES)
使用上述两个单位就可以有效避免因沟通不同步或文档不健全导致的信息不对称问题,从根本上解决了问题,避免产生误读。
Druation常用单位如下:
DataSize常用单位如下:
2-4.校验
目前我们在进行属性绑定时可以通过松散绑定规则在书写时放飞自我了,但是在书写时由于无法感知模型类中的数据类型,就会出现类型不匹配的问题,比如代码中需要int类型,配置中给了非法的数值,例如写一个“a",这种数据肯定无法有效的绑定,还会引发错误。 SpringBoot给出了强大的数据校验功能,可以有效的避免此类问题的发生。在JAVAEE的JSR303规范中给出了具体的数据校验标准,开发者可以根据自己的需要选择对应的校验框架,此处使用Hibernate提供的校验框架来作为实现进行数据校验。书写应用格式非常固定,话不多说,直接上步骤
步骤①:开启校验框架,这两个框架都被springboot管理,不用配置版本:
<!--1.导入JSR303校验规范--> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> </dependency> <!--使用hibernate框架提供的校验器做实现--> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> </dependency>
步骤②:在需要开启校验功能的类上使用注解@Validated开启校验功能
@Component @Data @ConfigurationProperties(prefix = "servers") //开启对当前bean的属性注入校验 @Validated public class ServerConfig { }
步骤③:对具体的字段设置校验规则
@Component @Data @ConfigurationProperties(prefix = "servers") //开启对当前bean的属性注入校验 @Validated public class ServerConfig { //设置具体的规则 @Max(value = 8888,message = "最大值不能超过8888") @Min(value = 202,message = "最小值不能低于202") private int port; }
通过设置数据格式校验,就可以有效避免非法数据加载,其实使用起来还是挺轻松的,基本上就是一个格式。
总结
- 开启Bean属性校验功能一共3步:导入JSR303与Hibernate校验框架坐标、使用@Validated注解启用校验功能、使用具体校验规则规范数据校验格式
2-5.数据类型转换
有关spring属性注入的问题到这里基本上就讲完了,但是最近一名开发者向我咨询了一个问题,我觉得需要给各位学习者分享一下。在学习阶段其实我们遇到的问题往往复杂度比较低,单一性比较强,但是到了线上开发时,都是综合性的问题,而这个开发者遇到的问题就是由于bean的属性注入引发的灾难。
先把问题描述一下,这位开发者连接数据库正常操作,但是运行程序后显示的信息是密码错误。
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
其实看到这个报错,几乎所有的学习者都能分辨出来,这是用户名和密码不匹配,就就是密码输入错了,但是问题就在于密码并没有输入错误,这就比较讨厌了。给的报错信息无法帮助你有效的分析问题,甚至会给你带到沟里。如果是初学者,估计这会心态就崩了,我密码没错啊,你怎么能说我有错误呢?来看看用户名密码的配置是如何写的:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC username: root password: 0127
这名开发者的生日是1月27日,所以密码就使用了0127,其实问题就出在这里了。
之前在基础篇讲属性注入时,提到过类型相关的知识,在整数相关知识中有这么一句话,支持二进制,八进制,十六进制
这个问题就处在这里了,因为0127在开发者眼中是一个字符串“0127”,但是在springboot看来,这就是一个数字,而且是一个八进制的数字。当后台使用String类型接收数据时,如果配置文件中配置了一个整数值,他是先安装整数进行处理,读取后再转换成字符串。巧了,0127撞上了八进制的格式,所以最终以十进制数字87的结果存在了。
这里提两个注意点,第一,字符串标准书写加上引号包裹,养成习惯,第二,遇到0开头的数据多注意吧。
总结
- yaml文件中对于数字的定义支持进制书写格式,如需使用字符串请使用引号明确标注
3.测试
测试是保障程序正确性的唯一屏障,在企业级开发中更是不可缺少,但是由于测试代码往往不产生实际效益,所以一些小型公司并不是很关注,导致一些开发者从小型公司进入中大型公司后,往往这一块比较短板,所以还是要拿出来把这一块知识好好说说,做一名专业的开发人员。
3-1.加载测试专用属性
测试过程本身并不是一个复杂的过程,但是很多情况下测试时需要模拟一些线上情况,或者模拟一些特殊情况。如果当前环境按照线上环境已经设定好了,例如是下面的配置
env: maxMemory: 32GB minMemory: 16GB
但是你现在想测试对应的兼容性,需要测试如下配置
env: maxMemory: 16GB minMemory: 8GB
这个时候我们能不能每次测试的时候都去修改源码application.yml中的配置进行测试呢?显然是不行的。每次测试前改过来,每次测试后改回去,这太麻烦了。于是我们就想,需要在测试环境中创建一组临时属性,去覆盖我们源码中设定的属性,这样测试用例就相当于是一个独立的环境,能够独立测试,这样就方便多了。
临时属性:
springboot已经为我们开发者早就想好了这种问题该如何解决,并且提供了对应的功能入口。在测试用例程序中,可以通过对注解@SpringBootTest添加属性来模拟临时属性,具体如下:
//properties属性可以为当前测试用例添加临时的属性配置 @SpringBootTest(properties = {"test.prop=testValue1"}) public class PropertiesAndArgsTest { @Value("${test.prop}") private String msg; @Test void testProperties(){ System.out.println(msg); } }
使用注解@SpringBootTest的properties属性就可以为当前测试用例添加临时的属性,覆盖源码配置文件中对应的属性值进行测试。
临时参数:
回顾上一篇文章在引导类中使用临时属性:
除了上述这种情况,在前面讲解使用命令行启动springboot程序时讲过,通过命令行参数也可以设置属性值。
具体看运维文章里的2.1临时属性设置:SpringBoot基础2——运维实用_vincewm的博客-CSDN博客
public static void main(String[] args) { String[] arg = new String[1]; arg[0] = "--server.port=8082"; SpringApplication.run(SSMPApplication.class, arg); }
或者在idea配置临时参数:
而且线上启动程序时,通常都会添加一些专用的配置信息。作为运维人员他们才不懂java,更不懂这些配置的信息具体格式该怎么写,那如果我们作为开发者提供了对应的书写内容后,能否提前测试一下这些配置信息是否有效呢?当时是可以的,还是通过注解@SpringBootTest的另一个属性来进行设定。
//args属性可以为当前测试用例添加临时的命令行参数 @SpringBootTest(args={"--test.prop=testValue2"}) public class PropertiesAndArgsTest { @Value("${test.prop}") private String msg; @Test void testProperties(){ System.out.println(msg); } }
注意:
- 使用注解@SpringBootTest的args属性就可以为当前测试用例模拟命令行参数并进行测试。
- @SpringBootTest的args优先级高于properties属性。
说到这里,好奇宝宝们肯定就有新问题了,如果两者共存呢?其实如果思考一下配置属性与命令行参数的加载优先级,这个结果就不言而喻了。在属性加载的优先级设定中,有明确的优先级设定顺序,还记得下面这个顺序吗?
在这个属性加载优先级的顺序中,明确规定了命令行参数的优先级排序是11,而配置属性的优先级是3,结果不言而喻了,args属性配置优先于properties属性配置加载。
到这里我们就掌握了如果在测试用例中去模拟临时属性的设定。
总结
- 加载测试临时属性可以通过注解@SpringBootTest的properties和args属性进行设定,此设定应用范围仅适用于当前测试用例
思考
应用于测试环境的临时属性解决了,如果想在测试的时候临时加载一些bean能不做呢?也就是说我测试时,想搞一些独立的bean出来,专门应用于测试环境,能否实现呢?咱们下一节再讲。
3-2.加载测试专用配置
上一节提出了临时配置一些专用于测试环境的bean的需求,这一节我们就来解决这个问题。
学习过Spring的知识,我们都知道,其实一个spring环境中可以设置若干个配置文件或配置类,若干个配置信息可以同时生效。现在我们的需求就是在测试环境中再添加一个配置类,然后启动测试环境时,生效此配置就行了。其实做法和spring环境中加载多个配置信息的方式完全一样。具体操作步骤如下:
步骤①:在测试包test.com.example.config中创建专用的测试环境配置类
@Configuration public class MsgConfig { @Bean public String msg(){ return "bean msg"; } }
上述配置仅用于演示当前实验效果,实际开发可不能这么注入String类型的数据
步骤②:在启动测试环境时,导入测试环境专用的配置类,使用@Import注解即可实现
@SpringBootTest @Import({MsgConfig.class}) public class ConfigurationTest { @Autowired private String msg; @Test void testConfiguration(){ System.out.println(msg); } }
到这里就通过@Import属性实现了基于开发环境的配置基础上,对配置进行测试环境的追加操作,实现了1+1的配置环境效果。这样我们就可以实现每一个不同的测试用例加载不同的bean的效果,丰富测试用例的编写,同时不影响开发环境的配置。
总结
- 定义测试环境专用的配置类,然后通过@Import注解在具体的测试中导入临时的配置,例如测试用例,方便测试过程,且上述配置不影响其他的测试类环境
思考
当前我们已经可以实现业务层和数据层的测试,并且通过临时配置,控制每个测试用例加载不同的测试数据。但是实际企业开发不仅要保障业务层与数据层的功能安全有效,也要保障表现层的功能正常。但是我们目的对表现层的测试都是通过postman手工测试的,并没有在打包过程中体现表现层功能被测试通过。能否在测试用例中对表现层进行功能测试呢?还真可以,咱们下一节再讲。
3-3.Web环境模拟测试
在测试中对表现层功能进行测试需要一个基础和一个功能。所谓的一个基础是运行测试程序时,必须启动web环境,不然没法测试web功能。一个功能是必须在测试程序中具备发送web请求的能力,不然无法实现web功能的测试。所以在测试用例中测试表现层接口这项工作就转换成了两件事,一,如何在测试类中启动web测试,二,如何在测试类中发送web请求。下面一件事一件事进行,先说第一个
3.3.1 测试类中启动web环境
每一个springboot的测试类上方都会标准@SpringBootTest注解,而@SpringBootTest注解带有一个属性,叫做webEnvironment。通过该属性就可以设置在测试用例中启动web环境,具体如下:
//@SpringBootTest注解的属性webEnvironment。通过该属性就可以设置在测试用例中启动web环境,redom_port就是随机端口启动web环境 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WebTest { }
测试类中启动web环境时,可以指定启动的Web环境对应的端口,springboot提供了4种设置值,分别如下:
- MOCK:根据当前设置确认是否启动web环境,例如使用了Servlet的API就启动web环境,属于适配性的配置
- DEFINED_PORT:使用自定义的端口作为web服务器端口
- RANDOM_PORT:使用随机端口作为web服务器端口
- NONE:默认值,不启动web环境
通过上述配置,现在启动测试程序时就可以正常启用web环境了,建议大家测试时使用RANDOM_PORT,避免代码中因为写死设定引发线上功能打包测试时由于端口冲突导致意外现象的出现。就是说你程序中写了用8080端口,结果线上环境8080端口被占用了,结果你代码中所有写的东西都要改,这就是写死代码的代价。现在你用随机端口就可以测试出来你有没有这种问题的隐患了。
测试环境中的web环境已经搭建好了,下面就可以来解决第二个问题了,如何在程序代码中发送web请求。
3.3.2 测试类中发送请求
对于测试类中发送请求,其实java的API就提供对应的功能,只不过平时各位小伙伴接触的比较少,所以较为陌生。springboot为了便于开发者进行对应的功能开发,对其又进行了包装,简化了开发步骤,具体操作如下:
步骤①:在测试类中开启web虚拟调用功能,通过注解@AutoConfigureMockMvc实现此功能的开启
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //开启虚拟MVC调用 @AutoConfigureMockMvc public class WebTest { }
步骤②:定义发起虚拟调用的对象MockMVC,通过自动装配的形式初始化对象
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //开启虚拟MVC调用 @AutoConfigureMockMvc public class WebTest { @Test void testWeb(@Autowired MockMvc mvc) { } }
步骤③:创建一个虚拟请求对象,封装请求的路径,并使用MockMVC对象的perform方法发送对应请求
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //开启虚拟MVC调用 @AutoConfigureMockMvc public class WebTest { @Test void testWeb(@Autowired MockMvc mvc) throws Exception { //http://localhost:8080/books //创建虚拟请求,当前访问/books,仅指定请求的具体路径即可。mock译为模拟的,假的。 MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); //执行对应的请求 mvc.perform(builder); } }
执行测试程序,现在就可以正常的发送/books对应的请求了,注意访问路径不要写http://localhost:8080/books,因为前面的服务器IP地址和端口使用的是当前虚拟的web环境,无需指定,仅指定请求的具体路径即可。
总结
- 在测试类中测试web层接口要保障测试类启动时启动web容器,使用@SpringBootTest注解的webEnvironment属性可以虚拟web环境用于测试
- 为测试方法注入MockMvc对象,通过MockMvc对象可以发送虚拟请求,模拟web请求调用过程
思考
目前已经成功的发送了请求,但是还没有起到测试的效果,测试过程必须出现预计值与真实值的比对结果才能确认测试结果是否通过,虚拟请求中能对哪些请求结果进行比对呢?咱们下一节再讲。
3.3.3 web环境请求结果比对
上一节已经在测试用例中成功的模拟出了web环境,并成功的发送了web请求,本节就来解决发送请求后如何比对发送结果的问题。其实发完请求得到的信息只有一种,就是响应对象。至于响应对象中包含什么,就可以比对什么。常见的比对内容如下:
- 响应状态匹配
@Test void testStatus(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); ResultActions action = mvc.perform(builder); //设定预期值 与真实值进行比较,成功测试通过,失败测试失败 //定义本次调用的预期值 StatusResultMatchers status = MockMvcResultMatchers.status(); //预计本次调用时成功的:状态200 ResultMatcher ok = status.isOk(); //添加预计值到本次调用过程中进行匹配 action.andExpect(ok); }
- 响应体匹配(非json数据格式)
@Test void testBody(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); ResultActions action = mvc.perform(builder); //设定预期值 与真实值进行比较,成功测试通过,失败测试失败 //定义本次调用的预期值 ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher result = content.string("springboot2"); //添加预计值到本次调用过程中进行匹配 action.andExpect(result); }
- 响应体匹配(json数据格式,开发中的主流使用方式)
@Test void testJson(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); ResultActions action = mvc.perform(builder); //设定预期值 与真实值进行比较,成功测试通过,失败测试失败 //定义本次调用的预期值 ContentResultMatchers content = MockMvcResultMatchers.content(); //跟string形式内容不同点在于解析格式,string形式响应体是content.string()方法,json格式是content.json() ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot2\",\"type\":\"springboot\"}"); //添加预计值到本次调用过程中进行匹配 action.andExpect(result); }
- 响应头信息匹配
@Test void testContentType(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); ResultActions action = mvc.perform(builder); //设定预期值 与真实值进行比较,成功测试通过,失败测试失败 //定义本次调用的预期值 HeaderResultMatchers header = MockMvcResultMatchers.header(); ResultMatcher contentType = header.string("Content-Type", "application/json"); //添加预计值到本次调用过程中进行匹配 action.andExpect(contentType); }
基本上齐了,头信息,正文信息,状态信息都有了,就可以组合出一个完美的响应结果比对结果了。以下范例就是三种信息同时进行匹配校验,也是一个完整的信息匹配过程。
//Test类别忘了 启动随机端口web程序@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)和开启web虚拟调用功能@AutoConfigureMockMvc @Test //自动注入MockMvc对象 void testGetById(@Autowired MockMvc mvc) throws Exception { //创建虚拟请求,当前访问/books MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); //执行对应的请求,返回请求的结果动作 ResultActions action = mvc.perform(builder); //获取期望值,通过action.andExpect()看模拟值和期望值是否一样 StatusResultMatchers status = MockMvcResultMatchers.status(); ResultMatcher ok = status.isOk(); action.andExpect(ok); HeaderResultMatchers header = MockMvcResultMatchers.header(); ResultMatcher contentType = header.string("Content-Type", "application/json"); action.andExpect(contentType); ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot\",\"type\":\"springboot\"}"); action.andExpect(result); }
总结
- web虚拟调用可以对本地虚拟请求的返回响应信息进行比对,分为响应头信息比对、响应体信息比对、响应状态信息比对
3-4.数据层测试回滚
当前我们的测试程序可以完美的进行表现层、业务层、数据层接口对应的功能测试了,但是测试用例开发完成后,在打包的阶段由于test生命周期属于必须被运行的生命周期,如果跳过会给系统带来极高的安全隐患,所以测试用例必须执行。但是新的问题就呈现了,测试用例如果测试时产生了事务提交就会在测试过程中对数据库数据产生影响,进而产生垃圾数据。这个过程不是我们希望发生的,作为开发者测试用例该运行运行,但是希望过程中产生的数据不要在我的系统中留痕,这样该如何处理呢?
所以要想办法回滚事务,可以在保证观察到控制台正常数据库操作结果的情况下而不影响数据库真实内容。
回顾:回滚事务之后,测试数据库自增id就可以知道其实是执行过真实数据库操作的,只是执行后又返回到了操作前的状态。
springboot早就为开发者想到了这个问题,并且针对此问题给出了最简解决方案,在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交。当程序运行后,只要注解@Transactional出现的位置存在注解@SpringBootTest,springboot就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交。
给有业务提交的类注解@SpringBootTest:
@SpringBootTest //只要是测试类加事务注解,就会回滚事务。回顾业务层加@Transactional可以让每个方法里的多个dao操作绑定成一个事务,要么一起成功,要么一起失败。 @Transactional //回滚注解@Rollback的默认值就是true,改成false会提交事务,专门为了应对测试类想给方法里的多个dao操作绑定成一个事务的情况。也就加事务的测试类会用到@Rollback。因为jdbc是自动提交事务的,业务层不会是测试类 //@Rollback(true) public class DaoTest { @Autowired private BookService bookService; @Test void testSave(){ Book book = new Book(); book.setName("springboot3"); book.setType("springboot3"); book.setDescription("springboot3"); bookService.save(book); } }
如果开发者想提交事务,也可以,再添加一个@RollBack的注解,设置回滚状态为false即可正常提交事务,是不是很方便?springboot在辅助开发者日常工作这一块展现出了惊人的能力,实在太贴心了。
总结
- 在springboot的测试类中通过添加注解@Transactional来阻止测试用例提交事务
- @Rollback只会在加@Transactional的测试类用到,通过注解@Rollback控制springboot测试类执行结果是否提交事务,需要配合注解@Transactional使用
思考
当前测试程序已经近乎完美了,但是由于测试用例中书写的测试数据属于固定数据,往往失去了测试的意义,开发者可以针对测试用例进行针对性开发,这样就有可能出现测试用例不能完美呈现业务逻辑代码是否真实有效的达成业务目标的现象,解决方案其实很容易想,测试用例的数据只要随机产生就可以了,能实现吗?咱们下一节再讲。
3-5.测试用例数据设定
对于测试用例的数据固定书写肯定是不合理的,springboot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的。具体如下:
yml配置:
testcase: book: id: ${random.int} id2: ${random.int(10)} type: ${random.int!5,10!} name: ${random.value} uuid: ${random.uuid} publishTime: ${random.long}
当前配置就可以在每次运行程序时创建一组随机数据,避免每次运行时数据都是固定值的尴尬现象发生,有助于测试功能的进行。数据的加载按照之前加载数据的形式,使用@ConfigurationProperties注解封装实体类即可
@Component @Data @ConfigurationProperties(prefix = "testcase.book") public class BookCase { private int id; private int id2; private int type; private String name; private String uuid; private long publishTime; }
对于随机值的产生,还有一些小的限定规则,比如产生的数值性数据可以设置范围等,具体如下:
- ${random.int}表示随机整数
- ${random.int(10)}表示10以内的随机数
- ${random.int(10,20)}表示10到20的随机数
- 其中()可以是任意字符,例如[],!!均可
总结
- 使用随机数据可以替换测试用例中书写的固定数据,提高测试用例中的测试数据有效性
4.数据层解决方案
开发实用篇前三章基本上是开胃菜,从第四章开始,开发实用篇进入到了噩梦难度了,从这里开始,不再是单纯的在springboot内部搞事情了,要涉及到很多相关知识。本章节主要内容都是和数据存储与读取相关,前期学习的知识与数据层有关的技术基本上都围绕在数据库这个层面上,所以本章要讲的第一个大的分支就是SQL解决方案相关的内容,除此之外,数据的来源还可以是非SQL技术相关的数据操作,因此第二部分围绕着NOSQL解决方案讲解。至于什么是NOSQL解决方案,讲到了再说吧。下面就从SQL解决方案说起。
4-1.SQL
回忆一下之前做SSMP整合的时候数据层解决方案涉及到了哪些技术?MySQL数据库与MyBatisPlus框架,后面又学了Druid数据源的配置,所以现在数据层解决方案可以说是Mysql+Druid+MyBatisPlus。而三个技术分别对应了数据层操作的三个层面:
- 数据源技术:Druid
- 持久化技术:MyBatisPlus
- 数据库技术:MySQL
下面的研究就分为三个层面进行研究,对应上面列出的三个方面,咱们就从第一个数据源技术开始说起。
4.1.1 数据源技术
目前我们使用的数据源技术是Druid,运行时可以在日志中看到对应的数据源初始化信息,具体如下:
INFO 28600 --- [ main] c.a.d.s.b.a.DruidDataSourceAutoConfigure : Init DruidDataSource INFO 28600 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
如果不使用Druid数据源,程序运行后是什么样子呢?将Druid技术对应的starter去掉再次运行程序,会使用springboot内置数据源HikariDataSource:
注意:不使用druid,必须把druid的starter去掉,如果只去掉yml中的数据源type设置,会因为自动配置导致程序运行后数据源初始化还是使用的druid。自动配置相关内容在一篇springboot原理篇详细讲。
INFO 31820 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... INFO 31820 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
虽然没有DruidDataSource相关的信息了,但是我们发现日志中有HikariDataSource数据源信息,就算不懂这是个什么技术,看名字也能看出来,以DataSource结尾的名称,这一定是一个数据源技术。我们又没有手工添加这个技术,这个技术哪里来的呢?这就是这一节要讲的知识,springboot内嵌数据源。
数据层技术是每一个企业级应用程序都会用到的,而其中必定会进行数据库连接的管理。springboot根据开发者的习惯出发,开发者提供了数据源技术,就用你提供的,开发者没有提供,那总不能手工管理一个一个的数据库连接对象啊,怎么办?我给你一个默认的就好了,这样省心又省事,大家都方便。
springboot提供了3款内嵌数据源技术,分别如下:
- HikariCP
- Tomcat提供DataSource
- Commons DBCP
第一种,HikartCP,是轻量级数据源中运行速度最快的数据源。这是springboot官方推荐的数据源技术,作为默认内置数据源使用。啥意思?你不配置数据源,那就用这个。
第二种,Tomcat提供的DataSource,如果不想用HikartCP,并且使用tomcat作为web服务器进行web程序的开发,使用这个。为什么是Tomcat,不是其他web服务器呢?因为web技术导入starter后,默认使用内嵌tomcat,既然都是默认使用的技术了,那就一用到底,数据源也用它的。有人就提出怎么才能不使用HikartCP用tomcat提供的默认数据源对象呢?把HikartCP技术的坐标排除掉就OK了。
第三种,DBCP,这个使用的条件就更苛刻了,既不使用HikartCP也不使用tomcat的DataSource时,默认给你用这个。
springboot这心操的,也是稀碎啊,就怕你自己管不好连接对象,给你一顿推荐,真是开发界的最强辅助。既然都给你奶上了,那就受用吧,怎么配置使用这些东西呢?之前我们配置druid时使用druid的starter对应的配置如下:
spring: datasource: druid: url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root
换成是默认的数据源HikariCP后,直接吧druid删掉就行了,如下:
spring: datasource: url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root
当然,也可以写上是对hikari做的配置,但是url地址要单独配置,如下:
spring: datasource: #注意:hikari必须把url放在上面 url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC hikari: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root
这就是配置hikari数据源的方式。如果想对hikari做进一步的配置,可以继续配置其独立的属性。例如:
spring: datasource: url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC hikari: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root maximum-pool-size: 50
如果不想使用hikari数据源,使用tomcat的数据源或者DBCP配置格式也是一样的。学习到这里,以后我们做数据层时,数据源对象的选择就不再是单一的使用druid数据源技术了,可以根据需要自行选择。
总结
- springboot技术提供了3种内置的数据源技术,分别是Hikari、tomcat内置数据源、DBCP
4.1.2 持久化技术
说完数据源解决方案,再来说一下持久化解决方案。springboot充分发挥其最强辅助的特征,给开发者提供了一套现成的、默认的持久化技术,叫做JdbcTemplate。其实这个技术不能说是springboot提供的,因为不使用springboot技术,一样能使用它,谁提供的呢?spring技术提供的,所以在springboot技术范畴中,这个技术也是存在的,毕竟springboot技术是加速spring程序开发而创建的。
这个技术其实就是回归到jdbc最原始的编程形式来进行数据层的开发,下面直接上操作步骤:
步骤①:导入spring-boot-starter-jdbc对应的坐标,里面包含了jdbcTemplate。如果导入了mybatis-plus-boot-starter就不用导入了。
注意:持久化技术Mybatis-plus 的starter依赖里包含了spring-boot-starter-jdbc。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency
步骤②:自动装配JdbcTemplate对象
@SpringBootTest class Springboot15SqlApplicationTests { @Test void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){ } }
步骤③:使用JdbcTemplate实现查询操作(非实体类封装数据的查询操作)
@Test void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){ String sql = "select * from tbl_book"; List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql); System.out.println(maps); }
步骤④:使用JdbcTemplate实现查询操作(实体类封装数据的查询操作)
@Test void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){ String sql = "select * from tbl_book"; RowMapper<Book> rm = new RowMapper<Book>() { @Override //重写mapRow方法,将查询到的ResultSet里的各字段封装到实体类里。。回顾jdbc中ResultSet 是封装查询结果的结果集,//ResultSet resultSet = stmt.executeQuery(sql); public Book mapRow(ResultSet rs, int rowNum) throws SQLException { Book temp = new Book(); temp.setId(rs.getInt("id")); temp.setName(rs.getString("name")); temp.setType(rs.getString("type")); temp.setDescription(rs.getString("description")); return temp; } }; List<Book> list = jdbcTemplate.query(sql, rm); System.out.println(list); }
步骤⑤:使用JdbcTemplate实现增删改操作
@Test void testJdbcTemplateSave(@Autowired JdbcTemplate jdbcTemplate){ String sql = "insert into tbl_book values(3,'springboot1','springboot2','springboot3')"; jdbcTemplate.update(sql); }
如果想对JdbcTemplate对象进行相关配置,可以在yml文件中进行设定,具体如下:
spring: jdbc: template: query-timeout: -1 查询超时时间 max-rows: 500 最大行数 fetch-size: -1 缓存行数
总结
- SpringBoot内置JdbcTemplate持久化解决方案
- 使用JdbcTemplate需要导入spring-boot-starter-jdbc的坐标
4.1.3 数据库技术
截止到目前,springboot给开发者提供了内置的数据源解决方案和持久化解决方案,在数据层解决方案三件套中还剩下一个数据库,莫非springboot也提供有内置的解决方案?还真有,还不是一个,三个,这一节就来说说内置的数据库解决方案。
springboot提供了3款内置的数据库:
- H2
- HSQL
- Derby
以上三款数据库除了可以独立安装之外,还可以像是tomcat服务器一样,采用内嵌的形式运行在spirngboot容器中。内嵌在容器中运行,那必须是java对象啊,对,这三款数据库底层都是使用java语言开发的。
我们一直使用MySQL数据库就挺好的,为什么有需求用这个呢?原因就在于这三个数据库都可以采用内嵌容器的形式运行,在应用程序运行后,如果我们进行测试工作,此时测试的数据无需存储在磁盘上,但是又要测试使用,内嵌数据库就方便了,运行在内存中,该测试测试,该运行运行,等服务器关闭后,一切烟消云散,多好,省得你维护外部数据库了。这也是内嵌数据库的最大优点,方便进行功能测试。
下面以H2数据库为例讲解如何使用这些内嵌数据库,操作步骤也非常简单,简单才好用嘛
步骤①:导入H2数据库对应的坐标,h2和spring-boot-starter-data-jpa
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
步骤②:将工程设置为web工程,即导入spring-boot-starter-web,启动工程时启动H2数据库
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
步骤③:通过配置开启H2数据库控制台访问程序,也可以使用其他的数据库连接软件操作
spring: h2: console: enabled: true path: /h2
web端访问路径/h2,访问密码123456,如果访问失败,先配置下列数据源,启动程序运行后再次访问/h2路径就可以正常访问了
datasource: url: jdbc:h2:~/test hikari: driver-class-name: org.h2.Driver username: sa password: 123456
步骤④:使用JdbcTemplate或MyBatisPlus技术操作数据库
(略)
其实我们只是换了一个数据库而已,其他的东西都不受影响。一个重要提醒,别忘了,上线时,把内存级数据库关闭,采用MySQL数据库作为数据持久化方案,关闭方式就是设置enabled属性为false即可。
注意:
总结
- H2内嵌式数据库启动方式,添加坐标,添加配置
- H2数据库线上运行时请务必关闭
到这里SQL相关的数据层解决方案就讲完了,现在的可选技术就丰富的多了。
- 数据源技术:Druid、Hikari、tomcat DataSource、DBCP
- 持久化技术:MyBatisPlus、MyBatis、JdbcTemplate
- 数据库技术:MySQL、H2、HSQL、Derby
现在开发程序时就可以在以上技术中任选一种组织成一套数据库解决方案了。
4-2.NoSQL
这个NoSQL是什么意思呢?从字面来看,No表示否定,NoSQL就是非关系型数据库解决方案,意思就是数据该存存该取取,只是这些数据不放在关系型数据库中了,那放在哪里?自然是一些能够存储数据的其他相关技术中了,比如Redis等。本节讲解的内容就是springboot如何整合这些技术,在springboot官方文档中提供了10种相关技术的整合方案,我们将讲解国内市场上最流行的几款NoSQL数据库整合方案,分别是Redis、MongoDB、ES。
此外上述这些技术最佳使用方案都是在Linux服务器上部署,但是考虑到各位小伙伴的学习起点差异过大,所以下面的课程都是以Windows平台作为安装基础讲解,如果想看Linux版软件安装,可以再找到对应技术的学习文档查阅学习。
4.3 SpringBoot整合Redis
4.3.1 简介
Redis是一款采用key-value数据存储格式的内存级NoSQL数据库,重点关注数据存储格式,是key-value格式,也就是键值对的存储形式。与MySQL数据库不同,MySQL数据库有表、有字段、有记录,Redis没有这些东西,就是一个名称对应一个值,并且数据以存储在内存中使用为主。什么叫以存储在内存中为主?其实Redis有它的数据持久化方案,分别是RDB和AOF,但是Redis自身并不是为了数据持久化而生的,主要是在内存中保存数据,加速数据访问的,所以说是一款内存级数据库。
Redis支持多种数据存储格式,比如可以直接存字符串,也可以存一个map集合,list集合,后面会涉及到一些不同格式的数据操作,这个需要先学习一下才能进行整合,所以在基本操作中会介绍一些相关操作。下面就先安装,再操作,最后说整合
安装
windows版安装包下载地址:https://github.com/tporadowski/redis/releases
下载的安装包有两种形式,这里采用的是msi一键安装的msi文件进行安装的。
啥是msi,其实就是一个文件安装包,不仅安装软件,还帮你把安装软件时需要的功能关联在一起,打包操作。比如如安装序列、创建和设置安装路径、设置系统依赖项、默认设定安装选项和控制安装过程的属性。说简单点就是一站式服务,安装过程一条龙操作一气呵成,就是为小白用户提供的软件安装程序。
安装时除了路径,其他配置都可以,直接一路next。
安装完毕后会得到如下文件,其中有两个文件对应两个命令,是启动Redis的核心命令,需要再CMD命令行模式执行。
4.3.2 基本操作
启动服务器
进入安装文件夹下cmd,命令行启动redis-server.exe并指定配置
redis-server.exe redis.windows.conf
初学者无需调整服务器对外服务端口,默认6379。
报错:
解决:要先下一步的启动客户端redis-cli.exe,然后执行shutdown停止客户端、exit退出,再次启动服务器就可以了。
启动客户端
打开另一个cmd窗口:
redis-cli.exe
如果启动redis服务器失败,可以先启动客户端,然后执行shutdown停止客户端操作后退出,此时redis服务器就可以正常执行了。
基本操作
服务器启动后,使用客户端就可以连接服务器,类似于启动完MySQL数据库,然后启动SQL命令行操作数据库。
放置一个字符串数据到redis中,先为数据定义一个名称,比如name,age等,然后使用命令set设置数据到redis服务器中即可
set name itheima set age 12
从redis中取出已经放入的数据,根据名称取,就可以得到对应数据。如果没有对应数据就会得到(nil)
get name get age
以上使用的数据存储是一个名称对应一个值,如果要维护的数据过多,可以使用别的数据存储结构。例如hash哈希存储模型,它是一种一个名称下可以存储多个数据的存储模型,并且每个数据也可以有自己的二级存储名称。向hash结构中存储数据格式如下:
hset a a1 aa1 #对外key名称是a,在名称为哈希存储模型a中,a1这个key中保存了数据aa1 hset a a2 aa2
获取hash结构中的数据命令如下
hget a a1 #得到aa1 hget a a2 #得到aa2
有关redis的基础操作就普及到这里,需要全面掌握redis技术,请参看相关教程学习。
4.3.3 springboot整合redis
在进行整合之前先梳理一下整合的思想,springboot整合任何技术其实就是在springboot中使用对应技术的API。如果两个技术没有交集,就不存在整合的概念了。所谓整合其实就是使用springboot技术去管理其他技术,几个问题是躲不掉的。
第一,需要先导入对应技术的坐标,而整合之后,这些坐标都有了一些变化
第二,任何技术通常都会有一些相关的设置信息,整合之后,这些信息如何写,写在哪是一个问题
第三,没有整合之前操作如果是模式A的话,整合之后如果没有给开发者带来一些便捷操作,那整合将毫无意义,所以整合后操作肯定要简化一些,那对应的操作方式自然也有所不同
按照上面的三个问题去思考springboot整合所有技术是一种通用思想,在整合的过程中会逐步摸索出整合的套路,而且适用性非常强,经过若干种技术的整合后基本上可以总结出一套固定思维。
springboot整合redis步骤:
步骤①:导入springboot整合redis的starter坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
上述坐标可以在创建模块的时候通过勾选的形式进行选择,归属NoSQL分类中
tip:第二行spring data reactive redis包含了第一行Redis的驱动和访问,现在阶段只用第一行就够了。
步骤②:进行基础配置
spring: redis: host: localhost port: 6379
操作redis,最基本的信息就是操作哪一台redis服务器,所以服务器地址属于基础配置信息,不可缺少。但是即便你不配置,目前也是可以用的。因为以上两组信息都是默认配置值,刚好就是上述配置值。
步骤③:确保之前启动服务器后,自动注入Redis模板对象,获取值操作对象对数据增删改查。
此处使用的是注入Redis模板对象RedisTemplate的opsForValue()方法获取值操作对象ValueOperations ,通过ValueOperations对象的get和set方法操作数据库。
@SpringBootTest class Springboot16RedisApplicationTests { //自动注入RedisTemplate对象 @Autowired private RedisTemplate redisTemplate; //注意ValueOperations 添加是set,HashOperations 添加是put @Test void set() { //获取值操作对象。如果想获取hash操作对象要用opsForHash()方法。 ValueOperations ops = redisTemplate.opsForValue(); ops.set("age",41); } @Test void get() { ValueOperations ops = redisTemplate.opsForValue(); Object age = ops.get("name"); System.out.println(age); } @Test void hset() { HashOperations ops = redisTemplate.opsForHash(); ops.put("info","b","bb"); } @Test void hget() { HashOperations ops = redisTemplate.opsForHash(); Object val = ops.get("info", "b"); System.out.println(val); } }
在操作redis时,需要先确认操作何种数据,根据数据种类得到操作接口。例如使用opsForValue()获取string类型的数据操作接口,使用opsForHash()获取hash类型的数据操作接口,剩下的就是调用对应api操作了。各种类型的数据操作接口如下:
总结
- springboot整合redis步骤
- 导入springboot整合redis的starter坐标
- 进行基础配置
- 使用springboot整合redis的专用客户端接口RedisTemplate操作
StringRedisTemplate
RedisTemplate是以对象为操作的基本单元,StringRedisTemplate是以字符串为操作的基本单元。两个类创建的对象获取数据是不通用的,命令行客户端redis-cli.exe默认使用StringRedisTemplate。
由于redis内部不提供java对象的存储格式,因此当操作的数据以对象的形式存在时,会进行转码,转换成字符串格式后进行操作。为了方便开发者使用基于字符串为数据的操作,springboot整合redis时提供了专用的API接口StringRedisTemplate,你可以理解为这是RedisTemplate的一种指定数据泛型的操作API。
@SpringBootTest public class StringRedisTemplateTest { @Autowired private StringRedisTemplate stringRedisTemplate; @Test void get(){ ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); String name = ops.get("name"); System.out.println(name); } }
redis客户端选择
springboot整合redis技术提供了多种客户端兼容模式,默认提供的是lettucs客户端技术,也可以根据需要切换成指定客户端技术,例如jedis客户端技术。jedis是Redis传统的客户端技术。
从默认客户端技术lettucs切换成jedis客户端技术:
步骤①:导入jedis坐标。不用加版本号,该坐标被springboot管理。
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
jedis坐标受springboot管理,无需提供版本号
步骤②:配置客户端技术类型,设置为jedis
spring: redis: host: localhost port: 6379 client-type: jedis
步骤③:根据需要设置对应的配置
spring: redis: host: localhost port: 6379 client-type: jedis lettuce: pool: max-active: 16 jedis: pool: max-active: 16
lettcus与jedis区别
- jedis连接Redis服务器是直连模式,当多线程模式下使用jedis会存在线程安全问题,解决方案可以通过配置连接池使每个连接专用,这样整体性能就大受影响
- lettcus基于Netty框架进行与Redis服务器连接,底层设计中采用StatefulRedisConnection。 StatefulRedisConnection自身是线程安全的,可以保障并发访问安全问题,所以一个连接可以被多线程复用。当然lettcus也支持多连接实例一起工作
总结
- springboot整合redis提供了StringRedisTemplate对象,以字符串的数据格式操作redis
- 如果需要切换redis客户端实现技术,可以通过配置的形式进行
4.4 SpringBoot整合MongoDB
4.4.1 简介
使用Redis技术可以有效的提高数据访问速度,但是由于Redis的数据格式单一性,无法操作结构化数据,当操作对象型的数据时,Redis就显得捉襟见肘。在保障访问速度的情况下,如果想操作结构化数据,看来Redis无法满足要求了,此时需要使用全新的数据存储结束来解决此问题,本节讲解springboot如何整合MongoDB技术。
MongoDB是一个开源、高性能、无模式的文档型数据库,它是NoSQL数据库产品中的一种,是最像关系型数据库的非关系型数据库。
回顾:Redis是键值对存储格式的内存级数据库。
上述描述中几个词,其中对于我们最陌生的词是无模式的。什么叫无模式呢?简单说就是作为一款数据库,没有固定的数据存储结构,第一条数据可能有A、B、C一共3个字段,第二条数据可能有D、E、F也是3个字段,第三条数据可能是A、C、E3个字段,也就是说数据的结构不固定,这就是无模式。有人会说这有什么用啊?灵活,随时变更,不受约束。基于上述特点,MongoDB的应用面也会产生一些变化。以下列出了一些可以使用MongoDB作为数据存储的场景,但是并不是必须使用MongoDB的场景。
可以使用MongoDB但没必要:
- 淘宝用户数据
-
- 存储位置:数据库
- 特征:永久性存储,修改频度极低
- 游戏装备数据、游戏道具数据
-
- 存储位置:数据库、Mongodb
- 特征:永久性存储与临时存储相结合、修改频度较高
- 直播数据、打赏数据、粉丝数据
-
- 存储位置:数据库、Mongodb
- 特征:永久性存储与临时存储相结合,修改频度极高
必须使用MongoDB:
- 物联网数据
-
- 存储位置:Mongodb
- 特征:临时存储,修改频度飞速
快速了解一下MongoDB,下面直接开始我们的学习,老规矩,先安装,再操作,最后说整合
安装
windows版安装包下载地址:Download MongoDB Software Locally for Free | MongoDB
这里下的版本是5.0.11.
下载的安装包也有两种形式,一种是一键安装的msi文件,还有一种是解压缩就能使用的zip文件,哪种形式都行,本课程采用解压缩zip文件进行安装。
解压缩完毕后会得到如下文件,其中bin目录包含了所有mongodb的可执行命令
mongodb在运行时需要指定一个数据存储的目录,所以在安装目录下创建一个数据存储目录data,data目录下再创建db文件夹,具体如下
如果在安装的过程中出现了如下警告信息,就是告诉你,你当前的操作系统缺少了一些系统文件,这个不用担心。
根据下列方案即可解决,在浏览器中搜索提示缺少的名称对应的文件,并下载,将下载的文件拷贝到windows安装目录的system32目录下,然后在命令行中执行regsvr32命令注册此文件。根据下载的文件名不同,执行命令前更改对应名称。
regsvr32 vcruntime140_1.dll
启动服务器
mongod --dbpath=..\data\db
启动服务器时需要指定数据存储位置,通过参数--dbpath进行设置,可以根据需要自行设置数据存储路径。默认服务端口27017。
启动客户端
在bin目录下的地址栏输入cmd回车,注意服务端和客户端要是两个窗口:
mongo --host=127.0.0.1 --port=27017
4.4.2 基本操作
MongoDB虽然是一款数据库,但是它的操作并不是使用SQL语句进行的,因此操作方式各位小伙伴可能比较陌生,好在有一些类似于Navicat的数据库客户端软件,能够便捷的操作MongoDB,先安装一个客户端,再来操作MongoDB。
同类型的软件较多,本次安装的软件时Robo3t,Robot3t是一款绿色软件,无需安装,解压缩即可。解压缩完毕后进入安装目录双击robot3t.exe即可使用。
这里下的是1.4.4版本,MongoDB版本5.0.11。
打开软件首先要连接MongoDB服务器,选择【File】菜单,选择【Connect...】
进入连接管理界面后,选择左上角的【Create】链接,创建新的连接设置
如果输入设置值即可连接(默认不修改即可连接本机27017端口),当然要确保服务器的cmd窗口还处于开启状态:
连接成功后在命令输入区域输入命令即可操作MongoDB。
创建数据库:右键链接创建数据库,输入数据库名称即可
创建集合:在Collections上使用右键创建,输入集合名称即可,集合等同于mysql数据库中的表的作用
创建后双击集合可以打开窗口。
文档的增删改查: (文档类似于json格式的数据,初学者可以先把数据理解为就是json数据)
新增文档:
db.集合名称.insert/save/insertOne(文档)
db.book.insert({"name":"张三","addr":"西安",age:23})
- 文档是JSON格式或js格式,都行。
- 即使指定id,系统也会自动生成id:
- robo 3T运行: 快捷键f5,也可以点击工具类运行按钮运行。可以点击图标切换查询结果的视图:
删除文档:
db.集合名称.remove(条件)
示例:
db.book.remove({addr:"西安"})
不加条件就是删除跑路。
db.book.remove({})
修改文档:
db.集合名称.update(条件,{$操作种类:{文档}})
db.book.update({addr:"西安"},$set={"addr":"北京"})
注意:修改set后的数据会完全覆盖,例如上面示例,没有定义name和age,这两个属性字段修改后值为空。
查询文档:
基础查询 查询全部: db.集合.find(); 查第一条: db.集合.findOne() 查询指定数量文档: db.集合.find().limit(10) //查10条文档 跳过指定数量文档: db.集合.find().skip(20) //跳过20条文档 统计: db.集合.count() 排序: db.集合.sort({age:1}) //按age升序排序 投影: db.集合名称.find(条件,{name:1,age:1}) //仅保留name与age域 条件查询 基本格式: db.集合.find({条件}) 模糊查询: db.集合.find({域名:/正则表达式/}) //等同SQL中的like,比like强大,可以执行正则所有规则 条件比较运算: db.集合.find({域名:{$gt:值}}) //等同SQL中的数值比较操作,例如:name>18 包含查询: db.集合.find({域名:{$in:[值1,值2]}}) //等同于SQL中的in 条件连接查询: db.集合.find({$and:[{条件1},{条件2}]}) //等同于SQL中的and、or
有关MongoDB的基础操作就普及到这里,需要全面掌握MongoDB技术,请参看相关教程学习。
4.4.3 springboot整合MongoDB
整合要先确保服务器处于开启状态。
使用springboot整合MongDB该如何进行呢?其实springboot为什么使用的开发者这么多,就是因为他的套路几乎完全一样。导入坐标,做配置,使用API接口操作。整合Redis如此,整合MongoDB同样如此。
第一,先导入对应技术的整合starter坐标
第二,配置必要信息
第三,使用提供的API操作即可
下面就开始springboot整合MongoDB,操作步骤如下:
步骤①:导入springboot整合MongoDB的starter坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>
或者,上述坐标也可以在创建模块的时候通过勾选的形式进行选择,同样归属NoSQL分类中
步骤②:进行基础配置
spring: data: mongodb: uri: mongodb://localhost/itheima
操作MongoDB需要的配置与操作redis一样,最基本的信息都是操作哪一台服务器,区别就是连接的服务器IP地址和端口不同,书写格式不同,redis配置的是host和port,而MongoDB配置的是uri,端口不写默认是27017。
步骤③:自动注入springboot整合MongoDB的专用客户端接口MongoTemplate来进行操作
@SpringBootTest class Springboot17MongodbApplicationTests { @Autowired private MongoTemplate mongoTemplate; @Test void contextLoads() { Book book = new Book(); //必须得指定id,不然会报错 book.setId(2); book.setName("springboot2"); book.setType("springboot2"); book.setDescription("springboot2"); mongoTemplate.save(book); } @Test void find(){ //注意,直接查询会出错,因为MongoDB自动生成的id(如ObjectId("6308c1d2df1e39dd090d5a9d"))跟java不兼容,这里直接删除重加数据 List<Book> all = mongoTemplate.findAll(Book.class); System.out.println(all); } }
踩坑:
- java里增加操作必须指定id,而MongoDB里增加即使指定id也会自己生成id,java没指定id会报错Cannot autogenerate id of type java.lang.Integer for entity of type com.example.domain.Book!
- java里直接查询会报错,因为MongoDB自动生成的id(如ObjectId("6308c1d2df1e39dd090d5a9d"))跟java不兼容,这里直接删除重加数据
- 而MongoDB查询就不会报错:
整合工作到这里就做完了,感觉既熟悉也陌生。熟悉的是这个套路,三板斧,就这三招,导坐标做配置用API操作,陌生的是这个技术,里面具体的操作API可能会不熟悉,有关springboot整合MongoDB我们就讲到这里。有兴趣可以继续学习MongoDB的操作,然后再来这里通过编程的形式操作MongoDB。
总结:
对于整合要会举一反三,基本都是这个思想:先导入starter坐标,再进行配置、再自动注入相关接口。
- springboot整合MongoDB步骤
- 导入springboot整合MongoDB的starter坐标
- 进行基础配置
- 使用springboot整合MongoDB的专用客户端接口MongoTemplate操作
4.5 springboot整合ES
elasticsearch基础1——索引、文档_elasticsearch 索引文档_vincewm的博客-CSDN博客
4.5.1 ES简介
NoSQL解决方案已经讲完了两种技术的整合了,Redis可以使用内存加载数据并实现数据快速访问,MongoDB可以在内存中存储类似对象的数据并实现数据的快速访问,在企业级开发中对于速度的追求是永无止境的。下面要讲的内容也是一款NoSQL解决方案,只不过他的作用不是为了直接加速数据的读写,而是加速数据的查询的,叫做ES技术。
ES(Elasticsearch)是一个分布式全文搜索引擎,重点是全文搜索。
那什么是全文搜索呢?比如用户要买一本书,以Java为关键字进行搜索,不管是书名中还是书的介绍中,甚至是书的作者名字,只要包含java就作为查询结果返回给用户查看,上述过程就使用了全文搜索技术。搜索的条件不再是仅用于对某一个字段进行比对,而是在一条数据中使用搜索条件去比对更多的字段,只要能匹配上就列入查询结果,这就是全文搜索的目的。而ES技术就是一种可以实现上述效果的技术。
要实现全文搜索的效果,不可能使用数据库中like操作去进行比对,这种效率太低了。ES设计了一种全新的思想,来实现全文搜索。具体操作过程如下。
ES实现全文思想:
- 将被查询的字段的数据全部文本信息拆分成若干个词
-
- 例如“中华人民共和国”就会被拆分成三个词,分别是“中华”、“人民”、“共和国”,此过程有专业术语叫做分词。分词的策略不同,分出的效果不一样,不同的分词策略称为分词器。
- 将这些分词存储起来,分别对应每个分词查到的各个id及简短信息
-
- 例如id为1的数据中名称这一项的值是“中华人民共和国”,那么分词结束后,就会出现“中华”对应id为1,“人民”对应id为1,“共和国”对应id为1
- 例如id为2的数据中名称这一项的值是“人民代表大会“,那么分词结束后,就会出现“人民”对应id为2,“代表”对应id为2,“大会”对应id为2
- 此时就会出现如下对应结果,按照上述形式可以对所有文档进行分词。需要注意分词的过程不是仅对一个字段进行,而是对每一个参与查询的字段都执行,最终结果汇总到一个表格中
- 例如id为1的数据中名称这一项的值是“中华人民共和国”,那么分词结束后,就会出现“中华”对应id为1,“人民”对应id为1,“共和国”对应id为1
分词结果关键字 | 对应id |
中华 | 1 |
人民 | 1,2 |
共和国 | 1 |
代表 | 2 |
大会 | 2 |
-
- 当进行查询时,如果输入“人民”作为查询条件,可以通过上述表格数据进行比对,得到id值1,2,然后根据id值就可以得到查询的结果数据了。
- 当进行查询时,如果输入“人民”作为查询条件,可以通过上述表格数据进行比对,得到id值1,2,然后根据id值就可以得到查询的结果数据了。
倒排索引:数据库是根据索引(主键id)查数据,ES刚好相反,是根据数据的关键字查索引。
文档:每一行数据都可以称之为文档。
上述过程中分词结果关键字内容每一个都不相同,作用有点类似于数据库中的索引(主键id),是用来加速数据查询的。但是数据库中的索引是对某一个字段进行添加索引,而这里的分词结果关键字不是一个完整的字段值,只是一个字段中的其中的一部分内容。并且索引使用时是根据索引内容查找整条数据,全文搜索中的分词结果关键字查询后得到的并不是整条的数据,而是数据的id,要想获得具体数据还要再次查询,因此这里为这种分词结果关键字起了一个全新的名称,叫做倒排索引。
通过上述内容的学习,发现使用ES其实准备工作还是挺多的,必须先建立文档的倒排索引,然后才能继续使用。快速了解一下ES的工作原理,下面直接开始我们的学习,老规矩,先安装,再操作,最后说整合。
安装
windows版安装包下载地址:https://www.elastic.co/cn/downloads/elasticsearch
注意:
- 建议7.6.2版本下载地址,目前下载最新的可能出现问题:Elasticsearch 7.16.2 | Elastic
- 安装路径中不要出现中文和空格,像Program Files文件夹有空格就不行。
安装包里包括了jdk。
下载的安装包是解压缩就能使用的zip文件,解压缩完毕后会得到如下文件
- bin目录:包含所有的可执行命令
- config目录:包含ES服务器使用的配置文件
- jdk目录:此目录中包含了一个完整的jdk工具包,版本17,当ES升级时,使用最新版本的jdk确保不会出现版本支持性不足的问题
- lib目录:包含ES运行的依赖jar文件
- logs目录:包含ES运行后产生的所有日志文件
- modules目录:包含ES软件中所有的功能模块,也是一个一个的jar包。和jar目录不同,jar目录是ES运行期间依赖的jar包,modules是ES软件自己的功能jar包
- plugins目录:包含ES软件安装的插件,默认为空
启动服务器
elasticsearch.bat
双击elasticsearch.bat文件即可启动ES服务器,默认服务端口9200。通过浏览器访问http://localhost:9200看到如下信息视为ES服务器正常启动
{ "name" : "CZBK-**********", "cluster_name" : "elasticsearch", "cluster_uuid" : "j137DSswTPG8U4Yb-0T1Mg", "version" : { "number" : "7.16.2", "build_flavor" : "default", "build_type" : "zip", "build_hash" : "2b937c44140b6559905130a8650c64dbd0879cfb", "build_date" : "2021-12-18T19:42:46.604893745Z", "build_snapshot" : false, "lucene_version" : "8.10.1", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }
4.5.2 基本操作
ES中保存有我们要查询的数据,只不过格式和数据库存储数据格式不同而已。在ES中我们要先创建倒排索引,这个索引的功能又点类似于数据库的表,然后将数据添加到倒排索引中,添加的数据称为文档。所以要进行ES的操作要先创建索引,再添加文档,这样才能进行后续的查询操作。
要操作ES可以通过Rest风格的请求来进行,也就是说发送一个请求就可以执行一个操作。比如新建索引,删除索引这些操作都可以使用发送请求的形式来进行。
- 创建索引,books是索引名称,注意请求方式必须是put,现在先不加请求体,如下是创建一个名为books的索引(类似表)。
PUT请求 http://localhost:9200/books
注意:
- 如果报错access_control_exception 就是安装路径有空格或中文,像Program Files文件夹就有中文。
发送请求后,看到如下信息即索引创建成功
{ "acknowledged": true, "shards_acknowledged": true, "index": "books" }
重复创建已经存在的索引会出现错误信息,reason属性中描述错误原因
{ "error": { "root_cause": [ { "type": "resource_already_exists_exception", "reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists", "index_uuid": "VgC_XMVAQmedaiBNSgO2-w", "index": "books" } ], "type": "resource_already_exists_exception", "reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists", books索引已经存在 "index_uuid": "VgC_XMVAQmedaiBNSgO2-w", "index": "book" }, "status": 400 }
- 查询索引
GET请求 http://localhost:9200/books
- 查询索引得到索引相关信息,如下,其中第三行"mappings"用来设定索引的详细信息,会用到分词器,例如ik分词器。
{ "book": { "aliases": {}, "mappings": {}, "settings": { "index": { "routing": { "allocation": { "include": { "_tier_preference": "data_content" } } }, "number_of_shards": "1", "provided_name": "books", "creation_date": "1645768584849", "number_of_replicas": "1", "uuid": "VgC_XMVAQmedaiBNSgO2-w", "version": { "created": "7160299" } } } } }
- 如果查询了不存在的索引,会返回错误信息,例如查询名称为book的索引后信息如下
{ "error": { "root_cause": [ { "type": "index_not_found_exception", "reason": "no such index [book]", "resource.type": "index_or_alias", "resource.id": "book", "index_uuid": "_na_", "index": "book" } ], "type": "index_not_found_exception", "reason": "no such index [book]", "resource.type": "index_or_alias", "resource.id": "book", "index_uuid": "_na_", "index": "book" }, "status": 404 }
- 删除索引
DELETE请求 http://localhost:9200/books
- 删除所有后,给出删除结果
{ "acknowledged": true }
- 如果重复删除,会给出错误信息,同样在reason属性中描述具体的错误原因
{ "error": { "root_cause": [ { "type": "index_not_found_exception", "reason": "no such index [books]", "resource.type": "index_or_alias", "resource.id": "book", "index_uuid": "_na_", "index": "book" } ], "type": "index_not_found_exception", "reason": "no such index [books]", #没有books索引 "resource.type": "index_or_alias", "resource.id": "book", "index_uuid": "_na_", "index": "book" }, "status": 404 }
- 创建索引并指定分词器
前面创建的索引是未指定分词器的,可以在创建索引时添加请求参数,设置分词器。目前国内较为流行的分词器是IK分词器,使用前先在下对应的分词器,然后使用。IK分词器下载地址(注意插件版本必须与es版本一致):https://github.com/medcl/elasticsearch-analysis-ik/releases
分词器下载后解压到ES安装目录的plugins目录中即可,安装分词器后需要重新启动ES服务器。如果报错检查版本。使用IK分词器创建索引格式:
PUT请求 http://localhost:9200/books 请求参数如下(注意是json格式的参数,属性类似于mysql中的字段) { "mappings":{ #定义mappings属性,替换创建索引时对应的mappings属性 "properties":{ #定义索引中包含的属性设置 "id":{ #设置索引中包含id属性 "type":"keyword" #设置当前属性即id为关键词,可以被直接搜索 }, "name":{ #设置索引中包含name属性 "type":"text", #设置当前属性是文本信息。es的text类型类似于mysql的varchar类型 "analyzer":"ik_max_word", #设置分析器为ik分词器 "copy_to":"all" #分词结果拷贝到全新属性all属性中 }, "type":{ "type":"keyword" }, "description":{ "type":"text", "analyzer":"ik_max_word", "copy_to":"all" }, "all":{ #定义全新属性all,整合所有设置了分词器的属性,用来描述多个字段的分词结果集合,当前属性可以参与查询。all是一个设计型字段。 "type":"text", "analyzer":"ik_max_word" } } } }
- 创建完毕后返回结果和不使用分词器创建索引的结果是一样的,都提示success。
- 查看添加属性后的索引信息
{ "books": { "aliases": {}, "mappings": { #mappings属性已经被替换 "properties": { "all": { "type": "text", "analyzer": "ik_max_word" }, "description": { "type": "text", "copy_to": [ "all" ], "analyzer": "ik_max_word" }, "id": { "type": "keyword" }, "name": { "type": "text", "copy_to": [ "all" ], "analyzer": "ik_max_word" }, "type": { "type": "keyword" } } }, "settings": { "index": { "routing": { "allocation": { "include": { "_tier_preference": "data_content" } } }, "number_of_shards": "1", "provided_name": "books", "creation_date": "1645769809521", "number_of_replicas": "1", "uuid": "DohYKvr_SZO4KRGmbZYmTQ", "version": { "created": "7160299" } } } } }
目前我们已经有了索引了,但是索引中还没有数据,所以要先添加数据,ES中称数据为文档,下面进行文档操作。
- 添加文档post,有三种方式
POST请求 http://localhost:9200/books/_doc #使用系统生成id POST请求 http://localhost:9200/books/_create/1 #使用指定id POST请求 http://localhost:9200/books/_doc/1 #使用指定id,不存在创建,存在更新(版本递增) 文档通过请求参数传递,数据格式json { "name":"springboot", "type":"springboot", "description":"springboot" }
- 查询文档
GET请求 http://localhost:9200/books/_doc/1 #查询单个文档 GET请求 http://localhost:9200/books/_search #查询全部文档
- 条件查询的分词查询
GET请求 http://localhost:9200/books/_search?q=name:springboot q=查询属性名:查询包含属性值的文档
- id删除文档
DELETE请求 http://localhost:9200/books/_doc/1
- 修改文档(全量更新put)
PUT请求 http://localhost:9200/books/_doc/1 文档通过请求参数传递,数据格式json { "name":"springboot", "type":"springboot", "description":"springboot" }
- 修改文档(部分更新post)
POST请求 http://localhost:9200/books/_update/1 文档通过请求参数传递,数据格式json { "doc":{ #部分更新并不是对原始文档进行更新,而是对原始文档对象中的doc属性中的指定属性更新 "name":"springboot" #仅更新提供的属性值,未提供的属性值不参与更新操作 } }
注意:
- 创建索引是put,添加文档是post,修改文档是put(全覆盖)和post(半覆盖)
- 回顾mysql里的修改是部分更新。
幂等性:
- PUT会将新的json值完全替换掉旧的;而POST方式只会更新相同字段的值,其他数据不会改变,新提交的字段若不存在则增加。
- PUT和DELETE操作是幂等的。所谓幂等是指不管进行多少次操作,结果都一样。比如用PUT修改一篇文章,然后在做同样的操作,每次操作后的结果并没有什么不同,DELETE也是一样。POST操作不是幂等的,比如常见的POST重复加载问题:当我们多次发出同样的POST请求后,其结果是创建了若干的资源。
4.5.3 springboot整合ES
使用springboot整合ES该如何进行呢?老规矩,导入坐标,做配置,使用API接口操作。整合Redis如此,整合MongoDB如此,整合ES依然如此。太没有新意了,其实不是没有新意,这就是springboot的强大之处,所有东西都做成相同规则,对开发者来说非常友好。
springboot整合ES的客户端有低级别和高级别之分。
整合低级别客户端(不建议):ElasticsearchRestTemplate
步骤①:导入springboot整合ES的starter坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
步骤②:进行基础配置
spring: elasticsearch: rest: uris: http://localhost:9200
配置ES服务器地址,端口9200
步骤③:老版客户端,使用springboot整合ES的专用客户端接口ElasticsearchRestTemplate来进行操作
@SpringBootTest class Springboot18EsApplicationTests { @Autowired private ElasticsearchRestTemplate template; }
上述操作形式是ES早期的操作方式,使用的客户端被称为Low Level Client,这种客户端操作方式性能方面略显不足,于是ES开发了全新的客户端操作方式,称为High Level Client。高级别客户端与ES版本同步更新,但是springboot最初整合ES的时候使用的是低级别客户端,所以企业开发需要更换成高级别的客户端模式。
高级别客户端方式进行springboot整合ES,操作步骤如下:
步骤①:导入springboot整合ES高级别客户端的坐标,此种形式目前没有对应的starter
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
步骤②:进行基础配置
方法一(最新官方推荐):
依赖换成这个:
<dependency> <groupId>co.elastic.clients</groupId> <artifactId>elasticsearch-java</artifactId> <version>8.1.0</version> </dependency>
@Test public void create() throws IOException { // 创建低级客户端 RestClient restClient = RestClient.builder( new HttpHost("localhost", 9200) ).build(); // 使用Jackson映射器创建传输层 ElasticsearchTransport transport = new RestClientTransport( restClient, new JacksonJsonpMapper() ); // 创建API客户端 ElasticsearchClient client = new ElasticsearchClient(transport); // 关闭ES客户端 transport.close(); restClient.close(); }
方法二(简单快速,已过时):(过时不推荐,下面使用的是此方法):编程配置客户端:这里使用编程的形式设置连接的ES服务器,并获取客户端对象
依赖换成这个(加上版本) :
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.17.5</version> </dependency>
@SpringBootTest class Springboot18EsApplicationTests { //这里使用方法二自己创建客户端,所以不是@Autowired,如果用方法一配置类就要@Autowired private RestHighLevelClient client; @Test void testCreateClient() throws IOException { //先使用http主机类HttpHost的create()方法创建对象 HttpHost host = HttpHost.create("http://localhost:9200"); //使用RestClient的静态builder()方法创建RestClientBuilder对象 RestClientBuilder builder = RestClient.builder(host); client = new RestHighLevelClient(builder); client.close(); } }
后面步骤④会把创建关闭封装成配置类或@BeforeEach方法。
配置ES服务器地址与端口9200,记得客户端使用完毕需要手工关闭。由于当前客户端是手工维护的,因此不能通过自动装配的形式加载对象。
步骤③:创建不带分词器ik的索引
@SpringBootTest class Springboot18EsApplicationTests { private RestHighLevelClient client; @Test void testCreateIndex() throws IOException { HttpHost host = HttpHost.create("http://localhost:9200"); RestClientBuilder builder = RestClient.builder(host); client = new RestHighLevelClient(builder); //创建索引请求 CreateIndexRequest request = new CreateIndexRequest("books"); //request.source("{}", XContentType.JSON); //添加请求体 //客户端创建索引,create()方法参数是创建索引请求类 client.indices().create(request, RequestOptions.DEFAULT); client.close(); } }
高级别客户端操作是通过发送请求的方式完成所有操作的,ES针对各种不同的操作,设定了各式各样的请求对象,上例中创建索引的对象是CreateIndexRequest,其他操作也会有自己专用的Request对象。
当前操作我们发现,无论进行ES何种操作,第一步永远是获取RestHighLevelClient对象,最后一步永远是关闭该对象的连接。在测试中可以使用测试类的特性去帮助开发者一次性的完成上述操作,但是在业务书写时,还需要自行管理。将上述代码格式转换成使用测试类的初始化方法和销毁方法进行客户端对象的维护。
步骤④:封装客户端创建关闭方法。
方法一:配置类
@Configuration public class RestClientConfig { @Bean public RestHighLevelClient restHighLevelClient() { // 如果有多个从节点可以持续在内部new多个HttpHost,参数1是IP,参数2是端口,参数3是通信协议 return new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http"))); } }
方法二: 封装方法
@SpringBootTest class Springboot18EsApplicationTests { @BeforeEach //在测试类中每个操作运行前运行的方法 void setUp() { HttpHost host = HttpHost.create("http://localhost:9200"); RestClientBuilder builder = RestClient.builder(host); client = new RestHighLevelClient(builder); } @AfterEach //在测试类中每个操作运行后运行的方法 void tearDown() throws IOException { client.close(); } private RestHighLevelClient client; @Test void testCreateIndex() throws IOException { CreateIndexRequest request = new CreateIndexRequest("books"); client.indices().create(request, RequestOptions.DEFAULT); } }
现在的书写简化了很多,也更合理。下面使用上述模式将所有的ES操作执行一遍,测试结果
创建索引(IK分词器):
@Test void testCreateIndexByIK() throws IOException { CreateIndexRequest request = new CreateIndexRequest("books"); String json = "{\n" + " \"mappings\":{\n" + " \"properties\":{\n" + " \"id\":{\n" + " \"type\":\"keyword\"\n" + " },\n" + " \"name\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\",\n" + " \"copy_to\":\"all\"\n" + " },\n" + " \"type\":{\n" + " \"type\":\"keyword\"\n" + " },\n" + " \"description\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\",\n" + " \"copy_to\":\"all\"\n" + " },\n" + " \"all\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\"\n" + " }\n" + " }\n" + " }\n" + "}"; //设置请求中的参数 request.source(json, XContentType.JSON); client.indices().create(request, RequestOptions.DEFAULT); }
IK分词器是通过请求参数的形式进行设置的,设置请求参数使用request对象中的source方法进行设置,至于参数是什么,取决于你的操作种类。当请求中需要参数时,均可使用当前形式进行参数设置。
添加文档:
@Test //添加文档 void testCreateDoc() throws IOException { Book book = bookDao.selectById(1); //指定id创建索引请求IndexRequest IndexRequest request = new IndexRequest("books").id(book.getId().toString()); String json = JSON.toJSONString(book); request.source(json,XContentType.JSON); client.index(request,RequestOptions.DEFAULT); }
添加文档使用的请求对象是IndexRequest,与创建索引使用的请求对象不同。
批量添加文档:
@Test //批量添加文档 void testCreateDocAll() throws IOException { List<Book> bookList = bookDao.selectList(null); BulkRequest bulk = new BulkRequest(); for (Book book : bookList) { IndexRequest request = new IndexRequest("books").id(book.getId().toString()); String json = JSON.toJSONString(book); request.source(json,XContentType.JSON); bulk.add(request); } client.bulk(bulk,RequestOptions.DEFAULT); }
批量做时,先创建一个BulkRequest的对象,可以将该对象理解为是一个保存request对象的容器,将所有的请求都初始化好后,添加到BulkRequest对象中,再使用BulkRequest对象的bulk方法,一次性执行完毕。此时如果查询所有所有数据就只能查询到10条数据,因为ES自己做了分页处理。
按id查询文档:
@Test //按id查询 void testGet() throws IOException { //查询用GetRequest ,添加用IndexRequest GetRequest request = new GetRequest("books","1"); GetResponse response = client.get(request, RequestOptions.DEFAULT); String json = response.getSourceAsString(); System.out.println(json); }
根据id查询文档使用的请求对象是GetRequest。
按条件查询文档:
@Test //按条件查询 void testSearch() throws IOException { SearchRequest request = new SearchRequest("books"); SearchSourceBuilder builder = new SearchSourceBuilder(); builder.query(QueryBuilders.termQuery("all","spring")); request.source(builder); //获取查询结果,是查询响应类SearchResponse SearchResponse response = client.search(request, RequestOptions.DEFAULT); //从查询响应中获取命中hits的信息,从postman发送查询请求可以看到查询数据是包在hits里的source里 SearchHits hits = response.getHits(); for (SearchHit hit : hits) { String source = hit.getSourceAsString(); //System.out.println(source); Book book = JSON.parseObject(source, Book.class); System.out.println(book); } }
按条件查询文档使用的请求对象是SearchRequest,查询时调用SearchRequest对象的termQuery方法,需要给出查询属性名,此处支持使用合并字段,也就是前面定义索引属性时添加的all属性。
springboot整合ES的操作到这里就说完了,与前期进行springboot整合redis和mongodb的差别还是蛮大的,主要原始就是我们没有使用springboot整合ES的客户端对象。至于操作,由于ES操作种类过多,所以显得操作略微有点复杂。有关springboot整合ES就先学习到这里吧。
总结
- springboot整合ES步骤
- 导入springboot整合ES的High Level Client坐标
- 手工管理客户端对象,包括初始化和关闭操作
- 使用High Level Client根据操作的种类不同,选择不同的Request对象完成对应操作
5.整合第三方技术
通过第四章的学习,我们领略到了springboot在整合第三方技术时强大的一致性,在第五章中我们要使用springboot继续整合各种各样的第三方技术,通过本章的学习,可以将之前学习的springboot整合第三方技术的思想贯彻到底,还是那三板斧。导坐标、做配置、调API。
springboot能够整合的技术实在是太多了,可以说是万物皆可整。本章将从企业级开发中常用的一些技术作为出发点,对各种各样的技术进行整合。
5-1.缓存
5.1.0 概述
企业级应用主要作用是信息处理,当需要读取数据时,由于受限于数据库的访问效率,导致整体系统性能偏低。
应用程序直接与数据库打交道,访问效率低
为了改善上述现象,开发者通常会在应用程序与数据库之间建立一种临时的数据存储机制,该区域中的数据在内存中保存,读写速度较快,可以有效解决数据库访问效率低下的问题。这一块临时存储数据的区域就是缓存。
使用缓存后,应用程序与缓存打交道,缓存与数据库打交道,数据访问效率提高
缓存:
缓存是一种介于数据永久存储介质与应用程序之间的数据临时存储介质,使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘IO),提高系统性能。此外缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间。而springboot提供了对市面上几乎所有的缓存技术进行整合的方案,下面就一起开启springboot整合缓存之旅。
springboot提供了缓存的统一整合接口,方便缓存技术的开发和管理:
5.1.1 SpringBoot内置缓存Simple
springboot技术提供有内置的缓存解决方案,可以帮助开发者快速开启缓存技术,并使用缓存技术进行数据的快速操作,例如读取缓存数据和写入数据到缓存。
步骤①:导入springboot提供的缓存技术对应的starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
步骤②:开启缓存功能,在引导类上方标注注解@EnableCaching配置springboot程序中可以使用缓存
@SpringBootApplication //开启缓存功能 @EnableCaching public class Springboot19CacheApplication { public static void main(String[] args) { SpringApplication.run(Springboot19CacheApplication.class, args); } }
步骤③:设置操作的数据是否使用缓存
@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; //Cacheable译为可缓存的,可缓冲的。@Cacheable的value属性是存储空间名,key属性是此方法返回值在存储空间内的键名。 //key属性名必须和形参名一样才能缓存,别忘了#号 @Cacheable(value="cacheSpace",key="#id") public Book getById(Integer id) { return bookDao.selectById(id); } }
注意:
- 一定一定别忘了#号
- 缓存的key属性名必须方法的形参名一样才能缓存。 只要此key对应的缓存已存在,下次不管查询出什么数据,返回的结果都是直接从缓存的这个key里取。
- 被注解@Cacheable声明的方法不能被本类中其他方法调用,原因是spring容器管理问题。
在业务方法上面使用注解@Cacheable声明当前方法的返回值放入缓存中,其中要指定缓存的存储位置,以及缓存中保存当前方法返回值对应的名称。上例中value属性描述缓存的存储位置,可以理解为是一个存储空间名,key属性描述了缓存中保存数据的名称,使用#id读取形参中的id值作为缓存名称。
使用@Cacheable注解后,执行当前操作,如果发现对应名称在缓存中没有数据,就正常读取数据,然后放入缓存;如果对应名称在缓存中有数据,就终止当前业务方法执行,直接返回缓存中的数据。
测试缓存已经实现:
使用postman根据id查询:
查询多次同一个id,会发现控制台只会输出第一次数据库查询日志:
5.1.2 手机验证码模拟案例,@CachePut
为了便于下面演示各种各样的缓存技术,我们创建一个手机验证码的案例环境,模拟使用缓存保存手机验证码的过程。
手机验证码案例需求如下:
- 输入手机号获取验证码,组织文档以短信形式发送给用户(页面模拟)
- 输入手机号和验证码验证结果
为了描述上述操作,我们制作两个表现层接口,一个用来模拟发送短信的过程,其实就是根据用户提供的手机号生成一个验证码,然后放入缓存,另一个用来模拟验证码校验的过程,其实就是使用传入的手机号和验证码进行匹配,并返回最终匹配结果。下面直接制作本案例的模拟代码,先以上例中springboot提供的内置缓存技术来完成当前案例的制作。
步骤①:导入springboot提供的缓存技术对应的starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
步骤②:启用缓存,在引导类上方标注注解@EnableCaching配置springboot程序中可以使用缓存
@SpringBootApplication //开启缓存功能 @EnableCaching public class Springboot19CacheApplication { public static void main(String[] args) { SpringApplication.run(Springboot19CacheApplication.class, args); } }
步骤③:定义验证码对应的实体类,封装手机号与验证码两个属性
@Data public class SMSCode { private String tele; private String code; }
步骤④:定义验证码功能的业务层接口与实现类
这里是要新建另外一个工具类,把验证码的生成和获取写在工具类中,并加上缓存注解,然后通过service调用,从而实现缓存。注意不能直接给service的获取和检查方法设置缓存注解,因为检查时候它不能直接调用本类的获取方法,spring管理bean的问题。
public interface SMSCodeService { public String sendCodeToSMS(String tele); public boolean checkCode(SMSCode smsCode); } @Service public class SMSCodeServiceImpl implements SMSCodeService { @Autowired private CodeUtils codeUtils; public String sendCodeToSMS(String tele) { //使用工具类是为了使用后面的检查方法 String code = codeUtils.generator(tele); return code; } //取出内存中的验证码与传递过来的验证码比对,如果相同,返回true。 //参数是包括手机号和验证码的实体类 public boolean checkCode(SMSCode smsCode) { //获取controller传来实体对象的验证码 String code = smsCode.getCode(); //获取缓存中的验证码,注意这里必须要使用工具类调用get方法,如果只设置service,直接调用本service里的get方法不是spring容器管理的。 String cacheCode = codeUtils.get(smsCode.getTele()); return code.equals(cacheCode); } }
获取验证码后,当验证码失效时必须重新获取验证码,因此在获取验证码的功能上不能使用@Cacheable注解,@Cacheable注解是缓存中没有值则放入值,缓存中有值则取值。此处的功能仅仅是生成验证码并放入缓存,并不具有从缓存中取值的功能,因此不能使用@Cacheable注解,应该使用仅具有向缓存中保存数据的功能,使用@CachePut注解即可。
对于校验验证码的功能建议放入工具类中进行。
步骤⑤:定义验证码的生成策略与根据手机号读取验证码的功能
不能只用service就实现所检查验证码功能 ,因为被注解@Cacheable声明的方法不能被本类中其他方法调用,原因是spring容器管理问题。
@Component public class CodeUtils {