文档章节

桌面程序的启动器 第一次重构

吴晨禹
 吴晨禹
发布于 2017/03/23 16:54
字数 1467
阅读 27
收藏 1

背景:放弃 web start 的方式后,也就没有了自动更新。但是可以自己实现自动更新。

原理:用启动器引导主程序。启动器工作的时候先去服务端查询待更新的文件并下载,全部下载完毕后再启动主程序

启动器要做的工作:

1)装载配置内容

2)去服务端查询需要更新的文件并下载。这里所谓的更新是指服务端文件内容与本地磁盘上的文件内容不一致。文件内容的比较是用 MD5 的方式进行比较

3)在下载文件的时候更新启动画面,譬如显示下载进度条,显示正在下载的文件

4)生成一些额外的文件(这个步骤不是必须的)

5)启动主程序

重构的原因:旧的程序中,所有步骤全部在源代码里写死了,一旦有缺省的步骤不能满足需求,就要翻出源代码去修改;一旦需要增加步骤,还得去源代码里找到合适的位置并续写。大家都知道这样的方式很烦。

改进的方式:启动器实质上就是各个步骤的串联,为了简化,我就规定“步骤”没有前后联结关系(没有联结关系就是说,在程序代码上看,步骤不依赖前一个步骤),仅有步骤的序号(序号的目的是为了按照次序执行)

抽象:

先来定义“步骤”

package com.timing.appLaunch.interfaces;
/**
 * 抽象的执行步骤
 *
 * @param <T> 参数类型
 * @author hardneedl
 */
public interface Step<T> {
    /**
     * 执行步骤定义的内容
     *
     * @param param 执行期间用的参数
     * @return <code>true</code> 表示成功; <code>false</code>表示失败
     * @throws RuntimeException 运行期间异常
     */
    void perform(T param) throws RuntimeException;

    /**
     * 步骤的次序号
     * @return 步骤的次序号
     */
    int getSequnce();

    /**
     * 启动
     * @return 初始化后得到的内容
     * @throws RuntimeException 运行期异常
     */
    T start()throws RuntimeException;

    /**
     * 步骤结束的时候进行操作
     * @throws RuntimeException 运行期异常
     */
    void stop()throws RuntimeException;
}

把“步骤”串联起来的 Caller

package com.timing.appLaunch.interfaces;
/**
 * 步骤的启动器
 * @author hardneedl
 */
public interface StepsCaller {
    /**
     * 调动全部的步骤
     */
    void callAll() throws RuntimeException;

    /**
     * 得到全部必须的步骤
     * @return 步骤的列表
     */
    java.util.List<Step> getSteps();
}

至此,这个启动器的大骨架已经出来。先看一下启动器是如何开始工作

package com.timing.appLaunch;
import com.timing.appLaunch.interfaces.*;
/**
 * 启动的入口。预先编排好的执行步骤用 Step 接口表示,步骤有序号,按照序号的升序依次执行步骤
 * @author hardneedl
 */
final public class AppLaunch {
    public static void main(String... args) {
        StepsCaller stepsCaller = (StepsCaller) ObjectFactory.getObject(StepsCaller.class);
        stepsCaller.callAll();
    }
}

怎么体现出来“可替换”?那就是 Service Provider 的打包方式了。具体参看 JDK API 中 java.util.ServiceLoader

查询变更了的文件

服务端输出一个xml文档

服务端的每个文件都给出了 MD5 ,让启动器去比对本地的每个文件:当 MD5 不一致的,即是待下载的文件。服务端就是一个简单的servlet:列出能被客户端下载的文件,组装成 xml 文档。

关于可替换的实现

ObjectFactory 类里使用 ServiceLoader 进行类的实例装载。因此,一旦缺省的步骤不满足需求,那就重写一个步骤的实现并按照 Service Provider 的要求打包成 jar 替换掉旧的步骤。这个新实现的步骤需要被指派正确的步骤序号。

package com.timing.appLaunch.interfaces;
import java.util.*;
/**
 * @author hardneedl
 */
public class ObjectFactory {
    public static Object getObject(Class c){
        Iterator itr = ServiceLoader.load(c).iterator();

        Object o = null;

        while(itr.hasNext()) {
            o = itr.next();
            if (o != null)
                break;
        }

        return o;
    }
}

 

代码示例

    StepsCaller 的一个缺省实现

package com.timing.appLaunch.impl;
import com.timing.appLaunch.interfaces.*;
import java.util.*;
/**
 * 缺省的步骤执行器。按照步骤序号升序排列每个步骤并执行
 * @author hardneedl
 */
