背景:放弃 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];
}
}
}