当前位置: 首页 > news >正文

Java入门级教程21——Java 缓存技术、RMI远程方法调用、多线程分割大文件

目录

1.Java 缓存代理模式的实现

1.1 数据库准备

1.2 添加Maven依赖

1.3 创建缓存文件夹

1.4 具体实现

1.4.1 定义抽象主题接口 DataAop.java(约束行为)

1.4.2 定义数据模型 Student.java(封装数据)

1.4.3 实现真实主题类 DBData.java(操作数据库)

1.4.4 实现拦截器类 InterceptorData.java(处理缓存检查)

1.4.5 实现代理类 ProxyData.java(核心代理逻辑)

2.优化应用于更多数据库表

2.1 数据库准备

2.2 创建缓存文件夹

2.3 具体实现

2.3.1 定义抽象主题接口 DataAop.java(约束行为)

2.3.2 实现真实主题类 DBData.java(操作数据库,核心升级)

2.3.3 实现拦截器类 InterceptorData.java(处理缓存检查)

2.3.4 实现代理类 ProxyData.java(核心代理逻辑)

2.3.5 实现测试类 Test.java(运行和验证)

3.Java RMI 远程方法调用

3.1 什么是 RMI?

3.2 服务端的实现(提供远程查询服务)

3.2.1 定义远程接口 DataAop.java(服务端与客户端共用)

3.2.2 实现数据访问类 DBData.java(服务端本地业务)

3.2.3 实现远程服务类 DataAopImpl.java(服务端核心)

3.2.4 实现服务启动类 App.java(发布远程服务)

3.3 客户端的实现(调用远程服务 + 本地缓存)

3.3.1 创建缓存文件夹

3.3.2 复用远程接口 DataAop.java(与服务端完全一致)

3.3.3 实现缓存拦截器 InterceptorData.java(本地缓存管理)

3.3.4 实现 RMI 连接器 RMIData.java(客户端远程通信)

3.3.5 实现本地代理类 ProxyData.java(客户端核心逻辑)

3.3.6 实现客户端测试类 Test.java(客户端入口)

4.Java RMI 分布式缓存查询的实现

4.1 服务端实现:提供远程查询 + 统一缓存服务

4.1.1 服务端目录结构

4.1.2 服务端分步实现

① 定义远程接口 DataAop.java(服务端与客户端共用)

② 实现数据访问类 DBData.java(操作 MySQL 数据库)

③ 实现服务端缓存工具 ServerCacheUtil.java(统一缓存管理)

④ 实现远程服务类 DataAopImpl.java(集成缓存 + 远程能力)

⑤ 实现服务端启动类 App.java(发布 RMI 服务)

4.2 客户端实现:发起 RMI 请求 + 展示查询结果

4.2.1  客户端目录结构(明确文件组织)

4.2.2 客户端分步实现(按依赖顺序)

① 复用远程接口 DataAop.java(关键!必须与服务端一致)

② 实现 RMI 连接器 RMIData.java(与服务端建立通信)

③ 实现客户端代理类 ProxyData.java(统一查询入口)

④ 实现客户端入口类 Test.java(接收输入 + 展示结果)

5.多线程分割大文件

5.1 在 Windows 系统下配置 Nginx 的步骤

5.1.1 下载 Nginx

5.1.2 解压 Nginx

5.1.3 创建文件夹

5.1.4 存放文件

5.1.5 修改配置文件

5.1.6 检查配置文件语法

5.1.7 启动 Nginx

5.1.8 验证 Nginx 是否启动成功

​编辑

5.2 具体实现

5.2.1 DownThread 类:多线程下载的 “工作单元”

5.2.2 Test 类:多线程下载的 “调度中心”


1.Java 缓存代理模式的实现

通过 代理模式 实现 “数据库查询 + 本地缓存” 功能:首次查询从数据库获取数据并写入缓存,后续查询直接从缓存读取,减少数据库交互。

1.1 数据库准备

-- 创建表 --
CREATE TABLE t_stus (sid INT PRIMARY KEY AUTO_INCREMENT,sname VARCHAR(20) NOT NULL,saddress VARCHAR(200)
);-- 删除数据库表,用于先前如果创建好的表 --
DROP TABLE t_stus-- 插入数据 --
INSERT INTO t_stus(sname,saddress) VALUES("张三","南京");
INSERT INTO t_stus(sname,saddress) VALUES("李四","盐城");
INSERT INTO t_stus(sname,saddress) VALUES("王五","苏州");INSERT INTO t_stus(sname,saddress) VALUES("张六","南京");
INSERT INTO t_stus(sname,saddress) VALUES("王七","盐城");
INSERT INTO t_stus(sname,saddress) VALUES("李八","苏州");-- 查询表 --
SELECT * FROM t_stus

实现效果:

1.2 添加Maven依赖

<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.24</version>
</dependency>

1.3 创建缓存文件夹

在所属Maven项目下创建名为cachedata的Folder文件夹📂

1.4 具体实现

1.4.1 定义抽象主题接口 DataAop.java(约束行为)

先定义统一接口,确保真实类和代理类实现相同的 “查询方法”,符合代理模式的 “接口一致性” 原则。

package com.hy.demo12;import java.util.List;/*** 抽象主题接口:定义数据查询的统一方法* 作用:* 1. 约束真实类(DBData)和代理类(ProxyData)的行为,确保两者方法一致;* 2. 代理类可通过接口引用真实类,降低耦合(符合依赖倒置原则)。*/
public interface DataAop {/*** 数据查询方法* @param name 数据库表名(如t_stus)* @return 查询结果(存储Student对象的List集合)*/public List queryDatas(String name);}

1.4.2 定义数据模型 Student.java(封装数据)

数据库查询结果需要封装为对象,且该对象需实现序列化接口(后续要写入缓存文件,序列化是前提)。

