简介本资源为OpenJDK 21完整开发工具包压缩包面向Java开发者、后端工程师及需要搭建Java运行环境的学习者解决从官方渠道获取开源JDK发行版的问题。压缩包共484个文件约188.28MB以dll动态链接库、jmod模块文件、exe可执行程序、license许可说明及md文档为主另含policy安全策略、properties配置、cacerts证书库等覆盖编译器、运行时与标准类库组件解压后即可按操作系统配置使用。目前已有205人学习下载。资源完整收录JDK 21所需的开发与运行文件目录结构清晰便于开发者快速搭建编译、调试与运行环境也可用于版本升级、兼容性验证及新API特性学习是Java开发入门与进阶的实用基础包。1. open sdk 21 下载文件从版本号到落地路径的完整拆解很多人第一次看到「open sdk 21 下载文件」这个说法会下意识以为是一个叫 OpenSDK 的库出了 21 版本然后要拿它去下载文件。实际在一线干活时这个标题通常对应两类真实场景一类是 Android 开发里把compileSdkVersion/targetSdkVersion定在 21也就是 Android 5.0之后用系统或第三方 SDK 去下载文件另一类是某个厂商的开放平台 SDK版本号走到 21需要集成它的文件下载能力。不管哪一种核心矛盾都一样——版本号卡在 21 这个节点上API 行为、权限模型、存储路径都跟新版本不一样直接抄新版本的代码大概率翻车。这篇东西面向的是手上真有这么一个 SDK、需要把「下载文件」这条链路跑通的人。我会把版本 21 的边界、下载文件的几种实现路径、参数怎么设、坑在哪按能复现的顺序讲清楚。新手可以照着步骤走熟手可以直接跳到参数和排查那几章看边界。2. 先搞清楚 open sdk 21 到底指什么版本边界决定下载方案在动手写下载逻辑之前必须先确认「21」这个数字落在哪个坐标系里。这一步不做后面所有代码都是空中楼阁。我见过太多人拿着 Android 的targetSdkVersion 21去套某个支付 SDK 的 21 版本结果权限申请那一段完全对不上。2.1 三种常见的「21」含义与对应下载能力第一种是 Android API Level 21也就是 Android 5.0 Lollipop。这个版本在文件下载上是个分水岭它引入了JobScheduler但还没有Scoped Storage那是 API 29 的事所以外部存储还是可以随便写的只要在AndroidManifest.xml里声明WRITE_EXTERNAL_STORAGE。如果你手上的 SDK 要求minSdkVersion 21那下载文件的落盘路径通常还是Environment.getExternalStorageDirectory()那一套。第二种是某个开放平台 SDK 自身的版本号比如某地图、某推送、某登录 SDK 的 21.x。这类 SDK 的「下载文件」往往指的是它内部封装的资源下载接口比如地图离线包、推送图标资源。这种要看它的 release note21 版本大概率是修了下载断点续传的 bug或者改了缓存目录。第三种是构建工具链里的版本比如 Gradle 插件、NDK、某个依赖库的 21 版本。这种「下载文件」多半是构建期下载依赖跟运行时下载是两码事。判断方法很简单看你的build.gradle里compileSdkVersion是多少看 SDK 的接入文档里版本号写在哪一栏。如果文档里写的是「Android SDK 21」那就是 API Level如果写的是「OpenSDK v21.0.3」那就是厂商版本号。2.2 为什么版本 21 的下载逻辑不能直接套用新版本API 21 到 API 29 之间存储权限模型变了三次。21 的时候WRITE_EXTERNAL_STORAGE是安装即授予的normal 权限用户不用点同意。到了 23Android 6.0变成运行时权限必须动态申请。到了 29又引入分区存储getExternalStorageDirectory()基本废了。所以如果你拿一份 API 29 的下载代码把targetSdkVersion改成 21会发现两个问题一是requestPermissions那套逻辑在 21 上不会触发回调因为权限默认给了二是MediaStore那套 API 在 21 上根本不存在。反过来拿 21 的代码去跑 29会直接抛FileUriExposedException。我一般的做法是先确认targetSdkVersion然后按这个版本写下载路径和权限逻辑不要跨版本抄。如果非要兼容就用Build.VERSION.SDK_INT做分支但分支不要超过两层否则维护成本爆炸。2.3 下载文件的三种落地路径选型确认了版本边界之后下载文件本身有三条路可走。第一条是HttpURLConnection或OkHttp自己写流式下载。这是最可控的适合需要断点续传、进度回调、自定义缓存目录的场景。API 21 上HttpURLConnection完全够用OkHttp3.x 也支持到 API 21。第二条是用 SDK 自带的下载接口。很多开放平台 SDK 会封装一个downloadFile(url, savePath, callback)之类的方法内部帮你处理了线程和重试。这种最省事但可控性差出问题只能看日志。第三条是交给系统下载管理器DownloadManager。这是 API 9 就有的东西21 上很稳定适合大文件、后台下载、不需要实时进度的场景。缺点是自定义能力弱查询进度要注册ContentObserver。选型建议小文件、要进度、要断点续传选第一条SDK 有现成接口且文档清楚选第二条大文件、后台、不关心进度选第三条。下面几章我会把第一条和第三条的代码都写出来因为这两条最通用。3. 用 HttpURLConnection 在 API 21 上写一个能跑的下载器这一章是核心我会把完整的下载代码、参数含义、以及为什么这么写讲清楚。代码基于 API 21用HttpURLConnection不引第三方库保证你能直接复制到项目里跑。3.1 最小可运行代码流式下载到外部存储先看代码再解释每一段在干什么。// DownloadTask.java // 适用于 targetSdkVersion 21 的流式下载实现 public class DownloadTask { private static final int BUFFER_SIZE 8192; // 8KB 缓冲区兼顾内存和 IO 次数 private static final int CONNECT_TIMEOUT 15000; // 连接超时 15 秒 private static final int READ_TIMEOUT 30000; // 读取超时 30 秒 public interface ProgressListener { void onProgress(long downloaded, long total); void onSuccess(File file); void onFailed(Exception e); } public void download(final String fileUrl, final File saveFile, final ProgressListener listener) { new Thread(new Runnable() { Override public void run() { HttpURLConnection conn null; InputStream is null; FileOutputStream fos null; try { URL url new URL(fileUrl); conn (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); conn.setRequestMethod(GET); // 支持断点续传的关键告诉服务端从哪个字节开始 if (saveFile.exists()) { conn.setRequestProperty(Range, bytes saveFile.length() -); } conn.connect(); int responseCode conn.getResponseCode(); // 206 表示服务端接受了 Range 请求 if (responseCode ! 200 responseCode ! 206) { throw new IOException(HTTP responseCode); } long totalLength conn.getContentLength(); if (responseCode 206) { totalLength saveFile.length(); } is conn.getInputStream(); fos new FileOutputStream(saveFile, true); // append 模式 byte[] buffer new byte[BUFFER_SIZE]; long downloaded saveFile.length(); int len; while ((len is.read(buffer)) ! -1) { fos.write(buffer, 0, len); downloaded len; if (listener ! null) { listener.onProgress(downloaded, totalLength); } } fos.flush(); if (listener ! null) listener.onSuccess(saveFile); } catch (Exception e) { if (listener ! null) listener.onFailed(e); } finally { // 关闭顺序先关流再断开连接 try { if (fos ! null) fos.close(); } catch (IOException ignored) {} try { if (is ! null) is.close(); } catch (IOException ignored) {} if (conn ! null) conn.disconnect(); } } }).start(); } }这段代码的逻辑说明整个下载跑在子线程里因为 API 21 上主线程做网络操作会直接抛NetworkOnMainThreadException。Range请求头是断点续传的核心如果本地文件已经存在就告诉服务端从已有长度开始传服务端返回 206 就说明支持。FileOutputStream用 append 模式保证续传时不会覆盖已下载的部分。参数说明BUFFER_SIZE设 8192 是个经验值太小会导致 IO 次数多太大会占内存8KB 在移动端比较平衡。CONNECT_TIMEOUT15 秒是给弱网留的余量READ_TIMEOUT30 秒是因为大文件读取间隔可能较长。这两个值不要设太短否则弱网下频繁超时重试反而更慢。3.2 权限声明与存储路径API 21 的写法API 21 上WRITE_EXTERNAL_STORAGE是安装时授予的不需要运行时申请。但你还是要在 manifest 里声明否则写入会失败。!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE /存储路径的写法// API 21 上可以直接用外部存储根目录 File dir new File(Environment.getExternalStorageDirectory(), myapp/download); if (!dir.exists()) { dir.mkdirs(); // 注意mkdirs 返回 false 不代表失败可能是已存在 } File saveFile new File(dir, target.zip);这里有个细节mkdirs()返回 false 有两种情况一是目录已存在二是创建失败。不要用返回值判断成败要用dir.exists() dir.isDirectory()来判断。这个坑我在早期项目里踩过日志里全是「创建目录失败」实际目录早就建好了。另外API 21 上外部存储的读写不需要FileProvider直接用file://URI 就行。但如果你后面要升级targetSdkVersion到 24 以上就必须换成FileProvider否则会抛FileUriExposedException。所以如果你有升级计划建议一开始就用FileProvider省得后面改。3.3 断点续传的三个必调参数断点续传能不能跑通取决于三个参数对不对。第一个是Range头的格式。必须是bytesstart-注意是英文横杠后面可以跟结束位置也可以不跟。如果写成bytesstart少了横杠服务端会忽略这个头返回 200 全量下载。第二个是FileOutputStream的 append 标志。必须是true否则每次续传都会从头覆盖。这个参数在构造函数第二个位置很容易漏。第三个是服务端是否支持 Range。不是所有服务器都支持判断方法是看返回码206 是支持200 是不支持。如果不支持你只能全量下载这时候要么删掉本地文件重下要么接受重复下载。我一般会在第一次请求时先发一个HEAD请求看Accept-Ranges头是不是bytes是才走续传逻辑。提示断点续传的进度计算要注意downloaded的初始值应该是本地文件已有长度而不是 0。否则进度条会从 0 开始跳用户体验很差。4. 用 DownloadManager 处理大文件省心但有边界如果你要下载的是几十兆甚至上百兆的文件而且不需要实时进度DownloadManager是更省心的选择。它在 API 21 上很稳定系统会自动处理重试、网络切换、通知栏进度。4.1 DownloadManager 的请求构造与参数// 使用系统 DownloadManager 下载大文件 DownloadManager dm (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Request request new DownloadManager.Request(Uri.parse(fileUrl)); // 允许在移动网络下下载默认只允许 WiFi request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE); // 通知栏显示 request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); // 保存路径API 21 上可以直接指定外部存储路径 request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, target.zip); // 设置标题和描述会显示在通知栏 request.setTitle(下载中); request.setDescription(正在下载目标文件); long downloadId dm.enqueue(request);参数说明setAllowedNetworkTypes默认是 WiFi only如果你不显式加上NETWORK_MOBILE用户在移动网络下点下载会一直排队不开始这个坑很隐蔽日志里看不出来。setDestinationInExternalPublicDir的第一个参数是公共目录类型比如DIRECTORY_DOWNLOADS、DIRECTORY_DOCUMENTS第二个参数是文件名。API 21 上这个方法不需要额外权限因为WRITE_EXTERNAL_STORAGE已经默认授予了。4.2 查询下载进度与完成状态DownloadManager不提供实时进度回调要自己查。有两种方式轮询和ContentObserver。轮询的写法// 轮询查询下载状态 DownloadManager.Query query new DownloadManager.Query(); query.setFilterById(downloadId); Cursor cursor dm.query(query); if (cursor ! null cursor.moveToFirst()) { int status cursor.getInt( cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); long downloaded cursor.getLong( cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); long total cursor.getLong( cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); if (status DownloadManager.STATUS_SUCCESSFUL) { // 下载完成从 COLUMN_LOCAL_URI 拿文件路径 String uri cursor.getString( cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); } else if (status DownloadManager.STATUS_FAILED) { int reason cursor.getInt( cursor.getColumnIndex(DownloadManager.COLUMN_REASON)); // reason 对应失败原因比如 404、网络错误 } } cursor.close();轮询的间隔建议 1 到 2 秒太频繁费电太慢进度条卡顿。COLUMN_REASON在失败时很有用常见值有ERROR_HTTP_DATA_ERROR网络问题、ERROR_FILE_ERROR存储问题、ERROR_INSUFFICIENT_SPACE空间不足。ContentObserver的写法更优雅但代码量大这里不展开。核心是注册一个监听DownloadManager.CONTENT_URI的 observer在onChange里查状态。4.3 DownloadManager 在 API 21 上的两个硬边界第一个边界是文件路径。setDestinationInExternalPublicDir只能写到公共目录不能写到应用私有目录。如果你需要写到getExternalFilesDir()下面得用setDestinationUri配合FileProvider但 API 21 上FileProvider的配置比较麻烦。第二个边界是并发。DownloadManager默认最多同时下载 3 个任务超出的会排队。这个限制在系统层面改不了。如果你需要更多并发只能自己用HttpURLConnection写。注意DownloadManager下载的文件在应用卸载后不会被删除因为它在公共目录。如果你不希望文件残留要么下载完手动移到私有目录要么一开始就用HttpURLConnection写到私有目录。5. 避坑与排查open sdk 21 下载文件最常见的五个翻车点这一章是我这些年踩过的坑的汇总每条都按「现象 → 原因 → 解决」写。如果你下载跑不通先来这里对号入座。5.1 下载到一半卡住日志无异常现象下载进度走到某个百分比就不动了没有报错线程也没死。原因READ_TIMEOUT设得太短或者服务端在传输过程中断开了连接但没发 FIN 包。API 21 上HttpURLConnection对这种情况的处理是等READ_TIMEOUT超时才抛异常如果超时设了 30 秒就会卡 30 秒。解决把READ_TIMEOUT设成 30 到 60 秒同时在读取循环里加一个心跳检测如果连续 N 次读到的字节数为 0主动断开重连。另外检查服务端是否支持Connection: keep-alive不支持的话每次请求都要新建连接。5.2 文件下载完打不开大小对但内容损坏现象文件大小跟服务端一致但打开报格式错误。原因FileOutputStream没有 flush 就 close或者下载过程中用了 append 模式但服务端返回的是 200 全量导致文件被追加了两遍。解决在close之前先flush。如果是续传场景先判断返回码206 才用 append200 要 truncate 重写。我一般会在续传前先发HEAD请求确认Accept-Ranges不支持就直接删本地文件重下。5.3 权限已声明但写入失败现象WRITE_EXTERNAL_STORAGE已经在 manifest 里声明了但FileOutputStream抛FileNotFoundException: Permission denied。原因API 21 上虽然权限是安装时授予但如果应用被用户手动在设置里撤销了存储权限或者设备是某些定制 ROM比如部分国产 ROM对存储权限做了额外限制就会写入失败。解决在写入前用checkCallingOrSelfPermission检查权限没有就引导用户去设置页开。另外检查存储是否真的挂载了用Environment.getExternalStorageState()判断返回MEDIA_MOUNTED才能写。5.4 下载速度极慢但网络正常现象同一个文件浏览器下载很快应用内下载很慢。原因BUFFER_SIZE设得太小比如 1024导致 IO 次数过多。或者没有设置Accept-Encoding服务端返回了未压缩的数据。解决把BUFFER_SIZE调到 8192 或 16384。如果服务端支持 gzip加上conn.setRequestProperty(Accept-Encoding, gzip)但要注意HttpURLConnection不会自动解压需要自己包一层GZIPInputStream。不过对于二进制文件zip、apkgzip 压缩率很低加了反而费 CPU所以只对文本类文件加。5.5 下载完成后文件找不到现象下载回调成功了但去目标路径找不到文件。原因路径拼接错了或者用了getExternalFilesDir()但没传参数。getExternalFilesDir(null)返回的是/sdcard/Android/data/包名/files传Environment.DIRECTORY_DOWNLOADS返回的是子目录。如果不传参数传了 null文件在 files 根目录很多人去 Downloads 目录找当然找不到。解决下载完成后把saveFile.getAbsolutePath()打到日志里确认路径。另外API 21 上外部存储的路径在不同设备上可能不一样有的设备是/storage/sdcard0有的是/mnt/sdcard不要硬编码路径用Environment.getExternalStorageDirectory()拿。6. 进阶把下载器做成可复用组件的一个技巧前面几章的代码都是单次下载实际项目里你肯定需要多个下载任务、并发控制、失败重试。这一章讲一个我在多个项目里用过的技巧用ThreadPoolExecutor加一个任务队列把下载器做成可复用的组件。核心思路是维护一个固定大小的线程池比如 3 个线程所有下载任务丢进队列池子满了就排队。每个任务有自己的状态等待、下载中、暂停、完成、失败用一个ConcurrentHashMap存downloadId到状态的映射。// 可复用下载器的核心结构 public class DownloadManagerCompat { // 核心线程 3最大 3队列无界空闲回收 60 秒 private final ThreadPoolExecutor executor new ThreadPoolExecutor( 3, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueueRunnable()); // 任务状态表key 是任务 id private final ConcurrentHashMapString, DownloadTask taskMap new ConcurrentHashMap(); public void enqueue(String id, String url, File saveFile) { DownloadTask task new DownloadTask(id, url, saveFile); taskMap.put(id, task); executor.execute(task); } public void cancel(String id) { DownloadTask task taskMap.get(id); if (task ! null) { task.cancel(); // 内部通过 volatile 标志位中断循环 } } public DownloadTask.Status getStatus(String id) { DownloadTask task taskMap.get(id); return task null ? null : task.getStatus(); } }这个结构的关键点有三个。第一线程池大小设 3 是因为移动端并发太多会抢带宽反而每个都慢。第二任务状态用ConcurrentHashMap存因为下载线程和 UI 线程会同时读写。第三取消操作不能用Thread.interrupt()因为InputStream.read()不响应中断要用一个volatile boolean cancelled标志位在读取循环里检查。参数方面LinkedBlockingQueue不设容量上限因为下载任务通常不会无限多设了上限反而要处理拒绝策略。空闲回收设 60 秒是因为下载任务通常集中在某个时间段过了这段时间线程可以回收省资源。验证这个组件能不能用我一般做三个测试一是同时丢 10 个任务进去看是不是只有 3 个在跑其余排队二是下载到一半调cancel看线程是不是在 1 秒内退出三是下载过程中杀进程再启动看能不能从断点续传这个需要把任务状态持久化到数据库或 SP代码里没写但思路是每次进度更新时存一下。最后说个我自己的习惯下载这种功能我从来不在主线程碰也不在 UI 层写业务逻辑。下载器只负责「把字节从 A 搬到 B」至于下载完了解压、校验、通知 UI都是回调里的事。这样拆开之后下载器可以复用到任何项目改都不用改。希望帮到你。本文还有配套的精品资源点击获取