从 0 实现一个文件搜索工具 (Java 项目)

背景

各文件系统下, 都有提供文件查找的功能, 但是一般而言搜索速度很慢

本项目仿照 everything 工具, 实现本地文件的快速搜索

实现功能

  1. 选择指定本地目录, 根据输入的信息, 进行搜索, 显示指定目录下的匹配文件信息
  2. 文件夹包含中文时, 支持汉语拼音搜索 (全拼 / 首字母匹配)

相关技术

Java + Servlet + Pinyin4j

JDBC + SQLite (SQLite 相对于 MySQL 更加轻量, 并且引入 jar 包即可使用, 不必安装配套应用)

JavaFx

数据库设计

SQLite 创建 SQL 的语句如下

java 复制代码
create table if not exists file_meta (
    id INTEGER primary key autoincrement,
    name varchar(50) not null,
    path varchar(512) not null,
    is_directory boolean not null,
    pinyin varchar(100) not null,
    pinyin_first varchar(50) not null,
    size BIGINT not null,
    last_modified timestamp not null
);

项目的基本框架

前端页面

app.fxml 文件

显示界面的图画化结构

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<GridPane fx:controller="gui.GUIController" fx:id="gridPane" vgap="10" alignment="center" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children >
      <Button fx:id="button"  onMouseClicked="#choose" prefWidth="90" text="选择目录" GridPane.rowIndex="0" GridPane.columnIndex="0"></Button>
      <Label fx:id="label" text="当前未选择目录" GridPane.rowIndex="0" GridPane.columnIndex="0">
        <GridPane.margin>
          <Insets left="100"></Insets>
        </GridPane.margin>
      </Label>
      <TextField fx:id="textField" prefWidth="900" GridPane.rowIndex="1" GridPane.columnIndex="0" ></TextField>
      <TableView fx:id="tableView" prefWidth="900" prefHeight="700" GridPane.rowIndex="2" GridPane.columnIndex="0">
        <columns>
          <TableColumn prefWidth="220" text="文件名">
              <cellValueFactory>
                  <PropertyValueFactory property="name"></PropertyValueFactory>
              </cellValueFactory>
          </TableColumn>
          <TableColumn prefWidth="400" text="路径">
              <cellValueFactory>
                  <PropertyValueFactory property="path"></PropertyValueFactory>
              </cellValueFactory>
          </TableColumn>
          <TableColumn prefWidth="100" text="大小">
              <cellValueFactory>
                  <PropertyValueFactory property="sizeText"></PropertyValueFactory>
              </cellValueFactory>
          </TableColumn>
          <TableColumn prefWidth="180" text="修改时间">
              <cellValueFactory>
                  <PropertyValueFactory property="lastModifiedText"></PropertyValueFactory>
              </cellValueFactory>
          </TableColumn>
        </columns>
      </TableView>
   </children>
</GridPane>

GUIController 类

与 app.fxml 文件配套使用, 该类离实现了 界面中按键的绑定事件, 以及对搜索框内容进行监听 , 当搜索框内容改变时, 重新搜索, 并将结果返回到查询结果显示处 (实现动态搜索功能)

java 复制代码
public class GUIController implements Initializable {
    @FXML
    private Label label;

    @FXML
    private GridPane gridPane;

    @FXML
    private Button button;

    @FXML
    private TextField textField;

    @FXML
    private TableView<FileMeta> tableView;

    private SearchService searchService = null;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // 在这里对 输入框 加一个监听器
        // 需要指定对 text 这个内容属性进行监听
        // textField.textProperty() 获取输入框里的内容
        textField.textProperty().addListener(new ChangeListener<String>() {
            /**
             *  会在用户每次修改 输入框内容 的时候, 被自动调用到
             * @param observable
             * @param oldValue 输入框被修改之前的值
             * @param newValue 输入框被修改之后的值
             */
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                // 此处要干的事情, 是根据新的值, 重新进行查询操作
                freshTable(newValue);
            }
        });
    }

    private void freshTable(String query) {
        // 重新查询数据库, 把查询结果, 设置到表格中
        if(searchService == null) {
            System.out.println("searchService 尚未初始化, 不能查询!");
            return;
        }

        // 把之前表里的内容清空掉
        ObservableList<FileMeta> fileMetas = tableView.getItems();
        fileMetas.clear();
        List<FileMeta> results = searchService.search(query);
        fileMetas.addAll(results);
    }

    /**
     * 使用该方法, 作为鼠标点击事件的回调函数
     * @param mouseEvent
     */
    public void choose(MouseEvent mouseEvent) {
        // 创建一个 目录选择器
        DirectoryChooser directoryChooser = new DirectoryChooser();
        // 把该对话框显示出来
        Window window = gridPane.getScene().getWindow();
        File file = directoryChooser.showDialog(window);

        if(file == null) {
            System.out.println("用户选择的路径为空");
        } else {
            System.out.println(file.getAbsolutePath());
        }

        // 把用户选择的路径,显示到 label 中
        label.setText(file.getAbsolutePath());

        // 如果不是首次扫描, 就应该停止上次扫描任务, 执行本次扫描任务
        if(searchService != null) {
            searchService.shutdown();
        }

        // 对用户选择的路径进行扫描, 初始化
        searchService = new SearchService();
        searchService.init(file.getAbsolutePath());
    }
}

