七夜java下载_〖編程·Java〗Java 多线程断点下载文件

2023-12-26 20:59

本文主要是介绍七夜java下载_〖編程·Java〗Java 多线程断点下载文件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

基本原理:利用URLConnection获取要下载文件的长度、头部等相关信息,并设置响应的头部信息。并且通过URLConnection获取输入流,将文件分成指定的块,每一块单独开辟一个线程完成数据的读取、写入。通过输入流读取下载文件的信息,然后将读取的信息用RandomAccessFile随机写入到本地文件中。同时,每个线程写入的数据都文件指针也就是写入数据的长度,需要保存在一个临时文件中。这样当本次下载没有完成的时候,下次下载的时候就从这个文件中读取上一次下载的文件长度,然后继续接着上一次的位置开始下载。并且将本次下载的长度写入到这个文件中。

一、下载文件信息类、实体

封装即将下载资源的信息

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngDownloadInfo.java

package com.hoo.entity;

/*** function: 下载文件信息类

*@authorhoojo

* @createDate 2011-9-21 下午05:14:58

* @file DownloadInfo.java

* @package com.hoo.entity

* @project MultiThreadDownLoad

* @bloghttp://blog.csdn.net/IBM_hoojo* @email hoojo_@126.com

*@version1.0*/

public class DownloadInfo {

//下载文件url private String url;

//下载文件名称 private String fileName;

//下载文件路径 private String filePath;

//分成多少段下载, 每一段用一个线程完成下载 private int splitter;

//下载文件默认保存路径 private final static String FILE_PATH = "C:/temp";

//默认分块数、线程数 private final static int SPLITTER_NUM = 5;

public DownloadInfo() {

super();

}

/***@paramurl 下载地址*/

public DownloadInfo(String url) {

this(url, null, null, SPLITTER_NUM);

}

/***@paramurl 下载地址url

*@paramsplitter 分成多少段或是多少个线程下载*/

public DownloadInfo(String url, int splitter) {

this(url, null, null, splitter);

}

/***

*@paramurl 下载地址

*@paramfileName 文件名称

*@paramfilePath 文件保存路径

*@paramsplitter 分成多少段或是多少个线程下载*/

public DownloadInfo(String url, String fileName, String filePath, int splitter) {

super();

if (url == null || "".equals(url)) {

throw new RuntimeException("url is not null!");

}

this.url = url;

this.fileName = (fileName == null || "".equals(fileName)) ? getFileName(url) : fileName;

this.filePath = (filePath == null || "".equals(filePath)) ? FILE_PATH : filePath;

this.splitter = (splitter < 1) ? SPLITTER_NUM : splitter;

}

/*** function: 通过url获得文件名称

*@authorhoojo

* @createDate 2011-9-30 下午05:00:00

*@paramurl

*@return*/

private String getFileName(String url) {

return url.substring(url.lastIndexOf("/") + 1, url.length());

}

public String getUrl() {

return url;

}

public void setUrl(String url) {

if (url == null || "".equals(url)) {

throw new RuntimeException("url is not null!");

}

this.url = url;

}

public String getFileName() {

return fileName;

}

public void setFileName(String fileName) {

this.fileName = (fileName == null || "".equals(fileName)) ? getFileName(url) : fileName;

}

public String getFilePath() {

return filePath;

}

public void setFilePath(String filePath) {

this.filePath = (filePath == null || "".equals(filePath)) ? FILE_PATH : filePath;

}

public int getSplitter() {

return splitter;

}

public void setSplitter(int splitter) {

this.splitter = (splitter < 1) ? SPLITTER_NUM : splitter;

}

@Override

public String toString() {

return this.url + "#" + this.fileName + "#" + this.filePath + "#" + this.splitter;

}

}

二、随机写入一段文件

这个类主要是完成向本地的指定文件指针出开始写入文件,并返回当前写入文件的长度(文件指针)。这个类将被线程调用,文件被分成对应的块后,将被线程调用。每个线程都将会调用这个类完成文件的随机写入。

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngSaveItemFile.java

package com.hoo.download;

import java.io.IOException;

import java.io.RandomAccessFile;

