根据jar名称动态打包带版本的镜像, 并创建对应容器的脚本实现

news2024/11/20 23:36:01

根据jar名称动态打包带版本的镜像以及容器

利用shell脚本, 实现根据jar名称中的项目名和版本号来动态制作带版本的Docker镜像以及带版本的容器

背景

人人都逃不过的墨菲定律

事情的原因来自最近发生的一次生产环境事故: 我们在甲方那里环境中有两个服务器, 一个用于灰度测试另一个用于盛传. 我负责的某项目在完成缺陷修复并测试通过后, 产品便提交生产环境发版流程. 流程走到我这里时, 我找到流程中负责现场发版的运维要求发版, 而他也发了版, 并且把脚本启动的成功截图给我了, 但是截图中没有当前服务器的ip, 这就为后来问题的暴露埋下伏笔…

在 ‘新版本’ 运行约一周之后, 实施反映之前修复的bug又复现了. 于是让运维下载下日志给我, 结果发现当前业务并没有走新版本修复后的逻辑, 并且未显示新版本追加的日志信息. 因此我推测包没有发上, 于是让运维比对下docker容器创建时间和jar包上传时间发现, 真的没有发上去. 于是我查了之前的聊天记录, 恰好发现之前运维截的图没有截到服务器的ip地址. 正如墨菲定律说的那样: 凡是可能出错的事情就一定出错.

事件反思

事后通过和运维和其他同事沟通了解到: 其实在工作中, 运维每天的量也很大. 即使之前帮过你发过包, 由于时间和个人的原因, 仍会导致再次发包时会忘记一些环境配置的情况. 而我这边也存在问题, 在发布后没有仔细确认…

基于以上反思, 我决定:

  1. 在每次需要运维协助发包前, 整理好发包文档, 让其严格按照文档来发包
  2. 优化原有启动脚本, 根据jar名称动态打包带版本的镜像,
    让运维那边也可以看到新版本jar是否发送成功

辛路历程

整个脚本制作过程结合了ChatGPT使用, 中间过程有些曲折且漫长,
因此特地花点时间复盘一下, 希望能够带给自己和大家一些启发.
如果想要直接看最终脚本请直接跳转至最终脚本部分即可

前提准备

在idea开启远程调试这一章节, 我就分享过docker打包jar包环境的脚本.
这个脚本的核心是利用DockerFIle, 将jar制作成镜像, 然后利用启动脚本将镜像生成容器并启动.
但因为之前分享的jar都是0.0.1版本(图1), 因此无论是镜像的tag(图2), 还是启动时容器中的镜像信息(图3)都是0.0.1.
那么如何动态的将jar包中的版本信息动态的打入镜像的tag和容器名称中呢?

图1

图2

在这里插入图片描述

图3

在这里插入图片描述

再次将该脚本再一次分享与大家

# 1.DockerFIle
FROM java:8

COPY 待启动jar-0.0.1-SNAPSHOT.jar 待启动jar-0.0.1-SNAPSHOT.jar

EXPOSE 项目对外端口

ENTRYPOINT ["java","-Duser.timezone=GMT+8","-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=远程调试端口","-jar","待启动jar-0.0.1-SNAPSHOT.jar"]


# 2.start.sh
sudo docker build --no-cache -f DockerFile -t 待启动jar:0.0.1 .;

sudo docker stop 待启动jar;

sudo docker rm 待启动jar;

sudo docker run -d --restart=always --name 待启动jar \
	-p 项目对外端口:项目对外端口 -p 远程调试端口:远程调试端口 \
	-v /home:/home \
	-v /home/待启动jar/logs:/logs \
	待启动jar:0.0.1 --spring.profiles.active=sit

1. 脚本启动顺序

在这里需要梳理下脚本的启动顺序, 这对我们后面问问题有所帮助, 正确的顺序为

->运行脚本start.sh
->通过DockerFIle制作新版本镜像
->停止并删除原来镜像对应的容器
->根据新的镜像创建容器
->执行根据DockerFIle中ENTRYPOINT关键字添加的, 启动容器所需执行的命令

2. 前置知识

