最近思考了下客户端的自动更新问题,写了个demo。
主要思路是客户端通过api获取最新版本并下载文件,之后调用脚本替换文件,关闭客户端,再重启,实现自动更新的效果。
github release
为了实现客户端更新,那么就需要有服务端存放最新的文件和版本信息,github release提供了存放文件以及其对应版本的功能,并且可以提供对应的api来获取下载链接和版本信息。
Github release的api url格式为https://api.github.com/repos///releases/latest,有name和代码库名即可。
返回数据为JSON格式,示例如下:
其中tag_name就是当前release中最新的文件版本,assets即为release中的文件,并且每个文件都提供了browser_download_url,直接调用url就可实现下载。
版本获取实现
利用http client实现get请求发送,获取api信息。
Maven依赖
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
Get请求
// 利用http client发起get请求获取信息
public static String sendGetRequest(String url){
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = null;
String result = null;
int status = 0;
try {
response = httpClient.execute(httpGet);
// 从响应模型中获取响应实体
HttpEntity responseEntity = response.getEntity();
// 响应状态
status = response.getStatusLine().getStatusCode();
// 响应结果
result = EntityUtils.toString(responseEntity);
System.out.println(status);
System.out.println(result);
if(status != 200)
result = null;
return result;
}catch(SocketException e){
e.printStackTrace();
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (httpClient != null) {
httpClient.close();
}
if (response != null) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
利用httpGet发送请求,获取responseEntity即可。
JSON数据处理,获取当前version信息。
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
public class VersionInfo {
private static String VERSION = "0.2";// 当前版本
private static String VERSIONURL = "https://api.github.com/repos/huiluczP/autoupdate/releases/latest";// 获取当前最新版本的地址
private Map map;
public String getVersionInfo(){
String result = HttpRequest.sendGetRequest(VERSIONURL);
if(result!=null){
Map map = (Map) JSON.parse(result);
this.map = map;
return "Version info get success";
}else{
return "Version info get failed";
}
}
public String getLatestVersion(){
if(this.map!=null){
return map.get("tag_name").toString();
}else{
return null;
}
}
VersionInfo类中利用一个常量来存储当前版本和api url。同时,对JSON处理使用了fastjson
,支持直接将JSON字符串反序列化为map对象,还是挺方便的。
文件下载链接获取与文件下载
文件下载提供了download url,也使用http client
进行下载处理。
要注意的是,github的https是TSL1.2
协议的,而jdk1.7
以下不支持,使用时可能出现connect reset错误。
为了文件下载链接对应,设计一个bean类,包含文件名和对应的url。
public class DownloadInfo {
private String name;
private String url;
public DownloadInfo(String name,String url){
this.name = name;
this.url = url;
}
// setter&getter
}
利用api获取的map获取下载连接和文件信息
public ArrayList<DownloadInfo> getDownLoadUrl(){
// 返回当前文件的下载列表
ArrayList<DownloadInfo> downloadInfos = new ArrayList<DownloadInfo>();
if(this.map!=null){
List l =JSON.parseArray(map.get("assets").toString());
for (Object s:l){
Map simpleMap = (Map) JSON.parse(s.toString());
downloadInfos.add(new DownloadInfo(simpleMap.get("name").toString(), simpleMap.get("browser_download_url").toString()));
}
return downloadInfos;
}else{
return downloadInfos;
}
}
利用http client实现下载
public class HttpDownload {
private static final int cache = 10 * 1024;
private static final String splash;
private static final String root;
static {
splash = "/";
root = "download";
}
// 根据url下载文件,保存到filepath中
public static boolean download(String url, String filename, JProgressBar bar) {
System.out.println("start downloading");
try {
// cookie时间可能会出错,设置下
CloseableHttpClient client= HttpClients.custom()
.setDefaultRequestConfig(RequestConfig.custom()
.setCookieSpec(CookieSpecs.STANDARD).build())
.build();
HttpGet httpget = new HttpGet(url);
HttpResponse response = client.execute(httpget);
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
String filepath = getFilePath(filename);
File file = new File(filepath);
boolean makeDir = file.getParentFile().mkdir();
System.out.println(file.getAbsolutePath());
FileOutputStream fileOut = new FileOutputStream(file);
// 根据实际运行效果 设置缓冲区大小
byte[] buffer = new byte[cache];
int ch = -1;
while ((ch = is.read(buffer)) != -1) {
// 假进度条
int valueNow = bar.getValue();
if(valueNow <= 80) {
bar.setValue(valueNow + 5);
}else{
bar.setValue(valueNow + 2);
}
System.out.println("cache " + filename);
fileOut.write(buffer, 0, ch);
}
bar.setValue(100);
is.close();
fileOut.flush();
fileOut.close();
System.out.println(filename + " download success");
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
// 获取下载路径
private static String getFilePath(String fileName) {
String filepath = root + splash;
filepath += fileName;
return filepath;
}
download方法中实现下载,并保证文件的父文件夹存在。需要注意的是,进行下载的get请求可能会出现cookie中时间格式不匹配
的错误,使用setCookieSpec
方法将cookie的解析方式改为标准。方法中的bar参数是客户端的进度条组件,这边耦合度比较高,有空重新设计一下。
客户端界面
客户端简单使用Swing实现,考虑到后台操作可能导致主界面假死的问题,使用SwingWorker实现AWT线程和后台线程的分离处理。
Swing界面:
public class MainScreen extends JFrame {
private VersionInfo versionInfo;
private String latestVersion = null;
private String currentVersion = null;
private JFrame frame = this;
private JPanel mainPanel;
private JPanel versionPanel;
private JPanel processPanel;
private JPanel buttonPanel;
private JLabel versionLabel;
private JLabel processLabel;
private JProgressBar uploadProcess;
private JButton checkButton;
private JButton updateButton;
public MainScreen(){
versionInfo = new VersionInfo();
initComponent();
}
private void initComponent(){
mainPanel = new JPanel();
mainPanel.setLayout(new MigLayout("",
"10px[grow]10px",
"5px[grow]5px[grow]5px[grow]5px"));
this.add(mainPanel, BorderLayout.CENTER);
versionPanel = new JPanel();
versionLabel = new JLabel();
currentVersion = versionInfo.getCurrentVersion();
versionLabel.setText("current version:" + currentVersion);
versionPanel.add(versionLabel, BorderLayout.CENTER);
mainPanel.add(versionPanel, "cell 0 0");
processPanel = new JPanel();
processPanel.setLayout(new MigLayout("",
"10px[grow]10px",
"[grow]5px[grow]"));
uploadProcess = new JProgressBar();
uploadProcess.setStringPainted(true);
uploadProcess.setValue(0);
processLabel = new JLabel("");
processPanel.add(processLabel, "cell 0 0");
processPanel.add(uploadProcess, "cell 0 1");
mainPanel.add(processPanel, "cell 0 1");
buttonPanel = new JPanel();
buttonPanel.setLayout(new MigLayout("",
"10px[grow]10px[grow]10px",
"[grow]5px"));
checkButton = new JButton("check version");
updateButton = new JButton("download update");
checkButton.addActionListener(new VersionGetListener());
updateButton.addActionListener(new UploadListener());
buttonPanel.add(checkButton, "cell 0 0");
buttonPanel.add(updateButton, "cell 1 0");
mainPanel.add(buttonPanel, "cell 0 2");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.setPreferredSize(new Dimension(300,400));
this.pack();
this.setVisible(true);
}
这边是导入了MigLayout
的依赖进行的页面设计,MigLayout能将Swing组件分割成cell块,排版比较方便。页面中主要组件为显示版本信息的label,进度条和两个按钮。按钮分别对应最新版本显示和文件下载。
客户端显示最新版本
为了将网络操作和界面更新线程分离,利用SwingWorke
r创建新线程进行处理,并利用ActionListener接口实现按钮的监听。SwingWorker中doInBackground
方法中代码在新线程中执行,done
为阻塞方法,当background执行完毕return时执行,同时该类实现get方法来获取后台返回内容。
// 获取最新version
private class VersionGetSwingWorker extends SwingWorker<String, Void>{
@Override
protected String doInBackground() throws Exception {
versionInfo.getVersionInfo();
return versionInfo.getLatestVersion();
}
@Override
protected void done() {
try {
latestVersion = get();
if(latestVersion!=null){
versionLabel.setText("<html> current version:" + currentVersion + "<br/>" +
"latest version:" + latestVersion + "</html>");
}else{
versionLabel.setText("<html> current version:" + currentVersion + "<br/>" +
"can't check latest version</html>");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 设置版本查询按钮监听方法
private class VersionGetListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
versionLabel.setText("<html>current version:" + currentVersion + "<br/>" +
"checking latest version......</html>");
new VersionGetSwingWorker().execute();
}
}
获取不到最新version时显示提示。Swing的label控件换行需要html标签和<br/>
,比较奇怪。
客户端实现文件下载和自动更新
调用上述文件下载方法,同时将JProcessBar进度条控件对象作为参数输入。
// 下载最新版本对应文件
private class updateSwingWorker extends SwingWorker<String, Void>{
@Override
protected String doInBackground() throws Exception {
String result = null;
if(latestVersion == null){
processLabel.setText("please check latest version first");
}else{
processLabel.setText("Downloading...");
ArrayList<DownloadInfo> infos = versionInfo.getDownLoadUrl();
result = Update.download(infos, uploadProcess);
}
return result;
}
@Override
protected void done() {
String result = null;
try {
result = get();
if(result!=null && !result.equals("")){
processLabel.setText("Download Success:" + result);
Runtime.getRuntime().exec("cmd /k start .\\update.bat");
close();
}else if(result!=null){
processLabel.setText("Download failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 设置下载更新按钮监听器
private class UploadListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
new updateSwingWorker().execute();
}
}
要注意的是,为了实现自动更新,在done中执行 Runtime.getRuntime().exec("cmd /k start .\\update.bat");
该行执行一个bat脚本文件,用来实现文件的替换。要注意的是,该行代码执行并不阻塞,所以该行执行后立即调用close
方法实现客户端关闭。
Close方法
private void close(){
frame.dispose();
}
这边实现比较简单,真实需求可能会保存些当前客户端数据等。
启动与文件替换bat脚本
Update.bat
@ping 127.0.0.1 -n 1 & move .\download\autoupdate.jar .\ & .\start.bat
这边ping一下本地是为了控制下时间,免得close没执行完文件就被换了,之后执行move命令进行更换,最后执行start脚本
Start.bat
@echo off
java -jar autoupdate.jar
start简单执行jar中的启动类,没什么好说的。
jar打包
因为使用idea进行开发,直接使用它提供的build->build artifact->build即可。要注意的是在这之前需要使用project structure在项目中创建MF文件并指定启动类。
实现效果
总结
项目实现了简单的客户端自动更新功能,利用github release做为版本控制媒介来实现。整个实现还是比较粗糙,当前客户端版本直接写在代码里,同时文件更新脚本也是写死的。后续优化可以利用xml
文件进行各个组件的版本控制,同时对照多个文件的版本,仅下载对应更新文件即可。总的来说,简单实现下验证下思路,可以看一看。
项目代码已上传至github https://github.com/huiluczP/autoupdate