GUIClient

继承 Application 方法, 为界面启动类, 调用 javafx 提供的 launch 方法来启动整个程序

java 复制代码
public class GUIClient extends Application {
    /**
     * 程序启动时, 会立即执行的方法
     * @param primaryStage
     * @throws Exception
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        // 加载 fxml 文件, 把 fxml 文件里的内容, 给设置到舞台中
        Parent parent = FXMLLoader.load(GUIClient.class.getClassLoader().getResource("app.fxml"));
        primaryStage.setScene(new Scene(parent, 1000, 800));
        primaryStage.setTitle("文件搜索工具");
        // 准备工作完成, 显示场景界面
        primaryStage.show();
    }

    public static void main(String[] args) {
        // 调用 javafx 提供的 launch 方法来启动整个程序
        launch(args);
    }
}

后端代码

实体类

FileMeta

本类对应着数据库的 file_meta 表

因为没引入 lombok, 因此只能手写 Setter 和 Getter 方法

java 复制代码
// 本类的示例就代表 file_meta 表里的每个记录.
public class FileMeta {
    private int id;
    private String name;
    private String path;
    private boolean isDirectory;
    // 这里存储的 size 是字节, 但是界面上输出的不应该以字节位单位, k, m, g
    private long size;
    // 这个存储的是时间戳(机器能看懂)
    private long lastModified;
    // 这个是进行格式化转换之后的时间格式(人能看懂的)
//    private long lastModifiedText;

    // 构造方法
    public FileMeta(String name, String path, boolean isDirectory, long size, long lastModified) {
        this.name = name;
        this.path = path;
        this.isDirectory = isDirectory;
        this.size = size;
        this.lastModified = lastModified;
    }

    public FileMeta(File f) {
        this(f.getName(), f.getParent(), f.isDirectory(), f.length(), f.lastModified());
    }

    public String getPinyin() {
        return PinyinUtil.get(name, true);
    }

    public String getPinyinFirst() {
        return PinyinUtil.get(name, false);
    }

    public String getSizeText() {
        // 常见单位: Byte, KB, MB, GB, TB
        // 如果 size < 1024, 使用 Byte
        // 如果 1024 <= size < 1024*1024, 使用 MB
        // ...

        double curSize = size;
        String[] units = {"Byte", "KB", "MB", "GB", "TB"};
        for(int i=0;i<units.length;i++) {
            if(curSize < 1024) {
                return String.format("%.2f " + units[i], new BigDecimal(curSize));
            }
            curSize /= 1024;
        }
        return String.format("%.2f TB", new BigDecimal(curSize));
    }

    public String getLastModifiedText() {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(lastModified);
    }

    // -------------------------------

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public boolean isDirectory() {
        return isDirectory;
    }

    public void setDirectory(boolean directory) {
        isDirectory = directory;
    }

    public long getSize() {
        return size;
    }

    public void setSize(long size) {
        this.size = size;
    }

    public long getLastModified() {
        return lastModified;
    }

    public void setLastModified(long lastModified) {
        this.lastModified = lastModified;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        FileMeta fileMeta = (FileMeta) o;
        return isDirectory == fileMeta.isDirectory  && name.equals(fileMeta.name) && path.equals(fileMeta.path);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, path, isDirectory);
    }
}

DBUtil

本类封装 JDBC 的 连接/关闭/管理 操作

java 复制代码
public class DBUtil {
    // 使用 单例模式(懒汉模式) 来提供 DataSource
    private static volatile DataSource dataSource = DBUtil.getDataSource();

    // 创建数据源: Datasource
    public static DataSource getDataSource() {
        if (dataSource == null) { //外层 if 判断是否要加锁 (加锁是要消耗资源的, if判断一下比 synchronized 加一次锁消耗资源要少的多)
            synchronized (DBUtil.class) {
                if(dataSource == null ) { //内层 if 判断是否要创建 DataSource
                    dataSource = new SQLiteDataSource();
                    ((SQLiteDataSource)dataSource).setUrl("jdbc:sqlite://D:/AAASpringBootProject/sqlite/fileSearcher.db");
                }
            }
        }
        return dataSource;
    }

    // 建立连接
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    // 断开连接
    public static void close(Connection connection, Statement statement, ResultSet resultSet) {
        if(resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(connection != null ) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

FileDao 类

通过该类来实现数据库的增删改查操作

java 复制代码
// 通过这个类来封装针对 file_meta 表的操作
public class FileDao {
    // 1.初始化数据库 (建表)
    public void initDB() {
        // 1) 先能够读取到 db.sql 中的 SQL 语句
        // 2) 根据 SQL 语句调用 jdbc 执行操作
        Connection connection = null;
        Statement statement = null;
        try {
            connection = DBUtil.getConnection();
            // 使用 connection.createStatement() 来执行建库建表 sql
            statement = connection.createStatement();
            String[] sqls = getInitSql();
            for(String sql : sqls) {
                System.out.println("[initDB] sql:" + sql);
                statement.executeUpdate(sql); //该方式用来执行一些基本不变的sql语句
            }
        } catch (SQLException | IOException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

    // 从 db.sql 中读取文件内容
    // 一个 sql 语句对应一个 String, 多个 sql 语句对应 String[]
    private String[] getInitSql() throws IOException {
        // 用这个 StringBuilder 来存储最终结果
        StringBuilder stringBuilder = new StringBuilder();
        // 此处需要动态获取到 db.sql 文件的路径, 而不是一个写死的绝对路径(运行在别人的电脑上的)
        try(InputStream inputStream = FileDao.class.getClassLoader().getResourceAsStream("db.sql")) {
            // 这里是字节流到字符流的转换(对字符能轻松的进行操作, 对字节的操作要难得多)
            try(InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf8")) {
                while(true) {
                    // char 类型读取不到 -1, 也就没有文件读取结束的标记了, 所以这里使用 int 来接收
                    int ch = inputStream.read(); //inputStream.read() 读取到文件结束符会返回 -1
                    if( ch == -1) break;  //文件读取完毕
                    stringBuilder.append((char) ch);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // sql 语句以 ';' 结束, 我们就以 ';' 来拆分字符串, 一句 sql 就是一个 String
        return stringBuilder.toString().split(";");
    }

    // 2.插入文件/目录数据到数据库中
    //   此处提供"批量插入"操作
    public void add(List<FileMeta> fileMetas) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = DBUtil.getConnection();
            // 关闭自动提交功能
            // (一次插入多条数据, 如果一个一个插,数据库就要打开-关闭-打开-关闭... 因此一次插入多条数据, 会减小数据库的资源消耗)
            connection.setAutoCommit(false);

            String sql = "insert into file_meta values(null, ?, ?, ?, ?, ?, ?, ?)";
            statement = connection.prepareStatement(sql);
            for(FileMeta fileMeta : fileMetas) {
                // 把当前 FileMeta 对象, 替换到 SQL 语句中.
                statement.setString(1,fileMeta.getName());
                statement.setString(2,fileMeta.getPath());
                statement.setBoolean(3, fileMeta.isDirectory());
                statement.setString(4,fileMeta.getPinyin());
                statement.setString(5,fileMeta.getPinyinFirst());
                statement.setLong(6, fileMeta.getSize());
                statement.setTimestamp(7, new Timestamp(fileMeta.getLastModified()));
                // 使用 addBatch 把构造好的片段连接起来
                // addBatch 会把已经构造好的 SQL 保存起来, 同时又会允许重新构造一个新的 SQL 出来
                statement.addBatch();
                System.out.println("[insert] 插入文件: " + fileMeta.getPath() + File.separator + fileMeta.getName());
            }
            // 执行所有的 sql 片段
            statement.executeBatch();
            // 执行完毕后, 通过 commit 告诉数据库, 添加完毕, 执行上述 batch 操作(自动提交已经关闭了)
            connection.commit();
        } catch (SQLException e) {
            try {
                if(connection != null) {
                    // 如果连接已建立, 并且出现异常, 那就是提交的内容有错误, 此时进行回滚操作
                    connection.rollback();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

    /**
     *  3.按照特定的关键词进行查询
     *  此处查询 pattern , 可能是文件名的一部分, 可能是文件名拼音的一部分, 也可能是拼音首字母的一部分 ...
     * @param pattern 根据 pattern 查询数据库匹配内容
     * @return
     */
    public List<FileMeta> searchByPattern(String pattern) {
        List<FileMeta> fileMetas = new ArrayList<>();

        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "select name, path, is_directory, size, last_modified from file_meta " +
                    " where name like ? or pinyin like ? or pinyin_first like ? " +
                    " order by path, name";
            statement = connection.prepareStatement(sql);
            statement.setString(1,"%" + pattern + "%");
            statement.setString(2,"%" + pattern + "%");
            statement.setString(3,"%" + pattern + "%");
            resultSet = statement.executeQuery();
            while(resultSet.next()) {
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModified = resultSet.getTimestamp("last_modified");
                FileMeta fileMeta = new FileMeta(name, path, isDirectory, size, lastModified.getTime());
                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }

        return fileMetas;
    }

