不使用第三方框架编写的多线程断线续传功能
一、背景
最近需要个断线续传功能,但是觉得一些框架不太适合,所以基于原理编写了一个多线程断线续传功能
支持技术分享,但是复制和转发我的博客时候请标明出处,谢谢 https://my.oschina.net/grkj/blog/2907188
二、断线续传的个人理解:
1、断线续传在个人理解,其实就是在出现正常下载流程之外的事情的时候,保存好当前文件下载的进度,然后点击继续下载的时候,从上次的下载进度继续进行下载。
2、如何从上次下载进度继续进行下载呢? 主要就是设置头部信息进行告知实现的
setRequestProperty("Range", "bytes=" + progress + "-" + total);//设置下载范围
三、主要功能有
1、支持多线程断线续传
2、支持回调事件拓展,使用泛型定义对象,支持更加灵活的去拓展对象
3、如果要下载的资源在要保存的文件夹中存在,那么会自动进行下载位置校准和下载
4、支持自定义资源请求的方式(GET和POST方式)和请求超时时间
5、我编不下了,如果你发现了就帮我写上去,谢谢....... 效果图如下
下载3只是装饰,你可以换个地址和修改一下MainActivity的按钮监控那块的代码,抄下载下载1和下载2的代码即可
后面我会完善个功能,只要连上网络就进行检查,然后自动进行资源下载,如果有需要可以给我留言
四、直接上源码讲解
篇幅太长,贴不了那么多,只贴8点 代码下载地址为:点击下载 源码里面DownLoadTask构造函数里面有个地方写错了,写死成了GET方式,如果下载源码的要使用,可以复制下面的DownLoadTask源码进去覆盖掉就好了
1、多线程实例,主要的内容都在这里了
//执行下载的线程 public class DownLoadTask implements Runnable { private static final String TAG = "DownLoadTask"; public static final int CACHE_SIZE = 4 * 1024;//缓冲区间,4应该足够了 public static final int DEFAULT_TIME_OUT = 5000;//单位是毫秒,默认是5秒,支持自定义 //线程安全的资源列表,key是文件名称,value是下载实例 private static ConcurrentHashMap<String, DownLoadEntity> mResourceMap = new ConcurrentHashMap<String, DownLoadEntity>(); /** * @Description 停止下载 * [@author](https://my.oschina.net/arthor) 姚旭民 * [@date](https://my.oschina.net/u/2504391) 2018/11/20 16:37 */ public static void stop(String key) throws NullPointerException { try { if (key == null) throw new NullPointerException(); mResourceMap.get(key).setStop(true); } catch (Exception e) { Log.e(TAG, e.toString()); } } /** * [@param](https://my.oschina.net/u/2303379) key 文件凭证 * @Description 资源删除 * @author 姚旭民 * @date 2018/11/20 17:22 */ public static void remove(String key) throws NullPointerException { if (key == null || mResourceMap.get(key) == null) throw new NullPointerException("参数为null或者下载数据不存在"); mResourceMap.get(key).setDelete(true); } //下载实体 DownLoadEntity mDownLoadEntity; //回调对象,只要进行实现,就可以获得各种事件的观察回调,IDownLoadCallBack源码在 第2点 有贴出来 IDownLoadCallBack mCallBack; //传输方式,是一个枚举类型,支持自定义传输 TransmissionType mType; //下载的超时时间 int mTimeout; public DownLoadTask(DownLoadEntity downLoadEntity, IDownLoadCallBack mCallBack) { this(downLoadEntity, mCallBack, TransmissionType.TYPE_GET); } public DownLoadTask(DownLoadEntity downLoadEntity, IDownLoadCallBack mCallBack, TransmissionType type) { this(downLoadEntity, mCallBack, type, DEFAULT_TIME_OUT); } public DownLoadTask(DownLoadEntity downLoadEntity, IDownLoadCallBack mCallBack, TransmissionType type, int timeout) { this.mDownLoadEntity = downLoadEntity; this.mCallBack = mCallBack; this.mType = type; this.mTimeout = timeout; //数据存储 mResourceMap.put(downLoadEntity.getKey(), downLoadEntity); Log.v(TAG, "存放数据进入键值对,key:" + downLoadEntity.getKey() + ",downLoadEntity:" + downLoadEntity); } @Override public void run() { //下载路径 String downUrl = mDownLoadEntity.getDownUrl(); //保存路径 String savePath = mDownLoadEntity.getSavePath(); //已经下载的进度 long progress = mDownLoadEntity.getProgress();//已经下载好的长度 long total = mDownLoadEntity.getTotal();//文件的总长度 String key = mDownLoadEntity.getKey(); HttpURLConnection connection = null; //有人可能觉得NIO 的 FileChannel 也可以的话,那么你也可以替换掉 RandomAccessFile randomAccessFile = null; try { //设置文件写入位置 File file = new File(savePath); //父类文件夹是否存在 File fileParent = file.getParentFile(); if (!fileParent.exists()) {//如果父类文件夹不存在,即创建文件夹 Log.v(TAG, "父类文件夹:" + fileParent.getPath() + ",不存在,开始创建"); fileParent.mkdirs(); } if (file != null) {//这一步是针对于断线续传的文件,用于比对数据库和真实的数据,避免出现误差 long fileSize = file.length(); if (progress != fileSize) {//如果文件有问题,以实际下载的文件大小为准 Log.v(TAG, "文件传输节点不一致,开始修复传数据节点"); progress = fileSize; mDownLoadEntity.setProgress(progress); } } int precent = (int) ((float) progress / (float) total * 100); //开始下载之前先回调开始下载的进度 mCallBack.onNext(key, precent); URL url = new URL(downUrl); connection = (HttpURLConnection) url.openConnection(); //请求方式默认为GET connection.setRequestMethod(mType.getType()); //超时时间 connection.setConnectTimeout(mTimeout); //从上次下载完成的地方下载 //设置下载位置(从服务器上取要下载文件的某一段) connection.setRequestProperty("Range", "bytes=" + progress + "-" + total);//设置下载范围 randomAccessFile = new RandomAccessFile(file, "rwd"); //从文件的某一位置开始写入 randomAccessFile.seek(progress); if (connection.getResponseCode() == 206) {//文件部分下载,返回码为206 InputStream is = connection.getInputStream(); byte[] buffer = new byte[CACHE_SIZE]; //接收到的资源大小 int len; while ((len = is.read(buffer)) != -1) { //写入文件 randomAccessFile.write(buffer, 0, len); progress += len; precent = (int) ((float) progress / (float) total * 100); //更新进度回调 mCallBack.onNext(key, precent); //停止下载 if (mDownLoadEntity.isStop()) { mDownLoadEntity.setProgress(progress); mCallBack.onPause(mDownLoadEntity, key, precent, progress, total); return; } //取消下载 if (mDownLoadEntity.isDelete()) { mResourceMap.remove(key); //文件删除 file.delete(); mCallBack.onDelete(mDownLoadEntity, key); return; } } } //资源删除 mResourceMap.remove(mDownLoadEntity.getFileName()); //下载完成 mCallBack.onSuccess(mDownLoadEntity, key); } catch (Exception e) { //资源删除 mResourceMap.remove(mDownLoadEntity.getFileName()); mDownLoadEntity.setProgress(progress); //防止意外 mDownLoadEntity.setStop(false); //失败原因回调 mCallBack.onFail(mDownLoadEntity, key, e.toString()); StringBuffer sb = new StringBuffer(); Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); e.printStackTrace(printWriter); Throwable cause = e.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.close(); //异常的详细内容 String result = writer.toString(); Log.e(TAG, result); } finally { if (connection != null) { connection.disconnect(); } try { if (randomAccessFile != null) { randomAccessFile.close(); } } catch (IOException e) { StringBuffer sb = new StringBuffer(); Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); e.printStackTrace(printWriter); Throwable cause = e.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.close(); //异常的详细内容 String result = writer.toString(); Log.e(TAG, result); } } } }
2、IDownLoadCallBack源码,这里的泛型主要是因为和公司一些业务有关,这里没有列出来,这里的泛型其实可以去掉的,因为基本这里没什么用的,T 都改成 DownLoadEntity实例即可
public interface IDownLoadCallBack<T> { /** * @param key 下载的文件的标识,主要用于显示的时候辨别是哪个文件在操作,由使用的人去定义 * @param precent 已经下载的百分比 取值区间为 [0,100] * @Description * @author 姚旭民 * @date 2018/11/20 9:46 */ public abstract void onNext(String key, int precent); /** * @param t 下载的文件的实体封装类 * @param key 下载的文件的标识,主要用于显示的时候辨别是哪个文件在操作,由使用的人去定义 * @param precent 已经下载的百分比 * @param downLoadSize 已经下载的长度 * @param total 资源的总长度 * @Description * @author 姚旭民 * @date 2018/11/20 10:48 */ public abstract void onPause(T t, String key, int precent, long downLoadSize, long total); /** * @Description 删除文件回调 * @author 姚旭民 * @date 2018/11/22 10:47 * * @param t 操作的下载对象 * @param key 文件凭证 */ public abstract void onDelete(T t, String key); /** * @param t 自定义的值 * @param key 下载的文件的标识,主要用于显示的时候辨别是哪个文件在操作,由使用的人去定义 * @Description * @author 姚旭民 * @date 2018/11/20 9:46 */ public abstract void onSuccess(T t, String key); /** * @param t 自定义的值 * @param key 下载的文件的标识,主要用于显示的时候辨别是哪个文件在操作,由使用的人去定义 * @param reason 失败原因 * @Description * @author 姚旭民 * @date 2018/11/20 9:46 */ public abstract void onFail(T t, String key, String reason);
3、IDownLoadCallBack包装类继承,包装类用于包装泛型对象,其实这一步可以不要的,只是有点别的考虑,所以这样写
/** * @Description 包装类 * @author 姚旭民 * @date 2018/11/20 13:57 */ public interface IResumeCallBack extends IDownLoadCallBack<ResumeEntity> { }
4、ResumeEntity对象源码主要继承了DownLoadEntity(第5点),其他没什么的
public class ResumeEntity extends DownLoadEntity { public static enum STATUS { FAIL(-1),//下载失败 DOWNLOAD(0),//下载中 SUCCESS(1);//下载成功,可以使用 private int value; private STATUS(int value) { this.value = value; } public int getValue() { return value; } } ResumeEntity(builder builder) { this.fileName = builder.fileName; this.downUrl = builder.downUrl; this.savePath = builder.savePath; this.total = builder.total; this.progress = builder.progress; this.status = builder.status; this.key = builder.key; } //链式编程,防止对象不一致,用static修饰,避免被保留强引用 public static class builder { private String fileName; private String downUrl; private String savePath; private long total; private long progress; private int status; private boolean stop; private String key; public builder fileName(String fileName) { this.fileName = fileName; return this; } public builder downUrl(String downUrl) { this.downUrl = downUrl; return this; } public builder savePath(String savePath) { this.savePath = savePath; return this; } public builder total(long total) { this.total = total; return this; } public builder progress(long progress) { this.progress = progress; return this; } public builder status(int status) { this.status = status; return this; } public builder stop(boolean stop) { this.stop = stop; return this; } public builder key(String key) { this.key = key; return this; } public ResumeEntity builder() { return new ResumeEntity(this); } } @Override public String toString() { return "{" + "fileName='" + fileName + '\'' + ", downUrl='" + downUrl + '\'' + ", savePath='" + savePath + '\'' + ", total=" + total + ", progress=" + progress + ", status=" + status + ", stop=" + stop + ", key='" + key + '\'' + '}'; } }
5、DownLoadEntity源码区域
public class DownLoadEntity { //资源文件的名称 protected String fileName; //资源文件的下载路径 protected String downUrl; //资源文件的保存完整路径 protected String savePath; //下载的资源的总长度 protected long total; //已经下载的进度 protected long progress; //资源的状态 //下载的状况 1为下载成功,0为可下载, -1为下载失败 默认为0 protected int status; //是否暂停下载 true为暂停下载, false代表可以下载, 默认为false protected boolean stop; //下载的文件的标识,让使用者更加灵活的去定义如何识别正在下载的文件 protected String key; //是否删除下载的文件 protected boolean delete; //这里是各种set和get,不花费篇幅粘贴了,直接用工具生成就好了 }
6、IDownLoadCallBack的实现类,我是不想每次都创建一个匿名类了,太长了也繁琐,我直接用activity去实现IDownLoadCallBack,感觉也挺好的,这里是随便写的activity,主要用来测试的,UI界面源码在第7点
public class MainActivity extends AppCompatActivity implements View.OnClickListener, IResumeCallBack, INetCallBack { private static final String TAG = "MainActivity"; //数据库操作辅助类 private ResumeDbHelper mHelper = ResumeDbHelper.getInstance(); private ResumeService mResumeService = ResumeService.getInstance(); private MainActivity mInstance = this; private Button downloadBtn1, downloadBtn2, downloadBtn3; private Button pauseBtn1, pauseBtn2, pauseBtn3; private Button cancelBtn1, cancelBtn2, cancelBtn3; private ProgressBar mProgress1, mProgress2, mProgress3; private String url1 = "http://192.168.1.103/2.bmp"; private String url2 = "http://192.168.1.103/testzip.zip"; private String url3 = "http://192.168.1.103/photo.png"; @Override protected void onCreate(Bundle savedInstanceState) { try { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); NetReceiver.setCallBack(this); downloadBtn1 = bindView(R.id.main_btn_down1); downloadBtn2 = bindView(R.id.main_btn_down2); downloadBtn3 = bindView(R.id.main_btn_down3); pauseBtn1 = bindView(R.id.main_btn_pause1); pauseBtn2 = bindView(R.id.main_btn_pause2); pauseBtn3 = bindView(R.id.main_btn_pause3); cancelBtn1 = bindView(R.id.main_btn_cancel1); cancelBtn2 = bindView(R.id.main_btn_cancel2); cancelBtn3 = bindView(R.id.main_btn_cancel3); mProgress1 = bindView(R.id.main_progress1); mProgress2 = bindView(R.id.main_progress2); mProgress3 = bindView(R.id.main_progress3); downloadBtn1.setOnClickListener(this); downloadBtn2.setOnClickListener(this); downloadBtn3.setOnClickListener(this); pauseBtn1.setOnClickListener(this); pauseBtn2.setOnClickListener(this); pauseBtn3.setOnClickListener(this); cancelBtn1.setOnClickListener(this); cancelBtn2.setOnClickListener(this); cancelBtn3.setOnClickListener(this); } catch (Exception e) { Log.e(TAG, e.toString()); } } @Override public void onClick(View v) { try { switch (v.getId()) { case R.id.main_btn_down1: Log.d(TAG, "点击了下载1,url1:" + url1); ThreadUtils.exeMgThread3(new Runnable() { @Override public void run() { mResumeService.download("1", url1, FileConts.IMG_PATH, mInstance); } }); break; case R.id.main_btn_down2: Log.d(TAG, "点击了下载2"); ThreadUtils.exeMgThread3(new Runnable() { @Override public void run() { mResumeService.download("2", url2, FileConts.IMG_PATH, mInstance); } }); break; case R.id.main_btn_down3: Log.d(TAG, "点击了下载3"); break; case R.id.main_btn_pause1: ThreadUtils.exeMgThread3(new Runnable() { @Override public void run() { Log.v(TAG, "点击暂停1"); ResumeService.getInstance().stop("1"); } }); break; case R.id.main_btn_pause2: ThreadUtils.exeMgThread3(new Runnable() { @Override public void run() { Log.v(TAG, "点击暂停2"); ResumeService.getInstance().stop("2"); } }); break; case R.id.main_btn_pause3: // ResumeService.getInstance().cancel(url3); break; case R.id.main_btn_cancel1: ThreadUtils.exeMgThread3(new Runnable() { @Override public void run() { ResumeService.getInstance().remove(url1, "1"); } }); break; case R.id.main_btn_cancel2: ThreadUtils.exeMgThread3(new Runnable() { @Override public void run() { ResumeService.getInstance().remove(url2, "2"); } }); break; case R.id.main_btn_cancel3: // ResumeService.getInstance().cancel(url3); break; } } catch (Exception e) { StringBuffer sb = new StringBuffer(); Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); e.printStackTrace(printWriter); Throwable cause = e.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.close(); //异常的详细内容 String result = writer.toString(); Log.e(TAG, result); } } private <T extends View> T bindView(@IdRes int id) { View viewById = findViewById(id); return (T) viewById; } @Override protected void onDestroy() { super.onDestroy(); Log.v(TAG, "onDestroy"); } //IDownLoadCallBack 接口 的各种回调 事件 开始 //下载的进度回调 @Override public void onNext(String key, int precent) { if ("1".equals(key)) { mProgress1.setMax(100); mProgress1.setProgress(precent); } else if ("2".equals(key)) { mProgress2.setMax(100); mProgress2.setProgress(precent); } } //下载的停止回调,同时会将暂停状态保存进入数据库 @Override public void onPause(ResumeEntity resumeEntity, String key, int precent, long downLoadSize, long total) { Log.v(TAG, "onNext| 下载 暂停 回调方法,resumeEntity:" + resumeEntity); mHelper.update(resumeEntity); } //删除文件回调 @Override public void onDelete(ResumeEntity resumeEntity, String key) { Log.v(TAG, "onDelete| 下载 删除 回调方法,resumeEntity:" + resumeEntity); mHelper.delete(resumeEntity.getFileName()); } //下载成功的回调 @Override public void onSuccess(ResumeEntity resumeEntity, String key) { Log.v(TAG, "onNext| 下载 成功 回调方法,resumeEntity:" + resumeEntity); resumeEntity.setStatus(ResumeEntity.STATUS.SUCCESS.getValue()); mHelper.update(resumeEntity); } //下载失败的回调 @Override public void onFail(ResumeEntity resumeEntity, String key, String reason) { Log.v(TAG, "onFail| 下载 失败 回调方法,resumeEntity:" + resumeEntity + ",reason:" + reason); resumeEntity.setStatus(ResumeEntity.STATUS.FAIL.getValue()); mHelper.update(resumeEntity); } //IDownLoadCallBack 接口 的各种回调 事件 结束 //网络状态回调区域,这里是我用来接着编写网络重连之后继续下载的东西的 public void onStatusChange(String msg) { Log.v(TAG, "onStatusChange| 网络状态回调,内容为:" + msg); } }
7、UI界面
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.yxm.resume.activity.MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ProgressBar android:id="@+id/main_progress1" style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:progressDrawable="@drawable/progressbar" /> 进度条样式在第8点 <Button android:id="@+id/main_btn_down1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载1" /> <Button android:id="@+id/main_btn_pause1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="暂停1" /> <Button android:id="@+id/main_btn_cancel1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消1" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ProgressBar android:id="@+id/main_progress2" style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> <Button android:id="@+id/main_btn_down2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载2" /> <Button android:id="@+id/main_btn_pause2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="暂停2" /> <Button android:id="@+id/main_btn_cancel2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消2" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ProgressBar android:id="@+id/main_progress3" style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> <Button android:id="@+id/main_btn_down3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载3" /> <Button android:id="@+id/main_btn_pause3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="暂停3" /> <Button android:id="@+id/main_btn_cancel3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消3" /> </LinearLayout> </LinearLayout>
8、进度条样式
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@android:id/background"> <shape> <corners android:radius="5dip" /> <gradient android:angle="0" android:centerColor="#ff5a5d5a" android:centerY="0.75" android:endColor="#ff747674" android:startColor="#ff9d9e9d" /> </shape> </item> <item android:id="@android:id/secondaryProgress"> <clip> <shape> <corners android:radius="5dip" /> <gradient android:angle="0" android:centerColor="#80ffb600" android:centerY="0.75" android:endColor="#a0ffcb00" android:startColor="#80ffd300" /> </shape> </clip> </item> <item android:id="@android:id/progress"> <clip> <shape> <corners android:radius="5dip" /> <gradient android:angle="0" android:endColor="#8000ff00" android:startColor="#80ff0000" /> </shape> </clip> </item> </layer-list>
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
JavaScript闭包原理与用法实例
###1、与闭包有关的两个概念: (1)变量的作用域 不带有关键字var的变量会成为全局变量; 在函数中使用关键字var声明的变量是局部变量。 局部变量只有在函数内部才能访问到,在函数外面是访问不到的。但在函数内部可以通过作用域链一直向上搜索直到全局对象,也就是说,函数内部可以访问函数外部的变量。 (2)变量的生存周期 对于全局变量,其生存周期是永久的,除非主动销毁这个全局变量; 而对于在函数内用关键字var声明的局部变量,当退出函数时,这些局部变量会随着函数调用结束而被销毁。 var func = function() { var i = 1; alert(i); // 输出:1 }; alert(i); // 报错:i is not defind. 例外情况:闭包 var func = function() { var i = 1; return function() { alert(i); i++; } }; var f1 = func(); f1(); // 输出:1 f1(); // 输出:2 var f2 = func(); f2(); // 输出:1 f2(); // 输...
- 下一篇
对 React 组件进行单元测试(unit testing)
在这里说一下前端开发的一个特点是更多的会涉及用户界面,当开发规模达到一定程度时,几乎注定了其复杂度会成倍的增长。 无论是在代码的初始搭建过程中,还是之后难以避免的重构和修正bug过程中,常常会陷入逻辑难以梳理、无法掌握全局关联的境地。 而单元测试作为一种“提纲挈领、保驾护航”的基础手段,为开发提供了“围墙和脚手架”,可以有效的改善这些问题。 作为一种经典的开发和重构手段,单元测试在软件开发领域被广泛认可和采用;前端领域也逐渐积累起了丰富的测试框架和最佳实践。 本文将按如下顺序进行说明: I. 单元测试简介 II. React 单元测试中用到的工具 III. 用测试驱动 React 组件重构 IV. React 单元测试常见案例 I. 单元测试简介 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。 简单来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。 测试框架 测试框架的作用是提供一些方便的语法来描述测试用例,以及对用例进行分组。 断言(asser...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL8.0.19开启GTID主从同步CentOS8
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Hadoop3单机部署,实现最简伪集群
- CentOS8编译安装MySQL8.0.19
- CentOS7,8上快速安装Gitea,搭建Git服务器