利用github release实现客户端程序的版本控制和自动更新

  最近思考了下客户端的自动更新问题,写了个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,进度条和两个按钮。按钮分别对应最新版本显示和文件下载。

客户端显示最新版本

  为了将网络操作和界面更新线程分离,利用SwingWorker创建新线程进行处理,并利用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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值