/*** function: 写入文件、保存文件

*@authorhoojo

* @createDate 2011-9-21 下午05:44:02

* @file SaveItemFile.java

* @package com.hoo.download

* @project MultiThreadDownLoad

* @bloghttp://blog.csdn.net/IBM_hoojo* @email hoojo_@126.com

*@version1.0*/

public class SaveItemFile {

//存储文件 private RandomAccessFile itemFile;

public SaveItemFile() throws IOException {

this("", 0);

}

/***@paramname 文件路径、名称

*@parampos 写入点位置 position

*@throwsIOException*/

public SaveItemFile(String name, long pos) throws IOException {

itemFile = new RandomAccessFile(name, "rw");

//在指定的pos位置开始写入数据 itemFile.seek(pos);

}

/*** function: 同步方法写入文件

*@authorhoojo

* @createDate 2011-9-26 下午12:21:22

*@parambuff 缓冲数组

*@paramstart 起始位置

*@paramlength 长度

*@return*/

public synchronized int write(byte[] buff, int start, int length) {

int i = -1;

try {

itemFile.write(buff, start, length);

i = length;

} catch (IOException e) {

e.printStackTrace();

}

return i;

}

public void close() throws IOException {

if (itemFile != null) {

itemFile.close();

}

}

}

三、单个线程下载文件

这个类主要是完成单个线程的文件下载,将通过URLConnection读取指定url的资源信息。然后用InputStream读取文件内容,然后调用调用SaveItemFile类,向本地写入当前要读取的块的内容。

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngDownloadFile.java

package com.hoo.download;

import java.io.IOException;

import java.io.InputStream;

import java.net.HttpURLConnection;

import java.net.MalformedURLException;

import java.net.URL;

import java.net.URLConnection;

import com.hoo.util.LogUtils;

/*** function: 单线程下载文件

*@authorhoojo

* @createDate 2011-9-22 下午02:55:10

* @file DownloadFile.java

* @package com.hoo.download

* @project MultiThreadDownLoad

* @bloghttp://blog.csdn.net/IBM_hoojo* @email hoojo_@126.com

*@version1.0*/

