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

背景

各文件系统下, 都有提供文件查找的功能, 但是一般而言搜索速度很慢
在这里插入图片描述
本项目仿照 everything 工具, 实现本地文件的快速搜索

实现功能

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

相关技术

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

数据库设计

在这里插入图片描述

SQLite 创建 SQL 的语句如下

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 文件

显示界面的图画化结构

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

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 方法来启动整个程序

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 方法

// 本类的示例就代表 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 的 连接/关闭/管理 操作

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 类

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

// 通过这个类来封装针对 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 的功能
汉字 => 拼音 (全拼 / 首字母)

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();
    }
}

功能处理

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

当选择搜索路径后, 会递归的扫描指定路径下的所有目录及文件, 并将扫描到的 所有文件/目录信息 存储到数据库中
(查数据库比查文件系统要快, 因此其实每次查找指定文件在文件系统中出现的位置, 都是查询数据库中预存的信息)

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());
    }
}

在这里插入图片描述

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

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

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);
    }
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/642973.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

java在类的定义中创建自己的对象?

当在main方法中新建自身所在类的对象&#xff0c;并调用main方法时&#xff0c;会不断循环调用main方法&#xff0c;直到栈溢出 package com.keywordStudy;public class mainTest {static int value 33;public static void main(String[] args) throws Exception{String[] sn…

营销短信XML接口对接发送示例

在现代社会中&#xff0c;通信技术日新月异&#xff0c;其中&#xff0c;短信作为一种快速、简便的通信方式&#xff0c;仍然在日常生活中占据着重要的地位。为了满足各种应用场景的需求&#xff0c;短信接口应运而生&#xff0c;成为了实现高能有效通信的关键。 短信接口是一种…

从机械尘埃到智能星河:探索从工业心脏到AI大脑的世纪跨越(一点个人感想)...

全文预计1400字左右&#xff0c;预计阅读需要8分钟。 近期&#xff0c;人工智能领域呈现出前所未有的活跃景象&#xff0c;各类创新成果如雨后春笋般涌现&#xff0c;不仅推动了科技的边界&#xff0c;也为全球经济注入了新的活力。 这不&#xff0c;最近报道16家国内外企业在A…

【铨顺宏RFID技术闪耀登场】广交会与您共绘智慧新篇章!

激动人心的时刻即将来临&#xff01;广交会作为中国最重要的综合性国际贸易盛会&#xff0c;每年都吸引着来自世界各地的参展商和观众。今年&#xff0c;我们铨顺宏公司也荣幸地参与其中&#xff0c;并将在广交会上展示我们最新的RFID技术产品。 &#x1f4cd;地点&#xff1a;…

Android Studio添加依赖 新版 和 旧版 的添加方式(Gradle添加依赖)(Java)

旧版的&#xff08;在线添加&#xff09; 1找 文件 在项目的build.gradle文件中添加依赖(在下面的节点中添加库 格式 ’ 组 &#xff1a;名字 &#xff1a; 版本号 ‘ ) dependencies {implementation com.example:library:1.0.0 }implementation 组:名字:版本…

网段与广播域

ip地址与子网掩码做与运算得到网络号&#xff0c;得到的网络号相同就是同一个网段&#xff0c;否则不是&#xff0c;跟他们在什么位置没有任何关系 这里面pc3和前两个pc虽然不在同一个网段&#xff0c;但是pc1发广播包的时候&#xff0c;pc3也能收到&#xff0c;因为路由器的所…

相关服务器介绍

服务器是一种高性能的计算机&#xff0c;它被设计用来为其他计算机或终端设备提供服务&#xff0c;如数据处理、文件存储、网络通信等。服务器通常具有强大的计算能力、大容量的存储空间和高效的网络连接能力。 常见的服务器种类及其特点 文件服务器 文件服务器主要负责中央存储…

[OpenGL] opengl切线空间

目录 一 引入 二 TBN矩阵 三 代码实现 3.1手工计算切线和副切线 3.2 像素着色器 3.3 切线空间的两种使用方法 3.4 渲染效果 四 复杂的物体 本章节源码点击此处 继上篇法线贴图 来熟悉切线空间是再好不过的。对于法线贴图来说,我们知道它就是一个2D的颜色纹理,根据rgb…

qmt量化教程4----订阅全推数据

