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

目录

[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.优化应用于更多数据库表

[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.多线程分割大文件

[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>> 中的元素(StringHashMapArrayList)均实现 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,下载完成

相关推荐
路弥行至2 小时前
C语言入门教程 | 第四讲:深入理解数制与码制,掌握基本数据类型的奥秘
服务器·c语言·开发语言·经验分享·笔记·其他·入门教程
渣哥2 小时前
Java线程池那些坑:我与线程池的恩怨情仇
java
hour_go2 小时前
BPEL:企业流程自动化的幕后指挥家
java·运维·自动化
建群新人小猿2 小时前
客户标签自动管理:标签自动化运营,画像持久保鲜
android·java·大数据·前端·git
龙茶清欢2 小时前
3、推荐统一使用 ResponseEntity<T> 作为控制器返回类型
java·spring boot·spring cloud
龙茶清欢2 小时前
3、Lombok进阶功能实战:Builder模式、异常处理与资源管理高级用法
java·spring boot·spring cloud
青柠编程2 小时前
基于Spring Boot与SSM的中药实验管理系统架构设计
java·开发语言·数据库
1710orange2 小时前
java设计模式:抽象工厂模式 + 建造者模式
java·设计模式·抽象工厂模式
远远远远子3 小时前
C++ --1 perparation
开发语言·c++