1.热部署
可以在不重启服务器的情况下重新加载更新后的程序.
1.1实现原理
①非springboot项目热部署实现原理
- 开发非springboot项目时,我们要制作一个web工程并通过tomcat启动,通常需要先安装tomcat服务器到磁盘中,开发的程序配置发布到安装的tomcat服务器上。如果想实现热部署的效果,这种情况其实有两种做法,一种是在tomcat服务器的配置文件中进行配置,这种做法与你使用什么IDE工具无关,不管你使用eclipse还是idea都行。还有一种做法是通过IDE工具进行配置,比如在idea工具中进行设置,这种形式需要依赖IDE工具,每款IDE工具不同,对应的配置也不太一样。但是核心思想是一样的,就是使用服务器去监控其中加载的应用,发现产生了变化就重新加载一次。
②springboot项目热部署实现原理
- 基于springboot开发的web工程其实有一个显著的特征,就是tomcat服务器内置了,还记得内嵌服务器吗?服务器是以一个对象的形式在spring容器中运行的。本来我们期望于tomcat服务器加载程序后由tomcat服务器盯着程序,你变化后我就重新启动重新加载,但是现在tomcat和我们的程序是平级的了,都是spring容器中的组件,这下就麻烦了,缺乏了一个直接的管理权,那该怎么做呢?简单,再搞一个程序X在spring容器中盯着你原始开发的程序A不就行了吗?确实,搞一个盯着程序A的程序X就行了,如果你自己开发的程序A变化了,那么程序X就命令tomcat容器重新加载程序A就OK了。并且这样做有一个好处,spring容器中东西不用全部重新加载一遍,只需要重新加载你开发的程序那一部分就可以了
1.2手动启动热部署
-
导入开发者工具对应的坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
-
构建项目
1.3自动启动热部署
-
设置自动构建项目
File
→settings
→Build,execution,deployment
→Compile
→勾选Build project automatically(自动构建项目)
-
允许在程序运行时进行自动构建
-
快捷键
Ctrl+Alt+Shift+/
打开维护面板→选择第一个 -
在选项中搜索comple,然后勾选对应项即可
-
旧版本
-
新版本:
setting-Advanced Settings -Allow auto-make to start.....
-
-
-
注意
- idea工具失去焦点5秒后才会进行热部署,否则每敲一个字母就构建一次电脑会吃不消
1.4重启与重载
- 一个springboot项目在运行时实际上是分两个过程进行的,根据加载的东西不同,划分成base类加载器与restart类加载器。
- base类加载器:用来加载jar包中的类,jar包中的类和配置文件由于不会发生变化,因此不管加载多少次,加载的内容不会发生变化
- restart类加载器:用来加载开发者自己开发的类、配置文件、页面等信息,这一类文件受开发者影响
- 当springboot项目启动时,base类加载器执行,加载jar包中的信息后,restart类加载器执行,加载开发者制作的内容。当执行构建项目后,由于jar中的信息不会变化,因此base类加载器无需再次执行,所以仅仅运行restart类加载即可,也就是将开发者自己制作的内容重新加载就行了,这就完成了一次热部署的过程,也可以说热部署的过程实际上是重新加载restart类加载器中的信息。
1.5参与热部署监控的文件范围配置
-
并不是所有的文件修改都会激活热部署的,原因在于在开发者工具中有一组配置,当满足了配置中的条件后,才会启动热部署,配置中默认不参与热部署的目录信息
- /META-INF/maven
- /META-INF/resources
- /resources
- /static
- /public
- /templates
-
以上目录中的文件如果发生变化,是不参与热部署的。如果想修改配置,可以通过application.yml文件进行设定哪些文件不参与热部署操作
spring: devtools: restart: # 设置不参与热部署的文件或文件夹 exclude: static/**,public/**,config/application.yml
1.6关闭热部署
-
线上环境运行时是不可能使用热部署功能的,所以需要强制关闭此功能,通过配置可以关闭此功能。
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
用来为bean绑定属性
-
在yml配置文件中以对象的格式添加若干属性
servers: ip-address: 192.168.0.1 port: 2345 timeout: -1
-
再开发一个用来封装数据的实体类,注意要提供属性对应的setter方法
@Component @Data public class ServerConfig { private String ipAddress; private int port; private long timeout; }
-
使用@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容器管理的当前方法的返回值对象绑定属性,其实本质上都一样。
使用@ConfigurationProperties注解可以为bean进行属性绑定,那在一个业务系统中,哪些bean通过注解@ConfigurationProperties去绑定属性了呢?为了解决这个问题,spring给我们提供了一个全新的注解,专门标注使用@ConfigurationProperties注解绑定属性的bean是哪些。这个注解叫做@EnableConfigurationProperties。
-
在配置类上开启@EnableConfigurationProperties注解,并标注要使用@ConfigurationProperties注解绑定属性的类
@SpringBootApplication @EnableConfigurationProperties(ServerConfig.class) public class Springboot13ConfigurationApplication { }
-
在对应的类上直接使用@ConfigurationProperties进行属性绑定
@Data @ConfigurationProperties(prefix = "servers") public class ServerConfig { private String ipAddress; private int port; private long timeout; }
当使用@EnableConfigurationProperties注解时,spring会默认将其标注的类定义为bean
-
使用@ConfigurationProperties注解时,会出现一个提示信息
-
出现这个提示后只需要添加一个坐标此提醒就消失了
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency>
-
2.2宽松绑定/松散绑定
-
在进行属性绑定时,可能会遇到如下情况,为了进行标准命名,开发者会将属性名严格按照驼峰命名法书写,在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官方推荐使用烤肉串模式,也就是中划线模式。
其中Reason描述了报错的原因,规范的名称应该是烤肉串(kebab)模式(case),即使用-分隔,使用小写字母数字作为标准字符,且必须以字母开头。然后再看我们写的名称dataSource,就不满足上述要求。闹了半天,在书写前缀时,这个词不是随意支持的,必须使用上述标准。
2.3常用计量单位绑定
-
在前面的配置中,我们书写了如下配置值,其中第三项超时时间timeout描述了服务器操作超时时间,当前值是-1表示永不超时。
servers: ip-address: 192.168.0.1 port: 2345 timeout: -1
-
除了加强约定之外,springboot充分利用了JDK8中提供的全新的用来表示计量单位的新数据类型,从根本上解决这个问题。以下模型类中添加了两个JDK8中新增的类,分别是Duration和DataSize
-
Duration:表示时间间隔,可以通过@DurationUnit注解描述时间单位,例如上例中描述的单位为小时(ChronoUnit.HOURS)
-
DataSize:表示存储空间,可以通过@DataSizeUnit注解描述存储空间单位,例如上例中描述的单位为MB(DataUnit.MEGABYTES)
@Component @Data @ConfigurationProperties(prefix = "servers") public class ServerConfig { @DurationUnit(ChronoUnit.HOURS) private Duration serverTimeOut; @DataSizeUnit(DataUnit.MEGABYTES) private DataSize dataSize; }
-
2.4校验
在进行属性绑定时可以通过松散绑定规则在书写时放飞自我了,但是在书写时由于无法感知模型类中的数据类型,就会出现类型不匹配的问题,比如代码中需要int类型,配置中给了非法的数值,例如写一个“a",这种数据肯定无法有效的绑定,还会引发错误。
SpringBoot给出了强大的数据校验功能,可以有效的避免此类问题的发生。在JAVAEE的JSR303规范中给出了具体的数据校验标准,开发者可以根据自己的需要选择对应的校验框架,此处使用Hibernate提供的校验框架来作为实现进行数据校验。书写应用格式非常固定
-
开启校验框架
<!--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; }
通过设置数据格式校验,就可以有效避免非法数据加载,其实使用起来还是挺轻松的,基本上就是一个格式。
3.测试
3.1加载测试专用属性
-
测试过程本身并不是一个复杂的过程,但是很多情况下测试时需要模拟一些线上情况,或者模拟一些特殊情况。如果当前环境按照线上环境已经设定好了,例如是下面的配置
env: maxMemory: 32GB minMemory: 16GB
-
但是你现在想测试对应的兼容性,需要测试如下配置
env: maxMemory: 16GB minMemory: 8GB
①临时属性
-
在测试环境中创建一组临时属性,去覆盖我们源码中设定的属性,这样测试用例就相当于是一个独立的环境,能够独立测试
-
通过对注解@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属性就可以为当前测试用例添加临时的属性,覆盖源码配置文件中对应的属性值进行测试。
-
②临时参数
-
线上启动程序时,通常都会添加一些专用的配置信息,通过命令行参数也可以设置属性值
//args属性可以为当前测试用例添加临时的命令行参数 @SpringBootTest(args={"--test.prop=testValue2"}) public class PropertiesAndArgsTest { @Value("${test.prop}") private String msg; @Test void testProperties(){ System.out.println(msg); } }
③优先级
- args>properties>文件中的配置
3.2加载测试专用配置
一个spring环境中可以设置若干个配置文件或配置类,若干个配置信息可以同时生效
- 在测试环境中再添加一个配置类,然后启动测试环境时,生效此配置即可。做法和spring环境中加载多个配置信息的方式完全一样。
-
在测试包test中创建专用的测试环境配置类
@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的效果,丰富测试用例的编写,同时不影响开发环境的配置。
3.3Web环境模拟测试
在测试中对表现层功能进行测试需要一个基础和一个功能。所谓的一个基础是运行测试程序时,必须启动web环境,不然没法测试web功能。一个功能是必须在测试程序中具备发送web请求的能力,不然无法实现web功能的测试。
所以在测试用例中测试表现层接口这项工作就转换成了两件事,
一,如何在测试类中启动web测试
二,如何在测试类中发送web请求。
①测试类中启动web环境
-
每一个springboot的测试类上方都会标准@SpringBootTest注解,而注解带有一个属性,叫做webEnvironment。通过该属性就可以设置在测试用例中启动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环境
- MOCK:根据当前设置确认是否启动web环境
- 测试时建议使用RANDOM_PORT,避免代码中因为写死设定引发线上功能打包测试时由于端口冲突导致意外现象的出现。
- 测试类中启动web环境时,可以指定启动的Web环境对应的端口,springboot提供了4种设置值
②测试类中发送请求
-
所以较为陌生。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对象发送对应请求
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //开启虚拟MVC调用 @AutoConfigureMockMvc public class WebTest { @Test void testWeb(@Autowired MockMvc mvc) throws Exception { //http://localhost:8080/books //创建虚拟请求,当前访问/books MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); //执行对应的请求 mvc.perform(builder); } }
-
执行测试程序,现在就可以正常的发送/books对应的请求了,注意访问路径不要写http://localhost:8080/books,因为前面的服务器IP地址和端口使用的是当前虚拟的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(); 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 void testGetById(@Autowired MockMvc mvc) throws Exception { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); ResultActions action = mvc.perform(builder); 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); }
3.4数据层测试回滚
-
在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交。当程序运行后,只要注解@Transactional出现的位置存在注解@SpringBootTest,springboot就会认为这是一个测试程序,无需提交事务,所以也就可以避免事务的提交。
@SpringBootTest @Transactional @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即可正常提交事务
3.5测试用例数据设定
-
springboot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的
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的随机数
-
其中()可以是任意字符,例如[],!!均可
-