本文的编写用到了大量的DockerFIle的关键字和shell脚本的基础知识.
如果后续阅读遇到了困难可以点击这里的链接, 来简单了解下(或者通过ChatGPT来简单了解下)

PS: 如果不理解上面的脚本是干什么的, 可以直接让ChatGPT为你翻译

在这里插入图片描述

一股脑提问法

  1. 在一开始, 我便选择最"简单粗暴的方式", 直接提问
    在这里插入图片描述

  2. 可以看到, 给出DockerFIle文件看起来还是没有问题的, 但是继续往下看
    在这里插入图片描述

  3. 可以看到启动脚本这里就有问题了, 我想要的是根据jar名称动态获取项目名称和版本号
    但这里提供的脚本是在一开始就写死的
    , 也就是说GPT并没有正确理会我这个动态读取的意思,
    在这里插入图片描述

  4. 并且, 我即使使用上面的方式, 运行ChatGPT提供给我的DockerFIle和启动脚本,
    发现还是未成功. 原因是启动脚本中的参数的值虽然被--build-arg 传入到DockerFIle,
    但是在ENTRYPOINT中在替换jar名称后依然执行报错,
    并且DockerFIle中通过arg定义的参数, 在ENTRYPOINT通过${}获取不到,
    我尝试用CMD来执行该参数, 但是这种方式下仍然失败.
    因此, 发现在整体上一点点调试, 不如重头在细节上一点点攻破.
    所以我调整了提问策略: 由整体提问, 改为分步骤提问

分步骤提问法

在规划步骤时, 核心就是动态获取jar包版本号和jar名称,
因此在一开始时, 我把主要步骤分为3步:动态读取jar包名称中的信息->动态打包镜像->动态创建并运行容器
在后续的思考中, 我又添加了: 获取服务器ip-> 删除原有容器, 镜像 这两步.
如果时间有限的同学, 想直接获取最终脚本, 可以跳转至"最终脚本"这一部分.
下面就开始介绍如何分步提问, 来让ChatGP解决问题

1. 动态读取jar包名称中的信息

根据上面前提准备-脚本启动顺序那部分我们归纳得到启动顺序, 我决定先对启动文件start.sh进行改造,
将动态获取jar名称和版本好的问题交给ChatGPT, 可以看到很快就得到了一个方案

在这里插入图片描述
通过阅读上面脚本发现, 提供脚本还是存在瑕疵, 因为jar包名称JAR_FILE还是在脚本里面写死的,
而不是动态获取的, 可以看出来当前ChatGPT并没有理解我的需求.
因此我们需要将问题继续抛给ChatGPT
在这里插入图片描述
可以看到之前的问题已经得以解决
至此, 整个脚本最核心的三行脚本已经获取完毕

	# 获取当前jar全称
	JAR_FILE=$(ls  *.jar | head -n 1)
	# 获取当前jar实际名称(通过-分隔的第1列信息, 这里后续需要根据自己jar的格式进行调整)
	BASE_NAME=$(echo "$JAR_FILE" | awk -F '-' '{print $1}')
	# 获取当前jar的版本号(通过-分隔的第2列信息, 这里后续需要根据自己jar的格式进行调整)
	VERSION=$(echo "$JAR_FILE" | awk -F '-' '{print $2}')

2. 动态创建并运行容器

根据上一步的结果, 我们便可以改造启动脚本:

	# start.sh
	JAR_FILE=$(ls  *.jar | head -n 1)
	BASE_NAME=$(echo "$JAR_FILE" | awk -F '-' '{print $1}')
	VERSION=$(echo "$JAR_FILE" | awk -F '-' '{print $2}')
	sudo docker build --no-cache -f DockerFile -t ${BASE_NAME}:${VERSION} .;
	
	sudo docker stop ${BASE_NAME};
	
	sudo docker rm ${BASE_NAME};
	
	sudo docker run -d --restart=always --name ${BASE_NAME} \
		-p 项目对外端口:项目对外端口 -p 远程调试端口:远程调试端口 \
		-v /home:/home \
		-v /home/${BASE_NAME}/logs:/logs \
		${BASE_NAME}:${VERSION} --spring.profiles.active=sit

