【应用自更新】一、简单的更新程序

我觉得能够自更新的应用应该具备一下特点:

  • 首先应该独立于主程序,程序再完备,也不可能将结构设计的几近完美。所以独立于主程序的更新程序,主要目的在于更新主程序这样可以改变主程序的一切文件以及工程结构,甚至构建方式。
  • 更新程序至少有一个触发点,这个触发点能够使整个程序从头到尾更新,并且确保更新后能正常运行。
  • 至少有一个远程地址提供应用下载。
  • 除了以上必须内容之外,也需要有版本管理的能力,能够随意运行任意一个版本的程序。
  • 对于一个程序来说,能够修复问题,恢复有效的版本是对用户的工作的最大的保障。

基于以上自更新程序的功能设想,我觉得可以使用git稍作修饰,从而实现自更新程序。

自更新应用程序的目录结构:

  • ssh
    • id_rsa
    • id_rsa.pub
    • known_hosts
  • update.jar
  • update.json
    1
    2
    3
    4
    5
    6
    7
    {
    "version":"更新应用程序版本",
    "url":"更新url连接",
    "branch":"检出的分支名称",
    "out":"更新的目录位置",
    "ssh":"验证权限文件所在的目录"
    }

原则上更新的应用程序应该是可执行的应用,如果是java应用的话那就应该是打包好的jar文件才对。这样不至于在运行环境内进行构建浪费时间。如果需要构建的话,我倒是觉得可以写成运行本地命令的方式去构建应用程序,但是这样同时也要求程序开源了。暂时不考虑这种想法。还是以实现程序更新为主。

先将jgit源码拷贝的本地,并写几个测试类用于下载,解密等操作。

git clone 远程仓库地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Git.cloneRepository()
// 设置远程地址
.setURI(remoteRepoURI)
// 设置本地clone地址
.setDirectory(temp)
// 设置权限验证
.setTransportConfigCallback( new TransportConfigCallback() {
@Override
public void configure( Transport transport ) {
SshTransport sshTransport = ( SshTransport )transport;
sshTransport.setSshSessionFactory( sshSessionFactory );
}
})
// 设置检出分支
.setBranch(deployBranch)
.call();

其中的sshSessionFactory可以以这种形式生成:

1
2
3
4
5
6
7
8
9
10
11
SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() {
@Override
protected void configure(OpenSshConfig.Host hc, Session session){}
@Override
protected JSch createDefaultJSch(FS fs) throws JSchException {
JSch defaultJSch = super.createDefaultJSch(fs);
defaultJSch.addIdentity("/path/to/id_rsa");
defaultJSch.setKnownHosts("/path/to/known_hosts");
return defaultJSch;
}
};

使用maven构建了项目,并且已经可以使用ssh加密连接检出项目了,我想这些应该就足够了。

期间考虑过使用账号密码的形式去访问远程仓库检出代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13

public static void checkout(String uri,String key){
// 解密key 得到账号密码、
// ....
CredentialsProvider credentialsProvider =
new UsernamePasswordCredentialsProvider("username", "password");

Git git = Git.cloneRepository()
.setURI(remoteRepoURI).setDirectory(new File(localRepoPath))
.setCredentialsProvider(credentialsProvider)
.setBranch("deploy")
.call();
}

但是由于程序会被反编译,在客户端获取远程仓库的账号密码,这点有些不妥。所以经过考虑最终决定使用git仓库上自有的ssh只读加密方式进行授权检出仓库代码。建议:部署git仓库地址只有编译过的程序即可,如果有源码的话,经过某些手段还是可以看到这些代码的。毕竟更新应用本身能够访问远程仓库地址这点是不能限制的。

最终项目初步完成,但是再删除缓存文件夹的时候总是删除不干净。可能是因为文件在删除的时候被占用导致的。之后可以先完善一下应用然后解决一下这个问题。

最终这个update.json文件可以通过远程调用获取,这样就能控制更新内容以及版本了。同时更新update.json的同时还需要考虑更新ssh文件夹内的文件,让授权通过。一般情况下更新逻辑不变的话,这些文件应该是一直不变的。

解决问题之后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 先将需要的参数全部获取
String userDir = System.getProperty("user.dir"); // 工作目录
String updateJson = FileHelper.toString(new File(userDir+File.separator+"update.json")); // 更新json 可以通过远程获取
JSONObject jsonObj = JSON.parseObject(updateJson); // 使用fastJson进行转换获取参数
String version = jsonObj.getString("version"); // 版本
String sshDirName = jsonObj.getString("ssh"); // 授权文件目录名
String sshDir = userDir + ("".equalsIgnoreCase(sshDirName)?"":(File.separator + sshDirName)) ; // 授权文件目录
String remoteRepoURI = jsonObj.getString("url"); // 远程仓库连接
String outDirName = jsonObj.getString("out"); // 输出目录名
String outDir = userDir + ("".equalsIgnoreCase(outDirName)?"":(File.separator + outDirName)); // 输出目录
String deployBranch = jsonObj.getString("branch"); // 检出的分支名称
String tempNow = Code.md5(UUID.randomUUID().toString()); // 缓存文件夹名称
String tempPath = userDir + File.separator + "temp"; // 缓存主目录

// 清理遗留文件夹
File tempDir = new File(tempPath);
if(tempDir.exists()) FileHelper.del(tempDir);
File outDirFile = new File(outDir);
if(outDirFile.exists()) FileHelper.del(outDirFile);

// 创建ssh连接工厂类 处理授权
SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() {
@Override
protected void configure(OpenSshConfig.Host hc, Session session) {}
@Override
protected JSch createDefaultJSch(FS fs) throws JSchException {
JSch defaultJSch = super.createDefaultJSch(fs);
defaultJSch.addIdentity(sshDir + File.separator + "id_rsa");
defaultJSch.setKnownHosts( sshDir + File.separator + "known_hosts" );
return defaultJSch;
}
};

String tempGitPath = tempPath + File.separator + tempNow;
File tempGitFile = new File(tempGitPath);
if(!tempGitFile.exists()) tempGitFile.mkdirs();

// 检出远程仓库
Git.cloneRepository()
.setURI(remoteRepoURI).setDirectory(tempGitFile)
.setTransportConfigCallback( new TransportConfigCallback() {
@Override
public void configure( Transport transport ) {
SshTransport sshTransport = ( SshTransport )transport;
sshTransport.setSshSessionFactory( sshSessionFactory );
}
}).setBranch(deployBranch).call();

if(!outDirFile.exists())outDirFile.mkdirs();

// 复制缓存 过来.git文件夹
for(File f:tempGitFile.listFiles()){
if(f.getName().equalsIgnoreCase(".git")){
continue;
}else{
FileHelper.copy(f,outDirFile);
}
}

// 尝试删除一次缓存目录 可以不进行这一步
if(tempDir.exists()) FileHelper.del(tempDir);

log.info("update finish!!!");

至此通过利用git版本控制,一个简单的更新程序就完成了。但是这个更新程序还没有触发器,还不能自动更新。自动更新的方式无非就是让程序循环运行去监听一个触发点,一旦触发则直接运行更新程序。还有就是更新过程中程序不能运行占用文件,这样会导致删除或者覆盖不成功。这篇文章主要解决更新程序的逻辑问题,至于触发点,我想在之后的文章里在探讨。暂时的想法是通过邮件系统,或者是tcp维护心跳的方式进行更新。还有就是如果更新程序本身需要更新怎么办?可能需要程序主体,对更新程序先更新,在通过更新程序更新程序主体这种方式。这些问题会在今后的文章里一一探讨的。

欢迎访问博客地址:https://www.zhoyq.com