package com.hy.demo12;import java.io.Serializable;/*** 数据模型类:封装数据库表(如t_stus)的一行数据* 必须实现Serializable接口:* - 因为对象要通过ObjectOutputStream写入文件(序列化),* - 反序列化时也需该接口确保类结构兼容。*/
public class Student implements Serializable {// 1. 成员变量:对应数据库表的字段(假设t_stus表有sid、sname、saddress三列)private int sid;         // 学生ID(对应表中sid字段,int类型)private String sname;    // 学生姓名(对应表中sname字段,varchar类型)private String saddress; // 学生地址(对应表中saddress字段,varchar类型)// 2. Getter方法:提供外部访问私有变量的接口(反序列化后获取对象属性)public int getSid() {return sid;}// 3. Setter方法:提供外部修改私有变量的接口(数据库查询后给对象赋值)public void setSid(int sid) {this.sid = sid;}public String getSname() {return sname;}public void setSname(String sname) {this.sname = sname;}public String getSaddress() {return saddress;}public void setSaddress(String saddress) {this.saddress = saddress;}}

1.4.3 实现真实主题类 DBData.java(操作数据库)

实现 DataAop 接口,编写真实的数据库查询逻辑,是代理类最终要 “委托” 的对象。

package com.hy.demo12;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;/*** 真实主题类:实现DataAop接口,负责真实的数据库查询* 角色:被代理对象,代理类(ProxyData)会在缓存未命中时调用此类的方法。*/
public class DBData implements DataAop {// 数据库连接对象(用于与MySQL建立连接)Connection conn;/*** 数据库连接方法:初始化Connection对象* 作用:加载MySQL驱动,建立与指定数据库的连接*/public void connDB() {try {// 1. 加载MySQL 8.0+驱动(驱动类全路径,旧版本是com.mysql.jdbc.Driver)Class.forName("com.mysql.cj.jdbc.Driver");// 2. 建立数据库连接:URL、用户名、密码// URL格式:jdbc:mysql://IP:端口/数据库名?参数(useSSL=false避免SSL警告,serverTimezone=UTC解决时区问题)conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mysql2025?useSSL=false&serverTimezone=UTC", "root",  // 数据库用户名"yourpassword"  // 数据库密码);// (可选)连接成功日志// System.out.println("数据库连接成功!");} catch (ClassNotFoundException e) {// 捕获驱动类未找到异常(如MySQL驱动包未导入)System.err.println("错误:MySQL驱动类未找到!");e.printStackTrace();} catch (SQLException e) {// 捕获数据库连接异常(如IP错误、端口错误、用户名密码错误、数据库不存在)System.err.println("错误:数据库连接失败!");e.printStackTrace();}}/*** 实现DataAop接口的查询方法:从指定数据库表查询数据* @param name 数据库表名(用户输入的表名,如t_stus)* @return 封装好的Student对象列表(表中所有行数据)*/@Overridepublic List queryDatas(String name) {// 1. 先调用connDB()建立数据库连接this.connDB();// 2. 定义SQL语句:查询指定表的所有数据(select * from 表名)String sql = "select * from " + name;// 3. 创建List集合,用于存储查询结果(每行数据封装为一个Student对象)List<Student> lists = new ArrayList<Student>();try {// 4. 创建PreparedStatement对象(预编译SQL,防止SQL注入)PreparedStatement pstmt = conn.prepareStatement(sql);// 5. 执行SQL查询,返回ResultSet结果集(存储查询到的所有行数据)ResultSet rs = pstmt.executeQuery();// 6. 遍历ResultSet结果集:逐行读取数据,封装为Student对象while (rs.next()) { // rs.next():移动到下一行,返回true表示有数据Student s = new Student(); // 创建一个Student对象,对应表中一行数据s.setSid(rs.getInt(1));       // 给sid赋值:读取当前行第1列数据(int类型)s.setSname(rs.getString(2));  // 给sname赋值:读取当前行第2列数据(String类型)s.setSaddress(rs.getString(3));// 给saddress赋值:读取当前行第3列数据(String类型)lists.add(s); // 将封装好的Student对象添加到List集合}} catch (SQLException e) {// 捕获SQL执行异常(如表名不存在、字段不存在、SQL语法错误)System.err.println("错误:SQL查询失败!");e.printStackTrace();} finally {// 7. 关闭数据库连接(无论查询成功与否,都必须关闭,避免资源泄露)if (null != conn) { // 先判断conn是否为null(防止连接未建立时调用close()抛空指针)try {conn.close();// System.out.println("数据库连接已关闭!");} catch (SQLException e) {System.err.println("错误:数据库连接关闭失败!");e.printStackTrace();}}}// 8. 返回查询结果(List集合,存储所有Student对象)return lists;}}

1.4.4 实现拦截器类 InterceptorData.java(处理缓存检查)

专门负责 “缓存相关的辅助操作”:检查缓存目录是否存在、目标缓存文件是否存在、读取缓存文件数据。将缓存逻辑抽离,让代理类更专注于 “代理逻辑”(符合单一职责原则)。

package com.hy.demo12;import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;/*** 拦截器类:专门处理缓存检查与读取* 职责:* 1. 检查缓存目录(./cachedata)是否存在、是否有文件;* 2. 检查目标缓存文件(如t_stus.datas)是否存在;* 3. 若缓存文件存在,读取文件数据并反序列化为List集合。*/
public class InterceptorData {/*** 缓存检查与读取方法* @param name 数据库表名(如t_stus),用于匹配缓存文件名(t_stus.datas)* @return 缓存数据(List<Student>):若缓存存在则返回数据,否则返回空List*/public List checkFile(String name) {List lists = null; // 存储缓存数据的List集合// 1. 定义缓存目录:当前项目根目录下的cachedata文件夹(./表示项目根目录)File file = new File("./cachedata");// 2. 获取缓存目录下的所有文件(返回File数组)File[] fs = file.listFiles();// 3. 处理“缓存目录为空或无文件”的情况// 注意:若cachedata目录不存在,file.listFiles()返回null,直接判断fs.length会抛空指针!if (fs == null || fs.length == 0) {System.out.println("该目录下没有缓冲的目录数据文件");lists = new ArrayList(); // 返回空List,表示无缓存} else {// 4. 缓存目录有文件,遍历所有文件,查找目标缓存文件System.out.println("该目录下有缓冲的目录数据文件");for (File f : fs) {// 判断当前文件是否包含目标表名(如文件t_stus.datas包含"t_stus")if (f.getName().contains(name)) {System.out.println("目标数据缓存文件存在");ObjectInputStream objIn = null; // 反序列化流:将文件字节转为对象try {// 5. 创建ObjectInputStream:读取缓存文件(./cachedata/表名.datas)objIn = new ObjectInputStream(new FileInputStream("./cachedata/" + name + ".datas"));// 6. 反序列化:将文件中的字节序列转为List集合(需强制类型转换)lists = (List) objIn.readObject();} catch (FileNotFoundException e) {// 捕获文件未找到异常(理论上不会触发,因前面已判断文件存在)e.printStackTrace();} catch (IOException e) {// 捕获IO异常(如文件损坏、流关闭异常)e.printStackTrace();} catch (ClassNotFoundException e) {// 捕获类未找到异常(反序列化时找不到Student类,如类被删除、包路径修改)e.printStackTrace();} finally {// 7. 关闭反序列化流(避免资源泄露)if (objIn != null) {try {objIn.close();} catch (IOException e) {e.printStackTrace();}}}}}// 补充逻辑:若遍历后未找到目标缓存文件,返回空Listif (lists == null) {lists = new ArrayList();}}// 8. 返回缓存数据(空List或真实缓存数据)return lists;}}

1.4.5 实现代理类 ProxyData.java(核心代理逻辑)

实现 DataAop 接口,整合 “缓存检查(InterceptorData)” 和 “数据库查询(DBData)”:先查缓存,缓存命中则直接返回,未命中则调用真实类查询并写入缓存。

package com.hy.demo12;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.List;/*** 代理类:实现DataAop接口,核心是“控制访问真实类+缓存管理”* 工作流程:* 1. 调用拦截器检查缓存;* 2. 缓存命中 → 直接返回缓存数据;* 3. 缓存未命中 → 调用真实类查询数据库 → 将查询结果写入缓存 → 返回数据。*/
public class ProxyData implements DataAop {// 1. 持有真实类(DBData)的引用(通过接口DataAop,降低耦合)private DataAop dataAop;// 2. 持有拦截器(InterceptorData)的引用(用于检查缓存)private InterceptorData interceptor;/*** 构造方法:通过依赖注入初始化真实类和拦截器* 作用:避免代理类内部硬编码创建对象,提高灵活性(可替换不同的真实类或拦截器)* @param dataAop 真实类对象(如new DBData())* @param interceptor 拦截器对象(如new InterceptorData())*/public ProxyData(DataAop dataAop, InterceptorData interceptor) {this.dataAop = dataAop;this.interceptor = interceptor;}/*** 实现DataAop接口的查询方法:核心代理逻辑* @param name 数据库表名(用户输入,如t_stus)* @return 查询结果(缓存数据或数据库数据)*/@Overridepublic List<Student> queryDatas(String name) {// 3. 第一步:调用拦截器检查缓存(传入表名,匹配缓存文件)List lists = this.interceptor.checkFile(name);// 4. 第二步:判断缓存是否命中(lists.size() > 0 表示有缓存)if (lists.size() > 0) {System.out.println("直接从缓存中获取数据....");return lists; // 缓存命中,直接返回缓存数据} else {// 5. 缓存未命中:调用真实类(DBData)查询数据库System.out.println("要去数据库查询数据....");List<Student> dbList = this.dataAop.queryDatas(name);// 6. 第三步:将数据库查询结果写入缓存(下次查询可直接用)ObjectOutputStream objOut = null; // 序列化流:将对象转为字节写入文件try {// 【原代码错误点】:缓存目录不一致!读取用./cachedata,写入用./cachemapdata// 修复:统一为./cachedata,确保后续查询能找到缓存文件String cachePath = "./cachedata/" + name + ".datas";// 补充:自动创建缓存目录(若./cachedata不存在,创建目录避免FileNotFoundException)File cacheFile = new File(cachePath);if (!cacheFile.getParentFile().exists()) {cacheFile.getParentFile().mkdirs(); // mkdirs()创建多级目录}// 7. 创建ObjectOutputStream:将数据写入缓存文件objOut = new ObjectOutputStream(new FileOutputStream(cachePath));// 8. 序列化:将数据库查询结果(dbList)写入文件(转为二进制字节序列)objOut.writeObject(dbList);System.out.println("数据已写入缓存文件:" + cachePath);} catch (FileNotFoundException e) {// 捕获文件未找到异常(如目录不存在,已通过mkdirs()修复)e.printStackTrace();} catch (IOException e) {// 捕获IO异常(如磁盘满了、无写入权限、文件损坏)e.printStackTrace();} finally {// 9. 关闭序列化流(避免资源泄露,确保数据刷入文件)if (objOut != null) {try {objOut.close();} catch (IOException e) {e.printStackTrace();}}}// 10. 返回数据库查询结果return dbList;}}}

2.优化应用于更多数据库表

不使用固定的 Student 类来封装数据,通过 List<Map<String, String>> 这种更通用的结构来存储任意表的查询结果。这种设计使得缓存代理模式可以应用于任何数据库表,而无需为每个表都创建一个对应的 Java 实体类。

2.1 数据库准备

-- 创建t_emps表 --
CREATE TABLE t_emps(eid INT PRIMARY KEY auto_increment, -- 员工的编号ename VARCHAR(20) NOT NULL, -- 员工的姓名epwd CHAR(8) NOT NULL, -- 员工的密码ebirthday datetime, -- 员工的出生年月,不设计年龄字段,会造成字段冗余esalary DOUBLE NOT NULL, -- 员工的工资eaddress VARCHAR(100) -- 员工的地址
)-- 删除t_emps表 --
DROP TABLE t_emps-- 插入数据 --
INSERT INTO t_emps(ename,epwd,ebirthday,esalary,eaddress)
VALUES('赵五','11111','2000-05-28',90000.56,'南京');INSERT INTO t_emps(ename,epwd,ebirthday,esalary,eaddress)
VALUES('李六','22222','2004-06-15',88000.69,'盐城');INSERT INTO t_emps(ename,epwd,ebirthday,esalary,eaddress)
VALUES('李老八','22222','1996-12-30',5600,'无锡');INSERT INTO t_emps(ename,epwd,ebirthday,esalary,eaddress)
VALUES('赵二','22222','1996-12-30',5800,'无锡');-- 查询表 --
SELECT * FROM t_emps

实现效果:

2.2 创建缓存文件夹

在所属Maven项目下创建名为cachemapdata的Folder文件夹📂

2.3 具体实现

2.3.1 定义抽象主题接口 DataAop.java(约束行为)

首先,我们定义一个统一的接口,这次接口的方法返回一个更通用的数据结构。

package com.hy.demo13;import java.util.List;
import java.util.Map;/*** 抽象主题接口:定义数据查询的统一方法。* 【版本升级】:* - 与demo12相比,返回值类型从具体的 List<Student> 改为了通用的 List<Map<String, String>>。* - 这使得任何实现此接口的类都可以返回任意表的数据,只要将其封装为“列表套字典”的形式。*/
public interface DataAop {/*** 数据查询方法* @param name 数据库表名(如 t_emps, t_stus)* @return 查询结果,封装为一个List。List中的每个元素是一个Map,*         Map的key是列名(String),value是该列对应的值(String)。*/public List<Map<String, String>> queryDatas(String name);}

2.3.2 实现真实主题类 DBData.java(操作数据库,核心升级)

这是最关键的改动。DBData 类需要变得足够智能,能处理任意表的查询。

package com.hy.demo13;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 真实主题类:实现DataAop接口,负责真实的数据库查询。* 【核心升级】:* - 使用 ResultSetMetaData 动态获取表的元数据(如列名、列数)。* - 将查询结果封装为 List<Map<String, String>> 结构,实现了对任意表的通用支持。*/
public class DBData implements DataAop {Connection conn;/*** 数据库连接方法(与demo12相同)*/public void connDB() {try {Class.forName("com.mysql.cj.jdbc.Driver");conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mysql2025?useSSL=false&serverTimezone=UTC", "root", "yourpassword");} catch (ClassNotFoundException | SQLException e) {e.printStackTrace();}}/*** 实现DataAop接口的查询方法:从指定数据库表查询数据并封装为通用结构。* @param name 数据库表名* @return 通用的查询结果 List<Map<String, String>>*/@Overridepublic List<Map<String, String>> queryDatas(String name) {this.connDB(); // 建立数据库连接String sql = "select * from " + name;// 定义一个List,用于存储最终结果。// 它的每个元素都是一个Map,代表一行数据。List<Map<String, String>> lists = new ArrayList<>();try {PreparedStatement pstmt = this.conn.prepareStatement(sql);ResultSet rs = pstmt.executeQuery();// 【关键步骤1】获取结果集的元数据(MetaData)// ResultSetMetaData 像一个“数据字典”,包含了结果集的结构信息,如列名、列的数量、类型等。ResultSetMetaData rsmd = rs.getMetaData();// 【关键步骤2】获取列的总数int columnCount = rsmd.getColumnCount();// 遍历结果集中的每一行数据while (rs.next()) {// 为每一行数据创建一个Map对象// Map的key将是列名,value是从ResultSet中读取的值。Map<String, String> rowMap = new HashMap<>();// 【关键步骤3】循环遍历每一列// 使用 for 循环,从第1列遍历到最后一列(columnCount)for (int i = 1; i <= columnCount; i++) {// 1. 从元数据中获取第 i 列的列名String columnName = rsmd.getColumnName(i);// 2. 从结果集中获取第 i 列的值(统一转为String类型,方便通用处理)String columnValue = rs.getString(i);// 3. 将“列名-值”对放入当前行的Map中rowMap.put(columnName, columnValue);}// 将封装好一行数据的Map添加到List中lists.add(rowMap);}} catch (SQLException e) {e.printStackTrace();} finally {if (null != conn) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}}return lists; // 返回通用的查询结果}// 内部测试方法,验证DBData类的功能是否正常public static void main(String[] args) {DBData db = new DBData();List<Map<String, String>> lists = db.queryDatas("t_emps");for (Map<String, String> map : lists) {// 可以根据任意列名获取值,非常灵活System.out.println(map.get("ename"));}}
}

2.3.3 实现拦截器类 InterceptorData.java(处理缓存检查)

拦截器的逻辑基本不变,只需确保反序列化时处理的是 List<Map> 类型即可。

package com.hy.demo13;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;/*** 拦截器类:专门处理缓存检查与读取。* 【版本适配】:* - 逻辑与demo12类似,但内部操作的对象已变为 List<Map<String, String>>。* - 修复了demo12中的一些小问题,如目录不存在时的空指针异常。*/
public class InterceptorData {public List<Map<String, String>> checkFile(String name) {// 直接初始化一个空列表,避免后续判空List<Map<String, String>> lists = new ArrayList<>();File file = new File("./cachemapdata");// 【健壮性优化】先判断目录是否存在且是一个目录if (!file.exists() || !file.isDirectory()) {System.out.println("缓存目录不存在或不是一个目录。");return lists; // 返回空列表}File[] fs = file.listFiles();// 【健壮性优化】判断目录是否为空if (fs == null || fs.length == 0) {System.out.println("该目录下没有缓冲的数据文件");return lists; // 返回空列表}System.out.println("该目录下有缓冲的目录数据文件,正在查找目标文件...");boolean found = false;for (File f : fs) {// 使用 endsWith 更精确地匹配文件名(例如 "t_emps.datas")if (f.getName().endsWith(name + ".datas")) {System.out.println("目标数据缓存文件存在: " + f.getName());found = true;try (ObjectInputStream objIn = new ObjectInputStream(new FileInputStream(f))) {// 反序列化,并强制转换为 List<Map<String, String>>lists = (List<Map<String, String>>) objIn.readObject();break; // 找到后立即退出循环} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}}if (!found) {System.out.println("目标数据缓存文件不存在");}return lists;}
}

2.3.4 实现代理类 ProxyData.java(核心代理逻辑)

代理类的逻辑完全不需要改变!这正是面向接口编程和使用通用数据结构带来的好处。它与 DataAop 接口和序列化机制交互,而不关心具体的数据内容。

package com.hy.demo13;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.List;
import java.util.Map;/*** 代理类:实现DataAop接口,核心是“控制访问真实类+缓存管理”。* 【版本适配】:* - 代码逻辑与demo12完全相同!因为它依赖于DataAop接口和序列化机制,*   当接口的返回值类型变为通用的List<Map>后,代理类无需任何修改即可无缝支持。* - 这体现了“开闭原则”:对扩展开放(DBData的实现),对修改关闭(ProxyData无需修改)。*/
public class ProxyData implements DataAop {private DataAop dataAop;private InterceptorData interceptor;public ProxyData(DataAop dataAop, InterceptorData interceptor) {this.dataAop = dataAop;this.interceptor = interceptor;}@Overridepublic List<Map<String, String>> queryDatas(String name) {List<Map<String, String>> lists = this.interceptor.checkFile(name);if (!lists.isEmpty()) { // 使用 !lists.isEmpty() 更安全System.out.println("直接从缓存中获取数据....");return lists;} else {System.out.println("要去数据库查询数据....");List<Map<String, String>> dbList = this.dataAop.queryDatas(name);// 确保缓存目录存在File cacheDir = new File("./cachemapdata");if (!cacheDir.exists()) {cacheDir.mkdirs();}try (ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream("./cachemapdata/" + name + ".datas"))) {objOut.writeObject(dbList);System.out.println("数据已写入缓存文件。");} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return dbList;}}
}

2.3.5 实现测试类 Test.java(运行和验证)

测试类也相应升级,允许用户输入要查询的列名。

package com.hy.demo13;import java.util.List;
import java.util.Map;
import java.util.Scanner;/*** 测试类:启动程序,演示通用缓存代理模式的功能。* 【交互升级】:* - 除了接收表名,还接收一个列名,用于从查询结果中提取特定字段进行打印。* - 这完美展示了使用Map存储数据的灵活性。*/
public class Test {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);System.out.println("请问您查询的数据库表的名称是?(请输入表的全名)");String tableName = scanner.next();System.out.println("请问您想查询哪个字段的值?(请输入列名)");String keyName = scanner.next();// 创建代理对象并执行查询List<Map<String, String>> lists = new ProxyData(new DBData(), new InterceptorData()).queryDatas(tableName);// 遍历结果并打印用户指定的字段for (Map<String, String> map : lists) {// 使用 map.get(keyName) 从每一行数据中动态获取指定列的值System.out.println(map.get(keyName));}scanner.close();}
}

输出结果:

请问您查询的数据库表的名称是?(请输入表的全名)
t_emps
ename
该目录下有缓冲的目录数据文件
目标数据缓存文件不存在
要去数据库查询数据....
赵五
李六
李老八
赵二

3.Java RMI 远程方法调用

使用 Java RMI (Remote Method Invocation) 技术将之前的本地缓存代理模式升级为一个分布式服务

3.1 什么是 RMI?

RMI (Remote Method Invocation) 是 Java 提供的一种机制,允许一个 JVM 上的对象调用另一个 JVM 上的对象的方法,就像调用本地方法一样。

