项目源码
项目目标
- 巩固多线程、JDBC、集合框架等技术
- 满足文件搜索的需求
项目使用技术栈与平台
- 所用技术:JavaFX、多线程、SQLite、pinyin4j
- 平台与环境:Windows/Mac,IDEA,Maven
项目背景
各个操作系统下,都有提供文件搜索的功能,如windows中查找文件:
除系统自带的文件搜索功能外,也有大佬开发的很多搜索神器,比如Everything
- Everything是基于NTFS文件系统的USN Journal(Update SequenceNumber Journal),是利用操作系统记录的文件操作日志来进行搜索,特点是效率高,速度快,但是具有一定的局限性,只能用于NTFS文件系统。
- 本项目使用实时的本地文件进行搜索,保存文件信息以便于提高搜索效率。
项目功能
- 指定搜索目录,显示目录中的所有文件、文件夹信息
- 使用多线程进行文件搜索操作,文件信息保存在数据库。如果已保存有的文件信息,执行本地目录与数据库文件信息比对操作,在更新到数据库。
- 可以根据文件名进行搜索
- 文件名包含中文时,支持汉语拼音的搜索(全拼或是首字母匹配)
项目演示
选择文件目录
扫描本地文件,保存文件信息并显示
根据文件名搜索
根据全拼搜索
根据拼音首字母搜索
系统流程
整体流程
文件对比流程
技术栈介绍
SQLite介绍
- SQLite是一款轻量级的嵌入式内存数据库(嵌入在进程中,运行在内存中的数据库),使用 ANSIC 编写的,并提供了简单和易于使用的 API。
- 一个完整的 SQLite 数据库是存储在一个单一的跨平台的磁盘文件。
- 非常小,是轻量级的,完全配置时小于 400KiB,省略可选功能配置时小于250KiB。
- 无需安装、配置及管理。
- 完全兼容 ACID 事务,允许从多个进程或线程安全访问。
- 支持多种开发语言,C, C++, PHP, Perl, Java, C#,Python, Ruby等
- 支持 SQL92(SQL2)标准的大多数查询语言的功能。
Pinyin4j介绍
- 是一个Java的内库,提供对中文汉字到拼音的转换
- 存在多音字的情况,根据一个字符可以获取多个字符串
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray('长');
输出:
[zhang3, chang2]
- 可以配置输出格式,包括大小写、是否带音调(默认带)、是否使用v(如绿的拼音lv)
JavaFX介绍
- Java客户端UI库:JavaFX是一个强大的图形和多媒体处理工具包集合,它允许开发者来设计、创建、测试、调试和部署富客户端程序,并且和Java一样跨平台。
- 提供了丰富的客户端组件:面板、按钮、文本框、表格等,还提供了各类事件、动画效果支持。
- 提供了Web组件支持:HTML5的支持、CSS样式支持,JavaScript脚本支持。
- 还提供了Scene Builder 程序,来支持拖拽式界面开发。
- Java原有Swing、AWT客户端UI工具包,JavaFX 系统界面样式跟美观、系统架构对开发更友好、便捷
系统设计
sql表单 : 数据库设计
- 需要保存的文件基本信息有:文件名称、路径、大小、上次修改时间
- 文件名包含中文时,要满足全拼和拼音首字母的搜索,所以还需要保存全拼和拼音首字母
- 中文汉字可能存在多音字的情况,我们这里只实现简单的,取第一个拼音即可
工具类 : JDBC工具类设计
- 使用SQLite,需要先指定本地数据库文件路径,如果没有则创建该文件
- 提供获取数据库连接的方法
- 提供释放数据库资源的方法
数据库初始化任务 - 读取本地写好的sql文件,再执行数据库初始化建表操作
数据库初始化任务
- 读取本地写好的sql文件,再执行数据库初始化建表操作
工具类 : 拼音工具类
- 需要提供获取文件名的全拼及拼音首字母的方法
JavaFX界面设计
- 界面的搭建
- 绑定选择文件目录事件,在每次事件发生时,调用该事件方法:启动目录扫描任务,并在完成后刷新表格
- 绑定搜索框内容改变事件,在每次事件发生时,调用该事件方法:搜索文件信息,并刷新表格
任务型 :多线程目录扫描任务设计
- 入口是在选择文件目录事件发生后,根据选定的目录进行扫描
- 扫描到目录(文件夹)时,执行目录下一级子文件的信息对比、更新操作
- 如果目录下一级子文件是文件夹,则开启新的子任务处理这个子文件夹
- 在所有任务都完成以后,刷新表格
业务型:本地文件、数据库文件对比及更新
- 本地有,数据库没有,插入该文件信息
- 数据库有,本地没有,删除数据库中该文件信息。如果该文件是文件夹,还需要删除所有在该文件夹下的文件
功能:文件搜索
- 根据搜索值,模糊匹配文件名,全拼或拼音首字母字段,任意匹配成功都可以获取文件信息
开发步骤
创建Maven项目
<?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>tong</groupId>
<artifactId>ErLangShen</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.28.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<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>3.1.0</version>
<configuration>
<archive>
<manifest>
<mainClass>Main</mainClass> <!-- 指定入口类 -->
<addClasspath>true</addClasspath> <!-- 在jar的MF文件中
生成classpath属性 -->
<classpathPrefix>lib/</classpathPrefix> <!--
classpath前缀,即依赖jar包的路径 -->
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</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>
设计数据库表
drop table if exists file_meta;
CREATE TABLE IF NOT EXISTS file_meta (
name VARCHAR(50) NOT NULL,
path VARCHAR(1000) NOT NULL,
is_directory boolean not null ,
size BIGINT NOT NULL,
last_modified TIMESTAMP NOT NULL,
pinyin VARCHAR(50),
pinyin_first VARCHAR(50)
);
设计数据库工具类,及表初始化操作
- URL格式为:jdbc:sqlite://数据库本地文件路径,没有用户名密码。使用URL创建DataSource对象后,会以该路径文件作为数据库,没有则创建该文件。
private static String getUrl(){
//classes路径
URL classesURL = DBUtil.class.getClassLoader().getResource("./"); //classes路径
//target路径
String dir = new File(classesURL.getPath()).getParent();
//ErLangShen.db路径 jdbc:sqlite://D:\java%20code\%e9%a1%b9%e7%9b%ae\ErLangShen\target\ErLangShen.db
String url = "jdbc:sqlite://" + dir + File.separator + "ErLangShen.db";
return url;
}
- 设计单例模式获取DataSource连接池对象。及通过DataSource获取数据库连接的方法。
private static volatile DataSource DATA_SOURCE;
/**
*提供获取数据库连接词的功能
* 使用单例模式(多线程安全)
* 多线程安全版本的单例模式:
* 1.为什么在最外层判断是否为null?
* 2.synchronized加锁以后,为什么还要判断是否为null
* 3.为什么Datasource类变量要使用volatile关键字修饰
* 多线程操作:原子性,可见性(从主内存拷贝到工作内存),有序性
* synchronized保证前三个特性,volatile保证可见性,有序性
* @return
*/
private static DataSource getDataSource(){
if (DATA_SOURCE == null){ //提高效率
//保证只有一个不为空的DATA_SOURCE 单例
synchronized (DBUtil.class){
if (DATA_SOURCE == null){
SQLiteConfig config = new SQLiteConfig();
config.setDateStringFormat(Util.DATA_PATTERN);
DATA_SOURCE = new SQLiteDataSource();
((SQLiteDataSource)DATA_SOURCE).setUrl(getUrl());
}
}
}
return DATA_SOURCE;
}
- 设计释放资源的方法:关闭连接Conncetion、关闭数据库操作对象Statement、关闭结果集对象ResultSet。
public static void close(Connection connection, Statement statement) {
close(connection,statement,null);
}
public static void close(Connection connection, Statement statement, ResultSet resultSet) {
try {
if (connection != null) {
connection.close();
}
if (statement != null) {
statement.close();
}
if (resultSet != null){
resultSet.close();
}
}catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("释放资源失败",e);
}
}
- 建立连接
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
初始化数据库
/**
* 读取sql文件
*
*/
public static String[] readSql() {
try {
//通过ClassLoader获取流
InputStream in = DBInit.class.getClassLoader().getResourceAsStream("init.sql");
//字节流转换为字符流
BufferedReader br = new BufferedReader(new InputStreamReader(in,"UTF-8"));
StringBuilder sb = new StringBuilder();
String line ;
while((line = br.readLine()) != null){
if (line.contains("--")){
line = line.substring(0,line.indexOf("--"));
}
sb.append(line);
}
String[] sqls = sb.toString().split(";");
return sqls;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("读取错误");
}
}
public static void init() {
//数据库jdbc操作
//1.建立数据库连接connection
Connection connection = null;
Statement statement = null;
try {
//1.建立数据库连接connection
connection = DBUtil.getConnection();
//2.创建sql语句执行对象Statement
String[] sqls = readSql();
//2.创建sql语句执行对象Statement
statement = connection.createStatement();
for (String sql:sqls
) {
//3.执行sql
statement.executeUpdate(sql);
}
//4.如果是查询操作,获取结果集ResultSet,处理结果集
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("初始话数据库表操作失败",e);
}finally {
//5.释放资源
DBUtil.close(connection,statement);
}
}
public static void main(String[] args) {
System.out.println(Arrays.toString(readSql()));
}
设计汉语拼音工具
- 使用第三方库提供的汉字到拼音的转换,一个汉字转换出来可能有多个拼音。配置汉字的字符范围,及拼音的输出格式(小写、不带音调、包含V字符)
/**
* 中文字符格式
*/
private static final String CHINESE_PATTERN = "[\\u4E00-\\u9FA5]";
/**
* 汉语拼音格式化类
*/
private static final HanyuPinyinOutputFormat FORMAT = new HanyuPinyinOutputFormat();
static {
FORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE); //小写拼音
FORMAT.setToneType(HanyuPinyinToneType.WITHOUT_TONE); //不带声调
FORMAT.setVCharType(HanyuPinyinVCharType.WITH_V); //带v
}
/**
* 字符串中是否包含中文
* @param name
* @return
*/
public static boolean containsChinese(String name) {
//正则表达式
return name.matches(".*" + CHINESE_PATTERN + ".*");
}
- API有提供单个字符的拼音转换:
String[] temp = PinyinHelper.toHanyuPinyinStringArray(c,
PINYIN_OUTPUT_FORMAT);
- 多个字符组成的文件名,需要按照每个字符返回的字符串数组进行排列组合,我们只简单实现,每个字符取第一个返回的字符串作为拼音。拼音分全拼,和汉字首字母拼接的两种方式
/**
* 通过文件名获取全拼+拼音首字母
* 中华人民共和国--->zhongghuarenmingongheguo/zhrmghg(不考虑多音字)
* @param name 文件名
* @return 全拼+拼音首字母 数组
*/
public static String[] get(String name){
String[] result = new String[2];
StringBuilder all = new StringBuilder();//全拼
StringBuilder first = new StringBuilder();//首字母
for (char c:name.toCharArray()
) {
try{
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c,FORMAT);//多音字
if(pinyins == null || pinyins.length == 0){
all.append(c);
first.append(c);
}else {
all.append(pinyins[0]);
first.append(pinyins[0].charAt(0));
}
}catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
all.append(c);
first.append(c);
}
}
result[0] = all.toString();
result[1] = first.toString();
return result;
}
- 如果考虑多音字的话
/**
* 多音字版获取名称
* @param name 文件名
* @param fullSpell true为全拼,false为首字母
* @return 文件名多音字数组
*/
public static String[][] get(String name,boolean fullSpell){
char[] chars = name.toCharArray();
String[][] result = new String[chars.length][];
for (int i = 0; i < chars.length; i++) {
try{
//去除音调后,会有重复,"只":[zhi,zhi...]---------quchong
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(chars[i],FORMAT);//多音字
if(pinyins == null || pinyins.length == 0){
result[i] = new String[]{String.valueOf(chars[i])};
}else{
result[i] = unique(pinyins,fullSpell);
}
}catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
result[i] = new String[]{String.valueOf(chars[i])};
}
}
return result;
}
/**
* 数组去重操作
* @param array
* @param fullSpell
* @return
*/
public static String[] unique(String[] array,boolean fullSpell){
Set<String> set = new HashSet<>();
for (String s:array
) {
if(fullSpell){
set.add(s);
}else {
set.add(String.valueOf(s.charAt(0)));
}
}
return set.toArray(new String[set.size()]);
}
/**
* 每个中文字符返回的是字符串数组,每两个字符串数组合并为一个字符串数组,之后以此类推
* @param pinyinArray 文件名多音字数组
* @return 返回文件名的排列组合
*/
public static String[] compose(String[][] pinyinArray){
if (pinyinArray == null || pinyinArray.length == 0){
return null;
}else if (pinyinArray.length == 1){
return pinyinArray[0];
}else {
String[] result = pinyinArray[0];
for (int i = 1; i < pinyinArray.length; i++) {
result = compose(result,pinyinArray[i]);
}
return result;
}
}
/**
* 拼音数组两两组合
* @param pinyin1
* @param pinyin2
* @return 返回组合
*/
public static String[] compose(String[] pinyin1,String[] pinyin2){
String[] result = new String[pinyin1.length*pinyin2.length];
int k = 0;
for (int i = 0; i < pinyin1.length; i++) {
for (int j = 0; j < pinyin2.length; j++) {
result[k] = pinyin1[i] + pinyin2[j];
k++;
}
}
return result;
}
设计界面及文件信息搜索
- Main.java
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getClassLoader().getResource("app.fxml"));
primaryStage.setTitle("ErLangShen");
primaryStage.setScene(new Scene(root, 1000, 800));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
- app.fxml
<?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="700" prefWidth="900" 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="sizeColumn" prefWidth="120" text="大小">
<cellValueFactory>
<PropertyValueFactory property="sizeText" />
</cellValueFactory>
</TableColumn>
<TableColumn fx:id="lastModifiedColumn" prefWidth="160" text="修改时间">
<cellValueFactory>
<PropertyValueFactory property="lastModifiedText" />
</cellValueFactory>
</TableColumn>
</columns>
</TableView>
</children>
</GridPane>
- Controller
public class Controller implements Initializable {
@FXML
private GridPane rootPane;
@FXML
private TextField searchField;
@FXML
private TableView<FileMeta> fileTable;
@FXML
private Label srcDirectory;//标签
private Thread task;
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();
}
});
}
/**
* 点击选择目录
* @param event
*/
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();
srcDirectory.setText(path);
//选择目录就需要执行目录的扫描任务:该目录下的目录及子目录下的文件夹
if(task != null){
task.interrupt();//中断
}
task = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行文件扫描任务");
ScanCallback callback = new FileSave();
FileScanner scan = new FileScanner(callback);
scan.scan(path);//多线程扫描.提高效率
//等待扫描完毕;
try {
scan.waitFinish();
System.out.println("所有任务执行完毕,刷新表格");
//刷新表格:将扫描的子文件和子文件夹都展示在表格
freshTable();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
task.start();
}
// 刷新表格数据
private void freshTable(){
ObservableList<FileMeta> metas = fileTable.getItems();
metas.clear();
String dir = srcDirectory.getText();
if (dir != null && dir.trim().length() != 0){
String content = searchField.getText();
//提供数据库的查询方法
List<FileMeta> fileMetas = FileSearch.search(dir,content);
metas.addAll(fileMetas);
}
}
}
设计多线程扫描任务及文件信息保存
- 多线程执行大量任务,使用线程池来提高执行效率
private ExecutorService pool = Executors.newFixedThreadPool(4);
- 为便于在线程中执行的任务有较好的扩展性,可以考虑使用接口回调的方式实现。传入时设定为文件信息保存的任务。
public FileScanner(ScanCallback callback) {
this.callback = callback;
}
- 在线程执行时,待执行任务数+1, 执行完后,待执行任务数-1,开启子任务时,每个子任务都执行任务数+1操作,这样在最后可以判断出是否所有线程执行完毕。
private volatile AtomicInteger count = new AtomicInteger();
/**
* 扫描文件目录
* 开始不知道多少子文件夹
* @param path
*/
public void scan(String path) {
count.incrementAndGet();//根目录扫描任务,计数器++i
doScan(new File(path));
}
private void doScan(File dir){
callback.callback(dir);
pool.execute(new Runnable() {
@Override
public void run() {
try {
File[] children = dir.listFiles();
if (children != null) {
for (File file : children
) {
if (file.isDirectory()) {//如果是文件夹,递归
// System.out.println("文件夹" + file.getPath());
count.incrementAndGet();//++i
doScan(file);
}
}
}
}finally {
int c = count.decrementAndGet();
if (c == 0){
synchronized (lock){
lock.notify();
System.out.println("********************************************");
}
}
}
}
});
}
本地文件、数据库文件对比及更新
- 本地有,数据库没有,插入该文件信息
/**
* 文件保存在数据库
* @param file
*/
private void save(FileMeta file){
Connection connection = null;
PreparedStatement statement = null;
try {
connection = DBUtil.getConnection();
String sql = "insert into file_meta" +
"(name, path, is_directory, size, last_modified, pinyin, pinyin_first)" +
" values (?,?,?,?,?,?,?)";
statement = connection.prepareStatement(sql);
statement.setString(1,file.getName());
statement.setString(2,file.getPath());
statement.setBoolean(3,file.getDir());
statement.setLong(4,file.getSize());
statement.setTimestamp(5,new Timestamp(file.getLastModified().getTime()));
statement.setString(6,file.getPinyin());
statement.setString(7,file.getPinyinFirst());
System.out.printf("insert name=%s, path=%s\n", file.getName(), file.getPath());
statement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("文件保存失败" + e);
}finally {
DBUtil.close(connection,statement);
}
}
- 数据库有,本地没有,删除数据库中该文件信息。如果该文件是文件夹,还需要删除所有在该文件夹下的文件
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 = ? and is_directory = ?)" ;
if (meta.getDir()){
sql += " or path=?" +
" or path like ?";
}
ps = connection.prepareStatement(sql);
ps.setString(1,meta.getName());
ps.setString(2,meta.getPath());
ps.setBoolean(3,meta.getDir());
if (meta.getDir()){
ps.setString(4,meta.getPath() + File.separator + meta.getName());
ps.setString(5,meta.getPath() + File.separator + meta.getName() + File.separator +"%");
}
System.out.printf("删除文件信息,dir=%s\n",
meta.getPath()+File.separator+meta.getName());
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("删除文件信息出错,检查delete语句", e);
}finally {
DBUtil.close(connection,ps);
}
}
项目总结
- 项目优点:使用多线程来进行文件遍历,提高了效率。
- 项目缺点:中文汉字多音字没有进行排列组合,只能支持多音字拼音的一种组合搜索。
- 项目扩展:我们还能将该项目写的更完善,可以往以下几个发展方向走:
- 多音字支持
- 将项目打包成exe安装文件
项目参考资料
- SQLite Home Page
- Pinyin4j、http://pinyin4j.sourceforge.net/
- JavaFX官网、FX China、JavaFX 教程