JVM-Sandbox
jvm-sandbox-repeater
JVM沙箱容器,一种JVM的非侵入式运行期AOP解决方案,如jstack,jmap等都是attach方式,也就是进程之间通信。
JVM-SANDBOX还能帮助你做很多很多,取决于你的脑洞有多大了。
如果你有以上的想法或需求,jvm-sandbox-repeater 都将是你的不二选择方案;框架基于JVM-Sandbox,拥有JVM-Sandbox的一切特性,同时封装了以下能力:
jvm-sandbox-repeater是阿里在19年7月份的时候开源的流量录制回放工具,代码提供了录制回放的能力,以及一个简单的repeater-console的demo示例。github 地址:GitHub - alibaba/jvm-sandbox-repeater: A Java server-side recording and playback solution based on JVM-Sandbox。
jvm-sandbox-repeater框架基于JVM-Sandbox,具备了JVM-Sandbox的所有特点封装了以下能力:
1.录制/回放基础协议,可快速配置/编码实现一类中间件的录制/回放
2.开放数据上报,对于录制结果可上报到自己的服务端,进行监控、回归、问题排查等上层平台搭建
基于它,我们可以在业务系统无感知的情况下,快速扩展 api ,实现自己的插件,对流量进行录制,入口请求(HTTP/Dubbo/Java)流量回放、子调用(Java/Dubbo)返回值Mock能力。详细介绍可以看官方说明。
录制回放主要原理如下:
录制:如图,当repeater启动对service A的录制后,有请求到service A,sandbox感知到请求后通知repeater。repeater对事件进行给过滤和采样计算,对满足录制条件的请求会记录请求、响应、子调用和响应,序列化成后通知repeater-console进行处理和保存。
回放:回放时,用户请求repeater-console的回放接口,明确需要回放哪条录制数据。然后repeater-console通过调用repeater提供的回放任务接收接口下发回放任务。repeater在执行回放任务的过程中,会反序列化记录的wrapperRecord,根据信息构造相同的请求,对被挂载的任务进行请求,并跟踪回放请求的处理流程,以便记录回放结果以及执行mock动作。如图,当我们启用了redis插件,录制时,service A到reids等的子请求方法、参数、响应将被录制下来,回放时,当service A再对reids发起请求时,repeater会先判断是否需要mock,当需要mock时会根据回放上下文中的信息拼接出MockRequest,通过mock策略计算获取MockResponse。目前源码中是获取相似度100%的请求的响应来进行mock。回放结束,repeater会将回放信息和结果序列化后通知repeater-console进行处理和保存。
一、jvm-sandbox与jvm-sandbox-repeater
JVM-SANDBOX | jvm-sandbox-repeater | |
---|---|---|
简介 | JVM沙箱容器,一种JVM的非侵入式运行期AOP解决方案 | 基于JVM-Sandbox的录制/回放通用解决方案 jvm-sandbox-repeater是JVM-Sandbox生态体系下的重要模块,它具备了JVM-Sandbox的所有特点,插件式设计便于快速适配各种中间件,封装请求录制/回放基础协议,也提供了通用可扩展的各种丰富API。 |
目标群体 | 1. BTRACE好强大,也曾技痒想做一个更便捷、更适合自己的问题定位工具,既可支持线上链路监控排查,也可支持单机版问题定位。 2. 有时候突然一个问题反馈上来,需要入参才能完成定位,但恰恰没有任何日志,甚至出现在别人的代码里,好想开发一个工具可以根据需要动态添加日志,最好还能按照业务ID进行过滤。3. 系统间的异常模拟可以使用的工具很多,可是系统内的异常模拟怎么办,加开关或是用AOP在开发系统中实现,好想开发一个更优雅的异常模拟工具,既能模拟系统间的异常,又能模拟系统内的异常。4. 好想获取行调用链路数据,可以用它识别场景、覆盖率统计等等,覆盖率统计工具不能原生支持,统计链路数据不准确。想自己开发一个工具获取行链路数据。5. 我想开发录制回放、故障模拟、动态日志、行链路获取等等工具,就算我开发完成了,这些工具底层实现原理相同,同时使用,要怎么消除这些工具之间的影响,怎么保证这些工具动态加载,怎么保证动态加载/卸载之后不会影响其他工具,怎么保证在工具有问题的时候,快速消除影响,代码还原如果你有以上研发诉求,那么你就是JVM-SANDBOX(以下简称沙箱容器)的潜在客户。沙箱容器提供:1. 动态增强类你所指定的类,获取你想要的参数和行信息甚至改变方法执行2. 动态可插拔容器框架 |
1. 线上有个用户请求一直不成功,我想在测试环境Debug一下,能帮我复现一下吗? 2. 压测流量不知道怎么构造,数据结构太复杂,压测模型也难以评估,有什么好的办法吗?3. 不想写接口测试脚本了,我想做一个流量录制系统,把线上用户场景做业务回归,可能会接入很多服务系统,不想让每个系统都进行改造,有好的框架选择吗?4. 我想做一个业务监控系统,对线上核心接口采样之后做一些业务校验,实时监控业务正确性。如果你有以上的想法或需求,jvm-sandbox-repeater 都将是你的不二选择方案;框架基于JVM-Sandbox,拥有JVM-Sandbox的一切特性,同时封装了以下能力:1. 录制/回放基础协议,可快速配置/编码实现一类中间件的录制/回放2. 开放数据上报,对于录制结果可上报到自己的服务端,进行监控、回归、问题排查等上层平台搭建 |
项目简介 | JVM-SANDBOX(沙箱)实现了一种在不重启、不侵入目标JVM应用的AOP解决方案。 沙箱的特性无侵入:目标应用无需重启也无需感知沙箱的存在类隔离:沙箱以及沙箱的模块不会和目标应用的类相互干扰可插拔:沙箱以及沙箱的模块可以随时加载和卸载,不会在目标应用留下痕迹多租户:目标应用可以同时挂载不同租户下的沙箱并独立控制高兼容:支持JDK[6,11]沙箱常见应用场景线上故障定位线上系统流控线上故障模拟方法请求录制和结果回放动态日志打印安全信息监测和脱敏JVM-SANDBOX还能帮助你做很多很多,取决于你的脑洞有多大了。实时无侵入AOP框架在常见的AOP框架实现方案中,有静态编织和动态编织两种。静态编织:静态编织发生在字节码生成时根据一定框架的规则提前将AOP字节码插入到目标类和方法中,实现AOP;动态编织:动态编织则允许在JVM运行过程中完成指定方法的AOP字节码增强.常见的动态编织方案大多采用重命名原有方法,再新建一个同签名的方法来做代理的工作模式来完成AOP的功能(常见的实现方案如CgLib),但这种方式存在一些应用边界:侵入性:对被代理的目标类需要进行侵入式改造。比如:在Spring中必须是托管于Spring容器中的Bean固化性:目标代理方法在启动之后即固化,无法重新对一个已有方法进行AOP增强要解决无侵入的特性需要AOP框架具备 在运行时完成目标方法的增强和替换。在JDK的规范中运行期重定义一个类必须准循以下原则1. 不允许新增、修改和删除成员变量2. 不允许新增和删除方法3. 不允许修改方法签名JVM-SANDBOX属于基于Instrumentation的动态编织类的AOP框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束情况下实现对目标应用方法的无侵入运行时AOP拦截。 |
repeater的核心能力是什么? 1. 通用录制/回放能力无侵入式录制HTTP/Java/Dubbo入参/返回值录制能力(业务系统无感知)基于TTL提供多线程子调用追踪,完整追踪一次请求的调用路径入口请求(HTTP/Dubbo/Java)流量回放、子调用(Java/Dubbo)返回值Mock能力2. 快速可扩展API实现录制/回放插件式架构提供标准接口,可通过配置/简单编码实现一类通用插件3. standalone工作模式无需依赖任何服务端/存储,可以单机工作,提供录制/回放能力repeater的可以应用到哪些场景?1. 业务快速回归基于线上流量的录制/回放,无需人肉准备自动化测试脚本、准备测试数据2. 线上问题排查录制回放提供"昨日重现"能力,还原线上真实场景到线下做问题排查和Debug动态方法入参/返回值录制,提供线上快速问题定位3. 压测流量准备0成本录制HTTP/Dubbo等入口流量,作为压测流量模型进行压测4. 实时业务监控动态业务监控,基于核心接口数据录制回流到平台,对接口返回数据正确性进行校验和监控 |
核心原理 | 事件驱动、类隔离策略、类增强策略 | 流量录制、流量回放 |
安装包括 repeater 安装、repeater-console 安装
目前安装和使用,需要 mac 或者 linux 系统下进行,如果在 windows 下进行可能会遇到安装路径出错导致安装失败或者运行失败的情况。
PS:如果只是想简单运行,可以直接使用官方版本,参考官方用户手册,以standalone
模式把玩。
下载源码:
[root@k8s-worker27-65 jvm-sandbox-repeater]# git clone https://github.com/alibaba/jvm-sandbox-repeater.git
bootstrap.sh:
[root@k8s-worker27-65 bin]# cat bootstrap.sh
#!/usr/bin/env bash
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
typeset HOME=/opt/data/fll
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
PID=$(ps -ef | grep "repeater-bootstrap.jar" | grep "java" | grep -v grep | awk '{print $2}')
expr ${PID} "+" 10 &> /dev/null
# if occurred error,exit
if [ ! $? -eq 0 ] || [ "" = "${PID}" ] ;then
echo ""
else
echo "found target pid exist, pid is ${PID}, kill it..."
kill -9 ${PID}
fi
if [ ! -f "${HOME}/sandbox/sandbox-module/repeater-bootstrap.jar" ]; then
echo "repeater-bootstrap.jar not found, try to install";
sh ./install-local.sh || exit_on_err 1 "install repeater failed"
fi
${JAVA_HOME}/bin/java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 \
-javaagent:${HOME}/sandbox/lib/sandbox-agent.jar=server.port=8820\;server.ip=0.0.0.0 \
-Dapp.name=jettopro \
-Dapp.env=sit \
-jar ${HOME}/sandbox/sandbox-module/repeater-bootstrap.jar
install-local.sh:
[root@k8s-worker27-65 bin]# cat install-local.sh
#!/usr/bin/env bash
# repeater's target dir
REPEATER_TARGET_DIR=../target/repeater
typeset HOME=/opt/data/fll
typeset SANDBOX_HOME=/opt/data/fll/sandbox
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
# package
sh ./package.sh || exit_on_err 1 "install failed cause package failed"
# extract sandbox to ${HOME}
#curl -s https://github.com/alibaba/jvm-sandbox-repeater/releases/download/v1.0.0/sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
#cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
# copy module to ~/.sandbox-module
#mkdir -p ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not mkdir ~/.sandbox-module"
#cp -r ${REPEATER_TARGET_DIR}/* ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not copy module to ~/.sandbox-module"
cp -r ${REPEATER_TARGET_DIR}/* ${SANDBOX_HOME}/sandbox-module || exit_on_err 1 "permission denied, can not copy module to ${SANDBOX_HOME}/sandbox-module"
package.sh :
[root@k8s-worker27-65 bin]# cat package.sh
#!/usr/bin/env bash
# repeater's target dir
REPEATER_TARGET_DIR=../target/repeater
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
# maven package the sandbox
mvn clean package -Dmaven.test.skip=true -f ../pom.xml || exit_on_err 1 "package repeater failed."
mkdir -p ${REPEATER_TARGET_DIR}/plugins
mkdir -p ${REPEATER_TARGET_DIR}/cfg
cp ./repeater-logback.xml ${REPEATER_TARGET_DIR}/cfg/repeater-logback.xml \
&& cp ./repeater.properties ${REPEATER_TARGET_DIR}/cfg/repeater.properties \
&& cp ./repeater-config.json ${REPEATER_TARGET_DIR}/cfg/repeater-config.json \
&& cp ../repeater-module/target/repeater-module-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/repeater-module.jar \
&& cp ../repeater-console/repeater-console-start/target/repeater-console.jar ${REPEATER_TARGET_DIR}/repeater-bootstrap.jar \
&& cp ../repeater-plugins/ibatis-plugin/target/ibatis-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/ibatis-plugin.jar \
&& cp ../repeater-plugins/java-plugin/target/java-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/java-plugin.jar \
&& cp ../repeater-plugins/mybatis-plugin/target/mybatis-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/mybatis-plugin.jar \
&& cp ../repeater-plugins/dubbo-plugin/target/dubbo-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/dubbo-plugin.jar \
&& cp ../repeater-plugins/redis-plugin/target/redis-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/redis-plugin.jar \
&& cp ../repeater-plugins/http-plugin/target/http-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/http-plugin.jar \
&& cp ../repeater-plugins/hibernate-plugin/target/hibernate-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/hibernate-plugin.jar \
&& cp ../repeater-plugins/spring-data-jpa-plugin/target/spring-data-jpa-plugin-*-jar-with-dependencies.jar ${REPEATER_TARGET_DIR}/plugins/spring-data-jpa-plugin.jar
# tar the repeater.tar
cd ../target/
tar -zcvf repeater-stable-bin.tar repeater/
cd -
echo "package repeater-stable-bin.tar finish."
repeater-logback.xml :
[root@k8s-worker27-65 bin]# cat repeater-logback.xml
/opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log
/opt/data/fll/sandbox/logs/sandbox/repeater/repeater.log.%d{yyyy-MM-dd}
30
%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n
UTF-8
[root@k8s-worker27-65 bin]# cat repeater-config.json
{
"useTtl" : true,
"degrade" : false,
"exceptionThreshold" : 1000,
"sampleRate" : 10000,
"pluginsPath" : null,
"httpEntrancePatterns" : [ "^/greeting.*$" ],
"javaEntranceBehaviors" : [ {
"classPattern" : "hello.GreetingController",
"methodPatterns" : [ "greeting" ],
"includeSubClasses" : false
} ],
"javaSubInvokeBehaviors" : [],
"pluginIdentities" : [ "http", "java-entrance", "java-subInvoke", "mybatis", "ibatis" ],
"repeatIdentities" : [ "java", "http" ]
}
repeater.properties:
[root@k8s-worker27-65 bin]# cat repeater.properties
# 录制消息投递地址
broadcaster.record.url=http://192.168.1.65:8001/facade/api/record/save
# 回放结果投递地址
broadcaster.repeat.url=http://192.168.1.65:8001/facade/api/repeat/save
# 回放消息取数据地址
repeat.record.url=http://192.168.1.65:8001/facade/api/record/%s/%s
# 配置文件拉取地址
repeat.config.url=http://192.168.1.65:8001/facade/api/config/%s/%s
# 心跳上报配置
repeat.heartbeat.url=http://192.168.1.65:8001/module/report.json
# 是否开启脱机工作模式
repeat.standalone.mode=false
# 是否开启spring advice拦截
repeat.spring.advice.switch=false;
启动:
启动之前:
[root@k8s-worker27-65 jvm-sandbox-repeater]# cat bin/repeater.properties
# 录制消息投递地址
broadcaster.record.url=http://127.0.0.1:8001/facade/api/record/save
# 回放结果投递地址
broadcaster.repeat.url=http://127.0.0.1:8001/facade/api/repeat/save
# 回放消息取数据地址
repeat.record.url=http://127.0.0.1:8001/facade/api/record/%s/%s
# 配置文件拉取地址
repeat.config.url=http://127.0.0.1:8001/facade/api/config/%s/%s
# 心跳上报配置
repeat.heartbeat.url=http://127.0.0.1:8001/module/report.json
# 是否开启脱机工作模式
repeat.standalone.mode=true
# 是否开启spring advice拦截
repeat.spring.advice.switch=false;
# 是否开启脱机工作模式
repeat.standalone.mode=true 单击模式,且
[root@k8s-worker27-65 bin]# cat install-local.sh
#!/usr/bin/env bash
# repeater's target dir
REPEATER_TARGET_DIR=../target/repeater
# exit shell with err_code
# $1 : err_code
# $2 : err_msg
exit_on_err()
{
[[ ! -z "${2}" ]] && echo "${2}" 1>&2
exit ${1}
}
# package
sh ./package.sh || exit_on_err 1 "install failed cause package failed"
# extract sandbox to ${HOME}
#curl -s https://github.com/alibaba/jvm-sandbox-repeater/releases/download/v1.0.0/sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed"
# copy module to ~/.sandbox-module
mkdir -p ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not mkdir ~/.sandbox-module"
cp -r ${REPEATER_TARGET_DIR}/* ${HOME}/.sandbox-module || exit_on_err 1 "permission denied, can not copy module to ~/.sandbox-module"
cat sandbox-1.3.3-bin.tar | tar xz -C ${HOME} || exit_on_err 1 "extract sandbox failed" curl下载不下来,自己想办法然后用本地的
[root@k8s-worker27-65 bin]# ./bootstrap.sh
step1 开始录制
[root@k8s-worker27-65 sandbox]# curl -s 'http://192.168.1.65:8001/regress/slogan?Repeat-TraceId=127000000001156034386424510000ed'
JAVA是世界上最好的语言!
执行结果如下:
访问链接时,repeater 插件通过 Repeat-TraceId=127000000001156034386424510000ed,唯一追踪到了这一次请求,后台服务返回了
JAVA是世界上最好的语言!
,repeater 把画面定格在了这一秒并将结果和 firstId 绑定
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=127000000001156034386424510000ed'
Javascript是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=127000000001156034386424510000ed'
GO是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
浏览器:
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=127000000001156034386424510000ed'
GO是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=127000000001156034386424510000ed'
GO是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=127000000001156034386424510000ed'
GO是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
浏览器:
JAVA是世界上最好的语言!
;如果重新访问Slogan后又会将最新的返回结果绑定到 Repeat-TraceId=127000000001156034386424510000ed(为了快速演示,将链路追踪的标志提到参数中进行透传了)光是执行官方用例,当然不能满足我们需要啦。我们来测试下,如果有多个 Repeat-TraceId ,是否可以分别录制?
# 录制一个 128 开头的 traceId ,返回结果是 java
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=128000000001156034386424510000ed'
JAVA是世界上最好的语言!
您在 /var/spool/mail/root 中有新邮件
# 录制一个 129 开头的 traceId ,返回结果是 Python
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId=129000000001156034386424510000ed'
Python是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
# 回放前面 128 2次 的流量 结果相同
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=128000000001156034386424510000ed'
JAVA是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=128000000001156034386424510000ed'
JAVA是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
# 回放前面 129 2次 的流量 结果相同
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=129000000001156034386424510000ed'
Python是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
[root@k8s-worker27-65 sandbox]# curl -s 'http://127.0.0.1:8001/regress/slogan?Repeat-TraceId-X=129000000001156034386424510000ed'
Python是世界上最好的语言!
[root@k8s-worker27-65 sandbox]#
看来确实是有效的。
前面只是个简单的练手,实际用不用得了,当然的实际项目说话啦。
为了简单,此处使用了几个 spring boot 的示例项目当做实际项目使用。
Getting Started | Building a RESTful Web Service
项目地址:https://github.com/chenhengjie123/gs-rest-service(官方文档:Getting Started | Building a RESTful Web Service ,在官方的基础上增加了请求日志打印的功能,便于查看回放效果)
clone 后,直接用 complete
里面的完整示例,当做被测程序。
[root@k8s-worker27-65 gs-rest-service]# git clone https://github.com/spring-guides/gs-rest-service.git
切换到2.1.6.RELEASE tag
[root@k8s-worker27-65 gs-rest-service]# git branch -a
* (分离自 2.1.6.RELEASE)
completed
hide-show
main
no_cat_no_toc
refactor
remotes/origin/HEAD -> origin/main
remotes/origin/autowired-ctor
remotes/origin/boot-2.7
remotes/origin/categories
remotes/origin/completed
remotes/origin/gregturn-master
remotes/origin/hide-show
remotes/origin/main
remotes/origin/no_cat_no_toc
remotes/origin/refactor
[root@k8s-worker27-65 gs-rest-service]# git tag
0.1.0
1.4.1.RELEASE
1.4.2.RELEASE
1.4.3.RELEASE
1.5.1.RELEASE
1.5.10.RELEASE
1.5.2.RELEASE
1.5.5.RELEASE
1.5.9.RELEASE
2.0.0.RELEASE
2.0.1.RELEASE
2.0.2.RELEASE
2.0.3.RELEASE
2.0.5.RELEASE
2.0.8.RELEASE
2.1.3.RELEASE
2.1.4.RELEASE
2.1.6.RELEASE
edgware.release
edgware.sr2
finchley.sr2
maven
编译:
Build an executable JAR
You can run the application from the command line with Gradle or Maven. You can also build a single executable JAR file that contains all the necessary dependencies, classes, and resources and run that. Building an executable jar makes it easy to ship, version, and deploy the service as an application throughout the development lifecycle, across different environments, and so forth.
If you use Gradle, you can run the application by using ./gradlew bootRun. Alternatively, you can build the JAR file by using ./gradlew build and then run the JAR file, as follows:
java -jar build/libs/gs-rest-service-0.1.0.jar
If you use Maven, you can run the application by using ./mvnw spring-boot:run. Alternatively, you can build the JAR file with ./mvnw clean package and then run the JAR file, as follows:
java -jar target/gs-rest-service-0.1.0.jar
访问:
http://localhost:8080/greeting