启动脚本修改完毕了, 那么DockerFIle如何通过jar的名称, 动态获取项目名称和版本号呢?

3. 动态打包镜像

我这里尝试着在DockerFIle通过RUN关键字执行shell指令, 然后将参数通过export输出,
需要注意的是: COPY *.jar /是将当前根目录下的jar复制到镜像容器的根目录

FROM java:8

COPY *.jar /

RUN JAR_FILE=$(ls  *.jar | head -n 1) && \
    BASE_NAME=$(echo "$JAR_FILE" | awk -F '-' '{print $1}') && \
    VERSION=$(echo "$JAR_FILE" | awk -F '-' '{print $2}') && \
    echo "BASE_NAME=$BASE_NAME, VERSION=$VERSION" && \
    export BASE_NAME=$BASE_NAME && \
    export VERSION=$VERSION



EXPOSE 17885

ENTRYPOINT ["java","-Duser.timezone=GMT+8","-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7885","-jar","${BASE_NAME}:${VERSION}"]

然后在ENTRYPOINT中填充再运行, 结果仍然执行失败, 被
原因是填充的参数${BASE_NAME}, ${VERSION}仍无法获取到. 我这里采取了很多方法仍无法解决,
可能是由于我提问方式的原因, 多次询问ChatGPT无果, 之后我便决定改用搜索引擎,
然后就看到了这个大佬的博客: Dockerfile中通过ENV指定动态参数在RUN时传递参数(部署后台jar包时指定端口为例), 文章核心是利用docker run -e 将参数在创建容器时传入到DockerFIle中, 通过ENV关键字定义的变量来接收
然后利CMD定义容器启动时的执行命令,然后通过${}来获取ENV定义的参数

至此, 两个脚本文件start.sh和DockerFIle文件就改造完毕

# start.sh
JAR_FILE=$(ls  *.jar | head -n 1)
BASE_NAME=$(echo "$JAR_FILE" | awk -F '-' '{print $1}')
VERSION=$(echo "$JAR_FILE" | awk -F '-' '{print $2}')
sudo docker build --no-cache -f DockerFile -t ${BASE_NAME}:${VERSION} .;

sudo docker stop ${BASE_NAME};

sudo docker rm ${BASE_NAME};

sudo docker run -d --restart=always --name ${BASE_NAME} \
	-p 项目对外端口:项目对外端口 -p 远程调试端口:远程调试端口 \
	-e JAR_NAME=${JAR_FILE} \
	-v /home:/home \
	-v /home/${BASE_NAME}/logs:/logs \
	${BASE_NAME}:${VERSION} --spring.profiles.active=sit


# DockerFIle
FROM java:8

COPY *.jar /

ENV JAR_NAME=${JAR_NAME} 

EXPOSE 17885

CMD java -Duser.timezone=GMT+8 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7885 -jar ${JAR_NAME} --spring.profiles.active=sit

4. 获取服务器ip

其实步骤3已经实现了主要功能. 但仅仅是根据jar名称动态打包带版本的镜像, 并创建了对应容器
启动成功后, 通过docker ps和docker images也确实可以动态获取带版本的jar的消息.
如果恰好两台不同的服务器都部署了该项目, 那么即使启动信息显示正常, 但由于无法判断是在哪个服务器上启动的
也会影响你对项目是否在指定的环境启动成功的判断 .
因此, 我决定在脚本上追加获取当前服务器内网ip的功能
(主要是因为生产服务器部署在内网, 如果项目部署在外网, 可以选择获取外网地址).

老样子, 先问ChatGPT
在这里插入图片描述
在使用提供的前两种方式, 发现结果输出为null ,
究其原因, 是因为在本机电脑上未配置eth0的网卡信息, 直到运行第3种方式才得以解决

在这里插入图片描述
第3种方式命令执行情况
在这里插入图片描述

因此, 我这里便对启动脚本再次进行修改

# start.sh
JAR_FILE=$(ls  *.jar | head -n 1)
BASE_NAME=$(echo "$JAR_FILE" | awk -F '-' '{print $1}')
VERSION=$(echo "$JAR_FILE" | awk -F '-' '{print $2}')
sudo docker build --no-cache -f DockerFile -t ${BASE_NAME}:${VERSION} .;

