项目实战——search-everything搜索工具

article/2025/8/26 11:35:25

目录

项目概要

项目大纲

项目准备

项目代码与映射关系

工具包 util

拼音工具 PinyinUtil

创建SQLite数据源 DBUtil

通用工具类 Util

在界面初始化时创建文件信息数据表 DBInit

资源文件 resourse

数据表信息 init.sql

项目界面 app.fxml 

软件工作包 app

数据表记录 FileMeta

界面初始化类  Controller

文件扫描任务包 task

文件扫描器 FileScanner

文件查找器 FileSearch

回调子类包 callback

回调接口 FileScannerCallBack

文件信息保存到数据库的回调子类 FileSaveDB(核心)

主方法 Main

成品预览

项目概要 

项目名称:search-everything搜索工具

项目介绍:该项目是仿照Everything软件部分功能实现的本地文件搜索工具,支持Win、linux、MacOS等跨平台使用,支持全拼查询、模糊查询。

项目功能:
1.选择文件夹多线程扫描该文件夹下的子文件,展示文件的名称,大小,修改时间。

2.选择路径后,支持搜索相关文件内容(全拼或文件首字母或文件部分名称–支持模糊查询)

3.文件夹扫描完毕之后,显示搜索的所有文件以及文件夹的个数,以及总耗时

相关技术栈:Java8、JavaFX、多线程、IO流、SQLite数据库、JBDC编程
项目Gitte链接:https://gitee.com/mingzizhennanqi/search_everying

项目大纲 

项目准备 

1. Maven

maven是 apache (开源组织),提供的一个项目构建工具(project build tool) 。
构建(build):依赖处理(dependencies)、编译(compile)、打包(package)
依赖:之前写的代码,基本用到的类来自{我们自己写的类、JDK原生提供的类},随着代码变得复杂、庞大;需要用到来自第三方提供的类(比如:写JDBC时,使用的类)。

2. 引入lombok开发包——项目必备的开发包
普通的Java类,都需要写一大堆的getter,setter,toString,hashCode,equals等等一大堆的代码使用这个库可以直接使用个注解,注入这些代码。
 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>everything2</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><!--    汉语拼音的处理工具    --><dependency><groupId>com.belerweb</groupId><artifactId>pinyin4j</artifactId><version>2.5.1</version></dependency><!--   SQLite数据库     --><dependency><groupId>org.xerial</groupId><artifactId>sqlite-jdbc</artifactId><version>3.36.0.3</version></dependency><!--   lombok库     --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.24</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>2.4</version><configuration><archive><manifest><!-- 指定入口类 --><mainClass>Main</mainClass><!-- 在jar的MF文件中生成classpath属性 --><addClasspath>true</addClasspath><!-- classpath前缀,即依赖jar包的路径 --><classpathPrefix>lib/</classpathPrefix></manifest></archive></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><!-- 引入的第三方jar包执行package命令打包时也将第三方的jar包打包进来--><!-- 这样的话在执行可执行jar就会找到相应jar--><artifactId>maven-dependency-plugin</artifactId><version>2.8</version><executions><execution><id>copy</id><phase>package</phase><goals><goal>copy-dependencies</goal></goals><configuration><!-- 指定依赖包的输出路径,需与上方的classpathPrefix保持一致 --><outputDirectory>${project.build.directory}/lib</outputDirectory></configuration></execution></executions></plugin></plugins></build>
</project>

项目代码与映射关系

工具包 util

拼音工具 PinyinUtil

将一个汉字字符(有多音字)转为字母

如:“和” he2 huo2 he4等 

需要有一个工具,能帮我们将任意的中文字符转为字母字符串,从而支持我们的模糊查找:
快速排序=> kuaisupaixu / ksp  

​
​
package util;import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;import java.util.Arrays;/*** 写项目是一个搭积木的过程,一部分一部分完成,最终每个独立的模块拼装在一起* 一般来说,项目都是从工具类或是数据库相关的操作开始的* 拼音工具类* 汉语拼音的字符映射为字母字符串*/
public class PinyinUtil {// 定义汉语拼音的配置 全局常量,必须在定义时初始化,全局唯一// 这个配置就表示将汉字字符转为拼音字符串时的一些设置private static final HanyuPinyinOutputFormat FORMAT;// 所有的中文对应的Unicode编码区间private static final String CHINESE_PATTERN = "[\\u4E00-\\u9FA5]";// 代码块就是在进行一些项目配置的初始化操作static {// 当PinyinUtil类加载时执行静态块,除了产生对象外,还可以进行一些配置相关的工作FORMAT = new HanyuPinyinOutputFormat();// 设置转换后的英文字母为全小写 王 -> wangFORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE);// 设置转换后的英文字母是否带音调FORMAT.setToneType(HanyuPinyinToneType.WITHOUT_TONE);// 特殊拼音用v替代 绿 -> lvFORMAT.setVCharType(HanyuPinyinVCharType.WITH_V);}/*** 判断给定的字符串是否包含中文** @param str 要判断的字符串* @return*/public static boolean containsChinese(String str) {return str.matches(".*" + CHINESE_PATTERN + ".*");}/*** 传入任意的文件名称,就能将该文件名称转为字母字符串全拼和首字母小写字符串* eg : 文件名为 测试用例 =>* ceshiyongli / csyl* <p>* 若文件名中包含其他字符,英文数字等,不需要做处理,直接保存* eg : 张三Three117 =>* zhangsanthree117 / zsthree117** @param fileName* @return*/public static String[] getPinyinByFileName(String fileName) {// 第一个字符串为文件名全拼// 第二个字符串为首字母String[] ret = new String[2];// 核心操作就是遍历文件名中的每个字符,碰到非中文直接保留,碰到中文则处理StringBuilder allNameAppender = new StringBuilder();StringBuilder firstCaseAppender = new StringBuilder();// fileName = 测试d用例// c = 测for (char c : fileName.toCharArray()) {// 不考虑多音字,就使用第一个返回值作为我们的参数try {String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c, FORMAT);if (pinyins == null || pinyins.length == 0) {// 碰到非中文字符,直接保留allNameAppender.append(c);firstCaseAppender.append(c);} else {// 碰到中文字符,取第一个多音字的返回值 和 -> [he,huo,hu..]allNameAppender.append(pinyins[0]);// he ->  hfirstCaseAppender.append(pinyins[0].charAt(0));}} catch (BadHanyuPinyinOutputFormatCombination e) {allNameAppender.append(c);firstCaseAppender.append(c);}}ret[0] = allNameAppender.toString();ret[1] = firstCaseAppender.toString();return ret;}
}​​

 创建SQLite数据源 DBUtil

