问题
软件打包为什么需要版本呢?
我个人觉得,这个软件包的版本对于快速迭代的中小型公司是一个非常好的东西。
常见的场景是,测试会问当前跑的是哪个版本?作为一个协作团体,多个环境的情况下,如果要知道版本需要问前端,后端,运维一圈本身就是在浪费时间。
尽管软件带上版本号不能解决所有的问题,在一定程度上还是可以解决研发过程中可能存在的版本差异问题。
因此软件自身能够以某种形式暴露出版本号,对于定位问题代码起了很大的作用。
何为版本号
先看例子
origin/master#250616-154759-sha1:617bcf6
或者
origin/master#250616-154759-sha1:617bcf6_build108
一般来说,一个版本是分支+git commit id 组成。当然自己可以加入其他的信息。
大公司也有这种,atlassion 公司的jira产品会在页面最下方显示它的版本
Atlassian Jira Project Management Software (v8.5.0#805000-sha1:facbf8b)
根据我实际的经验,我建议jira的版本号是比较好一个组合。
- v8.5.0 迭代版本 (通常是Git 分支)
- 805000 多少次打包
- sha1:facbf8b 实际的Git commit id
对于Git Commit Id 作用等同于每一次提交。非常方便回查对应的源码。
分布式版本控制系统,每个 commit 都有一个唯一的 commit ID
这么说吧,没有Git commit id,则无法准确回溯到源码是什么。
所以说,缺少了Git commit id 的版本号的意义就不大了。
版本号总结
版本号是一串自定义的编码,它包括迭代版本号(虚拟)和源码记录号(真实,可追溯性)。
它的用途是呈现给用户,测试,开发,管理等多个岗位一个统一的当前运行软件是什么的。
在哪里呈现版本
以移动端和Web技术为主的软件可以简单分为前端和后端两个部分。如果有一些服务本身不产生响应,比如:中间件服务,也可以通过心跳等消息来向外暴露其版本号。
总结一下:
- 前端 - 页面下部位置,或者关于的地方
- 后端REST - header 比较合适
- 中间件 - 心跳消息中比较合适
版本号以什么形式存在
以配置文件的形式是常态,如果是容器打包,也可以直接作为环境变量导入容器中
不同的存储形式,在使用的时候,采用不同方式读取。
这个可以根据需求和便利做选择。
在什么阶段取得
通常是在打包阶段打入。如果是java则打包时候有plugin支持获取当前的Commint Id
如果是jenkins,则在环境变量中可以使用
需求和实现的例子
公司要求某Java开发系统每个REST需要返回版本号,以header 方式返回。
这样的考虑是因为:后端的系统 REST 非显式的反应即可。 从加载在 request body 还是 header ,更具有性价比。
整体流程
根据上述需求,我们可以简单设计为
- 软件编译时候打入版本号
- 软件运行时,响应REST服务的时候,将版本号附加到header
1-编译打包时候打入版本号
由于是java 平台,我们可以很简单找到一个maven build plugin - git-commit-id. 利用这个就很容易在打包阶段得到一个文本里面包括版本号。
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.2.3-withMavenProperties-SNAPSHOT</version>
<executions>
<execution>
<id>get-the-git-infos</id>
<goals>
<goal>revision</goal>
</goals>
</execution>
<execution>
<id>validate-the-git-infos</id>
<goals>
<goal>validateRevision</goal>
</goals>
<!-- *NOTE*: The default phase of validateRevision is verify, but in case you want to change it, you can do so by adding the phase here -->
<phase>package</phase>
</execution>
</executions>
<configuration>
<dotGitDirectory>${project.basedir}/.git</dotGitDirectory>
<prefix>git</prefix>
<!--<dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat>-->
<dateFormat>yyMMdd-HHmmss</dateFormat>
<dateFormatTimeZone>${user.timezone}</dateFormatTimeZone>
<!-- false is default here, it prints some more information during the build -->
<verbose>true</verbose>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<generateGitPropertiesFilename>${project.build.outputDirectory}/resources/version/latest.json
</generateGitPropertiesFilename>
<!-- <format>properties</format> -->
<format>json</format>
<skipPoms>true</skipPoms>
<injectAllReactorProjects>false</injectAllReactorProjects>
<failOnNoGitDirectory>true</failOnNoGitDirectory>
<failOnUnableToExtractRepoInfo>true</failOnUnableToExtractRepoInfo>
<skip>false</skip>
<runOnlyOnce>false</runOnlyOnce>
<includeOnlyProperties>
<includeOnlyProperty>^git.version.fullName$</includeOnlyProperty>
<includeOnlyProperty>^git.build.host$</includeOnlyProperty>
<includeOnlyProperty>^git.build.time$</includeOnlyProperty>
<includeOnlyProperty>^git.build.version$</includeOnlyProperty>
<includeOnlyProperty>^git.commit.id.abbrev$</includeOnlyProperty>
<includeOnlyProperty>^git.sprintName$</includeOnlyProperty>
<includeOnlyProperty>^git.jenkins$</includeOnlyProperty>
<includeOnlyProperty>^BUILD_TAG$</includeOnlyProperty>
</includeOnlyProperties>
<useNativeGit>false</useNativeGit>
<abbrevLength>7</abbrevLength>
<commitIdGenerationMode>flat</commitIdGenerationMode>
<gitDescribe>
<skip>false</skip>
<always>false</always>
<abbrev>7</abbrev>
<dirty>-dirty</dirty>
<match>*</match>
<tags>false</tags>
<forceLongFormat>false</forceLongFormat>
</gitDescribe>
<validationProperties>
<!--不能通过 x.x.-SNAPSHOT -->
<!-- <validationProperty>
<name>validating project version</name>
<value>${project.version}</value>
<shouldMatchTo><![CDATA[^.*(?<!-SNAPSHOT)$]]></shouldMatchTo>
</validationProperty> -->
</validationProperties>
<validationShouldFailIfNoMatch>true</validationShouldFailIfNoMatch>
</configuration>
</plugin>
上面是一个配置例子。
2 - 响应REST服务的时候,将版本号附加到header
如果是使用Java spring boot 2.x
@Component
@WebFilter(urlPatterns = "/*", filterName = "ApplicationHeaderFilter")
@Slf4j
public class ApplicationHeaderFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
JsonVersion jsonVersion = JsonVersion.instance();
if (jsonVersion == null) {
log.warn("jsonVersion not set!");
} else {
response.setHeader("x-your-app-version", jsonVersion.getBuildVersion());
response.setHeader("x-your-app-build", jsonVersion.getJenkins());
}
response.setHeader("x-frame-options", "SAMEORIGIN");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
上述的 JsonVersion 是在 ApplicationVersionContextInitializer implements ApplicationContextInitializer 中进行初始化注入的。
Resource resource = applicationContext.getResource(VERSION_PATH);
if (resource.exists()) {
log.info("注入应用程序版本-{}", VERSION_PATH);
String jsonString = openJsonAsString(resource.getInputStream());
//JavaPropsMapper mapper = new JavaPropsMapper();
ObjectMapper mapper = new ObjectMapper();
try {
JsonVersion jsonVersion = mapper.readValue(jsonString, JsonVersion.class);
JsonVersion.setInstance(jsonVersion);
log.warn("GIT_TAG=[{}]", jsonVersion.getBuildVersion().toString());
log.warn("BUILD_TAG=[{}]", jsonVersion.getJenkins().toString());
applicationContext.getEnvironment().getSystemProperties().put("GIT_TAG", jsonVersion.getBuildVersion().toString());
} catch (IOException e) {
log.error("无法获取应用的版本信息");
e.printStackTrace();
}
} else {
log.warn("没有发现应用程序版本信息-{}", VERSION_PATH);
}
log.info(this.getClass().getName() + " 初始化完成 ... ");
效果
整个过程,总体上说还是比较简单。希望对从事总体设计和开发的朋友有所帮助。
也欢迎评价和讨论!
请收藏问题合集,方便随时查找
jenkins的全解-工欲善其事,必先利其器
最近夏天跳闸断电,莫名几个虚拟机数据丢了。。哎,
【某东 USB4 接口 的硬盘盒-一个字 快! USB 4 接口就是快,就是不用等那种快!