public class DownloadFile extends Thread {

//下载文件url private String url;

//下载文件起始位置 private long startPos;

//下载文件结束位置 private long endPos;

//线程id private int threadId;

//下载是否完成 private boolean isDownloadOver = false;

private SaveItemFile itemFile;

private static final int BUFF_LENGTH = 1024 * 8;

/***@paramurl 下载文件url

*@paramname 文件名称

*@paramstartPos 下载文件起点

*@paramendPos 下载文件结束点

*@paramthreadId 线程id

*@throwsIOException*/

public DownloadFile(String url, String name, long startPos, long endPos, int threadId) throws IOException {

super();

this.url = url;

this.startPos = startPos;

this.endPos = endPos;

this.threadId = threadId;

//分块下载写入文件内容 this.itemFile = new SaveItemFile(name, startPos);

}

@Override

public void run() {

while (endPos > startPos && !isDownloadOver) {

try {

URL url = new URL(this.url);

HttpURLConnection conn = (HttpURLConnection) url.openConnection();

//设置连接超时时间为10000ms conn.setConnectTimeout(10000);

//设置读取数据超时时间为10000ms conn.setReadTimeout(10000);

setHeader(conn);

String property = "bytes=" + startPos + "-";

conn.setRequestProperty("RANGE", property);

//输出log信息 LogUtils.log("开始 " + threadId + ":" + property + endPos);

//printHeader(conn);//获取文件输入流,读取文件内容 InputStream is = conn.getInputStream();

byte[] buff = new byte[BUFF_LENGTH];

int length = -1;

LogUtils.log("#start#Thread: " + threadId + ", startPos: " + startPos + ", endPos: " + endPos);

while ((length = is.read(buff)) > 0 && startPos < endPos && !isDownloadOver) {

//写入文件内容,返回最后写入的长度 startPos += itemFile.write(buff, 0, length);

}

LogUtils.log("#over#Thread: " + threadId + ", startPos: " + startPos + ", endPos: " + endPos);

LogUtils.log("Thread " + threadId + " is execute over!");

this.isDownloadOver = true;

} catch (MalformedURLException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

} finally {

try {

if (itemFile != null) {

itemFile.close();

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

if (endPos < startPos && !isDownloadOver) {

LogUtils.log("Thread " + threadId + " startPos > endPos, not need download file !");

this.isDownloadOver = true;

}

if (endPos == startPos && !isDownloadOver) {

LogUtils.log("Thread " + threadId + " startPos = endPos, not need download file !");

this.isDownloadOver = true;

}

}

/*** function: 打印下载文件头部信息

*@authorhoojo

* @createDate 2011-9-22 下午05:44:35

*@paramconn HttpURLConnection*/

public static void printHeader(URLConnection conn) {

int i = 1;

while (true) {

String header = conn.getHeaderFieldKey(i);

i++;

if (header != null) {

LogUtils.info(header + ":" + conn.getHeaderField(i));

} else {

break;

}

}

}

/*** function: 设置URLConnection的头部信息,伪装请求信息

*@authorhoojo

* @createDate 2011-9-28 下午05:29:43

*@paramcon*/

public static void setHeader(URLConnection conn) {

conn.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3");

conn.setRequestProperty("Accept-Language", "en-us,en;q=0.7,zh-cn;q=0.3");

conn.setRequestProperty("Accept-Encoding", "utf-8");

conn.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7");

conn.setRequestProperty("Keep-Alive", "300");

conn.setRequestProperty("connnection", "keep-alive");

conn.setRequestProperty("If-Modified-Since", "Fri, 02 Jan 2009 17:00:05 GMT");

conn.setRequestProperty("If-None-Match", "\"1261d8-4290-df64d224\"");

conn.setRequestProperty("Cache-conntrol", "max-age=0");

conn.setRequestProperty("Referer", "http://www.baidu.com");

}

public boolean isDownloadOver() {

return isDownloadOver;

}

public long getStartPos() {

return startPos;

}

public long getEndPos() {

return endPos;

}

}

四、分段多线程写入文件内容

这个类主要是完成读取指定url资源的内容,获取该资源的长度。然后将该资源分成指定的块数,将每块的起始下载位置、结束下载位置,分别保存在一个数组中。每块都单独开辟一个独立线程开始下载。在开始下载之前,需要创建一个临时文件,写入当前下载线程的开始下载指针位置和结束下载指针位置。

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngBatchDownloadFile.java

package com.hoo.download;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.net.HttpURLConnection;

import java.net.MalformedURLException;

import java.net.URL;

import com.hoo.entity.DownloadInfo;

import com.hoo.util.LogUtils;

/*** function: 分批量下载文件

*@authorhoojo

* @createDate 2011-9-22 下午05:51:54

* @file BatchDownloadFile.java

* @package com.hoo.download

* @project MultiThreadDownLoad

* @bloghttp://blog.csdn.net/IBM_hoojo* @email hoojo_@126.com

*@version1.0*/

public class BatchDownloadFile implements Runnable {

//下载文件信息 private DownloadInfo downloadInfo;

//一组开始下载位置 private long[] startPos;

//一组结束下载位置 private long[] endPos;

//休眠时间 private static final int SLEEP_SECONDS = 500;

//子线程下载 private DownloadFile[] fileItem;

//文件长度 private int length;

//是否第一个文件 private boolean first = true;

//是否停止下载 private boolean stop = false;

//临时文件信息 private File tempFile;

public BatchDownloadFile(DownloadInfo downloadInfo) {

this.downloadInfo = downloadInfo;

String tempPath = this.downloadInfo.getFilePath() + File.separator + downloadInfo.getFileName() + ".position";

tempFile = new File(tempPath);

//如果存在读入点位置的文件 if (tempFile.exists()) {

first = false;

//就直接读取内容 try {

readPosInfo();

} catch (IOException e) {

e.printStackTrace();

}

} else {

//数组的长度就要分成多少段的数量 startPos = new long[downloadInfo.getSplitter()];

endPos = new long[downloadInfo.getSplitter()];

}

}

@Override

public void run() {

//首次下载,获取下载文件长度 if (first) {

length = this.getFileSize();//获取文件长度 if (length == -1) {

LogUtils.log("file length is know!");

stop = true;

} else if (length == -2) {

LogUtils.log("read file length is error!");

stop = true;

} else if (length > 0) {

/*** eg

* start: 1, 3, 5, 7, 9

* end: 3, 5, 7, 9, length*/

for (int i = 0, len = startPos.length; i < len; i++) {

int size = i * (length / len);

startPos[i] = size;

//设置最后一个结束点的位置 if (i == len - 1) {

endPos[i] = length;

} else {

size = (i + 1) * (length / len);

endPos[i] = size;

}

LogUtils.log("start-end Position[" + i + "]: " + startPos[i] + "-" + endPos[i]);

}

} else {

LogUtils.log("get file length is error, download is stop!");

stop = true;

}

}

//子线程开始下载 if (!stop) {

//创建单线程下载对象数组 fileItem = new DownloadFile[startPos.length];//startPos.length = downloadInfo.getSplitter() for (int i = 0; i < startPos.length; i++) {

try {

//创建指定个数单线程下载对象,每个线程独立完成指定块内容的下载 fileItem[i] = new DownloadFile(

downloadInfo.getUrl(),

this.downloadInfo.getFilePath() + File.separator + downloadInfo.getFileName(),

startPos[i], endPos[i], i

);

fileItem[i].start();//启动线程,开始下载 LogUtils.log("Thread: " + i + ", startPos: " + startPos[i] + ", endPos: " + endPos[i]);

} catch (IOException e) {

e.printStackTrace();

}

}