在选择完文件夹后,启动文件的扫描任务,将当前选择的文件夹下的所有文件和子文件夹信息保存到SQLite数据库,在搜索框查询时,直接从数据库中查询,不会再次进行扫描,提高效率。
之所以用SQLite嵌入式数据库,当前这个项目本来就比较小,属于工具类的项目。

JDBC:
1.获取数据源,设置账号密码,连接地址等等
2.获取数据库的连接,Statement对象(执行SQL语句的对象)

3.执行Statement的查询或更新方法
executeQuery() : ResultSet对象,存储返回值信息

executeUpdate() : int,执行更新后的修改的行数
4.关闭连接和Statement,ResultSet对象,关闭资源操作
 

package util;import org.sqlite.SQLiteConfig;
import org.sqlite.SQLiteDataSource;import javax.sql.DataSource;
import java.io.File;
import java.sql.*;/*** SQLite数据库的工具类,创建数据源,创建数据库的连接* 只向外部提供SQLite数据库的连接即可,数据源不提供(封装在工具类的内部)* 无论是哪种关系型数据库,操作的流程都是JDBC四步走**/
public class DBUtil {// 单例数据源private volatile static DataSource DATASOURCE;// 单例数据连接private volatile static Connection CONNECTION;// 获取数据源方法,使用double-check单例模式获取数据源对象private static DataSource getDataSource() {if (DATASOURCE == null) {// 多线程场景下,只有一个线程能进入同步代码块synchronized (DBUtil.class) {if (DATASOURCE == null) {// SQLite没有账户密码,只需要配置日期格式即可// SQLiteConfig是SQLite数据源的配置SQLiteConfig config = new SQLiteConfig();// 设置日期格式config.setDateStringFormat(Util.DATE_PATTERN);DATASOURCE = new SQLiteDataSource(config);// 配置数据源的URL是SQLite子类独有的方法,因此向下转型((SQLiteDataSource) DATASOURCE).setUrl(getUrl());}}}return DATASOURCE;}/*** 配置SQLite数据库的地址* mysql: jdbc:mysql://127.0.0.1:3306/数据库名称?CharacterEncoding=utf8&useSSL=false;* 对于SQLite数据库来说,没有服务端和客户端,因此只需要指定SQLite数据库的地址即可* SQLite: jdbc:sqlite://D:\2022rocket\search_everything\target\search_everything.db;*/private static String getUrl() {// 自己的target文件夹的绝对路径String path = "D:\\2022rocket\\search_everything\\target";String url = "jdbc:sqlite://" + path + File.separator + "search_everything.db";System.out.println("获取数据库的连接为 : " + url);return url;}/*** 多线程场景下,SQLite要求多个线程使用同一个连接进行处理* @return* @throws SQLException*/public static Connection getConnection() throws SQLException {if (CONNECTION == null) {synchronized (DBUtil.class) {if (CONNECTION == null) {CONNECTION = getDataSource().getConnection();}}}return CONNECTION;}public static void main(String[] args) throws SQLException {System.out.println(getConnection());}public static void close(Statement statement) {if (statement != null) {try {statement.close();} catch (SQLException e) {throw new RuntimeException(e);}}}public static void close(PreparedStatement ps, ResultSet rs) {close(ps);if (rs != null) {try {rs.close();} catch (SQLException e) {throw new RuntimeException(e);}}}
}

 通用工具类 Util

package util;import java.text.SimpleDateFormat;
import java.util.Date;/*** 通用工具类*/
public class Util {public static final String DATE_PATTERN = "yyyy-MM-dd HH-mm-ss";/*** 根据传入的文件大小返回不同的单位* 支持的单位如下 B,KB,MB,GB*/public static String parseSize(Long size) {String[] util = {"B", "KB", "MB", "GB"};int flag = 0;while (size > 1024) {size /= 1024;flag++;}return size + util[flag];}public static String parseFileType(boolean directory) {return directory ? "文件夹" : "文件";}public static String parseDate(Date lastModified) {return new SimpleDateFormat(DATE_PATTERN).format(lastModified);}
}

在界面初始化时创建文件信息数据表 DBInit