    /**
     *  根据给定路径查询结果.
     * @param targetPath 给定路径
     * @return 该路径下的所有文件信息(一层)
     */
    public List<FileMeta> searchByPath(String targetPath) {
        List<FileMeta> fileMetas = new ArrayList<>();

        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            connection = DBUtil.getConnection();
            String sql = "select name, path, is_directory, size, last_modified from file_meta " +
                    " where path = ?";
            statement = connection.prepareStatement(sql);
            statement.setString(1, targetPath);
            resultSet = statement.executeQuery();
            while(resultSet.next()) {
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModified = resultSet.getTimestamp("last_modified");
                FileMeta fileMeta = new FileMeta(name, path, isDirectory, size, lastModified.getTime());
                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return fileMetas;
    }

    /**
     *  发现某个文件已从磁盘上删除, 此时需要把对应表里的内容删除掉
     * @param fileMetas 删除的 文件/目录
     */
    public void delete(List<FileMeta> fileMetas) {
        Connection connection = null;
        PreparedStatement statement = null;

        try {
            connection = DBUtil.getConnection();
            connection.setAutoCommit(false);  // 将自动提交关闭, 把下列批量删除操作看作一个事务进行

            for(FileMeta fileMeta : fileMetas) {
                String sql = null;
                if(!fileMeta.isDirectory()) {
                    // 对文件的sql语句构造
                    sql = "delete from file_meta where name = ? and path = ?";
                    statement = connection.prepareStatement(sql);
                    statement.setString(1, fileMeta.getName());
                    statement.setString(2, fileMeta.getPath());
                } else {
                    // 对目录的sql语句构造
                    sql = "delete from file_meta where (name = ? and path = ?) or (path like ?)";
                    statement = connection.prepareStatement(sql);
                    statement.setString(1, fileMeta.getName());
                    statement.setString(2, fileMeta.getPath());
                    statement.setString(3, fileMeta.getPath() + File.separator + fileMeta.getName() + File.separator + "%");
                }
                statement.executeUpdate();
                System.out.println("[delete] " + fileMeta.getPath() + "\\" + fileMeta.getName());

                // 此处对于每个 statement 对象都要单独关闭
                // (每个 statement 都可能是不同的 sql 语句, 以前可以统一关闭是因为 sql 模板相同, 只是填充参数不同, 修改一下参数就可以接着用)
                statement.close();
            }
            // 告知数据库, 事务构造完毕, 进行统一提交
            connection.commit();
        } catch (SQLException e) {
            try {
                connection.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            DBUtil.close(connection, null, null);
        }

    }
}

工具类

PinyinUtil

封装 Pinyin4j 的功能

汉字 => 拼音 (全拼 / 首字母)

java 复制代码
public class PinyinUtil {
    /**
     *  获取字符串的拼音
     *
     * @param src  第一个参数表示要获取拼音的字符串
     * @param fullSpell  第二个参数表示是否是全拼.
     *                   比如针对"你好啊"该字符串, true 对应全拼: nihaoa, false 对应首字母: nha
     *                   此处针对多音字不做过多考虑, 采用第一个元素代表的发音(也是最常用的发音)
     * @return  字符串的拼音
     */
    public static String get(String src, Boolean fullSpell) {
        // trim() 去除字符串两侧的空白字符. eg: \t  \n  \f  \v  空格 ...
        if(src == null || src.trim().length() == 0) {
            // 空字符不做处理
            return null;
        }

        // 针对 Pinyin4j 做一些配置, 让他将拼音 yu 使用 v 表示
        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setVCharType(HanyuPinyinVCharType.WITH_V);

        // 遍历字符串的每个字符, 针对每个字符分别进行转换, 将转换得到的拼音, 拼接到 StringBuilder 里
        StringBuilder stringBuilder = new StringBuilder();
        src = src.trim();
        for(int i=0;i<src.length();i++) {
            // 针对单个字符进行转换
            char ch = src.charAt(i);
            String[] tmp = null;

            try {
                tmp = PinyinHelper.toHanyuPinyinStringArray(ch, format);
            } catch (BadHanyuPinyinOutputFormatCombination e) {
                e.printStackTrace();
            }

            if(tmp == null || tmp.length == 0) {
                // 拼音转换失败, 返回空数组
                // 说明当前字符就不是汉字, 可能是字母,数字或符号, eg: a, b, c, 1, 2, 3
                // 此时保留原始字符就好
                stringBuilder.append(ch);
            }else if(fullSpell) {
                stringBuilder.append(tmp[0]);
            }else {
                stringBuilder.append(tmp[0].charAt(0));
            }
        }
        return stringBuilder.toString();
    }
}

功能处理

扫描文件目录, 将目录下所有文件/目录信息存储到数据库中

当选择搜索路径后, 会递归的扫描指定路径下的所有目录及文件, 并将扫描到的 所有文件/目录信息 存储到数据库中

(查数据库比查文件系统要快, 因此其实每次查找指定文件在文件系统中出现的位置, 都是查询数据库中预存的信息)

java 复制代码
public class GUIController implements Initializable {
    @FXML
    private Label label;

    @FXML
    private GridPane gridPane;

    @FXML
    private Button button;

    @FXML
    private TextField textField;

    @FXML
    private TableView<FileMeta> tableView;

    private SearchService searchService = null;

    /**
     * 使用该方法, 作为鼠标点击事件的回调函数
     * @param mouseEvent
     */
    public void choose(MouseEvent mouseEvent) {
        // 创建一个 目录选择器
        DirectoryChooser directoryChooser = new DirectoryChooser();
        // 把该对话框显示出来
        Window window = gridPane.getScene().getWindow();
        // 获取选定的文件
        File file = directoryChooser.showDialog(window);

        if(file == null) {
            System.out.println("用户选择的路径为空");
        } else {
            System.out.println(file.getAbsolutePath());
        }

        // 把用户选择的路径,显示到 label 中
        label.setText(file.getAbsolutePath());

        // 如果不是首次扫描, 就应该停止上次扫描任务, 执行本次扫描任务
        if(searchService != null) {
            searchService.shutdown();
        }

        // 对用户选择的路径进行扫描, 初始化
        searchService = new SearchService();
        searchService.init(file.getAbsolutePath());
    }
}

搜索框内容发生改变后, 自动进行数据库搜索, 将匹配内容展示到页面

当搜索框内容改变时, 会被系统绑定的事件监听到, 重新进行数据库搜索, 并将匹配信息作为结果返回到查询结果显示处

java 复制代码
public class GUIController implements Initializable {
    @FXML
    private Label label;

    @FXML
    private GridPane gridPane;

    @FXML
    private Button button;

    @FXML
    private TextField textField;

    @FXML
    private TableView<FileMeta> tableView;

    private SearchService searchService = null;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // 在这里对 输入框 加一个监听器
        // 需要指定对 text 这个内容属性进行监听
        // textField.textProperty() 获取输入框里的内容
        textField.textProperty().addListener(new ChangeListener<String>() {
            /**
             *  会在用户每次修改 输入框内容 的时候, 被自动调用到
             * @param observable
             * @param oldValue 输入框被修改之前的值
             * @param newValue 输入框被修改之后的值
             */
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                // 此处要干的事情, 是根据新的值, 重新进行查询操作
                freshTable(newValue);
            }
        });
    }

    private void freshTable(String query) {
        // 重新查询数据库, 把查询结果, 设置到表格中
        if(searchService == null) {
            System.out.println("searchService 尚未初始化, 不能查询!");
            return;
        }

        // 把之前表里的内容清空掉
        ObservableList<FileMeta> fileMetas = tableView.getItems();
        fileMetas.clear();
        List<FileMeta> results = searchService.search(query);
        fileMetas.addAll(results);
    }
}
相关推荐
逊嘘6 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
morris13113 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
七星静香38 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员39 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU39 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie643 分钟前
在IDEA中使用Git
java·git
Elaine2023911 小时前
06 网络编程基础
java·网络
G丶AEOM1 小时前
分布式——BASE理论
java·分布式·八股
落落鱼20131 小时前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀1 小时前
LRU缓存算法
java·算法·缓存