//循环写入下载文件长度信息 while (!stop) {

try {

writePosInfo();

LogUtils.log("downloading……");

Thread.sleep(SLEEP_SECONDS);

stop = true;

} catch (IOException e) {

e.printStackTrace();

} catch (InterruptedException e) {

e.printStackTrace();

}

for (int i = 0; i < startPos.length; i++) {

if (!fileItem[i].isDownloadOver()) {

stop = false;

break;

}

}

}

LogUtils.info("Download task is finished!");

}

}

/*** 将写入点数据保存在临时文件中

*@authorhoojo

* @createDate 2011-9-23 下午05:25:37

*@throwsIOException*/

private void writePosInfo() throws IOException {

DataOutputStream dos = new DataOutputStream(new FileOutputStream(tempFile));

dos.writeInt(startPos.length);

for (int i = 0; i < startPos.length; i++) {

dos.writeLong(fileItem[i].getStartPos());

dos.writeLong(fileItem[i].getEndPos());

//LogUtils.info("[" + fileItem[i].getStartPos() + "#" + fileItem[i].getEndPos() + "]"); }

dos.close();

}

/*** function:读取写入点的位置信息

*@authorhoojo

* @createDate 2011-9-23 下午05:30:29

*@throwsIOException*/

private void readPosInfo() throws IOException {

DataInputStream dis = new DataInputStream(new FileInputStream(tempFile));

int startPosLength = dis.readInt();

startPos = new long[startPosLength];

endPos = new long[startPosLength];

for (int i = 0; i < startPosLength; i++) {

startPos[i] = dis.readLong();

endPos[i] = dis.readLong();

}

dis.close();

}

/*** function: 获取下载文件的长度

*@authorhoojo

* @createDate 2011-9-26 下午12:15:08

*@return*/

private int getFileSize() {

int fileLength = -1;

try {

URL url = new URL(this.downloadInfo.getUrl());

HttpURLConnection conn = (HttpURLConnection) url.openConnection();

DownloadFile.setHeader(conn);

int stateCode = conn.getResponseCode();

//判断http status是否为HTTP/1.1 206 Partial Content或者200 OK if (stateCode != HttpURLConnection.HTTP_OK && stateCode != HttpURLConnection.HTTP_PARTIAL) {

LogUtils.log("Error Code: " + stateCode);

return -2;

} else if (stateCode >= 400) {

LogUtils.log("Error Code: " + stateCode);

return -2;

} else {

//获取长度 fileLength = conn.getContentLength();

LogUtils.log("FileLength: " + fileLength);

}

//读取文件长度 /*for (int i = 1; ; i++) {

String header = conn.getHeaderFieldKey(i);

if (header != null) {

if ("Content-Length".equals(header)) {

fileLength = Integer.parseInt(conn.getHeaderField(i));

break;

}

} else {

break;

}

}*/

DownloadFile.printHeader(conn);

} catch (MalformedURLException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

return fileLength;

}

}