​
package util;import java.io.FileNotFoundException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;/*** 在界面初始化时创建文件信息数据表*/
public class DBInit {/*** 从resources路径下读取init.sql文件,加载到程序中* 文件IO* @return*/public static List<String> readSQL() {List<String> ret = new ArrayList<>();// 从init.sql文件中获取内容,需要拿到文件的输入流try {// 采用类加载器的方式引入资源文件// JVM在加载类的时候用到的ClassLoader类// 所谓的类加载器简单理解就是告诉JVM从哪个文件夹去执行class文件。InputStream in = DBInit.class.getClassLoader().getResourceAsStream("init.sql");// 对于输入流来说,一律采用Scanner类来处理// 对于输出流来说,一律采用PrintStream类来处理Scanner scanner = new Scanner(in);// 自定义分隔符scanner.useDelimiter(";");// nextLine默认碰到换行分隔// next按照自定义的分隔符拆分while (scanner.hasNext()) {String str = scanner.next();if ("".equals(str) || "\n".equals(str)) {continue;}// 取出sql语句前面的--if (str.contains("--")) {str = str.replaceAll("--","");}ret.add(str);}} catch (Exception e) {throw new RuntimeException(e);}
//        System.out.println("读取到的内容为:");
//        System.out.println(ret);return ret;}/*** 在界面初始化时先初始化数据库,创建数据表*/public static void init() {Connection connection = null;Statement statement = null;try {connection = DBUtil.getConnection();// 获取要执行的sql语句List<String> sqls = readSQL();// 这采用了普通的Statement接口,没有用PrepareStatementstatement = connection.createStatement();for (String sql : sqls) {System.out.println("执行SQL操作 : " + sql);statement.executeUpdate(sql);}}catch (SQLException e) {System.err.println("数据库初始化失败");e.printStackTrace();}finally {DBUtil.close(statement);}}//    public static void main(String[] args) {
//        init();
//    }
}​

 

资源文件 resourse

数据表信息 init.sql

在界面初始化时创建文件信息数据表

数据库中保存的核心文件信息——蓝色部分为界面中显示的信息,其余内容不显示

文件名name
文件路径path
是文件还是文件夹is_directory

文件大小size
上次修改时间last_modified

文件字母全拼pinyin
文件全拼首字母pinyin_first

--drop table if exists file_meta;
create table if not exists file_meta(name varchar(50) not null,path varchar(100) not null,is_directory boolean not null,size bigint,last_modified timestamp not null,pinyin varchar(200),pinyin_first varchar(50)
);

项目界面 app.fxml 

JavaFX图形化编程采用类似Html的方式

*.fxml 就是项目界面的样子
前端的很多界面包括样式经常都会被后端开发根据现有需求修改,需要复制现有代码。

app.fxml中fx:id的名称要和app.Controller类中的属性名称完全一致,这样界面中的内容才会正确的被Controller类所接收。
 

<?xml version="1.0" encoding="UTF-8"?><?import javafx.geometry.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.cell.*?><GridPane fx:id="rootPane" alignment="center" hgap="10" vgap="10" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="app.Controller"><children><Button onMouseClicked="#choose" prefWidth="90" text="选择目录" GridPane.columnIndex="0" GridPane.rowIndex="0" /><Label fx:id="srcDirectory"><GridPane.margin><Insets left="100.0" /></GridPane.margin></Label><TextField fx:id="searchField" prefWidth="900" GridPane.columnIndex="0" GridPane.rowIndex="1" /><TableView fx:id="fileTable" prefHeight="1000" prefWidth="1300" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="2"><columns><TableColumn fx:id="nameColumn" prefWidth="220" text="名称"><cellValueFactory><PropertyValueFactory property="name" /></cellValueFactory></TableColumn><TableColumn prefWidth="400" text="路径"><cellValueFactory><PropertyValueFactory property="path" /></cellValueFactory></TableColumn><TableColumn fx:id="isDirectory" prefWidth="90" text="文件类型"><cellValueFactory><PropertyValueFactory property="isDirectoryText" /></cellValueFactory></TableColumn><TableColumn fx:id="sizeColumn" prefWidth="90" text="大小(B)"><cellValueFactory><PropertyValueFactory property="sizeText" /></cellValueFactory></TableColumn><TableColumn fx:id="lastModifiedColumn" prefWidth="190" text="修改时间"><cellValueFactory><PropertyValueFactory property="lastModifiedText" /></cellValueFactory></TableColumn></columns></TableView></children>
</GridPane>

软件工作包 app

数据表记录 FileMeta

和数据库中的表打交道的类,最终程序中获取数据库的记录就通过本类来描述。
这个类就对应我们数据库表名,数据表中的一行记录就对应我们这个类的一个对象,该类的一个对象就是数据表的一行。数据表的所有内容就对应FileMeta这个类的对象数组。

package app;/*** @author yuisama* @date 2022/07/06 15:08* 和数据库打交道的类,最终程序中获取数据库的记录就通过本类来描述* 这个类就对应我们数据库表名,数据表中的一行记录就对应我们这个类的一个对象* 该类的一个对象就是数据表的一行* 数据表的所有内容就对应FileMeta这个类的对象数组**/import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import util.Util;import java.util.Date;/*** create table if not exists file_meta(* name varchar(50) not null,* path varchar(100) not null,* is_directory boolean not null,* size bigint not null,* last_modified timestamp not null,* pinyin varchar(200),* pinyin_first varchar(50)* );*/
@Data
@NoArgsConstructor
@ToString
@EqualsAndHashCode
public class FileMeta {private String name;private String path;private Boolean isDirectory;private Long size;private Date lastModified;// 若包含中文名称,名称全拼private String pinYin;// 拼音首字母private String pinYinFirst;// 以下三个属性需要在界面中展示,将当前属性值做处理之后展示// 这些属性名要和app.fxml中保持一致// 文件类型private String isDirectoryText;// 文件大小private String sizeText;// 上次修改时间private String lastModifiedText;public void setSize(Long size) {this.size = size;this.sizeText = Util.parseSize(size);}public void setIsDirectory(Boolean directory) {isDirectory = directory;this.isDirectoryText = Util.parseFileType(directory);}public void setLastModified(Date lastModified) {this.lastModified = lastModified;this.lastModifiedText = Util.parseDate(lastModified);}public FileMeta(String name, String path, Boolean isDirectory, Long size, Date lastModified) {this.name = name;this.path = path;this.isDirectory = isDirectory;this.size = size;this.lastModified = lastModified;}
}

界面初始化类  Controller

package app;import callback.impl.FileSave2DB;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Window;
import task.FileScanner;
import task.FileSearch;
import util.DBInit;import java.io.File;
import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;public class Controller implements Initializable {@FXMLprivate GridPane rootPane;@FXMLprivate TextField searchField;@FXMLprivate TableView<FileMeta> fileTable;@FXMLprivate Label srcDirectory;private List<FileMeta> fileMetas;private Thread scanThread;// 点击运行项目,界面初始化时加载的一个方法// 就相当于运行一个主类,首先要加载主类的静态块一个道理public void initialize(URL location, ResourceBundle resources) {// 想要在界面初始化时初始化数据库DBInit.init();// 添加搜索框监听器,内容改变时执行监听事件searchField.textProperty().addListener(new ChangeListener<String>() {public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {freshTable();}});}// 点击选择目录,就会获取到最终界面上选择的是哪个文件夹public void choose(Event event) {// 选择文件目录DirectoryChooser directoryChooser=new DirectoryChooser();Window window = rootPane.getScene().getWindow();File file = directoryChooser.showDialog(window);if(file == null)return;// 获取选择的目录路径,并显示String path = file.getPath();// 在界面中显示路径的内容this.srcDirectory.setText(path);// 获取要扫描的文件夹路径之后,进行文件的扫描工作// 此时将文件信息保存到数据库中FileScanner fileScanner = new FileScanner(new FileSave2DB());if (scanThread != null) {// 创建过任务,且该任务还没执行结束,中断当前正在扫描的任务scanThread.interrupt();fileTable.getItems().clear();}// 开启新线程扫描新选择的目录scanThread = new Thread(() -> {fileScanner.scan(file);// 刷新界面,展示刚才扫描到的文件信息freshTable();});scanThread.start();}// 刷新表格数据private void freshTable(){// 前端表格metasObservableList<FileMeta> metas = fileTable.getItems();metas.clear();String dir = srcDirectory.getText();if (dir != null && dir.trim().length() != 0) {// 界面中已经选择了文件,此时已经将最新的数据保存到了数据库中,// 只需要取出数据库中的内容展示到界面上即可// 获取用户在搜索框中输入的内容String content = searchField.getText();// 根据选择的路径 + 用户的输入(若为空就展示所有内容) 将数据库中的指定内容刷新到界面中List<FileMeta> filesFromDB = FileSearch.search(dir,content);metas.addAll(filesFromDB);}}
}

文件扫描任务包 task

文件扫描器 FileScanner

listFiles方法只能展示当前这一级目录下的所有file对象,若存在子文件夹,就需要进行递归遍历。

每当碰到一个文件夹,新建一个任务(线程)
a. 先把当前目录下的所有文件夹和文件信息保存到数据库中

b. 遍历当前目录下的所有文件和文件夹:
        l. 碰到文件,只是进行文件个数的累加;
        ll. 若碰到文件夹,调用递归函数,创建新的任务(线程)去执行子文件夹的扫描和保存工作; 

package task;import app.FileMeta;
import callback.FileScannerCallBack;
import lombok.Data;
import lombok.Getter;import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;/*** 进行文件扫描任务*/
@Getter
public class FileScanner {// 当前扫描的文件个数private AtomicInteger fileNum = new AtomicInteger();// 当前扫描的文件夹个数// 最开始扫描的根路径没有统计,因此初始化文件夹的个数为1,表示从根目录下开始进行扫描任务private AtomicInteger dirNum = new AtomicInteger(1);// 所有扫描文件的子线程个数,只有当子线程个数为0时,主线程再继续执行private AtomicInteger threadCount = new AtomicInteger();// 当最后一个子线程执行完任务之后,再调用countDown方法唤醒主线程private CountDownLatch latch = new CountDownLatch(1);private Semaphore semaphore = new Semaphore(0);// 获取当前电脑的可用CPU个数private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();// 使用线程池创建对象private ThreadPoolExecutor pool = new ThreadPoolExecutor(CPU_COUNT,CPU_COUNT * 2, 10,TimeUnit.SECONDS,new LinkedBlockingQueue<>(),new ThreadPoolExecutor.AbortPolicy());// 文件扫描回调对象private FileScannerCallBack callBack;public FileScanner(FileScannerCallBack callBack) {this.callBack = callBack;}/*** 根据传入的文件夹进行扫描任务* @param filePath 要扫描的根目录* 选择要扫描的菜单之后,执行的第一个方法,主线程需要等待所有子线程全部扫描结束之后再恢复执行* scan方法是我们选择要扫描的文件夹之后的入口方法,所有文件夹和文件的具体扫描工作交给子线程,* scan等待所有子线程执行结束之后,统计扫描到的文件夹和文件个数,计算扫描时间*/public void scan(File filePath) {System.out.println("开始文件扫描任务,根目录为 : " + filePath);long start = System.nanoTime();// 将具体的扫描任务交给子线程处理// 此时根目录下的扫描任务已经创建线程处理scanInternal(filePath);// 统计根目录扫描的线程threadCount.incrementAndGet();try {
//            latch.await();semaphore.acquire();} catch (InterruptedException e) {System.err.println("扫描任务中断,根目录为 : " + filePath);}finally {System.out.println("关闭线程池.....");// 当所有子线程已经执行结束,就是正常关闭// 中断任务,需要立即停止所有还在扫描的子线程pool.shutdownNow();}long end = System.nanoTime();System.out.println("文件扫描任务结束,共耗时 : " + (end - start) * 1.0 / 1000000 + "ms");System.out.println("文件扫描任务结束,根目录为 : " + filePath);System.out.println("共扫描到 : " + fileNum.get() + "个文件");System.out.println("共扫描到 : " + dirNum.get() + "个文件夹");}/*** 具体扫描任务的子线程递归方法* @param filePath*/private void scanInternal(File filePath) {if (filePath == null) {return;}// 将当前要扫描的任务交给线程处理pool.submit(() -> {// 使用回调函数,将当前目录下的所有内容保存到指定终端this.callBack.callback(filePath);// 先将当前这一级目录下的file对象获取出来File[] files = filePath.listFiles();// 遍历这些file对象,根据是否是文件夹进行区别处理for (File file : files) {if (file.isDirectory()) {// 等同于 ++idirNum.incrementAndGet();// 将子文件夹的任务交给新线程处理// 碰到文件夹递归创建新线程threadCount.incrementAndGet();scanInternal(file);}else {// 等同于 i++fileNum.getAndIncrement();}}// 当前线程将这一级目录下的文件夹(创建新线程递归处理)和文件的保存扫描任务执行结束System.out.println(Thread.currentThread().getName() + "扫描 : " + filePath + "任务结束");// 子线程数 --threadCount.decrementAndGet();if (threadCount.get() == 0) {// 所有线程已经结束任务System.out.println("所有扫描任务结束");// 唤醒主线程
//                latch.countDown();semaphore.release();}});}
}

文件查找器 FileSearch

根据选择的文件夹路径和用户输入的内容从数据库中查找出指定的内容并返回

将数据库中的内容展示到界面中同时支持界面搜索框中的文件检索,支持全名称搜索,支持拼音搜索,支持拼音首字母搜索(数据库的select)。

package task;import app.FileMeta;
import util.DBUtil;import java.io.File;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;/*** 根据选择的文件夹路径和用户输入的内容从数据库中查找出指定的内容并返回*/
public class FileSearch {/*** @param dir     用户选择的检索的文件夹路径 一定是不为空的* @param content 用户搜索框中的内容 - 可能为空,若为空就展示当前数据库中选择的路径下的所有内容即可* @return*/public static List<FileMeta> search(String dir, String content) {List<FileMeta> result = new ArrayList<>();Connection connection = null;PreparedStatement ps = null;ResultSet rs = null;try {connection = DBUtil.getConnection();// 先根据用户选择的文件夹dir查询内容String sql = "select name,path,size,is_directory,last_modified from file_meta " +" where (path = ? or path like ?)";if (content != null && content.trim().length() != 0) {// 此时用户搜索框中的内容不为空,此处支持文件全名称,拼音全名称,以及拼音首字母的模糊查询sql += " and (name like ? or pinyin like ? or pinyin_first like ?)";}ps = connection.prepareStatement(sql);ps.setString(1, dir);ps.setString(2, dir + File.separator + "%");// 根据搜索框的内容查询数据库,都是模糊匹配if (content != null && content.trim().length() != 0) {// 此时用户搜索框中的内容不为空,此处支持文件全名称,拼音全名称,以及拼音首字母的模糊查询ps.setString(3, "%" + content + "%");ps.setString(4, "%" + content + "%");ps.setString(5, "%" + content + "%");}
//            System.out.println("正在从数据库中检索信息,sql为:" + ps);rs = ps.executeQuery();while (rs.next()) {FileMeta meta = new FileMeta();meta.setName(rs.getString("name"));meta.setPath(rs.getString("path"));meta.setIsDirectory(rs.getBoolean("is_directory"));if (!meta.getIsDirectory()) {// 是文件,保存大小meta.setSize(rs.getLong("size"));}meta.setLastModified(new Date(rs.getTimestamp("last_modified").getTime()) );
//                System.out.println("检索到文件信息 : name = " + meta.getName() + ",path = " + meta.getPath());result.add(meta);}} catch (SQLException e) {System.err.println("从数据库中搜索用户查找内容时出错,请检查SQL语句");e.printStackTrace();} finally {DBUtil.close(ps, rs);}return result;}
}

回调子类包 callback

回调函数:接口回调,基于接口方式的回调函数使用。

这不是一种新的结构,是一种程序设计的思想,将两个互相独立的功能拆分为不同的方法——解耦,但是这两个方法又是互相配合完成同一个功能。其实就是在一个类中调用另一个类的方法来辅助解决问题。

回调接口 FileScannerCallBack
文件扫描的回调接口,扫描文件时由具体的子类决定将当前目录下的文件信息持久化到哪个终端。可以是数据库,也可以通过网络传输等
 

package callback;import java.io.File;
/*** 文件信息扫描的回调接口*/
public interface FileScannerCallBack {/*** 文件扫描的回调接口,扫描文件时由具体的子类决定将当前目录下的文件信息持久化到哪个终端* 可以是数据库,可以通过网络传输* @param dir 当前目录*/void callback(File dir);
}

scan()函数就是在指定的文件夹中扫描所有的子文件和子文件夹-功能1
callback()函数就是将扫描到的文件夹信息保存到终端中(此时我们是数据库)——>将当前路径下的所有文件信息保存到数据库中-功能2

scan()函数不断的打开子文件夹,callback()函数就是不断在打开的文件夹中将当前目录中的所有内容保存到数据库,二者共同搭配使得:将指定目录下的所有文件和文件夹,扫描出来之后保存到数据库中。

之所以将保存文件信息的操作拆分为独立的接口模块,核心在于解耦。
假设此时保存的是数据库,在FileScanner类中传入保存到数据库的回调子类;假设此时保存到的是通过网络传输,在FileScanner类中传入传输到网络的回调子类。

文件信息保存到数据库的回调子类 FileSaveDB(核心)
1. 选择一个目录后,先将当前目录中的所有文件信息保存到缓存中(内存)–保证数据一定是操作系统中最新的文件信息

视图1——最新从os扫描的文件信息保存到内存中

2. 从数据库中查询当前目录下的所有文件信息

视图2——从数据库扫描到的路径为test的所有文件信息

3. 对比这两个文件列表
视图1存在但是视图2不存在的——>将最新的内容插入
视图2存在但是视图1不存在的——>过期数据,需要从数据库中删除
 

package callback.impl;import app.FileMeta;
import callback.FileScannerCallBack;
import util.DBUtil;
import util.PinyinUtil;import java.io.File;
import java.sql.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;/*** 文件信息保存到数据库的回调子类**/
public class FileSave2DB implements FileScannerCallBack {@Overridepublic void callback(File dir) {// 列举出当前dir路径下的所有文件对象File[] files = dir.listFiles();// 边界条件if (files != null && files.length != 0) {// 1.先将当前dir下的所有文件信息保存到内存中,缓存中的信息一定是从os中读取到的最新数据 - 视图1List<FileMeta> locals = new ArrayList<>();for (File file : files) {FileMeta meta = new FileMeta();if (file.isDirectory()) {// 是个文件夹,需要递归扫描setCommonFiled(file.getName(), file.getParent(), true, file.lastModified(), meta);
//                    setCommonFiled(file.getName(), file.getPath(), true, file.lastModified(), meta);} else {// 是个文件setCommonFiled(file.getName(), file.getParent(), false, file.lastModified(), meta);
//                    setCommonFiled(file.getName(), file.getPath(), false, file.lastModified(), meta);// 设置文件大小meta.setSize(file.length());}locals.add(meta);}// 2.从数据库中查询出当前路径下的所有文件信息 - 视图2List<FileMeta> dbFiles = query(dir);// 3.对比视图1和视图2// 数据库有的,本地没有,做删除 - b// 遍历dbFiles,本地不存在,做删除for (FileMeta meta : dbFiles) {if (!locals.contains(meta)) {delete(meta);}}// 本地有,数据库没有的,做插入 - a// 遍历locals,若数据库不存在该FileMeta,就做插入for (FileMeta meta : locals) {if (!dbFiles.contains(meta)) {save(meta);}}}// 若files = null || files.length == 0 说明该文件夹下就没有文件或者dir压根就不是文件夹,什么也不干}/*** 删除数据库中指定记录*/private void delete(FileMeta meta) {Connection connection = null;PreparedStatement ps = null;try {connection = DBUtil.getConnection();// 此时删除的是文件本身String sql = "delete from file_meta where" +" (name = ? and path = ?)";if (meta.getIsDirectory()) {// 还需要删除文件夹内部的子文件和子文件夹sql += " or path = ?"; // 删除的第一级目录sql += " or path like ?"; // 删除的是多级子目录}ps = connection.prepareStatement(sql);ps.setString(1, meta.getName());ps.setString(2, meta.getPath());if (meta.getIsDirectory()) {ps.setString(3, meta.getPath() + File.separator + meta.getName());ps.setString(4, meta.getPath() + File.separator + meta.getName()+ File.separator + "%");}
//            System.out.println("执行删除操作,SQL为 : " + ps);int rows = ps.executeUpdate();
//            if (meta.getIsDirectory()) {
//                System.out.println("删除文件夹 " + meta.getName() + "成功,共删除" + rows + "个文件");
//            } else {
//                System.out.println("删除文件 " + meta.getName() + "成功");
//            }} catch (SQLException e) {System.err.println("文件删除出错,请检查SQL语句");e.printStackTrace();} finally {DBUtil.close(ps);}}/*** 将指定文件对象信息保存到数据库中* @param meta*/private void save(FileMeta meta) {Connection connection = null;PreparedStatement ps = null;try {connection = DBUtil.getConnection();String sql = "insert into file_meta values(?,?,?,?,?,?,?)";ps = connection.prepareStatement(sql);String fileName = meta.getName();ps.setString(1, fileName);ps.setString(2, meta.getPath());ps.setBoolean(3, meta.getIsDirectory());if (!meta.getIsDirectory()) {// 只有是文件的时候才设置size值ps.setLong(4, meta.getSize());}ps.setTimestamp(5, new Timestamp(meta.getLastModified().getTime()));// 到底是否需要存入拼音,要看文件名是否包含中文// 需要判断文件名是否包含中文的if (PinyinUtil.containsChinese(fileName)) {String[] pinyins = PinyinUtil.getPinyinByFileName(fileName);ps.setString(6, pinyins[0]);ps.setString(7, pinyins[1]);}
//            System.out.println("执行文件的保存工作,SQL为:" + ps);int rows = ps.executeUpdate();
//            System.out.println("成功保存" + rows + "行文件信息");} catch (SQLException e) {System.err.println("保存文件信息出错,请检查SQL语句");e.printStackTrace();} finally {DBUtil.close(ps);}}/*** 查询数据库中指定路径下的文件信息*/private List<FileMeta> query(File dir) {Connection connection = null;PreparedStatement ps = null;ResultSet rs = null;List<FileMeta> dbFiles = new ArrayList<>();try {connection = DBUtil.getConnection();String sql = "select name,path,is_directory,size,last_modified from file_meta" +// 切记sql拼接时,换行需要加空格" where path = ?";ps = connection.prepareStatement(sql);ps.setString(1, dir.getPath());rs = ps.executeQuery();
//            System.out.println("查询指定路径的SQL为 : " + ps);while (rs.next()) {FileMeta meta = new FileMeta();meta.setName(rs.getString("name"));meta.setPath(rs.getString("path"));meta.setIsDirectory(rs.getBoolean("is_directory"));meta.setLastModified(new Date(rs.getTimestamp("last_modified").getTime()));// 只有是文件时才设置size大小,若是文件夹,不设置size大小// 此处有个bug,数据库中文件夹的size大小为null,但是调用rs.getLong方法若返回值为null,返回0if (!meta.getIsDirectory()) {// 文件meta.setSize(rs.getLong("size"));}dbFiles.add(meta);}} catch (SQLException e) {System.err.println("查询数据库指定路线下的文件出错,请检查SQL语句");e.printStackTrace();} finally {DBUtil.close(ps, rs);}return dbFiles;}private void setCommonFiled(String name, String path, boolean isDirectory, Long lastModified, FileMeta meta) {meta.setName(name);meta.setPath(path);meta.setIsDirectory(isDirectory);// file对象的lastModified是一个长整型,以时间戳为单位的meta.setLastModified(new Date(lastModified));}
}

主方法 Main 

项目启动

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;public class Main extends Application {//在JavaFX中,图形化界面也是个线程//此处的start方法就是加载app.fxml这个界面样式,启动界面的线程//等同于普通项目中的main线程@Overridepublic void start(Stage primaryStage) throws Exception {Parent root = FXMLLoader.load(getClass().getClassLoader().getResource("app.fxml"));primaryStage.setTitle("search_everything");primaryStage.setScene(new Scene(root, 1000, 800));primaryStage.show();}public static void main(String[] args) {launch(args);}
}

成品预览


http://chatgpt.dhexx.cn/article/8Zk0JM4V.shtml

相关文章

everything搜索结果 如何不显示已经删除的文件 排除回收站 或者其他目录

按图操作&#xff1a; 添加过滤器 ?:$recycle.bin &#xff0c; 其中&#xff1f;代表任意字符&#xff0c;即包括各个盘下的回收站。之后搜索结果中不再显示已经删除的文件 排除其他文件夹或者过滤器类似&#xff0c;举例&#xff1a; 点击添加文件夹&#xff0c;添加C:\…

everything-链接与安装操作

网址链接&#xff1a;https://www.voidtools.com/zh-cn/下载步骤&#xff1a; 安装步骤&#xff1a; 下载完成后&#xff0c;直接按照即可

Everything+cpolar搭建在线资料库,实现随时随地访问

蒋先生是一家公司的技术人员&#xff0c;偶尔会跟销售出差见客户&#xff0c;而见客户常用的资料都保存在U盘中&#xff0c;但在一次见客户时资料损坏&#xff0c;幸好安装了CpolarEverything&#xff0c;及时从办公室电脑上下载了资料&#xff0c;顺利完成对客户的技术讲解。现…

搜索工具推荐 Windows中的everyting 和 mac下的alfred

1、Windows中的everyting&#xff0c;百度搜索安装包即可 图标&#xff1a; 2、和 mac下的alfred&#xff0c; app store搜索即可 图标&#xff1a; 注意&#xff0c;如果安装后搜索不到电脑内容&#xff0c;只能百度搜索&#xff0c;可勾选下方截图选项

Everything的使用-初级篇

Everything的使用-初级篇 这篇介绍如何使用Everyting这个神兵利器安装初步使用日常需要的搜索技巧需要满足多个条件的&#xff0c;用空格把条件隔开多个条件至少满足一个的&#xff0c;用 | 把条件隔开复杂条件组合,用<>尖括号来组合条件全句匹配的搜索一个名字在全句匹配…

Java 中删除List中指定的元素

最近在写软件构造实验&#xff0c;需要删除List中一个满足条件的值&#xff0c;自然而然想到通过调用List中的remove()函数进行删除&#xff1a;代码如下&#xff1a; private final List<Edge<L>> edges new ArrayList<>();Iterator<Edge<L>>…

Python list 删除指定多个下标位置的元素——方法汇总

缘起 最近有段时间不敲代码了&#xff0c;昨天一个好兄弟突然跑过来说问我一道面试题&#xff0c;欣然答应之后发现自己一下被问懵了&#xff0c;由此做一下简单记录。关于该问题的博客数目很多&#xff0c;这里只是给一个总结&#xff0c;也算是记录一下自己的心得。 题目 …

python中删除list中某指定元素

python要删除一个列表中的某个元素&#xff0c;知道这个元素是什么但不知道它的索引&#xff0c;就可以用list.romove 知道索引的话就可以用del来删除

高清图片网站

目录&#xff1a; 1、高清壁纸网站&#xff08;免费&#xff09; 2、动漫图片网站&#xff08;部分需翻墙&#xff09; 3、高清素材网站&#xff08;免费&#xff09; 分割线 一、高清壁纸网站 免费的高清壁纸网站介绍。 2.alphacoders 地址&#xff1a;https://wall.a…

5个高清图片素材网站,免费商用,赶紧收藏~

本期给大家推荐6个超好用的高清图片素材网站&#xff0c;免费下载&#xff0c;还可以商用&#xff0c;建议收藏起来&#xff01; 1、菜鸟图库 风景图片,高清风景图片大全 - 菜鸟图库 菜鸟图库是我推荐过很多次的一个设计素材网站&#xff0c;除了设计类&#xff0c;还有很多自…

8个好用的图片素材网站,免费资源多,够用一辈子

分享8个好用的免费图片素材网站&#xff0c;不仅图片资源丰富&#xff0c;还能免费商用&#xff0c;强烈建议收藏&#xff01; 1、Pixabay 一个令人惊叹的免费素材网站&#xff0c;它里面的素材类型有图片、插画、矢量图、视频、音乐、声音特效和GIF&#xff0c;这些素材都可以…

免费好用的图片压缩网站,有这五个就够了(收藏备用)

前言 当我们想要线上报名提交照片时&#xff0c;大小却超过范围&#xff0c;这时就需要压缩软件的帮助。但大多主流的软件可以压缩但想要下载就需要收费了&#xff0c;下面是五个国内外免费的压缩平台。 一、OnlineImage 优点&#xff1a;图片可以压缩90%以上&#xff0c;可以…

找高清图片素材,这8个网站就够了

相信很多设计师、自媒体都为找素材而烦恼&#xff0c;很多朋友不知道去哪里找图片素材&#xff0c;找到了版权还不明确&#xff0c;怕造成侵权&#xff0c;今天我就把我独家珍藏的8个图片素材网站分享给大家&#xff0c;免费下载&#xff0c;还可以商用&#xff0c;建议收藏起来…

这6个超好用的免费图片素材网站,赶紧收藏~

6个高质量图片素材网站&#xff0c;免费可商用&#xff0c;记得收藏&#xff01; 1、菜鸟图库 https://www.sucai999.com/pic.html?vNTYxMjky 菜鸟图库是我推荐过很多次的一个设计素材网站&#xff0c;除了设计类&#xff0c;还有很多自媒体可以用到的素材&#xff0c;比如高…

几个免费的高清图片网站推荐

搞设计找配图&#xff0c;真的很揪心&#xff0c;虽然百度可以搜索到很多&#xff0c;但是可能不是免费的。今天为大家介绍几个实用的图片网站&#xff0c;所有的图片都是免费高清&#xff0c;几个高清图片素材网站推荐&#xff0c;都是免费图片素材下载的网站&#xff0c;非常…

高清图片免费下载网站

高清图片免费下载网站 &#xff08;如果下面的博客没有能解决你的问题或者你还有其他关于计算机方面的问题需要咨询可以加博主QQ&#xff1a;1732501467&#xff09; 当你看到一张心仪的照片。想把它当做自己的电脑&#xff08;手机、平板&#xff09;壁纸&#xff0c;或者单纯…

优质且免费的10个在线图片设计网站!

1.即时设计 即时设计资源社区是一个开源式免费商用图片素材网站&#xff0c;将社交、作品浏览和模板复用融合在一起。它内置了来自国内外优秀设计系统如TDesign、Arco Design、Ant Design和Material Design等的海量设计规范&#xff0c;以及超过3000个UI组件库和每月更新的上百…

10 个免费的高清图库网站,强烈推荐

转自&#xff1a;https://zhuanlan.zhihu.com/p/23411438 写东西找配图&#xff0c;真的很揪心&#xff0c;虽然从搜索引擎可以搜索很多&#xff0c;但是可能不是免费使用的&#xff0c;今天为大家介绍的这是个网站&#xff0c;所有的图片都是免费高清&#xff0c;你想怎么用就…

8个免费图片素材网,赶紧收藏起来

现在图片素材变得越来越重要了&#xff0c;除了平常设计经常要用到图片之外&#xff0c;大到平常文章、自媒体、视频制作配图&#xff0c;小到我们发朋友圈、日志、说说&#xff0c;都会用到图片来衬托。 但图片版权一直是设计师、自媒体和各大企业的一大难题。 要不就花钱买…

MySQL 开发小型商城管理系统

一、MySQL workbench 的使用 1、打开MySQL workbench的主界面可以看见 2、连接已经存在的数据库&#xff08;例如上方的local instance mysql 80&#xff09; 连接时的密码是安装时设置的&#xff0c;仅本机使用可以不设计密码&#xff0c;在弹出的界面中填好sql数据库的连接…