文章链接 qmt量化教程4----订阅全推数据 (qq.com) 上次写了订阅单股数据的教程 量化教程3---miniqmt当作第三方库设置&#xff0c;提供源代码 全推就主动推送&#xff0c;当行情有变化就会触发回调函数&#xff0c;推送实时数据&#xff0c;可以理解为数据驱动类型&#xff0…

并发编程笔记7--并发编程基础

1、线程简介 1.1、什么是线程 现代操作系统中运行一个程序&#xff0c;会为他创建一个进程。而每一个进程中又可以创建许多个线程。现代操作系统中线程是最小的调度单元。 两者关系&#xff1a;一个线程只属于一个进程&#xff0c;而一个进程可以拥有多个线程。线程是一个轻量…

测试基础05:软件测试的分类

课程大纲 1、两种架构&#xff08;Architecture&#xff09; 1.1、B/S&#xff08;Browser/Server&#xff09; 浏览器服务器架构&#xff08;大体3步&#xff09;&#xff1a;用户通过浏览器向服务器发出请求&#xff0c;服务器处理请求&#xff0c;将结果通过网络返回到用户…

【数据挖掘】四分位数识别数据中的异常值(附代码)

写在前面&#xff1a; 首先感谢兄弟们的订阅&#xff0c;让我有创作的动力&#xff0c;在创作过程我会尽最大能力&#xff0c;保证作品的质量&#xff0c;如果有问题&#xff0c;可以私信我&#xff0c;让我们携手共进&#xff0c;共创辉煌。 路虽远&#xff0c;行则将至&#…

口碑比较好的相亲交友平台有哪些?正规靠谱的相亲软件排行榜测评

在网络时代&#xff0c;越来越多的人热衷于使用相亲交友软件来寻找生命中的另一半。这些软件确实为许多用户提供了真实可靠的交友平台。然而&#xff0c;市面上的相亲软件种类繁多&#xff0c;质量良莠不齐&#xff0c;让人难以选择。今天&#xff0c;我将介绍几款我使用过且认…

【ARM 裸机】按键输入

本节学习按键输入&#xff0c;先拷贝上一节工程文件&#xff0c; 1、驱动编写 新建 key 的 .h 和 .c 文件&#xff1b; 再查看一下硬件原理图如下&#xff1b; 由此可知&#xff0c;KEY0 按键接在 UART1_CTS 引脚上&#xff0c;默认情况下为高电平&#xff0c;按键按下为…

【LeetCode】30.串联所有单词的子串

串联所有单词的子串 题目描述&#xff1a; 给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。 s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。 例如&#xff0c;如果 words ["ab","cd",&qu…

超值分享50个DFM模型格式的素人直播资源,适用于DeepFaceLive的DFM合集

50直播模型&#xff1a;点击下载 作为直播达人&#xff0c;我在网上购买了大量直播用的模型资源&#xff0c;包含男模女模、明星脸、大众脸、网红脸及各种稀缺的路人素人模型。现在&#xff0c;我将这些宝贵的资源整理成合集分享给大家&#xff0c;需要的朋友们可以直接点击下…

工业路由器在工厂数字化的应用及价值

随着科技的飞速发展&#xff0c;数字化转型已成为工厂提高效率、降低成本、实现智能化管理的关键途径。在这个过程中&#xff0c;工业路由器凭借其独特的优势&#xff0c;正逐渐成为工厂数字化建设不可或缺的核心组件。本文将深入探讨工业路由器在工厂数字化中的应用及价值&…

c# 画一个正弦函数

1.概要 c# 画一个正弦函数 2.代码 using System; using System.Drawing; using System.Windows.Forms;public class SineWaveForm : Form {private const int Width 800;private const int Height 600;private const double Amplitude 100.0;private const double Period…

光电直读抄表技术详细说明

1.技术简述 光电直读抄表是一种智能化智能计量技术&#xff0c;主要是通过成像原理立即载入电度表里的标值&#xff0c;不用人工干预&#xff0c;大大提升了抄表效率数据可靠性。此项技术是智慧能源不可或缺的一部分&#xff0c;为电力公司的经营管理提供了有力的适用。 2.原…

2024年5月26日 十二生肖 今日运势

小运播报&#xff1a;2024年5月26日&#xff0c;星期日&#xff0c;农历四月十九 &#xff08;甲辰年己巳月庚寅日&#xff09;&#xff0c;法定节假日。 红榜生肖&#xff1a;马、猪、狗 需要注意&#xff1a;牛、蛇、猴 喜神方位&#xff1a;西北方 财神方位&#xff1a;…