final public class SimpleStepsCaller implements StepsCaller {
    public void callAll() throws RuntimeException {
        getSteps().stream().sorted((o1, o2) -> {
            int a = o1.getSequnce(),
                b = o2.getSequnce();

            if (a == b) return 0;
            if (a > b) return 1;

            return -1;
        })
            .forEach(step -> {
                Object o = step.start();
                step.perform(o);
                step.stop();
            });
    }

    public List<Step> getSteps() {
        Iterator<Step> it = ServiceLoader.load(Step.class).iterator();
        List<Step> L = new ArrayList<>(5);
        it.forEachRemaining(L::add);
        return L;
    }
}

 

下载更新

 

package com.timing.appLaunch.impl;
import com.timing.appLaunch.interfaces.*;
import com.timing.common.config.*;
import java.awt.*;
import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.logging.*;
import java.util.stream.*;
/**
 * @author hardneedl
 */
public class UpdateStep implements Step<java.util.List<UpdateEntry>>{
    static private final Config CONFIG = ConfigFactory.getTextConfig();
    static final private Observable progressObservable = new Observable(){
        public void notifyObservers(Object arg) {
            setChanged();
            super.notifyObservers(arg);
        }
    };

    /**
     * 创建 jar 文件下载的路径。
     * 检查系统属性 targetDir , 当其值是空的时候使用缺省值 ./jars 表示当前工作路径上的目录 jars;
     * 否则使用系统属性 targetDir 的值所表示的目录
     * @return jar 文件下载的路径
     */
    synchronized static private File getTargetDir(){
        File targetDir = new File(CONFIG.getString("update.defaultDownloadPath"));
        if (!targetDir.exists())
            targetDir.mkdirs();
        return targetDir;
    }