  • 核心思想:将方法调用从 “本地” 扩展到 “网络”,实现分布式计算。
  • 使用场景:适用于需要将业务逻辑(如数据查询)部署在独立服务器上,供多个客户端(如 Web 服务器、桌面应用)远程调用的场景。

3.2 服务端的实现(提供远程查询服务)

3.2.1 定义远程接口 DataAop.java(服务端与客户端共用)

这个接口定义了客户端和服务器之间的 “契约”。

package com.hy.demo14;import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;/*** 远程接口:定义了可以被远程调用的方法。* 【RMI关键要求】:* 1. 必须继承 java.rmi.Remote 接口(标记接口)。* 2. 接口中的所有方法必须声明抛出 java.rmi.RemoteException。* 3. 方法的参数和返回值必须是可序列化的(serializable)。*/
public interface DataAop extends Remote {//定义一个远程数据查询方法。public List<Map<String, String>> queryDatas(String name) throws RemoteException;
}

3.2.2 实现数据访问类 DBData.java(服务端本地业务)

这是我们熟悉的数据库查询逻辑,它不关心自己是被本地调用还是远程调用。

package com.hy.demo14.dao;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class DBData {Connection conn;public void connDB() {try {Class.forName("com.mysql.cj.jdbc.Driver");conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mysql2025", "root", "Hy61573166!!!");} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}}public List<Map<String, String>> queryDatas(String name) {this.connDB();String sql = "select * from " + name;List<Map<String, String>> lists = new ArrayList<Map<String, String>>();try {PreparedStatement pstmt = this.conn.prepareStatement(sql);ResultSet rs = pstmt.executeQuery();ResultSetMetaData rsmd = rs.getMetaData();int columns = rsmd.getColumnCount();while (rs.next()) {Map<String, String> LineMap = new HashMap<String, String>();for (int i = 0; i < columns; i++) {LineMap.put(rsmd.getColumnName(i + 1), rs.getString(i + 1));}lists.add(LineMap);}} catch (SQLException e) {e.printStackTrace();} finally {if (null != conn) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}}return lists;}}

3.2.3 实现远程服务类 DataAopImpl.java(服务端核心)

这个类是 RMI 的核心,它将远程接口和本地业务逻辑连接起来。

package com.hy.demo14.impl;import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
import java.util.Map;import com.hy.demo14.DataAop;
import com.hy.demo14.dao.DBData;/*** 远程实现类:实现了远程接口 DataAop。* 【RMI关键要求】:* 1. 必须实现远程接口 (DataAop)。* 2. 必须继承 UnicastRemoteObject。*    - 这个类的构造函数会自动完成“远程对象”的导出(export)工作,*    - 即将对象与一个网络端口绑定,使其能够监听和响应客户端的远程调用。*    - 如果不继承此类,则需要在代码中手动调用 `UnicastRemoteObject.exportObject()`。*/
public class DataAopImpl extends UnicastRemoteObject implements DataAop {/*** 构造函数。* 【RMI关键要求】:* - 因为父类 UnicastRemoteObject 的构造函数会抛出 RemoteException,* - 所以子类的构造函数必须显式声明 `throws RemoteException`。*/public DataAopImpl() throws RemoteException {super(); // 调用父类构造函数,完成对象导出}/*** 实现远程接口的方法。* 【核心逻辑】:* - 这个方法是在 RMI 服务器上执行的。* - 它内部会创建一个本地的 DBData 对象,并调用其 queryDatas() 方法。* - 这样就将“远程调用”转换为了“本地方法调用”。*/public List<Map<String, String>> queryDatas(String name) throws RemoteException {System.out.println("RMI服务器调用数据库查询数据: " + name); // 服务器端日志DBData db = new DBData(); // 创建本地业务对象return db.queryDatas(name); // 调用本地方法并返回结果}
}

3.2.4 实现服务启动类 App.java(发布远程服务)

这个类负责启动 RMI 服务,注册远程对象,使其可供客户端调用。

package com.hy.demo14.rmiserver;import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;import com.hy.demo14.DataAop;
import com.hy.demo14.impl.DataAopImpl;/*** RMI 服务器启动类:负责发布 RMI 服务。*/
public class App {public static void main(String[] args) {try {// 1. 创建远程对象的实例// DataAopImpl 的构造函数会自动将其导出为远程对象DataAop dataAop = new DataAopImpl();// 2. 创建 RMI 注册表// 在指定端口(如 9200)上创建一个注册表服务。// 客户端将通过这个端口来查找远程服务。LocateRegistry.createRegistry(9200);System.out.println("RMI 注册表已在端口 9200 上启动。");// 3. 将远程对象绑定到注册表Naming.bind("rmi://127.0.0.1:9200/queryDatas", dataAop);System.out.println("RMI数据服务 'queryDatas' 创建成功,并已绑定到注册表。");System.out.println("服务器正在运行,等待客户端调用...");} catch (RemoteException e) {System.err.println("创建或导出远程对象时发生错误。");e.printStackTrace();} catch (MalformedURLException e) {System.err.println("RMI 服务 URL 格式不正确。");e.printStackTrace();} catch (AlreadyBoundException e) {System.err.println("该服务名称已经在注册表中绑定了。");e.printStackTrace();}}
}

输出结果:

RMI 注册表已在端口 9200 上启动。
RMI数据服务 'queryDatas' 创建成功,并已绑定到注册表。
服务器正在运行,等待客户端调用...

3.3 客户端的实现(调用远程服务 + 本地缓存)

客户端通过代理类先检查本地缓存,缓存未命中时再调用远程 RMI 服务查询数据,最后将远程数据写入本地缓存。

3.3.1 创建缓存文件夹

在所属Maven项目下创建名为cachermidata的Folder文件夹📂

3.3.2 复用远程接口 DataAop.java(与服务端完全一致)

首先定义 RMI 远程接口,所有远程调用的方法必须在此声明,且需遵循 RMI 规范。

package com.hy.demo14;import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;/*** 抽象主题(远程接口):定义RMI远程可调用的方法* 【RMI核心规范】:* 1. 必须继承 java.rmi.Remote 接口(标记接口,无实际方法,仅标识该接口可远程调用)* 2. 所有方法必须声明抛出 RemoteException(处理网络异常、服务端错误等远程调用风险)* 3. 方法参数/返回值必须可序列化(List、Map、String 均满足,支持网络传输)*/
public interface DataAop extends Remote {// 远程数据查询方法public List<Map<String,String>> queryDatas(String name) throws RemoteException;}

3.3.3 实现缓存拦截器 InterceptorData.java(本地缓存管理)

抽离缓存逻辑,让代理类专注于 “代理流程”,符合单一职责原则。该类仅处理本地文件操作,不涉及远程调用。

package com.hy.demo14;import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;/*** 缓存拦截器:专门处理本地缓存的检查与读取* 职责:* 1. 检查本地缓存目录(./cachermidata)是否存在、是否有文件* 2. 检查目标缓存文件(如 t_emps.datas)是否存在* 3. 若缓存存在,读取文件并反序列化为 List<Map> 数据*/
public class InterceptorData {public List checkFile(String name) {List<Map<String, String>> lists = null;// 1. 定义本地缓存目录File file = new File("./cachermidata");// 2. 获取目录下所有文件(返回 File 数组)File[] fs = file.listFiles();// 3. 处理“缓存目录为空或无文件”的情况if (fs == null || fs.length == 0) {System.out.println("该目录下没有缓冲的目录数据文件");lists = new ArrayList<Map<String, String>>(); // 返回空List,表示无缓存} else {// 4. 缓存目录有文件,遍历查找目标缓存文件System.out.println("该目录下有缓冲的目录数据文件");for (File f : fs) {// 判断文件名是否包含表名if (f.getName().contains(name)) {System.out.println("目标数据缓存文件存在");ObjectInputStream objIn = null; // 反序列化流:将文件字节转为对象try {// 5. 创建反序列化流,读取缓存文件objIn = new ObjectInputStream(new FileInputStream("./cachermidata/" + name + ".datas"));// 6. 反序列化:将文件中的字节序列转为 List<Map> 对象(强制类型转换)lists = (ArrayList<Map<String, String>>) objIn.readObject();break; // 找到目标文件后,跳出循环,避免继续遍历} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} finally {// 7. 关闭反序列化流(避免资源泄露)if (objIn != null) {try {objIn.close();} catch (IOException e) {e.printStackTrace();}}}} else {// 若当前文件不匹配,初始化空List(避免后续判断空指针)System.out.println("目标数据缓存文件不存在");lists = new ArrayList<Map<String, String>>();}}}return lists;}}

3.3.4 实现 RMI 连接器 RMIData.java(客户端远程通信)

该类是客户端与远程 RMI 服务的 “桥梁”,负责查找并连接远程服务,代理类会通过它调用远程方法。

package com.hy.demo14;import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;/*** RMI客户端连接器:负责连接远程RMI服务,实现DataAop接口*  作用:* 1. 通过 Naming.lookup() 查找远程RMI服务* 2. 封装远程调用逻辑,让代理类无需关心RMI连接细节*/
public class RMIData implements DataAop {// 持有远程服务接口的引用(通过 lookup 获得远程服务的代理对象)DataAop dataAop;/*** 连接远程RMI服务的方法* 核心逻辑:通过 RMI 注册表查找远程服务*/public void connRMIServer() {try {// Naming.lookup():从RMI注册表中查找远程服务dataAop = (DataAop) Naming.lookup("rmi://127.0.0.1:9200/queryDatas");System.out.println("RMI服务连接成功!");} catch (MalformedURLException e) {System.err.println("RMI服务URL格式错误!");e.printStackTrace();} catch (RemoteException e) {System.err.println("RMI服务连接失败(服务端未启动或网络异常)!");e.printStackTrace();} catch (NotBoundException e) {System.err.println("RMI服务名未绑定(服务名错误)!");e.printStackTrace();}}/*** 实现DataAop接口的远程查询方法* 作用:调用远程RMI服务的 queryDatas 方法*/@Overridepublic List<Map<String, String>> queryDatas(String name) throws RemoteException {this.connRMIServer(); // 先连接远程服务return dataAop.queryDatas(name); // 调用远程服务的方法,返回结果}}

3.3.5 实现本地代理类 ProxyData.java(客户端核心逻辑)

代理类整合 “缓存拦截器” 和 “RMI 连接器”,实现 “本地缓存优先,远程查询兜底” 的逻辑,是整个客户端的核心。

package com.hy.demo14;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;/*** 本地代理类:核心逻辑是“本地缓存+远程RMI查询”的结合* 工作流程:* 1. 调用拦截器检查本地缓存 → 缓存命中 → 返回数据* 2. 缓存未命中 → 调用RMI连接器查询远程服务 → 写入本地缓存 → 返回数据*/
public class ProxyData implements DataAop {// 持有被代理对象(RMI连接器,负责远程查询)private DataAop dataAop;// 持有缓存拦截器(负责本地缓存检查与读取)private InterceptorData interceptor;/*** 构造方法:通过依赖注入初始化被代理对象和拦截器* 作用:降低耦合,代理类无需硬编码创建对象,可灵活替换实现* @param dataAop 被代理对象(如 RMIData)* @param interceptor 缓存拦截器(如 InterceptorData)*/public ProxyData(DataAop dataAop, InterceptorData interceptor) {this.dataAop = dataAop;this.interceptor = interceptor;}/*** 核心代理方法:实现“缓存+远程查询”逻辑*/@Overridepublic List<Map<String, String>> queryDatas(String name) {// 1. 第一步:调用拦截器检查本地缓存List lists = this.interceptor.checkFile(name);// 2. 第二步:判断缓存是否命中(lists.size() > 0 表示有缓存)if (lists.size() > 0) {System.out.println("直接从缓存中获取数据....");return lists; // 缓存命中,直接返回本地数据} else {// 3. 缓存未命中:调用RMI远程服务查询数据System.out.println("要去分布式RMI服务器查询数据....");List<Map<String, String>> dbList = null;try {// 调用被代理对象(RMIData)的远程查询方法dbList = this.dataAop.queryDatas(name);// 4. 第三步:将远程查询结果写入本地缓存(下次查询可直接用)ObjectOutputStream objOut = null; // 序列化流:将对象转为字节写入文件// 确保缓存目录存在(若 ./cachermidata 不存在,创建目录避免FileNotFoundException)File cacheDir = new File("./cachermidata");if (!cacheDir.exists()) {cacheDir.mkdirs(); // mkdirs() 可创建多级目录}// 创建序列化流,写入缓存文件(路径:./cachermidata/表名.datas)objOut = new ObjectOutputStream(new FileOutputStream("./cachermidata/" + name + ".datas"));objOut.writeObject(dbList); // 序列化:将 List<Map> 转为字节写入文件System.out.println("远程数据已写入本地缓存!");} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}// 5. 返回远程查询结果return dbList;}}}

3.3.6 实现客户端测试类 Test.java(客户端入口)

测试类是客户端的入口,负责接收用户输入,创建代理对象,触发整个查询流程,并打印结果。

package com.hy.demo14;import java.util.List;
import java.util.Map;
import java.util.Scanner;/*** 客户端测试类:启动程序,演示“本地缓存+远程RMI查询”流程* 交互逻辑:* 1. 接收用户输入的“数据库表名”和“要查询的列名”* 2. 创建代理对象,调用查询方法* 3. 遍历结果,打印指定列的值*/
public class Test {public static void main(String[] args) {// 1. 接收用户输入:数据库表名System.out.println("请问您查询的数据库表的名称是?(请输入表的全名)");Scanner s = new Scanner(System.in);String tableName = s.next(); // 如输入:t_emps// 2. 接收用户输入:要查询的列名System.out.println("请问您想查询哪个字段的值?(请输入列名)");String keyname = s.next(); // 如输入:ename(员工姓名列)// 3. 创建代理对象:注入 RMI连接器 和 缓存拦截器ProxyData proxy = new ProxyData(new RMIData(), new InterceptorData());// 4. 调用代理方法,执行查询(缓存优先,远程兜底)List<Map<String, String>> lists = proxy.queryDatas(tableName);// 5. 遍历结果,打印用户指定列的值System.out.println("\n查询结果(" + keyname + "列):");for (Map<String, String> map : lists) {// Map.get(keyname):根据列名动态获取值(通用结构的优势)System.out.println(map.get(keyname));}s.close(); // 关闭Scanner}}

输出结果:

RMI 注册表已在端口 9200 上启动。
RMI数据服务 'queryDatas' 创建成功,并已绑定到注册表。
服务器正在运行,等待客户端调用...
RMI服务器调用数据库查询数据: t_stus

请问您查询的数据库表的名称是?(请输入表的全名)
t_stus
sname
该目录下没有notnot缓冲的目录数据文件
要去分布式RMI服务器查询数据....
张三
李四
王五
张六
王七
李八

4.Java RMI 分布式缓存查询的实现

作业:

    把本地缓存目录的策略转移到RMI服务器缓存目录。这样在服务器上就是统一的缓存目录策略。

采用 服务端统一缓存 架构,服务端集成数据库查询与缓存管理,客户端仅负责发起请求与展示结果,彻底解决 “客户端缓存分散、数据不一致” 问题。

4.1 服务端实现:提供远程查询 + 统一缓存服务

服务端核心目标:暴露 RMI 远程服务,优先从统一缓存返回数据,未命中则查询数据库并更新缓存,实现 “一次查询,多客户端复用”。

4.1.1 服务端目录结构

4.1.2 服务端分步实现

① 定义远程接口 DataAop.java(服务端与客户端共用)

远程接口是 RMI 通信的 “契约”,必须遵循 RMI 三大规范:继承 Remote、方法抛 RemoteException、参数 / 返回值可序列化。

package com.hy.demo15;import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;/*** RMI远程接口:服务端与客户端的通信契约* 规范说明:* 1. 必须继承 Remote 接口(标记接口,标识可远程调用)* 2. 所有方法必须声明抛出 RemoteException(处理网络/服务端异常)* 3. 返回值 List<Map> 可序列化(List、Map、String 均实现 Serializable)*/
public interface DataAop extends Remote {/*** 远程查询方法:根据表名查询数据库数据* @param name 数据库表名(如 t_emps)* @return 表数据(每行封装为 Map:key=列名,value=列值)* @throws RemoteException 远程调用异常*/List<Map<String, String>> queryDatas(String name) throws RemoteException;
}

关键注意:客户端必须复制此接口到 com.hy.demo15 包下,确保包名、类名、方法签名完全一致(否则会出现 ClassCastException)。

② 实现数据访问类 DBData.java(操作 MySQL 数据库)

封装数据库连接与查询逻辑,服务端通过此类从 MySQL 读取数据,不涉及远程通信。

package com.hy.demo15.dao;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 服务端数据访问工具类:负责连接MySQL并查询数据* 核心能力:将查询结果封装为通用 List<Map> 结构(适配任意表)*/
public class DBData {// 数据库连接对象(每次查询后关闭,避免资源泄露)private Connection conn;/*** 建立MySQL连接(MySQL 8.0+ 适配)* 注意:URL必须添加 serverTimezone=UTC,否则会抛出时区异常*/public void connDB() {try {// 1. 加载MySQL 8.0+ 驱动(旧版本驱动为 com.mysql.jdbc.Driver)Class.forName("com.mysql.cj.jdbc.Driver");// 2. 建立连接:URL(数据库地址+时区)、用户名、密码String url = "jdbc:mysql://127.0.0.1:3306/mysql2025?serverTimezone=UTC";conn = DriverManager.getConnection(url, "root", "yourpassword");} catch (ClassNotFoundException e) {System.err.println("数据库驱动加载失败!请检查MySQL驱动依赖是否引入");e.printStackTrace();} catch (SQLException e) {System.err.println("数据库连接失败!可能原因:IP/端口错误、数据库不存在、用户名/密码错误");e.printStackTrace();}}/*** 根据表名查询数据,返回通用 List<Map> 结构* @param name 数据库表名(如 t_emps)* @return 表数据(无数据返回空List,不返回null)*/public List<Map<String, String>> queryDatas(String name) {this.connDB(); // 先建立数据库连接String sql = "select * from " + name; // 查询表所有数据(实际项目建议加条件,避免全表扫描)List<Map<String, String>> resultList = new ArrayList<>();try {// 1. 创建预编译SQL语句对象(防SQL注入)PreparedStatement pstmt = conn.prepareStatement(sql);// 2. 执行查询,获取结果集ResultSet rs = pstmt.executeQuery();// 3. 获取结果集元数据(含列名、列数,实现“通用查询”)ResultSetMetaData rsmd = rs.getMetaData();int columnCount = rsmd.getColumnCount(); // 表的列总数// 4. 遍历结果集,封装为 List<Map>while (rs.next()) {Map<String, String> rowMap = new HashMap<>();for (int i = 1; i <= columnCount; i++) {String columnName = rsmd.getColumnName(i); // 获取列名(如 ename)String columnValue = rs.getString(i); // 获取列值(统一转为String,适配所有类型)rowMap.put(columnName, columnValue);}resultList.add(rowMap);}} catch (SQLException e) {System.err.println("SQL查询失败!可能原因:表名不存在(" + name + ")、字段错误");e.printStackTrace();} finally {// 5. 关闭数据库连接(必须在finally中执行,确保资源释放)if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}}return resultList;}
}

依赖说明:需在服务端项目中引入 MySQL 驱动(如 Maven 依赖 mysql:mysql-connector-java:8.0.32),否则会抛出 ClassNotFoundException

③ 实现服务端缓存工具 ServerCacheUtil.java(统一缓存管理)

封装服务端缓存的 “创建目录、检查缓存、读取缓存、写入缓存” 逻辑。

package com.hy.demo15.server.cache;import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;/*** 服务端统一缓存工具类:静态工具类,无需实例化* 缓存策略:* - 缓存目录:./server_cache(服务端启动时自动创建)* - 缓存文件:表名.datas(序列化存储 List<Map> 数据)* - 缓存逻辑:查询前先查缓存,未命中则查库并写入缓存*/
public class ServerCacheUtil {// 服务端缓存根目录(固定路径,所有客户端共享)private static final String SERVER_CACHE_DIR = "./server_cache";// 静态代码块:服务端启动时自动创建缓存目录(仅执行一次)static {File cacheDir = new File(SERVER_CACHE_DIR);if (!cacheDir.exists()) {// mkdirs():创建多级目录(若父目录不存在也会创建),区别于 mkdir()boolean isCreated = cacheDir.mkdirs();if (isCreated) {System.out.println("服务端缓存目录创建成功:" + SERVER_CACHE_DIR);} else {System.err.println("服务端缓存目录创建失败!可能原因:磁盘空间不足、权限不足");}}}/*** 检查并读取缓存:根据表名查找缓存文件,存在则返回数据* @param tableName 数据库表名(匹配缓存文件名)* @return 缓存数据(List<Map>):有缓存返回数据,无缓存返回空List*/public static List<Map<String, String>> getCache(String tableName) {// 1. 构建缓存文件完整路径(目录 + 文件名)String cacheFilePath = SERVER_CACHE_DIR + File.separator + tableName + ".datas";File cacheFile = new File(cacheFilePath);// 2. 检查缓存文件是否存在(不存在则返回空List)if (!cacheFile.exists() || !cacheFile.isFile()) {System.out.println("服务端缓存:" + tableName + " 缓存文件不存在");return new ArrayList<>();}// 3. 读取缓存文件(反序列化:将字节流转为 List<Map> 对象)try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(cacheFile))) {Object cacheData = ois.readObject();// 类型校验:确保缓存数据是 List<Map> 结构(避免序列化版本不一致导致的错误)if (cacheData instanceof List<?>) {return (List<Map<String, String>>) cacheData;} else {System.err.println("服务端缓存:" + tableName + " 缓存数据格式错误(非 List 类型)");return new ArrayList<>();}} catch (IOException e) {System.err.println("服务端缓存:读取 " + tableName + " 缓存失败(文件损坏/IO异常)");e.printStackTrace();return new ArrayList<>();} catch (ClassNotFoundException e) {System.err.println("服务端缓存:反序列化 " + tableName + " 缓存失败(类未找到,可能是序列化版本不一致)");e.printStackTrace();return new ArrayList<>();}}/*** 写入缓存:将数据库查询结果序列化到缓存文件* @param tableName 数据库表名(作为缓存文件名)* @param data 要缓存的数据(从数据库查询的 List<Map> 结果)* @return 写入结果:true=成功,false=失败*/public static boolean writeCache(String tableName, List<Map<String, String>> data) {// 数据为空时不写入缓存(避免无效缓存)if (data == null || data.isEmpty()) {System.err.println("服务端缓存:写入失败,数据为空(tableName=" + tableName + ")");return false;}// 1. 构建缓存文件完整路径String cacheFilePath = SERVER_CACHE_DIR + File.separator + tableName + ".datas";File cacheFile = new File(cacheFilePath);// 2. 写入缓存文件(序列化:将 List<Map> 对象转为字节流)try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(cacheFile))) {oos.writeObject(data);System.out.println("服务端缓存:" + tableName + " 写入成功(路径:" + cacheFilePath + ")");return true;} catch (IOException e) {System.err.println("服务端缓存:写入 " + tableName + " 失败(磁盘满/权限不足)");e.printStackTrace();return false;}}
}

序列化说明List<Map<String, String>> 中的元素(StringHashMapArrayList)均实现 Serializable 接口,可直接序列化存储。

④ 实现远程服务类 DataAopImpl.java(集成缓存 + 远程能力)

实现 DataAop 接口,继承 UnicastRemoteObject(RMI 提供的工具类,自动将本地对象 “导出” 为远程对象,处理网络通信细节),核心逻辑:缓存优先→未命中查库→写入缓存

package com.hy.demo15.impl;import com.hy.demo15.DataAop;
import com.hy.demo15.dao.DBData;
import com.hy.demo15.server.cache.ServerCacheUtil;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
import java.util.Map;/*** RMI远程服务实现类:服务端核心业务逻辑* 继承 UnicastRemoteObject 原因:* - 自动实现 Remote 接口的序列化/反序列化逻辑* - 自动绑定端口,处理客户端远程调用的网络传输*/
public class DataAopImpl extends UnicastRemoteObject implements DataAop {// 数据库访问对象(提前初始化,避免每次查询重复创建)private final DBData dbData;/*** 构造方法:必须声明抛出 RemoteException* 原因:父类 UnicastRemoteObject 构造方法会抛出 RemoteException(导出远程对象时可能出错)*/public DataAopImpl() throws RemoteException {super();this.dbData = new DBData(); // 初始化数据库访问工具}/*** 实现远程查询方法:集成缓存逻辑* 执行流程:* 1. 查询服务端缓存 → 命中则返回* 2. 缓存未命中 → 查询数据库* 3. 数据库查询成功 → 写入服务端缓存* 4. 返回查询结果(缓存/数据库)*/@Overridepublic List<Map<String, String>> queryDatas(String tableName) throws RemoteException {System.out.println("\n服务端接收查询请求:tableName=" + tableName);// 1. 第一步:查询服务端统一缓存List<Map<String, String>> cacheData = ServerCacheUtil.getCache(tableName);if (!cacheData.isEmpty()) {System.out.println("服务端缓存:命中,返回缓存数据(条数:" + cacheData.size() + ")");return cacheData;}// 2. 第二步:缓存未命中,查询数据库System.out.println("服务端缓存:未命中,开始查询数据库");List<Map<String, String>> dbDataResult = dbData.queryDatas(tableName);// 3. 第三步:数据库查询成功,写入缓存(供后续请求复用)if (!dbDataResult.isEmpty()) {ServerCacheUtil.writeCache(tableName, dbDataResult);} else {System.out.println("服务端:数据库查询结果为空,不写入缓存(tableName=" + tableName + ")");}// 4. 返回数据库查询结果return dbDataResult;}
}
⑤ 实现服务端启动类 App.java(发布 RMI 服务)

创建 RMI 注册表(相当于 “服务地址簿”),将远程对象 DataAopImpl 绑定到注册表,使客户端可通过 “IP + 端口 + 服务名” 查找服务。

package com.hy.demo15.rmiserver;import com.hy.demo15.DataAop;
import com.hy.demo15.impl.DataAopImpl;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;/*** 服务端入口类:负责启动RMI服务,核心操作:* 1. 创建RMI注册表(端口9200)* 2. 创建远程服务对象(DataAopImpl)* 3. 将远程对象绑定到注册表(服务名 queryDatas)*/
public class App {public static void main(String[] args) {try {// 1. 创建远程服务对象(DataAopImpl 构造时自动导出为远程对象,处理网络通信)DataAop remoteService = new DataAopImpl();// 2. 创建RMI注册表:在端口9200启动注册表(相当于“服务地址簿”,客户端通过此端口查找服务)// 注意:若已通过命令行(rmiregistry 9200)启动注册表,此句需删除,避免端口冲突LocateRegistry.createRegistry(9200);System.out.println("服务端:RMI注册表已启动(端口:9200)");// 3. 将远程对象绑定到注册表:指定服务URL(格式:rmi://IP:端口/服务名)String serviceUrl = "rmi://127.0.0.1:9200/queryDatas";Naming.bind(serviceUrl, remoteService); // 绑定服务,客户端通过“queryDatas”名称查找System.out.println("服务端:RMI服务绑定成功(URL:" + serviceUrl + ")");System.out.println("服务端:已就绪,等待客户端调用...");} catch (RemoteException e) {System.err.println("服务端错误:创建/导出远程对象失败(可能端口被占用)");e.printStackTrace();} catch (MalformedURLException e) {System.err.println("服务端错误:RMI服务URL格式错误(如IP/端口格式非法)");e.printStackTrace();} catch (AlreadyBoundException e) {System.err.println("服务端错误:服务名已绑定(需先关闭已启动的服务,或修改服务名)");e.printStackTrace();}}
}

输出结果:

RMI 注册表已在端口 9200 上启动。
RMI数据服务 'queryDatas' 创建成功,并已绑定到注册表。
服务器正在运行,等待客户端调用...

4.2 客户端实现:发起 RMI 请求 + 展示查询结果

客户端核心目标:通过 RMI 连接服务端,发起查询请求,接收结果并展示,不处理缓存逻辑(缓存统一由服务端管理)。

4.2.1  客户端目录结构(明确文件组织)

客户端无需数据库和缓存依赖,仅需与服务端一致的远程接口和 RMI 通信类:

4.2.2 客户端分步实现(按依赖顺序)

① 复用远程接口 DataAop.java(关键!必须与服务端一致)

客户端的 DataAop 接口必须完全复制服务端的同名接口(包名、类名、方法签名均不能修改),否则会出现 ClassCastException。代码如下(与服务端完全相同):

package com.hy.demo15;import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;/*** 远程接口:与服务端完全一致,客户端通过此接口调用远程方法* 注意:严禁修改包名、类名、方法签名,否则会导致类型转换失败*/
public interface DataAop extends Remote {List<Map<String, String>> queryDatas(String name) throws RemoteException;
}
② 实现 RMI 连接器 RMIData.java(与服务端建立通信)

封装 RMI 连接逻辑:查找服务端注册表、获取远程服务代理对象,为客户端提供 “连接 - 调用” 能力。

package com.hy.demo15;import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;/*** 客户端RMI连接器:负责与服务端建立连接,获取远程服务代理对象* 核心能力:自动连接服务端,隐藏 RMI 底层通信细节*/
public class RMIData implements DataAop {// 远程服务代理对象:客户端通过此对象调用服务端方法(本质是RMI生成的代理)private DataAop remoteService;/*** 连接服务端RMI注册表,查找远程服务* 执行逻辑:若未连接,则建立连接;已连接则直接复用*/public void connectRMIServer() {// 避免重复连接(已连接则跳过)if (remoteService != null) {System.out.println("客户端:已连接RMI服务,无需重复连接");return;}try {// 服务端RMI服务URL(必须与服务端绑定的URL完全一致:IP、端口、服务名)String serviceUrl = "rmi://127.0.0.1:9200/queryDatas";// 查找远程服务:通过 Naming.lookup() 从注册表获取远程服务代理对象remoteService = (DataAop) Naming.lookup(serviceUrl);System.out.println("客户端:RMI服务连接成功(URL:" + serviceUrl + ")");} catch (MalformedURLException e) {System.err.println("客户端错误:RMI服务URL格式错误(如IP/端口错误)");throw new RuntimeException("RMI URL格式错误", e); // 抛出运行时异常,终止查询} catch (RemoteException e) {System.err.println("客户端错误:RMI服务连接失败(服务端未启动/网络不通)");throw new RuntimeException("RMI服务连接失败", e);} catch (NotBoundException e) {System.err.println("客户端错误:服务名未绑定(服务端绑定的服务名不是 queryDatas)");throw new RuntimeException("RMI服务名未绑定", e);}}/*** 实现 DataAop 接口方法:调用远程服务的查询方法* 逻辑:先确保连接已建立,再调用远程方法*/@Overridepublic List<Map<String, String>> queryDatas(String tableName) throws RemoteException {if (remoteService == null) {connectRMIServer(); // 未连接则自动建立连接}// 调用远程服务方法:本质是通过 RMI 代理对象向服务端发送请求return remoteService.queryDatas(tableName);}
}
③ 实现客户端代理类 ProxyData.java(统一查询入口)

作为客户端的 “查询入口”,解耦 Test 类与 RMIData 的依赖,后续若修改 RMI 连接逻辑,无需改动 Test 类(符合 “开闭原则”)。

package com.hy.demo15;import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;/*** 客户端代理类:统一查询入口,隐藏 RMI 连接细节* 作用:* 1. 对 Test 类提供简单的查询方法,无需关心 RMI 连接逻辑* 2. 后续若修改 RMI 实现(如换用其他通信方式),仅需修改此类,无需改动 Test 类*/
public class ProxyData implements DataAop {// 持有 RMI 连接器实例(依赖注入,降低耦合)private final DataAop rmiConnector;/*** 构造方法:注入 RMI 连接器(通过参数传入,而非内部创建,便于测试和扩展)*/public ProxyData(DataAop rmiConnector) {this.rmiConnector = rmiConnector;}/*** 发起查询:调用 RMI 连接器的查询方法,添加客户端日志*/@Overridepublic List<Map<String, String>> queryDatas(String tableName) throws RemoteException {System.out.println("客户端:发起查询请求(表名:" + tableName + ")");// 委托 RMI 连接器执行实际查询return rmiConnector.queryDatas(tableName);}
}
④ 实现客户端入口类 Test.java(接收输入 + 展示结果)

客户端的 “用户交互层”:接收用户输入的表名和字段名,调用代理类发起查询,最终打印查询结果。

package com.hy.demo15;import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;
import java.util.Scanner;/*** 客户端入口类:用户交互+结果展示* 流程:接收用户输入 → 创建客户端组件 → 发起查询 → 打印结果*/
public class Test {public static void main(String[] args) {// 1. 创建 Scanner 对象,接收用户输入Scanner scanner = new Scanner(System.in);try {// 2. 接收用户输入的表名和字段名System.out.print("请输入查询的数据库表名(全名,如 t_emps):");String tableName = scanner.next();System.out.print("请输入查询的字段名(列名,如 ename):");String fieldName = scanner.next();// 3. 创建客户端组件(依赖注入,组装逻辑)DataAop rmiConnector = new RMIData(); // 创建 RMI 连接器ProxyData queryProxy = new ProxyData(rmiConnector); // 创建查询代理// 4. 发起查询:调用代理类的 queryDatas 方法,获取结果List<Map<String, String>> queryResult = queryProxy.queryDatas(tableName);// 5. 处理并展示查询结果System.out.println("\n=== 查询结果(字段:" + fieldName + ")===");if (queryResult.isEmpty()) {System.out.println("无匹配数据(可能表名错误或表中无数据)");} else {// 遍历结果集,根据用户输入的字段名获取对应值for (Map<String, String> row : queryResult) {// 若字段名不存在,row.get() 返回 null,需处理空值String fieldValue = row.getOrDefault(fieldName, "(字段不存在)");System.out.println(fieldValue);}}} catch (RemoteException e) {System.err.println("\n客户端查询失败:远程调用异常(服务端出错或网络中断)");e.printStackTrace();} catch (RuntimeException e) {System.err.println("\n客户端查询失败:" + e.getMessage());} finally {// 6. 关闭 Scanner,释放资源scanner.close();System.out.println("\n客户端:查询结束,已关闭输入流");}}
}

输出结果:

RMI 注册表已在端口 9200 上启动。
RMI数据服务 'queryDatas' 创建成功,并已绑定到注册表。
服务器正在运行,等待客户端调用...

服务端接收查询请求:tableName=t_stus
服务端缓存:t_stus 缓存文件不存在
服务端缓存:未命中,开始查询数据库
服务端缓存:t_stus 数据写入缓存成功(路径:./server_cache\t_stus.datas)

请输入查询的数据库表名(全名):
t_stus
请输入查询的字段名(列名):
sname
客户端:发起查询请求(tableName=t_stus)
客户端:RMI服务连接成功(服务URL:rmi://127.0.0.1:9200/queryDatas)

=== 查询结果(字段:sname)===
张三
李四
王五
张六
王七
李八

5.多线程分割大文件

5.1 在 Windows 系统下配置 Nginx 的步骤

5.1.1 下载 Nginx

从 Nginx 官网(https://nginx.org/en/download.html)下载 Windows 版的 Nginx 压缩包,推荐下载稳定版。

5.1.2 解压 Nginx

将下载好的压缩包解压到你希望安装 Nginx 的目录,如D:\nginx-1.28.0

5.1.3 创建文件夹

在 D:\nginx-1.28.0 目录下创建文件夹 static --> audio

5.1.4 存放文件

在audio下存放a0.mp3文件

5.1.5 修改配置文件

Nginx 的配置文件通常位于安装目录下的 conf文件夹中,文件名为 nginx.conf。可以使用任何文本编辑器打开该文件进行配置。

user  nobody;
worker_processes  1;#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;#pid        logs/nginx.pid;events {worker_connections  1024;
}http {include       mime.types;default_type  application/octet-stream;#log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '#                  '$status $body_bytes_sent "$http_referer" '#                  '"$http_user_agent" "$http_x_forwarded_for"';#access_log  logs/access.log  main;sendfile        on;#tcp_nopush     on;#keepalive_timeout  0;keepalive_timeout  65;#gzip  on;# 重点修改的 server 块server {listen       8900;server_name  127.0.0.1;# 映射 /audio/ 路径到本地文件夹location /audio/ {root   D:/nginx-1.28.0/static;autoindex on;expires 1d;}# 默认根路径配置location / {root   html;index  index.html index.htm;}# 错误页面配置(保留默认)error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}# 以下 PHP 相关配置保留默认注释即可,无需修改#location ~ \.php$ {#    proxy_pass   http://127.0.0.1;#}#location ~ \.php$ {#    root           html;#    fastcgi_pass   127.0.0.1:9000;#    fastcgi_index  index.php;#    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;#    include        fastcgi_params;#}#location ~ /\.ht {#    deny  all;#}}# 以下默认注释的 server 块(HTTPS 等)保留不变,无需修改#server {#    listen       8000;#    listen       somename:8080;#    server_name  somename  alias  another.alias;#    location / {#        root   html;#        index  index.html index.htm;#    }#}#server {#    listen       443 ssl;#    server_name  localhost;#    ssl_certificate      cert.pem;#    ssl_certificate_key  cert.key;#    ssl_session_cache    shared:SSL:1m;#    ssl_session_timeout  5m;#    ssl_ciphers  HIGH:!aNULL:!MD5;#    ssl_prefer_server_ciphers  on;#    location / {#        root   html;#        index  index.html index.htm;#    }#}
}

5.1.6 检查配置文件语法

修改完配置文件后,在命令提示符中输入nginx -t命令来检查配置文件的语法是否正确。如果语法正确,会显示 “nginx: the configuration file D:\nginx-1.28.0/conf/nginx.conf syntax is ok” 等类似信息。

5.1.7 启动 Nginx

打开命令提示符,切换到 Nginx 的安装目录,例如cd D:\nginx-1.28.0,然后输入start nginx启动 Nginx。

5.1.8 验证 Nginx 是否启动成功

打开浏览器,在地址栏中输入http://127.0.0.1:8900/audio/a0.mp3,如果浏览器自动下载 a0.mp3 文件,或弹出播放窗口,说明 Nginx 已经启动成功。

5.2 具体实现

5.2.1 DownThread 类:多线程下载的 “工作单元”

DownThread 继承 Thread,每个实例代表一个独立的下载线程,负责下载文件的一个 “分片”。

package com.hy.demo21;import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;/*** 下载线程类:继承 Thread,每个线程负责下载文件的一个指定片段* 核心能力:通过 HTTP Range 请求获取文件分片,用 RandomAccessFile 写入本地指定位置*/
public class DownThread extends Thread {// 成员变量:存储线程的下载参数int block;          // 每个线程负责下载的“理论数据块大小”(单位:字节)int i;              // 线程编号(从 0 开始,用于计算当前线程的下载范围)URL url;            // 远程文件的 URL 地址(如 http://127.0.0.1:8900/audio/a0.mp3)File f;             // 本地保存文件的路径(如 ./mp3/a.mp3)int startPoint = 0; // 当前线程的下载“起始字节位置”int endPoint = 0;   // 当前线程的下载“结束字节位置”/*** 构造方法:初始化线程的下载参数,计算当前线程的下载范围* @param block 每个线程的理论数据块大小* @param i 线程编号(0,1,2...)* @param url 远程文件 URL* @param f 本地保存文件*/public DownThread(int block, int i, URL url, File f) {this.block = block;    // 接收外部传入的“单块大小”this.i = i;            // 接收线程编号this.url = url;        // 接收远程文件地址this.f = f;            // 接收本地保存路径// 核心计算:确定当前线程的下载范围(字节索引,从 0 开始)this.startPoint = this.block * i;                  // 起始位置 = 块大小 × 线程编号this.endPoint = this.block * (i + 1) - 1;          // 结束位置 = 块大小 × (线程编号+1) - 1// 示例:若 block=100,i=0 → start=0,end=99;i=1 → start=100,end=199}/*** 线程执行逻辑:核心下载流程(调用 start() 时自动执行)*/public void run() {// 打印当前线程的下载范围(调试用,直观查看每个线程的分工)System.out.println(Thread.currentThread().getName() + "从this.startPoint=" + this.startPoint + ",this.endPoint=" + this.endPoint);RandomAccessFile raf = null; // 声明 RandomAccessFile(用于随机写入本地文件)try {// 1. 建立与远程服务器的 HTTP 连接HttpURLConnection conn = (HttpURLConnection) this.url.openConnection();// 关键:设置 HTTP 请求头“Range”,实现“分片下载”(仅支持 HTTP/1.1 及以上)// Range: bytes=start-end → 告诉服务器“只返回从 start 到 end 的字节片段”conn.setRequestProperty("Range", "bytes=" + this.startPoint + "-" + this.endPoint);// 2. 获取服务器返回的“分片数据”输入流( InputStream )InputStream in = conn.getInputStream(); // 流中仅包含当前线程请求的字节片段// 3. 创建字节缓冲区(1024 字节 = 1KB),提升读写效率// 原理:批量读取流数据到缓冲区,减少与磁盘/网络的交互次数byte[] buffer = new byte[1024];// 4. 初始化 RandomAccessFile,以“读写模式(rw)”操作本地文件raf = new RandomAccessFile(this.f, "rw");// 关键:将本地文件指针移动到“当前线程的起始位置”// 作用:确保当前线程下载的片段写入文件的正确位置,避免多线程写入冲突raf.seek(this.startPoint);// 5. 声明变量:存储每次从输入流读取的“实际字节数”int len = 0;// 6. 循环读取输入流数据,并写入本地文件(核心读写逻辑)// (len = in.read(buffer)) != -1:// - in.read(buffer):从输入流读取数据,填充到 buffer 数组,返回“实际读取的字节数”// - 若读取到流末尾(分片数据读完),返回 -1,循环结束while ((len = in.read(buffer)) != -1) {// raf.write(buffer, 0, len):将缓冲区的“有效数据”写入本地文件// 参数 0:从缓冲区的第 0 个字节开始写;参数 len:写入“实际读取的 len 个字节”// 避免写入缓冲区中未覆盖的“脏数据”(如最后一次读取不足 1024 字节时)raf.write(buffer, 0, len);}// 7. 打印线程下载完成信息(调试用)System.out.println(Thread.currentThread().getName() + ",下载完成");} catch (IOException e) {e.printStackTrace();} finally {if (null != raf) {try {raf.close();} catch (IOException e) {e.printStackTrace();}}}}
}

5.2.2 Test 类:多线程下载的 “调度中心”

Test 类是程序入口,负责初始化下载环境、计算分片参数、启动下载线程,是多线程下载的 “总调度”。

package com.hy.demo21;import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;/*** 多线程下载调度类:程序入口* 核心功能:创建本地目录、获取远程文件大小、计算分片参数、启动多个 DownThread 线程*/
public class Test {/*** 核心方法:封装多线程下载逻辑* @param path 本地文件保存路径(如 ./mp3/a.mp3)* @param url 远程文件的 URL 字符串(如 http://127.0.0.1:8900/audio/a0.mp3)* @param threadNum 下载线程数量(如 3 个线程)*/public static void downFile(String path, String url, int threadNum) {// 1. 创建本地文件对象(指向最终保存的文件)File f = new File(path);// 2. 检查并创建本地文件的父目录if (!f.getParentFile().exists()) {f.getParentFile().mkdirs(); }try {// 3. 将 URL 字符串转为 URL 对象(用于建立 HTTP 连接)URL musicUrl = new URL(url);// 4. 建立与远程服务器的 HTTP 连接(仅用于获取文件大小,不下载数据)HttpURLConnection conn = (HttpURLConnection) musicUrl.openConnection();// 5. 检查连接状态:若响应码为 200,说明连接成功(HTTP 200 = OK)if (conn.getResponseCode() == 200) {System.out.println("连接网络音乐文件成功");// 6. 获取远程文件的总大小(单位:字节)int fileSize = conn.getContentLength();System.out.println("下载文件的大小为:" + fileSize + " 字节");// 7. 核心算法:计算每个线程的“理论数据块大小”(向上取整,避免遗漏最后一块)// 问题:若文件大小不能被线程数整除(如 100 字节 ÷ 3 线程 = 33 余 1),// 直接除法会导致最后 1 字节无人下载,因此用“(总大小 + 线程数 -1) ÷ 线程数”向上取整// 示例:(100 + 3 -1)/3 = 102/3 = 34 → 3 个线程分别下载 34、33、33 字节,总和 100int block = (fileSize + threadNum - 1) / threadNum;System.out.println("每个线程理论下载大小为:" + block + " 字节");// 8. 循环创建并启动下载线程(按线程数分配任务)for (int i = 0; i < threadNum; i++) {// 创建 DownThread 实例:传入块大小、线程编号、URL、本地文件// start():启动线程(自动执行 run() 方法)new DownThread(block, i, musicUrl, f).start();}}} catch (MalformedURLException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {// 调用 downFile 方法,传入 3 个核心参数:// 1. 本地保存路径:./mp3/a.mp3(项目根目录下 mp3 文件夹中的 a.mp3)// 2. 远程文件 URL:http://127.0.0.1:8900/audio/a0.mp3(Nginx 提供的 MP3 地址)// 3. 线程数:3(启动 3 个线程并行下载)downFile("./mp3/a.mp3", "http://127.0.0.1:8900/audio/a0.mp3", 3);}
}

输出结果:

连接网络音乐文件成功
下载文件的大小为:3896677 字节
每个线程理论下载大小为:1298893 字节
Thread-0从this.startPoint=0,this.endPoint=1298892
Thread-2从this.startPoint=2597786,this.endPoint=3896678
Thread-1从this.startPoint=1298893,this.endPoint=2597785
Thread-1,下载完成
Thread-0,下载完成
Thread-2,下载完成

http://www.dtcms.com/a/420416.html

相关文章:

  • 苏州做网站的专业公司哪家好wordpress插件 网站跳转
  • 东莞中高端网站建设如何上传网页到网站
  • WIN7下安装RTX3050 6GB显卡驱动
  • 一般网站做哪些端口映射如何自助建网站
  • 广州最好的商城网站制作个人网站首页怎么做
  • 建站哪个便宜福州专业网站建设公司
  • 网站程序引擎网络黄推广软件
  • 安徽建设银行官方网站电商运营多少钱一个月
  • C语言速成秘籍——循环结构(while、do while、for)和跳转语句(break,continue)
  • 天津专门做企业网站公司签名能留链接的网站
  • 高效IO的理解
  • 做网站社区赚钱吗pc网站 手机网站 微网站
  • windowsKyLin配置:咖啡壶(chemex)
  • 杭州 高端网站建设 推荐西部数码网站管理助手 v3.0
  • 门户网站开发框架上海公共招聘网官网
  • 移动端减肥网站模板No酒店网站建设
  • 哪个网站教人做美食快速建站模板自助建站
  • h5电子商务网站门户网站百度百科
  • 我的南京网站找网络公司做的网站可以出售吗
  • 源码搭建网站流程织梦播放器网站
  • 秦皇岛网站搜索优化用wordpress建一个网站
  • SpringAI工具调用原理解析
  • 网站建设的维护工作有哪些宜昌云网站建设
  • 网站管理有哪些h5页面制作软件手机版
  • 网站设计与制作优点建设网站的要点
  • composer 安装与开启PHP扩展支持
  • lamp网站开发黄金组合 pdfapp手表
  • wordpress 站点错误东莞企业网络营销平台
  • html做网站的毕业设计酒水包装设计公司
  • wordpress设置教程视频快速网站优化技巧