目录
文件信息保存到数据库的回调子类 FileSaveDB(核心)
项目概要
项目名称:search-everything搜索工具
项目介绍:该项目是仿照Everything软件部分功能实现的本地文件搜索工具,支持Win、linux、MacOS等跨平台使用,支持全拼查询、模糊查询。
项目功能:
1.选择文件夹多线程扫描该文件夹下的子文件,展示文件的名称,大小,修改时间。2.选择路径后,支持搜索相关文件内容(全拼或文件首字母或文件部分名称–支持模糊查询)
3.文件夹扫描完毕之后,显示搜索的所有文件以及文件夹的个数,以及总耗时
相关技术栈:Java8、JavaFX、多线程、IO流、SQLite数据库、JBDC编程
项目Gitte链接:https://gitee.com/xht117/search_everything
项目大纲
项目准备
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(); // 设置转换后的英文字母为全小写 王 -> wang FORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE); // 设置转换后的英文字母是否带音调 FORMAT.setToneType(HanyuPinyinToneType.WITHOUT_TONE); // 特殊拼音用v替代 绿 -> lv FORMAT.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 -> h firstCaseAppender.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接口,没有用PrepareStatement statement = 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; /** * 和数据库打交道的类,最终程序中获取数据库的记录就通过本类来描述 * 这个类就对应我们数据库表名,数据表中的一行记录就对应我们这个类的一个对象 * 该类的一个对象就是数据表的一行 * 数据表的所有内容就对应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 { @FXML private GridPane rootPane; @FXML private TextField searchField; @FXML private TableView<FileMeta> fileTable; @FXML private 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(){ // 前端表格metas ObservableList<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()) { // 等同于 ++i dirNum.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()函数就是将扫描到的文件夹信息保存到终端中(此时我们是数据库)——>将当前路径下的所有文件信息保存到数据库中-功能2scan()函数不断的打开子文件夹,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 { @Override public void callback(File dir) { // 列举出当前dir路径下的所有文件对象 File[] files = dir.listFiles(); // 边界条件 if (files != null && files.length != 0) { // 1.先将当前dir下的所有文件信息保存到内存中,缓存中的信息一定是从os中读取到的最新数据 - 视图1 List<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.从数据库中查询出当前路径下的所有文件信息 - 视图2 List<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,返回0 if (!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线程 @Override public 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); } }
成品预览
拼音搜索
模糊搜索
SQLite视角