sudo docker stop ${BASE_NAME};

sudo docker rm ${BASE_NAME};

sudo docker run -d --restart=always --name ${BASE_NAME} \
	-p 项目对外端口:项目对外端口 -p 远程调试端口:远程调试端口 \
	-e JAR_NAME=${JAR_FILE} \
	-v /home:/home \
	-v /home/${BASE_NAME}/logs:/logs \
	${BASE_NAME}:${VERSION} --spring.profiles.active=sit

IP=$(hostname -I | awk '{print $1}')
echo "在 $IP 服务器上执创建镜像并启动容器完成, 请使用 docker ps 查看启动情况"

5. 删除原有容器, 镜像

一般的镜像删除都是根据镜像id来删除镜像, 但是每次运行DockerFIle新建镜像时, 镜像id都会重新生成,
那每次重新启动脚本时, 如何通过获取上一个镜像的镜像id来删除上个镜像就成了一个棘手的问题
这时, 翻看自己之前的docker文章->第三章 常用命令->镜像命令中, 还有一种镜像的删除方式,
就是通过 docker images image_name来获取指定镜像信息(需要将之前创建的同名但未删除的镜像删除),
然后利用 awk 'NR==2{print $3}获取镜像id, 结合 docker rmi 来删除之前的命令.
最后根据镜像名称删除镜像命令就变成了 docker rmi $( docker images image_name | awk 'NR==2{print $3}

结合docker容器和镜像的删除顺序: 停止容器->删除容器->删除镜像 , 此脚本修改后如下

# start.sh
JAR_FILE=$(ls  *.jar | head -n 1)
BASE_NAME=$(echo "$JAR_FILE" | awk -F '-' '{print $1}')
VERSION=$(echo "$JAR_FILE" | awk -F '-' '{print $2}')
IMAGE_ID=$(docker images "${BASE_NAME}" | awk 'NR==2{print $3}' )

sudo docker build --no-cache -f DockerFile -t ${BASE_NAME}:${VERSION} .;

sudo docker stop ${BASE_NAME};
sudo docker rm ${BASE_NAME};
docker rmi ${IMAGE_ID}

docker rmi $( docker images image_name | awk 'NR==2{print $3}'

sudo docker run -d --restart=always --name ${BASE_NAME} \
	-p 项目对外端口:项目对外端口 -p 远程调试端口:远程调试端口 \
	-e JAR_NAME=${JAR_FILE} \
	-v /home:/home \
	-v /home/${BASE_NAME}/logs:/logs \
	${BASE_NAME}:${VERSION} --spring.profiles.active=sit

IP=$(hostname -I | awk '{print $1}')
echo "在 $IP 服务器上执创建镜像并启动容器完成, 请使用 docker ps 查看启动情况"

最终脚本

在脚本中输出的关键信息进行打印以及维护后, 最终脚本终于出炉啦!
下面, 我们就来揭开他神秘的面纱:

  1. 启动脚本start.sh
    需要注意的是, 当前目录只能有一个.jar结尾的jar,
    如果想要留存历史版本的jar需要在后面额外增加一个后缀, 例如.bak123…, 如下图1

    #!/bin/bash
    JAR_FILE=$(ls  *.jar | head -n 1) 
    BASE_NAME=$(echo "$JAR_FILE" | awk -F '-' '{print $1}') 
    VERSION=$(echo "$JAR_FILE" | awk -F '-' '{print $2}') 
    IMAGE_ID=$(docker images "${BASE_NAME}" | awk 'NR==2{print $3}' )
    
    sudo docker stop ${BASE_NAME};
    sudo docker rm ${BASE_NAME};
    docker rmi ${IMAGE_ID};
    echo "删除镜像名为:${BASE_NAME},镜像id为: ${IMAGE_ID}";
    
    sudo docker build --no-cache -f DockerFile  -t ${BASE_NAME}:${VERSION} .;
    echo "创建并启动 $BASE_NAME 容器, 容器启动依赖的jar名称: $JAR_FILE, jar版本: $VERSION";
    
    sudo docker run -d --restart=always --name ${BASE_NAME} \
    	-p 17885:17885 -p 7885:7885 \
    	-e JAR_NAME=${JAR_FILE} \
    	-v /home:/home \
    	-v /home/${BASE_NAME}/logs:/logs \
    	${BASE_NAME}:${VERSION} 
    
    ip=`ifconfig -a|grep inet|grep -v 127.0.0.1 |grep -v inet6| grep -v 172.* | awk '{print $2}'|tr -d "addr:" | head -1`
    echo "在 $ip 服务器上执创建镜像并启动容器完成, 请使用 docker ps 查看启动情况"
    

  2. DockerFIle文件

    FROM java:8
    
    MAINTAINER "时间静止不是简史"
    
    LABEL description="开始生成xx项目镜像文件"
    
    WORKDIR app
    
    COPY *.jar /app
    
    ENV JAR_NAME=${JAR_NAME} 
    
    EXPOSE 项目对外端口
    
    CMD java -Duser.timezone=GMT+8 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=远程调试端口 -jar ${JAR_NAME} --spring.profiles.active=项目启动环境
    

反思

受限于博主技术水平以及向ChatGPT提问方式的原因, 大概花费了近1天的时间在将整个脚本调试完毕.
ChatGPT 3.5 依旧如我预想的一样只能作为工具(ChatGPT 4可能会在一定程度上解决问题但仍不可能完全解决),
由于其不能完全理解我们的想法(虽然沟通占一部分原因, 但更主要原因是可能是的训练参数量)
加上无法对其代码的正确性进行验证, 以及所存储的知识具有时间局限性等等原因,
当下不可能完全替代人类去写代码…

但是作为工具来说, 在细节的处理上, ChatGPT要比搜索引擎优秀得多. 因此在遇到工程问题时,
如果想从大方向上找解决方案, 可以选择搜索引擎. 例如本篇文章的标题这种解决方案
如果自己有能力将该工程问题分解为具体的技术细节, 可以将具体的细节交由ChatGPT实现, 然后再人工进行组装和优化.
这便是我通过这次脚本编写过程复盘, 所得到的收获.


越使用它, 越了解它, 便越不惧怕它. 与其惧怕技术会取代人类, 不如主动去拥抱和学习技术.
不知道大家对ChatGPT的看法又是如何呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/669221.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WPF开发txt阅读器16:自动编码检测

文章目录 更改编码重新载入自动编码检测更改编码并保存 txt阅读器系列: 需求分析和文件读写目录提取类💎列表控件与目录💎快捷键翻页字体控件绑定💎前景/背景颜色书籍管理系统💎用树形图管理书籍语音播放&#x1f48e…

Cortext-M3系统:储存器系统(2)

1、存储系统功能概览 Cortext-M3储存器有如下特点: 存储器映射是预定义的,并且还规定好了哪个位置使用哪条总线。 存储器系统支持所谓的“位带”(bit-band)操作。通过它,实现了对单一比特的原子操作,位带操…

STM32G0+EMW3080+阿里云实现单片机WiFi智能联网功能(一)EMW3080实现和PC之间的串口通讯

项目描述:该系列记录了STM32G0EMW3080实现单片机智能联网功能项目的从零开始一步步的实现过程; 硬件环境:单片机为STM32G030C8T6;物联网模块为EMW3080V2-P;网联网模块的开发板为MXKit开发套件,具体型号为XC…

基于tensorflow深度学习的猫狗分类识别

🤵‍♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞&#x1f4…

Triton教程 --- 速率限制器

Triton教程 — 速率限制器 Triton系列教程: 快速开始利用Triton部署你自己的模型Triton架构模型仓库存储代理模型设置优化动态批处理 速率限制器 速率限制器管理 Triton 在模型实例上调度请求的速率。 速率限制器在 Triton 中加载的所有模型上运行,以允许跨模型优…

带你用Python制作7个程序,让你感受到端午节的快乐

名字:阿玥的小东东 学习:Python、C/C 主页链接:阿玥的小东东的博客_CSDN博客-python&&c高级知识,过年必备,C/C知识讲解领域博主 目录 前言 程序1:制作粽子 程序2:龙舟比赛 程序3:艾草挂 程序4…

基于Java高校共享单车管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍: ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ 🍅 文末获取源码联系 🍅 👇🏻 精…

《网络安全0-100》网络安全的未来趋势

网络安全的未来趋势 网络安全是一个永恒的话题,随着技术的发展 和应用,网络安全也面临着新的挑战和威胁。 以下是网络安全未来的趋势: 人工智能和机器学习:人工智能和机器学习已 经成为网络安全领域的热门技术。未来&#xff…

编译原理笔记11:自上而下语法分析(1)基础概念、左递归和公共左因子处理、递归下降分析(咕咕咕)

目录 自上而下分析的一般方法用推导的方法分析输入序列左递归问题及其消除(消除左递归)消除直接左递归消除间接左递归左递归消除算法 公共左因子问题及其消除(提取左因子)提取左因子 递归下降分析 词法分析,是把源程序…

基于物联网及云平台的光伏运维系统

系统结构 在光伏变电站安装逆变器、以及多功能电力计量仪表,通过网关将采集的数据上传至服务器,并将数据进行集中存储管理。用户可以通过PC访问平台,及时获取分布式光伏电站的运行情况以及各逆变器运行状况。平台整体结构如图所示。 光伏背景…

Cortext-M3系列:调试组件(9)

1、调试组件简介 在 CM3 中有很多调试组件,使用它们可以执行各种调试功能:断点、数据观察点、闪存地址重载以及各种跟踪等。软件开发人员也许永远无需了解调试组 的细节,因为它们通常只是由调试器及其周边工具使用的。 本文对每种调试组件做一…

基于Java学生公寓管理中心系统设计实现(源码+lw+部署文档+讲解等)

博主介绍: ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ 🍅 文末获取源码联系 🍅 👇🏻 精…

IDEA2022.3.3支持Jrebel and Xrebel教程

目录 前言 思路 步骤 1、下载服务并启动 推荐下载windows环境的exe文件,直接点开就行。 如果用linux 需要安装go环境: 下载好后启动 2、idea安装Jrebel and XRebel插件 3、激活插件 前言 由于服务平台限制,只支持darwin、linux和windows环境。这…

(转载)无监督学习神经网络的分类(matlab实现)

对于监督学习神经网络,事先需要知道与输入相对应的期望输出,根据期望输出与网络输出间的偏差来调整网络的权值和阈值。然而,在大多数情况下,由于人们认知能力以及环境的限制,往往无法或者很难获得期望的输出&#xff0…

AbstractQueuedSynchronizer源码

介绍 基于队列的抽象同步器,它是jdk中所有显示的线程同步工具的基础,像ReentrantLock/DelayQueue/CountdownLatch等等,都是借助AQS实现的。 public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplemen…

Camera 基础知识点

和你一起终身学习,这里是程序员Android 经典好文推荐,通过阅读本文,您将收获以下知识点: 1.1 Camera 工作原理1.2 Camera 模组组成1.3 Camera 常见缩写解释1.4 Camera 部分名词解释1.5 参考文献 一、Camera 基础知识 1.1 Camera 工作原理 外部…

[进阶]Java:线程安全问题、取钱模拟

什么是线程安全问题? 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。 线程安全问题出现的原因? 存在多个线程在同时执行同时访问一个共享资源存在修改该共享资源 代码演示如下: 账户类&#xff…

深蓝学院C++基础与深度解析笔记 第 5 章 语句

1. 语句基础 ● 语句的常见类别 – 表达式语句:表达式后加分号,对表达式求值后丢弃,可能产生副作用 – 空语句:仅包含一个分号的语句,可能与循环一起工作 – 复合语句(语句体):由大…

软考A计划-系统集成项目管理工程师-信息系统集成及服务管理体系

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列 👉关于作者 专注于Android/Unity和各种游戏开发技巧&#xff…

Flutter Dart 变量和内置类型

目录 一、变量 1.1 var 1.2 Object 1.3 dynamic 1.4 final与const 二、内置类型 2.1 num(数值) 2.2 Strings(字符串) 2.3 bool(布尔值) 2.4 List(列表) 2.5 Map(映射集…