    public void perform(java.util.List<UpdateEntry> ue) throws RuntimeException {
        File targetDir = getTargetDir();

        //下载任务放进列表
        List<DownLoader> downloaderList = ue.stream().map(updateEntry -> {
            Object downLoaderObj = ObjectFactory.getObject(DownLoader.class);
            if (downLoaderObj instanceof DownLoader) {
                DownLoader downLoader = (DownLoader)downLoaderObj;
                downLoader.setTarget(new File(targetDir, updateEntry.getName()));
                downLoader.setUrl(updateEntry.getUrl());
                return downLoader;
            }
            else {
                return null;
            }
        })
        .filter(Objects::nonNull)//过滤掉空元素
        .collect(Collectors.toList());//收集成最终结果

        //画笔和下载进度条
        SplashScreenBrush brush = (SplashScreenBrush) ObjectFactory.getObject(SplashScreenBrush.class);
        if (brush != null) {
            progressObservable.addObserver(brush);

            SplashScreen splashScreen = SplashScreen.getSplashScreen();

            if (splashScreen != null) {

                Class<SplashScreenBrush> brushClass = (Class<SplashScreenBrush>) brush.getClass();

                try {
                    Method setSplashScreenMethod = brushClass.getDeclaredMethod("setSplashScreen", SplashScreen.class);
                    Method setTotalMethod = brushClass.getDeclaredMethod("setProgressTotal", int.class);

                    splashScreen.setImageURL(new URL(System.getProperty("splash.brush.imageURL")));
                    setSplashScreenMethod.invoke(brush, splashScreen);
                    setTotalMethod.invoke(brush, downloaderList.size());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

            }

            ExecutorService executorService = Executors.newCachedThreadPool();
            List<Callable<File>> downloadCallableList = new ArrayList<>(10);
            downloaderList.forEach(
                downLoader->downloadCallableList.add(
                    ()->{
                        downLoader.download();
                        return downLoader.getTarget();
                    }
            ));


            int timeout = CONFIG.getInteger("update.downloader.futureTimeout", 100);
            Convertor<String,Long> fileSizeConvertor = new FileSizeFormater();

            try {
                List<Future<File>> downloadFutures = executorService.invokeAll(downloadCallableList);

                for(Future<File> f : downloadFutures) {
                    File file = f.get(timeout, TimeUnit.SECONDS);
                    String fileSizeString = fileSizeConvertor.convert(file.length());
                    Logger.getLogger("AppLaunch").info(CONFIG.getFormattedString("update.downloader.fileCompleted",file.getAbsolutePath(), fileSizeString));
                    progressObservable.notifyObservers(file);
                }

            } catch (InterruptedException | ExecutionException | TimeoutException e) {
                throw new RuntimeException(e);
            }

            SplashScreen splashScreen1 = brush.getSplashScreen();
            if (splashScreen1 != null) {
                splashScreen1.close();
            }

        }

    }

    public int getSequnce() {return 1;}

    public java.util.List<UpdateEntry> start() throws RuntimeException {
        UpdateChecker updateChecker = (UpdateChecker) ObjectFactory.getObject(UpdateChecker.class);
        try {
            return updateChecker.getEntries(new URL(System.getProperty("updateXml")));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void stop() throws RuntimeException {}

    /**
     * 格式化文件体积适合阅读
     */
    final static private class FileSizeFormater implements Convertor<String,Long>{
        private static final String[] UNITS = new String[]{"B","KB","MB","GB","TB","PB"};
        private static final double MOD = 1024.0;
        public String convert(Long l) throws RuntimeException {
            double fileSize = (double)l;
            int i;
            for (i = 0; fileSize >= MOD; i++) {
                fileSize /= MOD;
            }
            return Math.round(fileSize) + " " + UNITS[i];
        }
    }

}

 

© 著作权归作者所有

共有 人打赏支持
吴晨禹
粉丝 3
博文 4
码字总数 3241
作品 0
大连
后端工程师
私信 提问
深度操作系统 15.4 —— 由内而外,与众不同

深度操作系统是一个致力于为全球用户提供美观易用、安全可靠的Linux发行版。 深度操作系统 15.4采用全新设计的控制中心以及重构桌面,模糊透明整体风格,全新的热区交互及窗口管理器动效,集...

melodyzou
2017/04/19
10.8K
135
Elive 2.9.12 发布,基于 Debian 的桌面 Linux 发行

Elive 2.9.12 发布了。Elive,或者称为 Enlightenment live CD,是基于 Debian 的桌面 Linux 发行及自启动运行光盘,其特色在于使用 Enlightenment 窗口管理器。除了进行预配置并适合于日常桌...

达尔文
2017/10/28
629
3
DevExpress v15.1:XAF控件升级

<下载最新版DevExpress eXpressApp Framework(XAF) v15.1> 为触摸设备优化网页模板和主题(CTP) 此版本包含了一个新的Web主题,专为设计XAF web应用程序和构建新的页面模板。这些改进将显著...

Miss_Hello_World
2015/09/08
174
0
Rider 2017.1 首个 RC 版发布:性能、重构改进

好消息!Rider 2017.1 首个 RC 版正式发布了,现可下载! 在功能方面,新版本改进了性能、重构和对 Unity 的支持,以及调试器方面的改进,启用了更多的 WebStorm 功能,添加了对 VB.NET 类型...

局长
2017/07/16
3.2K
11
深度操作系统 15.4.1 修正版本发布

深度操作系统是一个致力于为全球用户提供美观易用、安全可靠的Linux发行版。 深度操作系统15.4.1为修正版本,新增了经典菜单和打开应用等待效果、回归窗口预览效果、优化窗口管理器2D模式;修...

melodyzou
2017/07/21
2.5K
50

没有更多内容

加载失败,请刷新页面

加载更多

linux 静态IP、DNS、主机名配置

linux 静态IP、DNS、主机名配置 一、IP配置 进入网络配置文件目录 cd  /etc/sysconfig/network-scripts/ 列出当前目录下文件名 ll 编辑配置文件 注意:ifcfg-eth0是我电脑的一个网卡标识...

小儿
13分钟前
4
0
配置vagrant使用三种网络

使用vagrant安装之后一直使用127.0.0.1进行访问。但是一直使用这个ip肯定满足不了的。我们装肯定是有需求的,那么本节就讲解一下用vagrant的网络配置。 vagrant中一共提供了三种网络配置。 ...

echojson
14分钟前
0
0
重磅发布:阿里开源 OpenJDK 长期支持版本 Alibaba Dragonwell

3 月 21 日北京阿里云峰会,阿里巴巴正式宣布对外开源 OpenJDK 长期支持版本 Alibaba Dragonwell。作为 Java 全球管理组织 Java Community Process (JCP) 的最高执行委员会的唯一中国代表,以...

阿里云官方博客
14分钟前
1
0
重磅发布:阿里开源 OpenJDK 长期支持版本 Alibaba Dragonwell

3 月 21 日北京阿里云峰会,阿里巴巴正式宣布对外开源 OpenJDK 长期支持版本 Alibaba Dragonwell。作为 Java 全球管理组织 Java Community Process (JCP) 的最高执行委员会的唯一中国代表,以...

阿里云云栖社区
20分钟前
0
0
OAuth2实现单点登录SSO

1. 前言 技术这东西吧,看别人写的好像很简单似的,到自己去写的时候就各种问题,“一看就会,一做就错”。网上关于实现SSO的文章一大堆,但是当你真的照着写的时候就会发现根本不是那么回事...

java菜分享
20分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部