目录
[1.Java 缓存代理模式的实现](#1.Java 缓存代理模式的实现)
[1.1 数据库准备](#1.1 数据库准备)
[1.2 添加Maven依赖](#1.2 添加Maven依赖)
[1.3 创建缓存文件夹](#1.3 创建缓存文件夹)
[1.4 具体实现](#1.4 具体实现)
[1.4.1 定义抽象主题接口 DataAop.java(约束行为)](#1.4.1 定义抽象主题接口 DataAop.java(约束行为))
[1.4.2 定义数据模型 Student.java(封装数据)](#1.4.2 定义数据模型 Student.java(封装数据))
[1.4.3 实现真实主题类 DBData.java(操作数据库)](#1.4.3 实现真实主题类 DBData.java(操作数据库))
[1.4.4 实现拦截器类 InterceptorData.java(处理缓存检查)](#1.4.4 实现拦截器类 InterceptorData.java(处理缓存检查))
[1.4.5 实现代理类 ProxyData.java(核心代理逻辑)](#1.4.5 实现代理类 ProxyData.java(核心代理逻辑))
[2.1 数据库准备](#2.1 数据库准备)
[2.2 创建缓存文件夹](#2.2 创建缓存文件夹)
[2.3 具体实现](#2.3 具体实现)
[2.3.1 定义抽象主题接口 DataAop.java(约束行为)](#2.3.1 定义抽象主题接口 DataAop.java(约束行为))
[2.3.2 实现真实主题类 DBData.java(操作数据库,核心升级)](#2.3.2 实现真实主题类 DBData.java(操作数据库,核心升级))
[2.3.3 实现拦截器类 InterceptorData.java(处理缓存检查)](#2.3.3 实现拦截器类 InterceptorData.java(处理缓存检查))
[2.3.4 实现代理类 ProxyData.java(核心代理逻辑)](#2.3.4 实现代理类 ProxyData.java(核心代理逻辑))
[2.3.5 实现测试类 Test.java(运行和验证)](#2.3.5 实现测试类 Test.java(运行和验证))
[3.Java RMI 远程方法调用](#3.Java RMI 远程方法调用)
[3.1 什么是 RMI?](#3.1 什么是 RMI?)
[3.2 服务端的实现(提供远程查询服务)](#3.2 服务端的实现(提供远程查询服务))
[3.2.1 定义远程接口 DataAop.java(服务端与客户端共用)](#3.2.1 定义远程接口 DataAop.java(服务端与客户端共用))
[3.2.2 实现数据访问类 DBData.java(服务端本地业务)](#3.2.2 实现数据访问类 DBData.java(服务端本地业务))
[3.2.3 实现远程服务类 DataAopImpl.java(服务端核心)](#3.2.3 实现远程服务类 DataAopImpl.java(服务端核心))
[3.2.4 实现服务启动类 App.java(发布远程服务)](#3.2.4 实现服务启动类 App.java(发布远程服务))
[3.3 客户端的实现(调用远程服务 + 本地缓存)](#3.3 客户端的实现(调用远程服务 + 本地缓存))
[3.3.1 创建缓存文件夹](#3.3.1 创建缓存文件夹)
[3.3.2 复用远程接口 DataAop.java(与服务端完全一致)](#3.3.2 复用远程接口 DataAop.java(与服务端完全一致))
[3.3.3 实现缓存拦截器 InterceptorData.java(本地缓存管理)](#3.3.3 实现缓存拦截器 InterceptorData.java(本地缓存管理))
[3.3.4 实现 RMI 连接器 RMIData.java(客户端远程通信)](#3.3.4 实现 RMI 连接器 RMIData.java(客户端远程通信))
[3.3.5 实现本地代理类 ProxyData.java(客户端核心逻辑)](#3.3.5 实现本地代理类 ProxyData.java(客户端核心逻辑))
[3.3.6 实现客户端测试类 Test.java(客户端入口)](#3.3.6 实现客户端测试类 Test.java(客户端入口))
[4.Java RMI 分布式缓存查询的实现](#4.Java RMI 分布式缓存查询的实现)
[4.1 服务端实现:提供远程查询 + 统一缓存服务](#4.1 服务端实现:提供远程查询 + 统一缓存服务)
[4.1.1 服务端目录结构](#4.1.1 服务端目录结构)
[4.1.2 服务端分步实现](#4.1.2 服务端分步实现)
[① 定义远程接口 DataAop.java(服务端与客户端共用)](#① 定义远程接口 DataAop.java(服务端与客户端共用))
[② 实现数据访问类 DBData.java(操作 MySQL 数据库)](#② 实现数据访问类 DBData.java(操作 MySQL 数据库))
[③ 实现服务端缓存工具 ServerCacheUtil.java(统一缓存管理)](#③ 实现服务端缓存工具 ServerCacheUtil.java(统一缓存管理))
[④ 实现远程服务类 DataAopImpl.java(集成缓存 + 远程能力)](#④ 实现远程服务类 DataAopImpl.java(集成缓存 + 远程能力))
[⑤ 实现服务端启动类 App.java(发布 RMI 服务)](#⑤ 实现服务端启动类 App.java(发布 RMI 服务))
[4.2 客户端实现:发起 RMI 请求 + 展示查询结果](#4.2 客户端实现:发起 RMI 请求 + 展示查询结果)
[4.2.1 客户端目录结构(明确文件组织)](#4.2.1 客户端目录结构(明确文件组织))
[4.2.2 客户端分步实现(按依赖顺序)](#4.2.2 客户端分步实现(按依赖顺序))
[① 复用远程接口 DataAop.java(关键!必须与服务端一致)](#① 复用远程接口 DataAop.java(关键!必须与服务端一致))
[② 实现 RMI 连接器 RMIData.java(与服务端建立通信)](#② 实现 RMI 连接器 RMIData.java(与服务端建立通信))
[③ 实现客户端代理类 ProxyData.java(统一查询入口)](#③ 实现客户端代理类 ProxyData.java(统一查询入口))
[④ 实现客户端入口类 Test.java(接收输入 + 展示结果)](#④ 实现客户端入口类 Test.java(接收输入 + 展示结果))
[5.1 在 Windows 系统下配置 Nginx 的步骤](#5.1 在 Windows 系统下配置 Nginx 的步骤)
[5.1.1 下载 Nginx](#5.1.1 下载 Nginx)
[5.1.2 解压 Nginx](#5.1.2 解压 Nginx)
[5.1.3 创建文件夹](#5.1.3 创建文件夹)
[5.1.4 存放文件](#5.1.4 存放文件)
[5.1.5 修改配置文件](#5.1.5 修改配置文件)
[5.1.6 检查配置文件语法](#5.1.6 检查配置文件语法)
[5.1.7 启动 Nginx](#5.1.7 启动 Nginx)
[5.1.8 验证 Nginx 是否启动成功](#5.1.8 验证 Nginx 是否启动成功)
[5.2 具体实现](#5.2 具体实现)
[5.2.1 DownThread 类:多线程下载的 "工作单元"](#5.2.1 DownThread 类:多线程下载的 “工作单元”)
[5.2.2 Test 类:多线程下载的 "调度中心"](#5.2.2 Test 类:多线程下载的 “调度中心”)
1.Java 缓存代理模式的实现
通过 代理模式 实现 "数据库查询 + 本地缓存" 功能:首次查询从数据库获取数据并写入缓存,后续查询直接从缓存读取,减少数据库交互。
1.1 数据库准备
sql
-- 创建表 --
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依赖
XML
<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
(约束行为)
先定义统一接口,确保真实类和代理类实现相同的 "查询方法",符合代理模式的 "接口一致性" 原则。
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
(封装数据)
数据库查询结果需要封装为对象,且该对象需实现序列化接口(后续要写入缓存文件,序列化是前提)。
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
接口,编写真实的数据库查询逻辑,是代理类最终要 "委托" 的对象。
java
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对象列表(表中所有行数据)
*/
@Override
public 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
(处理缓存检查)
专门负责 "缓存相关的辅助操作":检查缓存目录是否存在、目标缓存文件是否存在、读取缓存文件数据。将缓存逻辑抽离,让代理类更专注于 "代理逻辑"(符合单一职责原则)。
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();
}
}
}
}
}
// 补充逻辑:若遍历后未找到目标缓存文件,返回空List
if (lists == null) {
lists = new ArrayList();
}
}
// 8. 返回缓存数据(空List或真实缓存数据)
return lists;
}
}
1.4.5 实现代理类 ProxyData.java
(核心代理逻辑)
实现 DataAop
接口,整合 "缓存检查(InterceptorData)" 和 "数据库查询(DBData)":先查缓存,缓存命中则直接返回,未命中则调用真实类查询并写入缓存。
java
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 查询结果(缓存数据或数据库数据)
*/
@Override
public 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 数据库准备
sql
-- 创建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
(约束行为)
首先,我们定义一个统一的接口,这次接口的方法返回一个更通用的数据结构。
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
类需要变得足够智能,能处理任意表的查询。
java
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>>
*/
@Override
public 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>
类型即可。
java
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
接口和序列化机制交互,而不关心具体的数据内容。
java
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;
}
@Override
public 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
(运行和验证)
测试类也相应升级,允许用户输入要查询的列名。
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
(服务端与客户端共用)
这个接口定义了客户端和服务器之间的 "契约"。
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
(服务端本地业务)
这是我们熟悉的数据库查询逻辑,它不关心自己是被本地调用还是远程调用。
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 的核心,它将远程接口和本地业务逻辑连接起来。
java
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 服务,注册远程对象,使其可供客户端调用。
java
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 规范。
java
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
(本地缓存管理)
抽离缓存逻辑,让代理类专注于 "代理流程",符合单一职责原则。该类仅处理本地文件操作,不涉及远程调用。
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 服务的 "桥梁",负责查找并连接远程服务,代理类会通过它调用远程方法。
java
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 方法
*/
@Override
public List<Map<String, String>> queryDatas(String name) throws RemoteException {
this.connRMIServer(); // 先连接远程服务
return dataAop.queryDatas(name); // 调用远程服务的方法,返回结果
}
}
3.3.5 实现本地代理类 ProxyData.java
(客户端核心逻辑)
代理类整合 "缓存拦截器" 和 "RMI 连接器",实现 "本地缓存优先,远程查询兜底" 的逻辑,是整个客户端的核心。
java
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;
}
/**
* 核心代理方法:实现"缓存+远程查询"逻辑
*/
@Override
public 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
(客户端入口)
测试类是客户端的入口,负责接收用户输入,创建代理对象,触发整个查询流程,并打印结果。
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
、参数 / 返回值可序列化。
java
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 读取数据,不涉及远程通信。
java
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
(统一缓存管理)
封装服务端缓存的 "创建目录、检查缓存、读取缓存、写入缓存" 逻辑。
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>>
中的元素(String
、HashMap
、ArrayList
)均实现Serializable
接口,可直接序列化存储。
④ 实现远程服务类 DataAopImpl.java
(集成缓存 + 远程能力)
实现 DataAop
接口,继承 UnicastRemoteObject
(RMI 提供的工具类,自动将本地对象 "导出" 为远程对象,处理网络通信细节),核心逻辑:缓存优先→未命中查库→写入缓存。
java
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. 返回查询结果(缓存/数据库)
*/
@Override
public 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 + 端口 + 服务名" 查找服务。
java
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
。代码如下(与服务端完全相同):
java
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 连接逻辑:查找服务端注册表、获取远程服务代理对象,为客户端提供 "连接 - 调用" 能力。
java
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 接口方法:调用远程服务的查询方法
* 逻辑:先确保连接已建立,再调用远程方法
*/
@Override
public List<Map<String, String>> queryDatas(String tableName) throws RemoteException {
if (remoteService == null) {
connectRMIServer(); // 未连接则自动建立连接
}
// 调用远程服务方法:本质是通过 RMI 代理对象向服务端发送请求
return remoteService.queryDatas(tableName);
}
}
③ 实现客户端代理类 ProxyData.java
(统一查询入口)
作为客户端的 "查询入口",解耦 Test
类与 RMIData
的依赖,后续若修改 RMI 连接逻辑,无需改动 Test
类(符合 "开闭原则")。
java
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 连接器的查询方法,添加客户端日志
*/
@Override
public List<Map<String, String>> queryDatas(String tableName) throws RemoteException {
System.out.println("客户端:发起查询请求(表名:" + tableName + ")");
// 委托 RMI 连接器执行实际查询
return rmiConnector.queryDatas(tableName);
}
}
④ 实现客户端入口类 Test.java
(接收输入 + 展示结果)
客户端的 "用户交互层":接收用户输入的表名和字段名,调用代理类发起查询,最终打印查询结果。
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
。可以使用任何文本编辑器打开该文件进行配置。

java
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,每个实例代表一个独立的下载线程,负责下载文件的一个 "分片"。
java
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 类是程序入口,负责初始化下载环境、计算分片参数、启动下载线程,是多线程下载的 "总调度"。
java
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 字节,总和 100
int 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,下载完成