Spring路径-10-SpringBoot开发部署与测试
1 开发的热部署
1.1 热部署/热加载
热部署(Hot Deploy):
热部署针对的是容器或者是整个应用,部署了新的资源或者修改了一些代码,需要在不停机的情况下的重新加载整个应用。
热加载(Hot Swap):
热加载针对的是单个字节码]文件,指的是重新编译后,不需要停机,应用程序就可以加载使用新的class文件。
1.2 spring boot 热部署原理
springBoot热部署原理是:当我们使用编译器启动项目后,在编译器上修改了代码后,编译器会将最新的代码编译成新的.class文件放到classpath下;而引入的spring-boot-devtools插件,插件会监控classpath下的资源,当classpath下的资源改变后,插件会触发重启;
而重启为什么速度快于我们自己启动呢?
我们自己启动的时候,是加载项目中所有的文件(自己编写的文件 + 所有项目依赖的jar)
而加入了spring-boot-devtools插件依赖后,我们自己编写的文件的类加载器为org.springframework.boot.devtools.restart.classloader.RestartClassLoader,是这个工具包自定义的类加载器, 项目依赖的jar使用的是JDK中的类加载器(AppClassLoader\ExtClassLoader\引导类加载器)
在插件触发的重启中,只会使用RestartClassLoader来进行加载(即:只加载我们自己编写的文件部分)
1.2.1 spring-boot-devtools
这是SpringBoot提供的热部署工具,添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
实现资源修改后的自动重启等功能。启动应用程序时,DevTools会自动配置热部署,并在保存文件时重新启动应用程序。DevTools还提供了其他功能,如自动重新启动、自动刷新页面等,以提高开发效率。
1.2.2 使用Spring Loaded
Spring LoadedSpring的热部署程序,实现修改类后的自动重载。实现原理是使用自定义ClassLoader,可以实现代码热替换。具体实现如下:
(1)在pom.xml文件中添加Spring Loaded的依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.8.RELEASE</version>
</dependency>
(2)在IDE或编译器中配置项目的自动构建功能。确保在保存文件时自动重新构建项目
(3)启动应用程序时,添加以下JVM参数:
-javaagent:/path/to/springloaded.jar -noverify
其中/path/to/springloaded.jar是Spring Loaded JAR文件的路径,根据你的实际情况进行相应的修改。
(4)启动应用程序并进行开发
每当保存文件时,Spring Loaded会自动检测到更改并重新加载修改后的类,使得你的更改能够立即生效。
需要注意的是,Spring Loaded是一个第三方库,使用它可能会有一些限制和不稳定性。Spring官方已经不再维护Spring Loaded
1.2.3 JRebel插件
JRebel收费的热部署软件,需要添加JRebel插件,可以实现代码热部署。效果非常好,但是需要付费使用。
1.2.4 Spring Boot Maven插件该插件
可以监控代码变动,自动重启应用。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
1.2.5 在IntelliJ IDEA中设置Spring Boot项目的热部署
(1)在IntelliJ IDEA中打开你的Spring Boot项目。
(2)确保已经安装了Spring Boot DevTools插件。可以通过 File -> Settings -> Plugins 进入插件管理页面,搜索并安装Spring Boot DevTools插件。
(3)在IntelliJ IDEA的顶部菜单栏中,选择 Run -> Edit Configurations。
(4)在弹出的Run/Debug Configurations对话框中,选择左侧的 Spring Boot。
(5)在右侧的 Spring Boot 配置窗口中,将 On-frame deactivation 和 On-update action 选项设置为 Update classes and resources。
- On-frame deactivation:当你切换到其他窗口时,配置的更新策略。
- On-update action:当检测到文件更改时,配置的更新策略。
这样设置后,当你切换到其他窗口时,应用程序会在后台重新启动,同时当检测到文件更改时,应用程序会更新相关的类和资源。
(6)点击 Apply 或 OK 按钮保存配置。
(7)点击IntelliJ IDEA的顶部菜单栏中的 Build -> Build Project 来构建你的项目。
(8)在构建完成后,点击工具栏上的绿色箭头图标或使用快捷键 Shift + F10 来运行你的Spring Boot应用程序。
现在,当你修改代码并保存文件时,IntelliJ IDEA会自动将更改的类和资源重新加载到运行的应用程序中,实现热部署。
请注意,热部署只适用于开发环境,并且对于某些修改,可能需要重启应用程序才能生效。因此,在生产环境中不建议使用热部署。
2 常规部署
2.1 jar形式
传统的Web应用进行打包部署,通常会打成war包形式,然后将War包部署到Tomcat等服务器中。
在Spring Boot项目在开发完成后,确实既支持打包成JAR文件也支持打包成WAR文件。然而,官方通常推荐将Spring Boot项目打包成JAR文件,这是因为Spring Boot内置了一个嵌入式的Tomcat服务器,使得应用能够作为一个独立的可执行JAR文件运行,无需部署到外部的Servlet容器中。
虽然Spring Boot也支持打包成WAR文件并部署到外部的Servlet容器中,但这种方式通常不是首选,因为它增加了额外的部署复杂性,并且可能无法充分利用Spring Boot提供的一些自动配置和简化功能。
(1)插件完整配置,在pom.xml文件中添加配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version><!-- 配置中的版本号 -->
<configuration>
<source>1.8</source><!-- 设置源代码的JDK版本 -->
<target>1.8</target><!-- 设置目标代码的JDK版本 -->
<encoding>UTF-8</encoding><!-- 设置编码方式 -->
</configuration>
</plugin>
<!--maven 打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.example.demo.DemoApplication</mainClass><!-- 配置启动类 -->
<skip>false</skip><!--是否忽略启动类-->
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
maven-compiler-plugin是Maven的一个插件,主要用于代码编译,并提供了很多可配置的选项来优化编译过程。主要作用:
- 指定JDK版本:可以明确指定项目源代码所使用的JDK版本,以及编译后的类库拟运行的JVM版本,从而确保项目在不同环境中的一致性和稳定性。
- 设置编码方式:允许设置源代码和目标代码的编码方式,以防止因编码不一致而导致的编译错误或乱码问题。
- 优化编译过程:可以对编译过程进行细粒度的控制。例如,可以设置是否使用增量编译、是否生成调试信息等,以提高编译效率和代码质量。
- spring-boot-maven-plugin是一个用于Spring Boot项目的Maven插件,它在项目的构建和打包过程中发挥着关键作用。主要作用:
- 打包可执行JAR/WAR文件:该插件可以将Spring Boot应用程序打包成一个可执行的JAR或WAR文件。
- 指定执行类:该插件可以指定要执行的类,如果未指定也能够自动检测项目中的main函数,并启动SpringBoot容器。
(2)然后在命令行或IDE中执行打包命令:
mvn clean package
这将清理旧的构建产物,编译项目,执行测试(如果有),并最终打包成一个可执行的JAR文件。生成的JAR通常位于target目录下,文件名格式为your-project-name-<version>.jar。
这将完成同样的清理、编译、测试和打包过程,生成的JAR文件同样位于build/libs目录下,文件名类似your-project-name-<version>.jar。
(3)部署JAR文件
要运行打包好的JAR文件,只需在命令行中使用java -jar命令:
java -jar target/your-project-name-<version>.jar
根据需要,可以指定各种运行参数、环境变量或配置文件位置。例如:
java -Dserver.port=8081 -jar your-project-name.jar --spring.config.location=file:/path/to/application.properties
(4)打包本地jar包
引入本地jar
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>
配置Maven插件:为了确保本地JAR包在打包时能够被正确识别和包含,需要配置spring-boot-maven-plugin插件。在pom.xml中添加以下配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>
这段配置中的<includeSystemScope>元素设置为true,以确保在依赖项解析过程中包括system作用域的依赖项。
或者更改 pom.xml 中的 <build> 新增或修改 <resources> 标签
<resources>
<resource>
<!-- 因为新增或修改,会覆盖原有配置,需要配置上默认的 resources 目录 -->
<directory>src/main/resources</directory>
</resource>
<resource>
<!-- 上面创建的用于放置第三方 jar 包的文件夹名称 -->
<directory>libs</directory>
<!-- 指定在最终的 jar 包中所在的目录 -->
<targetPath>BOOT-INF/lib</targetPath>
<!-- 需要打包的文件 -->
<includes>
<include>**/*.jar</include>
</includes>
</resource>
</resources>
maven clean package 然后在 target 目录中找到打包好的 jar 包文件,解压可查看已经打包进去第三方 jar 包。
2.2 注册为Linux的服务
Linux将Spring Boot项目的Jar包注册为开机自启动系统服务的操作方法
1)目录结构
以下是目录结构,jar文件是从maven package打包出来的,config/application.yml是原先在项目的resources文件夹里,外置出来方便适配开发环境和正式环境。static目录用来存放静态资源,比如vue前端或者上传目录。所有的.sh文件都是本文后续要写的。
/data
/start.sh // 启动脚本
/stop.sh // 关闭脚本
/serviceStart.sh // 服务启动脚本
/serviceStop.sh // 服务关闭脚本
/YumeisoftDemo-0.0.1-SNAPSHOT.jar // 打包的项目Jar包
/config // 配置文件目录
/application.yml // 项目配置文件
/jdk // jdk目录
/static // 静态资源目录
2)编写Service调用的脚本
配置脚本/data/config.sh,如果改包名,直接改这个文件即可
#!/bin/sh
# 配置JAR文件名,把它改成你的Jar文件名
SPRING_JARFILE=YumeisoftDemo-0.0.1-SNAPSHOT.jar
# 日志文件位置
LOG_FILE=system.log
# 获取.sh所在路径
INSTALL_DIR=$(cd $(dirname $0);pwd)
# 配置JDK路径
JAVA_HOME=$INSTALL_DIR/jdk
# 设定PATH,不设会无法使用java命令
PATH=$JAVA_HOME/bin:$PATH
手动启动服务脚本/data/start.sh,其中system.log是日志文件名
#!/bin/sh
# 读取config.sh定义的内容
source $INSTALL_DIR/config.sh
# 后台方式运行jar包
nohup java -jar $INSTALL_DIR/$SPRING_JARFILE > $INSTALL_DIR/$LOG_FILE 2>&1 &
# 显示日志
tail -f $INSTALL_DIR/$LOG_FILE
手动关闭服务脚本/data/stop.sh
#!/bin/sh
# 读取config.sh定义的内容
source $INSTALL_DIR/config.sh
# 获取当前项目运行的进程ID
PID=$(ps -ef | grep "java -jar $INSTALL_DIR/$SPRING_JARFILE" | grep -v grep | awk '{print $2}')
if [ -z "$PID" ]; then
# 如果没找到则提示未运行
echo "Spring Boot应用未在运行中."
else
# 如果找到了,正常终止进程
kill $PID
# 显示日志
tail -f $INSTALL_DIR/$LOG_FILE
echo "Spring Boot应用已停止."
fi
服务启动脚本/data/serviceStart.sh
#!/bin/sh
# 读取config.sh定义的内容
source $INSTALL_DIR/config.sh
# 后台方式运行jar包
nohup java -jar $INSTALL_DIR/$SPRING_JARFILE > $INSTALL_DIR/$LOG_FILE 2>&1 &
服务关闭脚本/data/serviceStop.sh
#!/bin/sh
# 读取config.sh定义的内容
source $INSTALL_DIR/config.sh
# 获取当前项目运行的进程ID
PID=$(ps -ef | grep "java -jar $INSTALL_DIR/$SPRING_JARFILE" | grep -v grep | awk '{print $2}')
if [ -z "$PID" ]; then
# 如果没找到则提示未运行
echo "Spring Boot应用未在运行中."
else
# 如果找到了,正常终止进程
kill $PID
echo "Spring Boot应用已停止."
fi
3)赋权
不赋权是无法运行的,所以我们要执行以下命令:
chmod a+x /data/*.sh
4)创建一个Service
接下来我们把这个项目注册为系统服务,myService改成你要改成的服务名:
vim /etc/systemd/system/myService.service
因为之前没有这个系统服务,会创建一个新文件,这个文件就是系统服务的启停配置文件,按一下a进入编辑模式,把下面的代码粘贴上去,然后按下Esc、冒号、输入wq、回车。
[Unit]
Description=MyService
After=network.target
[Service]
Type=forking
ExecStart=/data/serviceStart.sh
ExecStop=/data/serviceStop.sh
PrivateTmp=true
[Install]
WantedBy=multi-user.target
这里面的ExecStart和ExecStop都是服务启动和服务停止脚本的绝对路径。Description是指服务的描述信息,这里可以填中文,其他的不要改动。
5)启用并使用Service
做完以上步骤你就可以在服务器里执行systemctl enable myService命令,即可启用myService服务,然后使用systemctl start myService即可启动服务,systemctl stop myService即可关停服务,system status myService命令可以看到服务的状态。
2.3 启动或关闭脚本
2.3.1 Windows
2.3.1.1 启动脚本
startup.bat
@echo off
title Spring Boot Demo
java -jar spring-boot-demo.jar --server.config=application.yml
@pause
2.3.1.2 关闭脚本
shutdown.bat
@echo off
set port=8090
for /f "tokens=1-5" %%i in ('netstat -ano^|findstr ":%port%"') do (
echo kill the process %%m who use the port %port%
taskkill /pid %%m -t -f
)
2.3.1.3 重启脚本
restart.bat
@echo off
call ./shutdown.bat
call ./startup.bat
@pause
或
@echo off
set port=8090
for /f "tokens=1-5" %%i in ('netstat -ano^|findstr ":%port%"') do (
echo kill the process %%m who use the port
taskkill /pid %%m -t -f
goto start
)
cd %~dp0
start java -jar spring-boot-demo.jar --server.config=application.yml
exit
:start
start java -jar spring-boot-demo.jar --server.config=application.yml
exit
@pause
2.3.2 Linux
2.3.2.1 启动/重启脚本
startup.sh
startTime=`date +'%Y-%m-%d %H:%M:%S'`
#jar包文件路径
APP_PATH=/home/demo
#jar包文件名称
APP_NAME=$APP_PATH/spring-boot-demo.jar
#日志文件名称
LOG_FILE=$APP_PATH/spring-boot-demo_out.log
rm -rf $LOG_FILE
echo "开始停止服务"
#查询进程,并杀掉当前jar/java程序
pid=`ps -ef|grep $APP_NAME | grep -v grep | awk '{print $2}'`
if [ $pid ];then
echo "pid: $pid"
kill -9 $pid
echo "服务停止成功"
fi
sleep 2
#判断jar包文件是否存在,如果存在启动jar包,并实时查看启动日志
if test -e $APP_NAME;then
echo '文件存在,开始启动服务'
#启动jar包,指向日志文件,2>&1 & 表示打开或指向同一个日志文件
nohup java -jar -Duser.timezone=GMT+08 $APP_NAME --server.config=application.yml > spring-boot-demo_out.log 2>&1 &
echo "服务启动中"
sleep 10s
#通过检测日志来判断
while [ -f $LOG_FILE ]
do
success=`grep "Started SpringBootDemoApplication in " $LOG_FILE`
if [[ "$success" != "" ]]
then
break
else
sleep 1s
fi
#开始检测启动失败标记
fail=`grep "Fail" $LOG_FILE`
if [[ "$fail" != "" ]]
then
echo "服务启动失败"
tail -f $LOG_FILE
break
else
sleep 1s
fi
done
echo "服务启动成功"
endTime=`date +'%Y-%m-%d %H:%M:%S'`
startSecond=$(date --date="$startTime" +%s);
endSecond=$(date --date="$endTime" +%s);
total=$((endSecond-startSecond))
echo "运行时间:"$total"s"
echo "当前时间:"$endTime
else
echo $APP_NAME ' 文件不存在'
fi
2.3.2.2 关闭脚本
shutdown.sh
#jar包文件名称
APP_NAME=/data/demo/spring-boot-demo.jar
echo "开始停止服务"
#查询进程,并杀掉当前jar/java程序
pid=`ps -ef|grep $APP_NAME | grep -v grep | awk '{print $2}'`
echo "pid: $pid "
if [ $pid ];then
echo "pid: $pid"
kill -9 $pid
echo "服务停止成功"
else
echo "未找到对应服务"
fi
2.4 war形式
正常情况下,我们开发 SpringBoot 项目,由于内置了Tomcat,所以项目可以直接启动,部署到服务器的时候,直接打成 jar 包,就可以运行了。
有时我们会需要打包成 war 包,放入外置的 Tomcat 中进行运行,或者使用工具idea直接启动,便于开发调试。
2.4.1 实现步骤
(1)将pom文件打包方式更改为 war
<packaging>war</packaging>
(2)排除内置 Tomcat
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除内置的tomcat -->
<exclusions>
<exclusion>
<artifactId>org.springframework.boot</artifactId>
<groupId>spring-boot-starter-tomcat</groupId>
</exclusion>
</exclusions>
</dependency>
(3)添加tomcat依赖,需要用到 servlet-api 的相关 jar 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<!-- tomcat范围改成provided,否则后面就会出问题,tomcat无法解析jsp -->
<scope>provided</scope>
</dependency>
(4)继承 SpringBootServletInitializer 并重写 configure 方法
新建文件文件名随意,或者直接修改启动类继承 SpringBootServletInitializer 并重写 configure 方法,也是一样的。
package com.test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* 注意,使用war方式部署,需要开启此类
*
*/
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(ApplicationMain.class);
}
}
2.4.2 部署方式:
(1)使用外部tomcat启动
1)利用maven命令打包
2)将打的war包,复制粘贴到tomcat的webapps目录下(不用解压,启动tomcat自动解压)
3)启动tomcat
在tomcat安装目录下的bin目录下面找到startup.bat命令,启动tomcat
4)启动结果
war包自动解压了
5)测试结果,访问swagger页面:
访问路径这里需要注意,原来我们在application.properties配置的访问路径已经不生效了。
这是原来访问路径:http://localhost:8080/testservice/swagger-ui.html
#已经不生效了 server.servlet.context-path=/testservice
现在的访问路径:
http://localhost:[端口号]/[打包项目名]/
(2)方式二:使用工具idea直接启动
1)配置web.xml文件
点击File->Project Structure
创建src/main/webapp和web.xml
此时项目结构图如下:
2)配置artifacts
配置完后,tomcat启动才能找到这个war包,会生成out目录输出文件。
当然你也可以选择target下面已经打包好的war包,但是这样有个缺点,就是每次改文件你都需要用maven重新打包,输出到target目录下,不方便开发。
3)配置tomcat
在IDEA右上角的项目运行列表中选中 Edit Configurations
进入新的窗口点击"+",找到Toncat Server中的Local进行点击,配置Tomcat路径
4)tomcat 选择启动的war包
这里注意选择exploded结尾的,才是out目录输出的
Application context上下文配置访问路径
访问路径这里需要注意,原来我们在application.properties配置的访问路径已经不生效了。
#已经不生效了
server.servlet.context-path=/testservice
现在的访问路径:
http://localhost:8080/testservice/swagger-ui.html
testservice是我Application context上下文配置的访问路径 ,这个可以改的。
5)配置tomcat启动默认打开的页面
6)启动结果
点击启动
3 云部署-基于Docker的部署
3.1 Dockerfile
3.1.1 Dockerfile详解
Dockerfile是一个组合映像命令的文本;可以使用在命令行中调用任何命令;Docker通过dockerfile中的指令自动生成镜像。
通过docker build -t repository:tag ./ 即可构建,要求:./下存在Dockerfile文件
之前我们聊的镜像分层,这个层怎么分的,就是由Dockerfile中的每一条指令构成
3.1.2 编写规则
- 文件名必须是 Dockerfile
- Dockerfile中所用的所有文件一定要和Dockerfile文件在同一级父目录下
- Dockerfile中相对路径默认都是Dockerfile所在的目录
- Dockerfile中一能写到一行的指令,一定要写到一行,因为每条指令都被视为一层,层多了执行效率就慢
- Dockerfile中指令大小写不敏感,但指令都用大写(约定俗成)
- Dockerfile 非注释行第一行必须是 FROM
- Dockerfile 工作空间目录下支持隐藏文件(.dockeringore),类似于git的.gitingore
3.1.3 指令详解
FROM:基础镜像
FROM <image>:<tag> [as other_name] # tag可选;不写默认是latest版
- FROM是Dockerfile文件开篇第一个非注释行代码
- 用于为镜像文件构建过程指定基础镜像,后续的指令都基于该基础镜像环境运行
- 基础镜像可以是任何一个镜像文件
- as other_name是可选的,通常用于多阶段构建(有利于减少镜像大小)
- 使用是通过--from other_name使用,例如COPY --from other_name
LABEL:镜像描述信息
LABEL author="zp wang <test@qq.com>"
LABEL describe="test image"
# 或
LABEL author="zp wang <test@qq.com>" describe="test image"
# 或
LABEL author="zp wang <test@qq.com>" \
describe="test image"
- LABEL指令用来给镜像以键值对的形式添加一些元数据信息
- 可以替代MAINTAINER指令
- 会集成基础镜像中的LABEL,key相同会被覆盖
MAINTAINER:添加作者信息
MAINTAINER zp wang <test@163.com>
COPY:从构建主机复制文件到镜像中
COPY <src> <dest>
COPY ["<src>", "<src>", ... "<dest>"]
- <src>:要复制的源文件或目录,支持通配符 <src>必须在build所在路径或子路径下,不能是其父目录 <src>是目录。其内部的文件和子目录都会递归复制,但<src>目录本身不会被复制 如果指定了多个<src>或使用了通配符,这<dest>必须是一个目录,且必须以/结尾
- <dest>:目标路径,即镜像中文件系统的路径 <dest>如果不存在会自动创建,包含其父目录路径也会被创建
# 拷贝一个文件
COPY testFile /opt/
# 拷贝一个目录
COPY testDir /opt/testDir
testDir下所有文件和目录都会被递归复制
目标路径要写testDir,否则会复制到/opt下
ADD:从构建宿主机复制文件到镜像中
类似于COPY指令,但ADD支持tar文件还让URL路径
ADD <src> <dest>
ADD ["<src>","<src>"... "<dest>"]
- <src>如果是一个压缩文件(tar),被被解压为一个目录,如果是通过URL下载一个文件不会被解压
- <src>如果是多个,或使用了通配符,则<dest>必须是以/结尾的目录,否则<src>会被作为一个普通文件,<src>的内容将被写入到<dest>
WORKDIR:设置工作目录
类似于cd命令,为了改变当前的目录域
此后RUN、CMD、ENTRYPOINT、COPY、ADD等命令都在此目录下作为当前工作目录
WORKDIR /opt
- 如果设置的目录不存在会自动创建,包括他的父目录
- 一个Dockerfile中WORKDIR可以出现多次,其路径也可以为相对路径,相对路径是基于前一个WORKDIR路径
- WORKDIR也可以调用ENV指定的变量
ENV:设置镜像中的环境变量
# 一次设置一个
ENV <key> <value>
# 一次设置多个
ENV <key>=<value> <key1>=<value1> <key2>=<value2> .....
使用环境变量的方式
$varname
${varname}
${varname:-default value} # 设置一个默认值,如果varname未被设置,值为默认值
${varname:+default value} # 设置默认值;不管值存不存在都使用默认值
USER:设置启动容器的用户
# 使用用户名
USER testuser
# 使用用户的UID
USER UID
RUN:镜像构建时执行的命令
# 语法1,shell 形式
RUN command1 && command2
# 语法2,exec 形式
RUN ["executable","param1","[aram2]"]
# 示例
RUN echo 1 && echo 2
RUN echo 1 && echo 2 \
echo 3 && echo 4
RUN ["/bin/bash","-c","echo hello world"]
- RUN 在下一次建构期间,会优先查找本地缓存,若不想使用缓存可以通过--no-cache解除
- docker build --no-cache
- RUN 指令指定的命令是否可以执行取决于 基础镜像
- shell形式
- 默认使用/bin/sh -c 执行后面的command
- 可以使用 && 或 \ 连接多个命令
- exec形式
- exec形式被解析为JSON序列,这意味着必须使用双引号 ""
- 与 shell 形式不同,exec 形式不会调用shell解析。但exec形式可以运行在不包含shell命令的基础镜像中
- 例如:RUN ["echo","$HOME"] ;这样的指令 $HOME并不会被解析,必须RUN ["/bin/sh","-c","echo $HOME"]
EXPOSE:为容器打开指定的监听端口以实现与外部通信
EXPOSE <port>/<protocol>
EXPOSE 80
EXPOSE 80/http
EXPOSE 2379/tcp
- <port>:端口号
- <protocol>:协议类型,默认TCP协议,tcp/udp/http/https
- 并不会直接暴露出去,docker run时还需要-P指定才可以,这里更像是一个说明
VOLUME:实现挂载功能,将宿主机目录挂载到容器中
VOLUME ["/data"] # [“/data”]可以是一个JsonArray ,也可以是多个值
VOLUME /var/log
VOLUME /var/log /opt
- 三种写法都是正确的
- VOLUME类似于docker run -v /host_data /container_data 。
- 一般不需要在Dockerfile中写明,且在Kubernetes场景几乎没用
CMD:为容器设置默认启动命令或参数
# 语法1,shell形式
CMD command param1 param2 ...
# 语法2,exec形式
CMD ["executable","param1","param2"]
# 语法3,还是exec形式,不过仅设置参数
CMD ["param1","param2"]
- CMD运行结束后容器将终止,CMD可以被docker run后面的命令覆盖
- 一个Dockerfile只有顺序向下的最后一个CMD生效
- 语法1,shell形式,默认/bin/sh -c 此时运行为shell的子进程,能使用shell的操作符(if、环境变量、? *通配符等) 注意: 进程在容器中的 PID != 1,这意味着该进程并不能接受到外部传入的停止信号docker stop
- 语法2,exec形式CMD ["executable","param1","param2"] 不会以/bin/sh -c运行(非shell子进程),因此不支持shell的操作符 若运行的命令依赖shell特性,可以手动启动CMD ["/bin/sh","-c","executable","param1"...]
- 语法3,exec形式CMD ["param1","param2"] 一般结合ENTRYPOINT指令使用
ENTRYPOINT:用于为容器指定默认运行程序或命令
与CMD类似,但存在区别,主要用于指定启动的父进程,PID=1
# 语法1,shell形式
ENTRYPOINT command
# 语法2,exec形式
ENTRYPOINT ["/bin/bash","param1","param2"]
- ENTRYPOINT设置默认命令不会被docker run命令行指定的参数覆盖,指定的命令行会被当做参数传递给ENTRYPOINT指定的程序。
- docker run命令的 --entrypoint选项可以覆盖ENTRYPOINT指令指定的程序
- 一个Dockerfile中可以有多个ENTRYPOINT,但只有最后一个生效
- ENTRYPOINT主要用于启动父进程,后面跟的参数被当做子进程来启动
CMD和ENTRYPOINT组合情况说明
具体情况比较多,如表格所示
No ENTRYPOINT | ENTRYPOINT exec_entry p1_entry | ENTRYPOINT ["exec_entry","p1_entry"] | |
No CMD | error,not allowed | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry |
CMD ["exec_cmd","p1_cmd"] | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry exec_cmd p1_cmd |
CMD ["p1_cmd","p2_cmd"] | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry p1_cmd p2_cmd |
CMD exec_cmd p1_cmd | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |
ARG:指定环境变量用于构建过程
ARG name[=default value]
ARG test_name
ARG nother_name=wzp
- ARG指令定义的参数,在构建过程以docker build --build-arg test_name=test 形式赋值
- 若ARG中没有设置默认值,构建时将抛出警告:[Warning] One or more build-args..were not consumed
- Docker默认存在的ARG 参数,可以在--build-arg时直接使用
- HTTP_PROXY/http_proxy/HTTPS_PROXY/https_proxy/FTP_PROXY/ftp_proxy/NO_PROXY/no_proxy
ONBUILD:为镜像添加触发器
ONBUILD可以为镜像添加一个触发器,其参数可以是任意一个Dockerfile指令。
ONBUILD <dockerfile_exec> <param1> <param2>
ONBUILD RUN mkdir mydir
- 该指令,对于使用该Dockerfile构建的镜像并不会生效,只有当其他Dockerfile以当前镜像作为基础镜像时被触发
- 例如:Dockfile A 构建了镜像A,Dockfile B中设置FROM A,此时构建镜像B是会运行ONBUILD设置的指令
STOPSINGAL:设置停止时要发送给PID=1进程的信号
主要的目的是为了让容器内的应用程序在接收到signal之后可以先做一些事情,实现容器的平滑退出,如果不做任何处理,容器将在一段时间之后强制退出,会造成业务的强制中断,这个时间默认是10s。
STOPSIGNAL signal
- 默认的停止信号为:SIGTERM,也可以通过docker run -s指定
HEALTHCHECK:指定容器健康检查命令
当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy
HEALTHCHECK [OPTIONS] CMD command
# 示例
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1 # 如果执行不成功返回1
- 出现多次,只有最后一次生效
- OPTIONS选项
- --interval=30:两次健康检查的间隔,默认为 30 秒;
- --timeout=30:健康检查命令运行的超时时间,超过视为失败,默认30秒;
- --retries=3:指定失败多少次视为unhealth,默认3次
- 返回值
- 0:成功; 1:失败; 2:保留
SHELL:指定shell形式的默认值
SHELL 指令可以指定 RUN、ENTRYPOINT、CMD 指令的 shell,Linux 中默认为["/bin/sh", "-c"] ,Windows默认["CMD","/S","/C"]
通常用于构建Windows用的镜像
SHELL ["/bin/bash","-c"]
SHELL ["powershell", "-command"]
# 示例,比如在Windows时,默认shell是["CMD","/S","/C"]
RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
# docker调用的是cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
# 这样虽然没有调用cmd.exe 但写起来比较麻烦,所以可以通过SHELL ["powershell", "-command"]
3.1.4 方式一:直接构建jar包运行的镜像
- 将打包好程序,上传到服务器的指定目录 例如:/home/www/spring-boot-image/spring-boot-docker-1.0.jar
- 在该目录下创建Dockerfile文件
FROM openjdk:8u312
MAINTAINER zhangt
ADD spring-boot-docker-1.0.jar zhangt.jar
EXPOSE 8520
ENTRYPOINT ["java","-jar","zhangt.jar"]
Dockerfile的内容解释:
FROM openjdk:8u312 这一行指定了基础镜像,从openjdk:8u312镜像构建。它使用了OpenJDK 8的版本号为312的镜像作为基础。这是一个包含Java运行时环境的基础镜像。
MAINTAINER zhangt 这一行设置了维护者信息,尽管在较新版本的Docker中,MAINTAINER已不再建议使用,而可以使用LABEL来添加类似的元数据信息。
ADD spring-boot-docker-1.0.jar zhangt.jar 这一行使用ADD指令将本地的spring-boot-docker-1.0.jar文件复制到镜像中,并重命名为zhangt.jar。这个JAR文件包含了Spring Boot应用程序的可执行代码。
EXPOSE 8520 这一行使用EXPOSE指令声明容器将监听的端口号,这里指定为8520。请注意,这只是一个元数据声明,它不会自动将端口映射到主机上。
ENTRYPOINT ["java","-jar","zhangt.jar"] 这一行设置了容器启动时要执行的命令。在这种情况下,容器将以java -jar zhangt.jar命令启动,这会运行Spring Boot应用程序。java命令会启动Java虚拟机(JVM),并执行zhangt.jar中的可执行代码。
这Dockerfile的作用是基于OpenJDK 8u312镜像构建一个包含Spring Boot应用程序的Docker镜像。一旦构建完成,可以使用这个镜像来运行Spring Boot应用程序的容器,容器将监听8520端口,可以通过适当的端口映射来让外部访问应用程序。
- 创建好Dockerfile文件之后,执行命构建镜像
docker build -t zhangt .
注意最后的 . 表示Dockerfile在当前文件目录下。zhangt表示构建的镜像,构建成功后可以使用docker images命令查看镜像。 -t 选项用于指定镜像的名称和标签,你可以将zhangt替换为你想要的名称和标签。
- 镜像构建成功之后,就可以运行容器
docker run -d --restart=always --name zhangt -p 8520:8520 zhangt
各个参数的含义:
docker run: 用于启动 Docker 容器的命令。
-d: 这是一个选项,表示在后台(守护进程模式)运行容器。容器将在后台运行,不会占据终端。
--restart=always: 这是另一个选项,表示容器在退出时总是重新启动。即使容器因为错误或其他原因而停止,Docker 也会尝试自动重新启动容器。
--name zhangt: 这是用于给容器指定一个名称的选项。容器的名称被设置为 "zhangt"。
-p 8520:8520: 这是用于将主机端口与容器端口进行映射的选项。这个选项将主机的 8520 端口映射到容器的 8520 端口。这样,外部可以通过访问主机的 8520 端口来访问容器内运行的应用程序。
zhangt: 这是容器的名称或镜像名称,表示要运行的容器是基于名为 "zhangt" 的 Docker 镜像创建的。如果 "zhangt" 是一个镜像名称,Docker 将查找该镜像并在容器中运行它。
这个命令的目的是在后台运行一个 Docker 容器,该容器使用 "zhangt" 镜像创建,并将主机的 8520 端口映射到容器的 8520 端口。容器的名称设置为 "zhangt-p",并且如果容器在任何情况下退出,Docker 会自动重新启动它。这通常用于部署应用程序,以确保应用程序在意外情况下能够自动恢复。
启动容器后可以使用 **docker ps**命令查看启动的容器
docker logs -f --tail 1000 容器id ,可以查看服务的日志。
如果想更新jar包,只需要使用 docker cp spring-boot-docker-1.0.jar 容器ID:/zhangt.jar,就可以将spring-boot-docker-1.0.jar拷贝进容器并重命名,然后 docker restart 容器ID 重启容器。
3.15 方式二:基于jdk镜像运行容器
- 在服务器中来取jdk镜像
docker pull openjdk:8u181
- 创建目录,并将jar包上传到该目录
cd /home/ mkdir www/spring-boot-docker
- 在Jar放置的同级目录,编写shell脚本命名为:start.sh
#!/bin/bash
echo -e "\n################ build service start #########################"
# delete docker container
echo -e "\n1, delete docker container [developer-platform-basic-dev] start ......"
sudo docker rm -f spring-boot-docker-1.0
# docker run # docker run developer-platform-basic-1.0.0
echo -e "\n2, docker run build container [spring-boot-docker-1.0] start ......"
sudo docker run --name spring-boot-docker-1.0 -d -p 8741:8741 \
-v /home/www/spring-boot-docker:/jar openjdk:8u181 \
java -jar /jar/spring-boot-docker-1.0.jar --spring.profiles.active=dev
echo -e "\n3, docker ps container [spring-boot-docker-1.0] start ...."
sudo docker ps -a | grep spring-boot-docker-1.0
echo -e "\n4, docker logs container [spring-boot-docker-1.0] start ...."
sudo docker logs -f -t spring-boot-docker-1.0 > ./logs/log_$(date +%Y%m%d).out 2>&1 &
echo -e "\n################ build service end #########################"
核心脚本解释:
1.sudo docker run 这是用于在Docker中运行容器的命令。通常需要使用sudo权限来执行Docker命令,以确保具有足够的权限来管理容器。
--name spring-boot-docker-1.0: 这是为Docker容器指定的名称,容器的名称被设置为"spring-boot-docker-1.0"。
-d: 这是一个选项,表示在后台运行容器(即以守护进程模式运行),而不是在前台交互模式下运行。
-p 8741:8741: 这个选项用于将主机的端口与容器的端口进行映射。具体来说,将主机的8741端口映射到容器的8741端口,这样外部可以通过主机的8741端口访问容器中的应用程序。
-v /home/www/spring-boot-docker:/jar: 这个选项用于将主机的文件系统目录与容器内的目录进行挂载。在这种情况下,将主机上的/home/www/spring-boot-docker目录挂载到容器内的/jar目录。这通常用于将应用程序的代码和资源文件从主机复制到容器中,以便在容器内运行应用程序。
openjdk:8u181: 这是要在容器中使用的Docker镜像的名称和标签。在这里,使用的是一个基于OpenJDK 8u181的Java镜像,该镜像包含了Java运行时环境。
java -jar /jar/spring-boot-docker-1.0.jar --spring.profiles.active=dev: 这是在容器内运行的命令。它启动了Java虚拟机(JVM),并在JVM内运行了一个Spring Boot应用程序。具体来说,它运行了/jar/spring-boot-docker-1.0.jar这个JAR文件,并通过--spring.profiles.active=dev指定了一个Spring配置文件的激活配置。
这个脚本的作用是创建一个名为"spring-boot-docker-1.0"的Docker容器,该容器运行一个基于Spring Boot的Java应用程序,该应用程序监听8741端口,并将主机上的/home/www/spring-boot-docker目录挂载到容器内的/jar目录,以供应用程序使用。这样,可以通过主机的8741端口访问运行在容器中的Spring Boot应用程序。
- 运行脚本 sh start.sh
- 以后发布,只需要把宿主机目录里的jar包替换掉,重启容器。
4 SpringBoot的测试
4.1 概述
4.1.1 测试分类
- 单元测试:测试单个类的功能。
- 集成测试:测试多个类的协同工作。
- 端到端测试:测试整个程序的功能和流程。
4.1.2 测试分层
Controller层可以进行单元测试、集成测试(@WebMvcTest)和端到端测试。
- 在单元测试中,我们可以mock服务层的行为。
- 在集成测试中,我们可以使用@MockMvc来模拟HTTP请求。
- 在端到端测试中,我们则可以测试整个系统的工作流程。
Service层可以进行单元测试和集成测试。
- 在单元测试中,我们可以mock Repository层的行为。
- 在集成测试中,我们则需要确保Service层和其他层之间的交互是正确的。
Repository层可以进行集成测试(@DataJpaTest)。
- 在集成测试中,我们通常会使用内存数据库以模拟真实数据库的行为。
工具类可以进行单元测试。
- 工具类通常是独立的,我们可以直接进行单元测试,验证其功能是否正确。
4.1.3 测试策略
选择测试类型
选择适当的测试类型,根据需要选择单元测试、集成测试、端到端测试。
确定测试目标
明确你需要测试的具体功能或方法,理解其预期的行为。
准备测试环境和数据
根据测试需求设置适当的测试环境。
- 对于单元测试,你可能需要使用Mockito等库来创建mock对象,以模拟对其他类或接口的依赖。
- 对于集成测试,你可能需要配置一个嵌入式的数据库如H2。
- 对于端到端测试,你可能需要使用Selenium等库来模拟浏览器行为。
编写测试用例
根据测试目标编写对应的测试用例,包括正常场景、边界条件和异常场景。
执行测试和结果验证
- 执行测试:使用测试框架(如JUnit)运行你的测试。
- 结果断言:利用断言(assert)功能,验证你的代码产生的结果是否符合预期。
- 依赖验证:如果你的代码依赖于其他方法或对象,你需要使用verify方法来校验这些依赖项是否被正确地调用。
测试报告分析
评估测试结果,如果测试失败,进行bug修复,重新测试,直到所有测试用例都通过。
重构和优化
根据测试结果,进行代码的重构和优化,提高代码质量。
测试代码的维护
确保测试代码的可读性和可维护性,同时保证在项目构建过程中能够自动执行测试。
4.2 单元测试
4.2.1 定义
单元测试用于验证单个类的功能是否正确。
其特点是独立于其他类、数据库、网络或其他外部资源。
4.2.2 适用对象
在实际工作中,通常对Spring Boot中的Controller、Service等类进行单元测试。
4.2.3 JUnit
JUnit是Java中最流行的单元测试框架,用于编写和执行单元测试。
基本注解
- @Test: 标记一个测试方法,用于执行单元测试。
- @Before: 在每个测试方法执行之前运行,用于准备测试数据或初始化资源。
- @After: 在每个测试方法执行之后运行,用于清理测试数据或释放资源。
- @BeforeClass: 在所有测试方法执行前运行,通常用于执行一次性的初始化操作。
- @AfterClass: 在所有测试方法执行后运行,通常用于执行一次性的清理操作。
- @Ignore: 标记一个测试方法,用于暂时忽略这个测试。
- @RunWith(SpringRunner.class)用于运行Spring Boot的测试。
基本测试
@Test
public void testCount() {
// 测试代码
}
异常测试
有时候需要测试代码是否能正确地抛出异常,可以使用@Test的expected属性:
@Test(expected = SomeException.class)
public void testException() {
// 测试代码,期望抛出SomeException异常
}
超时测试
有时候需要测试某个操作是否能在规定时间内完成,可以使用@Test的timeout属性:
@Test(timeout = 1000)
public void testTimeout() {
// 测试代码,期望在1000毫秒内执行完毕
}
忽略测试
有时候暂时不想执行某个测试方法,可以使用@Ignore注解:
@Ignore("暂时忽略这个测试")
@Test
public void testIgnore() {
// 测试代码
}
4.2.4 Mockito
Mockito库用于模拟对象,隔离被测类与其他类的依赖关系。
基本概念
- Mock对象:使用Mockito创建的模拟对象,用于替代真实对象,以模拟对应的行为和返回值。
- Stubbing:为Mock对象设置方法调用的返回值,以模拟真实对象的行为。
- 验证:Mock对象的方法是否被调用,以及调用次数是否符合预期。
常用注解
- @Mock: 标记一个模拟对象。
- @InjectMocks: 标记一个被测类,用于注入模拟对象。
- @RunWith(MockitoJUnitRunner.class): 指定运行器,用于运行Mockito的测试。
常用方法
Mockito 提供了一系列方法,用于在单元测试中创建和设置模拟对象的行为:
- when(mock.method()).thenReturn(value): 设置当 mock 对象的指定方法被调用时,返回预设的值。
- any(): 表示任何值,用于 when 或 verify 方法的参数匹配。
- doReturn(value).when(mock).method(): 与 when-thenReturn 类似,但适用于无法通过 when-thenReturn 语句模拟的情况,如 void 方法。
- doThrow(Exception).when(mock).method(): 用于模拟当 mock 对象的指定方法被调用时,抛出异常。
- spy(object): 用于创建一个 spy 对象,它是对真实对象的包装,所有未被 stub 的方法都会调用真实的方法。
Mockito 还提供了一系列的方法,用于验证模拟对象的行为:
- verify(mock).method(): 验证 mock 对象的指定方法是否被调用。
- never(): 验证 mock 对象的指定方法从未被调用。
- times(n): 验证 mock 对象的指定方法被调用了 n 次。
- atLeast(n): 验证 mock 对象的指定方法至少被调用了 n 次。
- atMost(n): 验证 mock 对象的指定方法最多被调用了 n 次。
示例代码
@RunWith(MockitoJUnitRunner.class)
public class TeacherApiTest {
@Mock
private TeacherService teacherService;
@InjectMocks
private TeacherApi teacherApi;
@Test
public void testCount() {
when(teacherService.countByName("张三")).thenReturn(1);
TeacherCount result = teacherApi.count("张三");
verify(teacherService).countByName("张三"); // 验证方法是否被调用
assertEquals(1, result.getCount()); // 验证返回值是否符合预期
}
}
4.2.5 AssertJ
AssertJ库提供了丰富的断言方法,可以更简洁地编写测试代码。
常用断言
- assertThat(actual).isEqualTo(expected): 验证实际值是否等于预期值。
- assertThat(actual).isNotEqualTo(expected): 验证实际值是否不等于预期值。
- assertThat(actual).isNotNull(): 验证实际值是否不为null。
- assertThat(actual).isNull(): 验证实际值是否为null。
- assertThat(actual).isTrue(): 验证实际值是否为true。
- assertThat(actual).isFalse(): 验证实际值是否为false。
- assertThat(actual).isInstanceOf(ExpectedClass.class): 验证实际值是否是指定类的实例。
- assertThat(actual).isNotInstanceOf(UnexpectedClass.class): 验证实际值是否不是指定类的实例。
- assertThat(actual).isGreaterThan(expected): 验证实际值是否大于预期值。
- assertThat(actual).isGreaterThanOrEqualTo(expected): 验证实际值是否大于等于预期值。
- assertThat(actual).isLessThan(expected): 验证实际值是否小于预期值。
- assertThat(actual).isLessThanOrEqualTo(expected): 验证实际值是否小于等于预期值。
- assertThat(actual).contains(expected): 验证实际值是否包含指定字符串。
- assertThat(actualList).contains(expected): 验证集合是否包含指定元素。
- assertThat(actualMap).containsKey(expected): 验证Map是否包含指定键。
- assertThat(actualMap).containsValue(expected): 验证Map是否包含指定值。
- assertThat(actualMap).containsEntry(key, value): 验证Map是否包含指定元素。
- assertThat(actualException).isInstanceOf(ExpectedException.class).hasMessage(expectedMessage): 验证异常是否符合预期。
4.2.6 JSONAssert
JSONAssert库用于测试JSON字符串是否符合预期。
常用方法
- assertEquals(expected, actual, strict): 验证实际的JSON值是否等于预期的JSON值。这里的strict参数指定了比较的模式。如果设置为false,将会使用非严格模式比较,这意味着预期JSON字符串中没有的字段会被忽略;如果设置为true,将会使用严格模式比较,预期JSON字符串与实际JSON字符串必须完全匹配。
- assertNotEquals(expected, actual, strict): 验证实际的JSON值是否不等于预期的JSON值。
- assertJSONEquals(expected, actual, jsonComparator): 使用自定义的JSONComparator来验证实际的JSON值是否等于预期的JSON值。
示例代码
@Test
public void testJsonAssert() throws JSONException {
String expected = "{\"id\":1,\"name\":\"张三\"}";
String actual = "{\"id\":1,\"name\":\"张三\"}";
// 使用非严格模式进行比较
JSONAssert.assertEquals(expected, actual, false);
// 使用严格模式进行比较,预期字符串与实际字符串必须严格匹配
JSONAssert.assertEquals(expected, actual, true);
// 验证实际的JSON值是否不等于预期的JSON值
String unexpected = "{\"id\":2,\"name\":\"李四\"}";
JSONAssert.assertNotEquals(unexpected, actual, false);
// 使用自定义的JSONComparator进行比较
JSONAssert.assertJSONEquals(expected, actual, JSONCompareMode.LENIENT);
}
@JsonTest
SpringBoot的@JsonTest注解用于测试JSON序列化和反序列化。
示例代码
@JsonTest
public class TeacherJsonTest {
@Autowired
private JacksonTester<Teacher> json;
String content = "{\"id\":1,\"name\":\"张三\"}";
@Test
public void testSerialize() throws Exception {
Teacher teacher = new Teacher();
teacher.id = 1L;
teacher.name = "张三";
assertThat(json.write(teacher)).isEqualToJson(content);
}
@Test
public void testDeserialize() throws Exception {
Teacher teacher = new Teacher();
teacher.id = 1L;
teacher.name = "张三";
Teacher teacher1 = json.parseObject(content);
assertThat(teacher1.id).isEqualTo(teacher.id);
assertThat(teacher1.name).isEqualTo(teacher.name);
}
}
4.3 集成测试
4.3.1 定义
集成测试验证Spring Boot应用内各组件(Controller、Service、Repository等)之间的交互和协作。
其特点是需要启动Spring上下文环境(不一定需要启动Web服务器),以便在测试代码中直接使用 @Autowired 等注解。
4.3.2 适用对象
在实际工作中,通常对Spring Boot项目的各个组件进行集成测试,包括但不限于:
- 验证Controller层能否正确调用Service层方法,并处理返回的数据。
- 验证Service层能否正确地与Repository层交互,实现数据库的读写操作。
- 验证Repository层能否正确地与数据库交互,实现数据的读写操作。
4.3.3 @SpringBootTest
@SpringBootTest 注解在测试开始前创建一个完整的Spring应用程序上下文。
示例代码
@SpringBootTest
@ActiveProfiles("test") // 指定运行环境
public class TeacherServiceIntegrationTest {
@Autowired
private TeacherService teacherService; // 注入TeacherService
@Test
public void testCount() {
int result = teacherService.countByName("张三"); // 调用Service的方法
assertEquals(1, result); // 断言结果
}
}
4.3.4 @WebMvcTest
@WebMvcTest 注解在测试开始前启动一个针对Spring MVC的测试环境。
仅加载指定的Controller,而不会加载应用程序的其他组件。如果测试中需要这些其他组件,使用 @MockBean 来模拟这些组件的行为。
使用 MockMvc 对象模拟发送HTTP请求,并验证控制器的响应。
示例代码
@WebMvcTest(TeacherApi.class) // 指定需要测试的Controller
@ActiveProfiles("test") // 指定运行环境
public class TeacherApiWebMvcTest {
@Autowired
private MockMvc mockMvc; // 注入MockMvc
@MockBean
private TeacherService teacherService; // Mock掉Service层的接口
@Test
public void testCount() throws Exception {
when(teacherService.countByName("张三")).thenReturn(1); // Mock掉Service的方法,指定返回值
mockMvc.perform(MockMvcRequestBuilders.get("/api/teacher/count").param("name", "张三")) // 执行一个GET请求
.andExpect(MockMvcResultMatchers.status().isOk()) // 验证响应状态码为200
.andExpect(MockMvcResultMatchers.jsonPath("$.count").value(1)); // 断言响应结果中JSON属性"count"的值为1
}
}
MockMvc的常用方法
- perform(request): 执行一个HTTP请求。
- andExpect(status().isOk()): 断言响应状态码为200(HttpStatus.OK)。
- andExpect(content().contentType(MediaType.APPLICATION_JSON)): 断言响应内容类型为JSON。
- andExpect(content().string(containsString("expectedString")): 断言响应内容包含指定的字符串。
- andExpect(jsonPath("$.key").value("expectedValue")): 断言响应内容中JSON属性"key"的值为"expectedValue"。
- andExpect(content().json("expectedJson")): 断言响应内容为"expectedJson"。
- andReturn(): 获取MvcResult实例,可以在断言完成后获取详细的响应内容。
- print(): 在控制台上打印执行请求的结果。
jsonPath的常用表达式
- $.key: 获取根节点属性"key"的值。
- $..key: 获取所有名为"key"的属性的值,无论它们在哪一层。
- $.array[0]: 获取名为"array"的数组属性中的第一个元素的值。
- $.array[:2]: 获取名为"array"的数组属性中的前两个元素的值。
- $.array[-1:]: 获取名为"array"的数组属性中的最后一个元素的值。
4.3.5 @DataJpaTest
@DataJpaTest注解在测试开始前启动一个Spring上下文环境,只加载JPA相关的组件,如实体、仓库等。
配置一个内存数据库,以避免对真实数据库的影响,并提高测试的速度。
默认开启事务,并在测试完成后回滚事务。
示例代码
@DataJpaTest
@ActiveProfiles("test") // 指定运行环境
public class TeacherDaoTest {
@Autowired
private TeacherDao teacherDao; // 注入Repository
@Test
public void testCount() {
int result = teacherDao.countByName("张三"); // 调用Repository的方法
assertEquals(1, result); // 断言结果
}
}
TestEntityManager
TestEntityManager是Spring提供的一个用于测试的EntityManager,用于在测试中对实体进行增删改查操作。
@DataJpaTest
@ActiveProfiles("test") // 指定运行环境
public class TeacherDaoTest {
@Autowired
private TeacherDao teacherDao; // 注入Repository
@Autowired
private TestEntityManager entityManager; // 注入TestEntityManager
@Test
public void testCount() {
// 使用TestEntityManager创建一个Teacher实体
Teacher teacher = new Teacher();
teacher.name = "张三";
teacher.age = 20;
entityManager.persist(teacher);
// 调用Repository的方法
int result = teacherDao.countByName("张三");
// 断言结果
assertEquals(1, result);
}
}
4.3.6 测试数据库
集成测试通常需要使用数据库。
如果使用真实的数据库,每次测试都会对数据库进行读写操作,这样会导致测试变得缓慢,而且会对数据库造成影响。
为了解决这个问题,可以使用内存数据库。内存数据库是一种特殊的数据库,它将数据存储在内存中,而不是磁盘上,因此它的读写速度非常快,适合用于测试。
配置
在Spring Boot项目中使用H2数据库,需要在pom.xml中添加以下依赖:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
使用
在application-test.properties中添加以下配置:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.datasource.schema=classpath:test-schema.sql
spring.datasource.data=classpath:test-data.sql
在application-test.properties中,指定了H2数据库的连接信息。
还指定了初始化脚本test-schema.sql和test-data.sql,用于初始化数据库的表结构和数据。
test-schema.sql
CREATE TABLE teacher (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
age INT NOT NULL
);
test-data.sql
INSERT INTO teacher (name, age) VALUES ('张三', 20);
INSERT INTO teacher (name, age) VALUES ('李四', 30);
4.4 端到端测试
4.4.1 定义
端到端测试验证整个程序的功能是否按照预期工作。
其特点是需要启动完整的应用程序上下文,并模拟客户端与HTTP接口进行交互。
4.4.2 适用对象
在实际工作中,我们通常对使用SpringBoot开发的整个应用程序进行端到端测试。
4.4.3 RANDOM_PORT
端到端测试需要启动完整的应用程序上下文。
使用@SpringBootTest注解的webEnvironment属性,将应用程序上下文设置为随机端口启动。
4.4.4 TestRestTemplate
TestRestTemplate是Spring提供的类,用于发送HTTP请求并验证接口的响应是否符合预期。
示例代码
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 将应用上下文设置为随机端口启动
@ActiveProfiles("test")
public class TeacherApiEndToEndTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testSearchTeacher() {
// 发送GET请求,访问"/teachers?name=John"
ResponseEntity<TeacherCount> response = restTemplate.getForEntity("/teachers?name=John", TeacherCount.class);
// 验证响应状态码是否为200(HttpStatus.OK)
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
// 验证响应中JSON属性"count"的值是否为2
Assertions.assertEquals(2, response.getBody().getCount());
}
}
4.4.5 soup
Jsoup库用于HTML解析。
基本使用
以下是一些Jsoup的基本使用方法:
- 解析HTML字符串:
String html = "<html><head><title>测试页面</title></head>"
+ "<body><p>这是一个测试页面</p></body></html>";
Document doc = Jsoup.parse(html);
- 获取HTML元素:
Element titleElement = doc.select("title").first();
- 获取元素的文本内容:
String titleText = titleElement.text();
- 获取元素的属性值:
Element linkElement = doc.select("a").first();
String href = linkElement.attr("href");
测试网页
以下是一个使用Jsoup测试网页的例子:
@Test
public void testHtmlPage() {
// 测试HTML页面的标题
Document doc = Jsoup.connect("http://www.example.com").get();
String title = doc.title();
assertThat(title).isEqualTo("Example Domain");
}
4.4.6 Selenium
Selenium用于模拟浏览器行为。
通过Selenium,程序可以自动化地操作浏览器,模拟用户在浏览器中的交互行为。
核心类
- WebDriver:用于模拟浏览器的行为,如打开网页、输入内容、点击按钮等。
- WebElement:用于表示网页中的元素,如文本框、按钮等。
- By:用于定位网页中的元素,如根据ID、CSS选择器等。
WebDriver的常用方法
- get(url): 打开指定的网页。
- findElement(by): 根据定位器定位网页中的元素。
- sendKeys(text): 在文本框中输入文本。
- click(): 点击按钮。
- getText(): 获取元素的文本内容。
测试网页
以下是一个使用Selenium测试网页的例子:
public class TeacherSearchFrontendTest {
@Test
public void testSearchTeacher() {
// 设置ChromeDriver的路径(注意根据实际情况修改路径)
System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver");
// 创建Chrome浏览器的WebDriver
WebDriver driver = new ChromeDriver();
// 打开目标网页
driver.get("http://localhost:8080");
// 找到搜索框并输入教师姓名
WebElement searchInput = driver.findElement(By.id("search-input"));
searchInput.sendKeys("John");
// 找到搜索按钮并点击
WebElement searchButton = driver.findElement(By.id("search-button"));
searchButton.click();
// 验证搜索结果数量是否符合预期
WebElement resultCount = driver.findElement(By.id("result-count"));
Assertions.assertEquals("Found 2 teachers", resultCount.getText());
// 关闭浏览器
driver.quit();
}
}
4.5 附:测试思想
4.5.1 测试金字塔
- 单元测试:测试金字塔的基础,最为常见也最为频繁。针对代码中的最小单元进行测试,如方法和类。单元测试的重点在于找出方法的逻辑错误,确保所有的方法达到预期的结果。单元测试的粒度应保证足够小,有助于精确定位问题。
- 集成测试:测试金字塔的中层,数量通常少于单元测试,多于端对端测试。集成测试的重点是检查几个单元组合在一起后是否能正常工作。
- 端对端测试:测试金字塔的顶层,数量最少。端对端测试的目的是从用户的角度模拟完整的应用场景,验证多个单元/模块是否可以协同工作。
4.5.2 测试原则
- 单一职责:每个测试都应当专注于一个具体的功能或者逻辑,避免在一个测试中试图验证多个逻辑。
- 独立性:每个单元测试都应该独立于其他测试,不能互相调用,也不能依赖执行的顺序。
- 一致性:测试应当能够在任何时间、任何环境下得出相同的结果。
- 全面覆盖:尽可能覆盖所有可能的路径和情况。包括所有正常情况、边界情况以及可能的错误情况。
- 自动化:测试应当是自动化的,可以无人值守地运行。
- 易于理解:测试代码也是代码,应当遵循良好的代码编写实践。
- 快速反馈:优秀的测试应该可以在短时间内完成并给出反馈。
- BCDE原则:Border,边界值测试,Correct,正确的输入,并得到预期的结果,Design,与设计文档相结合,来编写单元测试,Error,强制错误信息输入,以及得到预期的结果。
- 测试数据:测试数据应由程序插入或导入,以准备数据。