五、工具类、测试类

日志工具类

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngLogUtils.java

package com.hoo.util;

/*** function: 日志工具类

*@authorhoojo

* @createDate 2011-9-21 下午05:21:27

* @file LogUtils.java

* @package com.hoo.util

* @project MultiThreadDownLoad

* @bloghttp://blog.csdn.net/IBM_hoojo* @email hoojo_@126.com

*@version1.0*/

public abstract class LogUtils {

public static void log(Object message) {

System.err.println(message);

}

public static void log(String message) {

System.err.println(message);

}

public static void log(int message) {

System.err.println(message);

}

public static void info(Object message) {

System.out.println(message);

}

public static void info(String message) {

System.out.println(message);

}

public static void info(int message) {

System.out.println(message);

}

}

下载工具类

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngDownloadUtils.java

package com.hoo.util;

import com.hoo.download.BatchDownloadFile;

import com.hoo.entity.DownloadInfo;

/*** function: 分块多线程下载工具类

*@authorhoojo

* @createDate 2011-9-28 下午05:22:18

* @file DownloadUtils.java

* @package com.hoo.util

* @project MultiThreadDownLoad

* @bloghttp://blog.csdn.net/IBM_hoojo* @email hoojo_@126.com

*@version1.0*/

public abstract class DownloadUtils {

public static void download(String url) {

DownloadInfo bean = new DownloadInfo(url);

LogUtils.info(bean);

BatchDownloadFile down = new BatchDownloadFile(bean);

new Thread(down).start();

}

public static void download(String url, int threadNum) {

DownloadInfo bean = new DownloadInfo(url, threadNum);

LogUtils.info(bean);

BatchDownloadFile down = new BatchDownloadFile(bean);

new Thread(down).start();

}

public static void download(String url, String fileName, String filePath, int threadNum) {

DownloadInfo bean = new DownloadInfo(url, fileName, filePath, threadNum);

LogUtils.info(bean);

BatchDownloadFile down = new BatchDownloadFile(bean);

new Thread(down).start();

}

}

下载测试类

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngTestDownloadMain.java

package com.hoo.test;

import com.hoo.util.DownloadUtils;

/*** function: 下载测试

*@authorhoojo

* @createDate 2011-9-23 下午05:49:46

* @file TestDownloadMain.java

* @package com.hoo.download

* @project MultiThreadDownLoad

* @bloghttp://blog.csdn.net/IBM_hoojo* @email hoojo_@126.com

*@version1.0*/

public class TestDownloadMain {

public static void main(String[] args) {

/*DownloadInfo bean = new DownloadInfo("http://i7.meishichina.com/Health/UploadFiles/201109/2011092116224363.jpg");

System.out.println(bean);

BatchDownloadFile down = new BatchDownloadFile(bean);

new Thread(down).start();*/

//DownloadUtils.download("http://i7.meishichina.com/Health/UploadFiles/201109/2011092116224363.jpg"); DownloadUtils.download("http://mp3.baidu.com/j?j=2&url=http%3A%2F%2Fzhangmenshiting2.baidu.com%2Fdata%2Fmusic%2F1669425%2F%25E9%2599%25B7%25E5%2585%25A5%25E7%2588%25B1%25E9%2587%258C%25E9%259D%25A2.mp3%3Fxcode%3D2ff36fb70737c816553396c56deab3f1", "aa.mp3", "c:/temp", 5);

}

}

多线程下载主要在第三部和第四部,其他的地方还是很好理解。源码中提供相应的注释了,便于理解。

——摘自博客园hoojo

这篇关于七夜java下载_〖編程·Java〗Java 多线程断点下载文件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/540756

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

常用的jdk下载地址

jdk下载地址 安装方式可以看之前的博客: mac安装jdk oracle 版本:https://www.oracle.com/java/technologies/downloads/ Eclipse Temurin版本:https://adoptium.net/zh-CN/temurin/releases/ 阿里版本: github:https://github.com/