玖叶教程网

前端编程开发入门

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 deactivationOn-update action 选项设置为 Update classes and resources

  • On-frame deactivation:当你切换到其他窗口时,配置的更新策略。
  • On-update action:当检测到文件更改时,配置的更新策略。

这样设置后,当你切换到其他窗口时,应用程序会在后台重新启动,同时当检测到文件更改时,应用程序会更新相关的类和资源。

(6)点击 ApplyOK 按钮保存配置。

(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 <[email protected]>"
LABEL describe="test image"

# 或
LABEL author="zp wang <[email protected]>" describe="test image"

# 或
LABEL author="zp wang <[email protected]>" \
      describe="test image"
  • LABEL指令用来给镜像以键值对的形式添加一些元数据信息
  • 可以替代MAINTAINER指令
  • 会集成基础镜像中的LABEL,key相同会被覆盖

MAINTAINER:添加作者信息

MAINTAINER zp wang <[email protected]>

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应用程序。

  1. 运行脚本 sh start.sh
  2. 以后发布,只需要把宿主机目录里的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中最流行的单元测试框架,用于编写和执行单元测试。

基本注解

  1. @Test: 标记一个测试方法,用于执行单元测试。
  2. @Before: 在每个测试方法执行之前运行,用于准备测试数据或初始化资源。
  3. @After: 在每个测试方法执行之后运行,用于清理测试数据或释放资源。
  4. @BeforeClass: 在所有测试方法执行前运行,通常用于执行一次性的初始化操作。
  5. @AfterClass: 在所有测试方法执行后运行,通常用于执行一次性的清理操作。
  6. @Ignore: 标记一个测试方法,用于暂时忽略这个测试。
  7. @RunWith(SpringRunner.class)用于运行Spring Boot的测试。

基本测试

@Test
public void testCount() {
    // 测试代码
}

异常测试

有时候需要测试代码是否能正确地抛出异常,可以使用@Testexpected属性:

@Test(expected = SomeException.class)
public void testException() {
    // 测试代码,期望抛出SomeException异常
}

超时测试

有时候需要测试某个操作是否能在规定时间内完成,可以使用@Testtimeout属性:

@Test(timeout = 1000)
public void testTimeout() {
    // 测试代码,期望在1000毫秒内执行完毕
}

忽略测试

有时候暂时不想执行某个测试方法,可以使用@Ignore注解:

@Ignore("暂时忽略这个测试")
@Test
public void testIgnore() {
    // 测试代码
}

4.2.4 Mockito

Mockito库用于模拟对象,隔离被测类与其他类的依赖关系。

基本概念

  1. Mock对象:使用Mockito创建的模拟对象,用于替代真实对象,以模拟对应的行为和返回值。
  2. Stubbing:为Mock对象设置方法调用的返回值,以模拟真实对象的行为。
  3. 验证:Mock对象的方法是否被调用,以及调用次数是否符合预期。

常用注解

  1. @Mock: 标记一个模拟对象。
  2. @InjectMocks: 标记一个被测类,用于注入模拟对象。
  3. @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库提供了丰富的断言方法,可以更简洁地编写测试代码。

常用断言

  1. assertThat(actual).isEqualTo(expected): 验证实际值是否等于预期值。
  2. assertThat(actual).isNotEqualTo(expected): 验证实际值是否不等于预期值。
  3. assertThat(actual).isNotNull(): 验证实际值是否不为null。
  4. assertThat(actual).isNull(): 验证实际值是否为null。
  5. assertThat(actual).isTrue(): 验证实际值是否为true。
  6. assertThat(actual).isFalse(): 验证实际值是否为false。
  7. assertThat(actual).isInstanceOf(ExpectedClass.class): 验证实际值是否是指定类的实例。
  8. assertThat(actual).isNotInstanceOf(UnexpectedClass.class): 验证实际值是否不是指定类的实例。
  9. assertThat(actual).isGreaterThan(expected): 验证实际值是否大于预期值。
  10. assertThat(actual).isGreaterThanOrEqualTo(expected): 验证实际值是否大于等于预期值。
  11. assertThat(actual).isLessThan(expected): 验证实际值是否小于预期值。
  12. assertThat(actual).isLessThanOrEqualTo(expected): 验证实际值是否小于等于预期值。
  13. assertThat(actual).contains(expected): 验证实际值是否包含指定字符串。
  14. assertThat(actualList).contains(expected): 验证集合是否包含指定元素。
  15. assertThat(actualMap).containsKey(expected): 验证Map是否包含指定键。
  16. assertThat(actualMap).containsValue(expected): 验证Map是否包含指定值。
  17. assertThat(actualMap).containsEntry(key, value): 验证Map是否包含指定元素。
  18. assertThat(actualException).isInstanceOf(ExpectedException.class).hasMessage(expectedMessage): 验证异常是否符合预期。

4.2.6 JSONAssert

JSONAssert库用于测试JSON字符串是否符合预期。

常用方法

  1. assertEquals(expected, actual, strict): 验证实际的JSON值是否等于预期的JSON值。这里的strict参数指定了比较的模式。如果设置为false,将会使用非严格模式比较,这意味着预期JSON字符串中没有的字段会被忽略;如果设置为true,将会使用严格模式比较,预期JSON字符串与实际JSON字符串必须完全匹配。
  2. assertNotEquals(expected, actual, strict): 验证实际的JSON值是否不等于预期的JSON值。
  3. 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项目的各个组件进行集成测试,包括但不限于:

  1. 验证Controller层能否正确调用Service层方法,并处理返回的数据。
  2. 验证Service层能否正确地与Repository层交互,实现数据库的读写操作。
  3. 验证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的常用方法

  1. perform(request): 执行一个HTTP请求。
  2. andExpect(status().isOk()): 断言响应状态码为200(HttpStatus.OK)。
  3. andExpect(content().contentType(MediaType.APPLICATION_JSON)): 断言响应内容类型为JSON。
  4. andExpect(content().string(containsString("expectedString")): 断言响应内容包含指定的字符串。
  5. andExpect(jsonPath("$.key").value("expectedValue")): 断言响应内容中JSON属性"key"的值为"expectedValue"。
  6. andExpect(content().json("expectedJson")): 断言响应内容为"expectedJson"。
  7. andReturn(): 获取MvcResult实例,可以在断言完成后获取详细的响应内容。
  8. print(): 在控制台上打印执行请求的结果。

jsonPath的常用表达式

  1. $.key: 获取根节点属性"key"的值。
  2. $..key: 获取所有名为"key"的属性的值,无论它们在哪一层。
  3. $.array[0]: 获取名为"array"的数组属性中的第一个元素的值。
  4. $.array[:2]: 获取名为"array"的数组属性中的前两个元素的值。
  5. $.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.sqltest-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的基本使用方法:

  1. 解析HTML字符串:
String html = "<html><head><title>测试页面</title></head>"
             + "<body><p>这是一个测试页面</p></body></html>";
Document doc = Jsoup.parse(html);
  1. 获取HTML元素:
Element titleElement = doc.select("title").first();
  1. 获取元素的文本内容:
String titleText = titleElement.text();
  1. 获取元素的属性值:
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,程序可以自动化地操作浏览器,模拟用户在浏览器中的交互行为。

核心类

  1. WebDriver:用于模拟浏览器的行为,如打开网页、输入内容、点击按钮等。
  2. WebElement:用于表示网页中的元素,如文本框、按钮等。
  3. By:用于定位网页中的元素,如根据ID、CSS选择器等。

WebDriver的常用方法

  1. get(url): 打开指定的网页。
  2. findElement(by): 根据定位器定位网页中的元素。
  3. sendKeys(text): 在文本框中输入文本。
  4. click(): 点击按钮。
  5. 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,强制错误信息输入,以及得到预期的结果。
  • 测试数据:测试数据应由程序插入或导入,以准备数据。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言