
该项目是仿照everthying的文件搜索软件,有Java语言编写可以跨平台使用。
项目使用的技术:Java8、JavaFX、IO流、SQLite嵌入式数据库
项目功能:
1.选择文件夹,使用多线程扫描该文件夹下的子文件,展示文件的名称、大小、修改时间
2.选择路径后,支持搜索相关文件内容(文件名称全拼、文件名称首字母和文件部分名称)
3.文件夹扫描完毕之后,显示搜索的所有文件以及文件夹的个数以及总耗时
功能模块介绍maven介绍:
项目管理工具,方便进行第三方jar包的导入和管理,方便对当前项目的整个生命周期进行跟踪
该项目主要有这几个模块:
Util包主要提供了一些工具类:
Util类一 . parseSize(Long size)方法返回文件单位该类是一个通用工具类,主要提供了返回文件单位,返回文件类型和返回文件修改日期的功能
public static String parseSize(Long size) {
String[] unit = {"B","KB","MB","GB"};
int flag = 0;
while (size > 1024) {
size /= 1024;
flag ++;
}
return size +unit[flag];
}
二 . parseFileType(Boolean directory)方法返回文件类型该方法通过一个String数组设置单位,在把文件传入的字节大小通过除以1204的方式返回最终带单位的文件大小
public static String parseFileType(Boolean directory) {
return directory ? "文件夹" : "文件";
}
三 . parseDate(Date lastModified)方法返回文件最后修改日期该方法通过传入的Boolean值返回"文件夹"或者"文件"
public static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static String parseDate(Date lastModified) {
return new SimpleDateFormat(DATE_PATTERN).format(lastModified);
}
Util类总代码:该方法将传入的日期类型通过设置好的FATE_PATTERN格式返回文件最后修改日期
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";
public static String parseSize(Long size) {
String[] unit = {"B","KB","MB","GB"};
int flag = 0;
while (size > 1024) {
size /= 1024;
flag ++;
}
return size +unit[flag];
}
public static String parseFileType(Boolean directory) {
return directory ? "文件夹" : "文件";
}
public static String parseDate(Date lastModified) {
return new SimpleDateFormat(DATE_PATTERN).format(lastModified);
}
}
PinyinUtil类
PinyinUtil类中的常量拼音工具类,该类的作用是将汉语拼音的字母映射成字母字符串
private static final HanyuPinyinOutputFormat FORMAT;
private static final String CHINESE_PATTERN = "[\u4E00-\u9FA5]";
static{
FORMAT = new HanyuPinyinOutputFormat();
//将转换后的拼音全小写
FORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE);
//设置转换后的英文字母是否带音调
FORMAT.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
//特殊拼音用v代替 绿
FORMAT.setVCharType(HanyuPinyinVCharType.WITH_V);
}
containsChinese(String fileName)方法判断给定的字符串是否包含中文该类中的FORMAT常量定义了汉语拼音的配置,表示将汉字转为拼音字符串时的一些设置;
CHINESE_PATTERN表示了中文对应的Unicode编码区间
public static boolean containsChinese(String fileName) {
return fileName.matches(".*" +CHINESE_PATTERN +".*");
}
getPinyinByFileName(String fileName)方法将文件名转为字母字符串和首字母小写字符串该方法tongguoString类的matches()方法与给定的CHINESE_PATTERN判断传入的文件名称是否包含中文
public static String[] getPinyinByFileName(String fileName) {
String[] ret = new String[2];
StringBuilder allNameAppender = new StringBuilder();
StringBuilder firstCaseAppender = new StringBuilder();
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 {
//中文字符,取第一个多音字的返回值
allNameAppender.append(pinyins[0]);
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;
}
PinyinUtil类总代码该方法通过pinyin4j这个jar包中的PinyinHelper类的toHanyuPinyinStringArray()方法和设置好的Format配置返回文件名中汉语的拼音,若碰到非中文直接保留,若是中文,将返回的汉语拼音分别添加到allNameAppender和firstCaseAppender中,因为有多音字的缘故,我们只添加返回的第一个拼音,其中allNameAppender添加这个拼音的全拼,firstCaseAppender添加这个拼音的第一个字母,最终将这两个StringBuilder对象添加到String数组里并返回
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;
private static final String CHINESE_PATTERN = "[\u4E00-\u9FA5]";
static{
FORMAT = new HanyuPinyinOutputFormat();
//将转换后的拼音全小写
FORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE);
//设置转换后的英文字母是否带音调
FORMAT.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
//特殊拼音用v代替 绿
FORMAT.setVCharType(HanyuPinyinVCharType.WITH_V);
}
public static String[] getPinyinByFileName(String fileName) {
String[] ret = new String[2];
StringBuilder allNameAppender = new StringBuilder();
StringBuilder firstCaseAppender = new StringBuilder();
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 {
//中文字符,取第一个多音字的返回值
allNameAppender.append(pinyins[0]);
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;
}
public static void main(String[] args) throws BadHanyuPinyinOutputFormatCombination {
String str = "中华人民共和国";
System.out.println(Arrays.toString(getPinyinByFileName(str)));
System.out.println("-------------------------------");
String str1 = "中123国456好abc";
System.out.println(Arrays.toString(getPinyinByFileName(str1)));
}
public static boolean containsChinese(String fileName) {
return fileName.matches(".*" +CHINESE_PATTERN +".*");
}
}
DBUtil类
getDataSource()方法提供数据源该类是SQLite数据库的工具类,创建数据源,创建数据库连接,只想外部提供数据库连接,数据源不提供。通过单例模式让外部有且只能拿到一个数据库连接。
//单例数据源
private volatile static DataSource DATASOURCE;
private static DataSource getDataSource() {
if(DATASOURCE == null) {
synchronized (DBUtil.class) {
if(DATASOURCE == null) {
SQLiteConfig config = new SQLiteConfig();
config.setDateStringFormat(Util.DATE_PATTERN);
DATASOURCE = new SQLiteDataSource(config);
//配置数据源得URL是SQLite独有的需要向下转型
((SQLiteDataSource)DATASOURCE).setUrl(getUrl());
}
}
}
return DATASOURCE;
}
getUrl()方法配置SQLite数据库地址通过创建单例数据源和使用double-check单例模式获取数据源对象,保证多线程场景下所有线程访问到的都是同一个数据源。创建数据源对象时,由于SQLite数据库没有账号密码,只需要配置日期格式(通过创建SQLiteConfig对象设置Util类中定义好的日期格式,再将这个SQLiteConfig对象传入SQLiteDataSource得构造方法中)和URL(配置数据源得URL是SQLite独有的方法需要向下转型)即可
private static String getUrl() {
String path = "D:\search-everything\target";
String url = "jdbc:sqlite://" + path + File.separator + "search_everything.db";
System.out.println("获取数据库的连接为 : " + url);
return url;
}
getConnection()方法创建数据库连接按照固定的配置写好路径,其中的path是这个项目中target文件得路径,url格式就是"jdbc:sqlite://" +path +“/” +“项目名称.db”
// 单例数据库连接
private volatile static Connection CONNECTION;
public static Connection getConnection() throws SQLException {
if (CONNECTION == null) {
synchronized (DBUtil.class) {
if (CONNECTION == null) {
CONNECTION = getDataSource().getConnection();
}
}
}
return CONNECTION;
}
close方法关闭资源因为多线程场景下,SQLite要求线程使用用一个连接进行处理,所以这里设置了单例数据库连接对象,这里也使用了double-check单例模式和volatile修饰数据库连接对象保证了线程安全
public static void close(Statement statement) {
if(statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void close(PreparedStatement preparedStatement, ResultSet resultSet) {
close(preparedStatement);
if(resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
DBUtil类总代码想要在SQLite数据库需要获取数据库连接,当操作执行完之后需要关闭资源,若执行查询操作完成之后需要关闭PreparedStatement和ResultSet资源对象
package util;
import org.sqlite.SQLiteConfig;
import org.sqlite.SQLiteDataSource;
import javax.sql.DataSource;
import java.io.File;
import java.sql.*;
public class DBUtil {
//单例数据源
private volatile static DataSource DATASOURCE;
//单例数据库连接
private volatile static Connection CONNECTION;
private static DataSource getDataSource() {
if(DATASOURCE == null) {
synchronized (DBUtil.class) {
if(DATASOURCE == null) {
SQLiteConfig config = new SQLiteConfig();
config.setDateStringFormat(Util.DATE_PATTERN);
DATASOURCE = new SQLiteDataSource(config);
//配置数据源得URL是SQLite独有的需要向下转型
((SQLiteDataSource)DATASOURCE).setUrl(getUrl());
}
}
}
return DATASOURCE;
}
private static String getUrl() {
String path = "D:\search-everything\target";
String url = "jdbc:sqlite://" + path + File.separator + "search_everything.db";
System.out.println("获取数据库的连接为 : " + url);
return url;
}
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) {
e.printStackTrace();
}
}
}
public static void close(PreparedStatement preparedStatement, ResultSet resultSet) {
close(preparedStatement);
if(resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
DBInit类
readSQL()方法读取sql文件,加载到程序中该类的作用是在界面初始化时创建文件信息数据表
//从resources路径下读取init.sql文件,加载到程序中
public static List readSQL() {
List ret = new ArrayList<>();
InputStream in = DBInit.class.getClassLoader().
getResourceAsStream("init.sql");
Scanner scanner = new Scanner(in);
scanner.useDelimiter(";");
while(scanner.hasNext()) {
String str = scanner.next();
if("".equals(str) || "n".equals(str)) continue;
if(str.contains("--")) {
str = str.replaceAll("--","");
}
ret.add(str);
}
return ret;
}
init()方法初始化数据库该方法得执行流程:
1 . 先从init.sql文件中获取内容,拿到文件的输入流:
Java程序的编译阶段,编译之后将所有的*.java源文件都会编译到target目录下,如果直接将路径写成这个init.sql文件当前所在位置,虽然在编译器上能运行程序,但当把这个项目打成jar包之后就无法找到这个文件,这个时候发现所有的resources目录下的内容编译之后都会放到targetclasses目录下,所以我i们用到了类加载器,所谓的类加载器就是告诉JVM从哪个文件夹区执行class文件,我们这里的用法是通过DBUtil.class.getClassLoader()方法获取到了编译后的classes目录,再通过getResourceAsStream()方法拿到资源文件的输入流
2 . 使用Scanner类处理输入流:
将获取到的输入流传入Scanner类的构造方法中,因为之后的处理是要执行sql指令,我们不能一行一行的去读,所以需要自定义分隔符,即使用Scanner类的useDelimiter()方法,sql指令是以分号结束的,所以这里我们设置分隔符为";",然后通过while循环拿到对应的sql语句将其放入List集合中
public static void init() {
Connection connection = null;
Statement statement = null;
try {
connection = DBUtil.getConnection();
List sqls = readSQL();
statement = connection.createStatement();
for (String sql : sqls) {
System.out.println("执行SQL操作语句:" +sql);
statement.executeUpdate(sql);
}
} catch (SQLException e) {
System.out.println("数据初始化失败");
e.printStackTrace();
} finally {
DBUtil.close(statement);
}
}
public static void main(String[] args) {
init();
}
DBInit类总代码想要初始化数据库必须获得数据库连接和执行sql语句所以要有Connection对象和Statement对象,具体流程如下:
1.通过DBUtil的getConnection()方法获取到SQLite数据库的单例连接
2.再调用readSQL()方法将返回的sql语句放到List集合中
3.调用connection对象的createStatement()方法获取statement对象
4.通过for-each循环,使用statement对象的executeUpdate()方法依次执行sql语句
5.执行完sql语句之后关闭statement资源对象
package util;
import java.io.InputStream;
import java.sql.Connection;
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文件,加载到程序中
public static List readSQL() {
List ret = new ArrayList<>();
InputStream in = DBInit.class.getClassLoader().
getResourceAsStream("init.sql");
Scanner scanner = new Scanner(in);
scanner.useDelimiter(";");
while(scanner.hasNext()) {
String str = scanner.next();
if("".equals(str) || "n".equals(str)) continue;
if(str.contains("--")) {
str = str.replaceAll("--","");
}
ret.add(str);
}
return ret;
}
public static void init() {
Connection connection = null;
Statement statement = null;
try {
connection = DBUtil.getConnection();
List sqls = readSQL();
statement = connection.createStatement();
for (String sql : sqls) {
System.out.println("执行SQL操作语句:" +sql);
statement.executeUpdate(sql);
}
} catch (SQLException e) {
System.out.println("数据初始化失败");
e.printStackTrace();
} finally {
DBUtil.close(statement);
}
}
public static void main(String[] args) {
init();
}
}
task包
task包主要执行文件扫描和文件搜索的任务:
FileScanner类//扫描文件个数
private AtomicInteger fileNum = new AtomicInteger();
//扫描文件夹的个数
private AtomicInteger dirNum = new AtomicInteger(1);
//所有扫描文件的子线程个数,只有当子线程个数为0时,主线程再继续执行
private AtomicInteger threadCount = new AtomicInteger();
//最后一个线程执行完,调用countDown方法唤醒线程
private CountDownLatch latch = new CountDownLatch(1);
//获取当前电脑可用的CPU个数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//使用线程池创建对象
private ThreadPoolExecutor pool = new ThreadPoolExecutor(CPU_COUNT,CPU_COUNT * 2,10, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),new ThreadPoolExecutor.AbortPolicy());
//文件扫描回调对象
private FileScannerCallBaack callBack;
scan(File filePath)方法传入文件夹进行扫描任务以上是FileScanner类的成员属性:
1 . 使用了原子类AtomicInteger来记录扫描的文件个数和扫描文件的子线程个数保证了计数时的原子性
2 . Latch对象的构造方法传入1,当最后一个子线程结束之后在调用countDown方法唤醒主线程
3 . 创建线程池的时候需要设置正式员工数和最大员工数,我们使用Runtime类的getRuntime()方法获取Runtime对象,再调用availableProcessors()方法获取当前电脑的可用CPU个数,线程池的各个参数为:
(1) 正式工数设置成CPU_COUNT
(2) 最大员工数设置成CPU_COUNT * 2
(3) 临时线程的等待时间设置为10s
(4) 阻塞队列为LinkedBlockingQueue
(5) 拒绝策略为AbortPolicy()–因为正式工设置的是当前可用CPU的数量,所以超出负荷的任务要直接拒绝并抛出异常
这里不用其他拒绝策略的原因:
1)若使用CallerRunsPolicy(),将超出限制的任务交给调用者处理,但是主线程又调用了latch对象的await()方法需要等待latch对象的countDown()方法次啊能被唤醒,所以无法执行此任务
2)若使用DiscardOldestPolicy(),将队列中最老的任务抛弃,那么最终得到的结果就不是该目录下所有的文件了
3)使用DiscardPolicy()和2)的原因一样
4 . callBack文件回调对象,方便后面使用回调函数
public void scan(File filePath) {
System.out.println("开始文件扫描任务,根目录为:" +filePath);
long start = System.nanoTime();
scanInternal(filePath);
threadCount.incrementAndGet();
try {
latch.await();
} catch (InterruptedException e) {
System.err.println("扫描任务中断,根目录为:" +filePath);
}finally {
System.out.println("关闭线程池......");
//当前所有子线程都执行完毕就正常关闭,若需中断任务需要立刻停止所有还在扫描的子线程
pool.shutdown();
}
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() +"个文件夹");
}
该方法是选择要扫描的菜单之后,执行的第一个方法,主线程需要等待所有子线程全部扫描结束之后再恢复执行。
scan方法是我们选择要扫描的文件夹之后的入口方法,所有文件夹和文件的具体扫描工作交给子线程
该方法的执行流程如下:
scanInternal(File filePath)方法扫描任务的具体执行操作1 . 设置开始时间戳,将传入的文件路径交给scanInternal方法处理,此时根本目录有子线程扫描了所以线程数要+1
2 . 调用latch对象的await()方法让主线程进入等待状态,当最后一个线程执行完毕之后再唤醒主线程
3 . 最后关闭线程池,根据设置的开始时间戳和结束时间戳计算出扫描任务执行的时间,并打印出相关数据
private void scanInternal(File filePath) {
if(filePath == null) return;
pool.submit(() -> {
//使用回调函数,将当前目录下的所有内容保存到终端
this.callBack.callback(filePath);
//获取当前这一级目录下的file对象
File[] files = filePath.listFiles();
for(File file : files) {
if(file.isDirectory()) {
dirNum.incrementAndGet();
threadCount.incrementAndGet();
scanInternal(file);
}else {
fileNum.incrementAndGet();
}
}
System.out.println(Thread.currentThread().getName() + "扫描:" +filePath +"任务结束");
threadCount.decrementAndGet();
if(threadCount.get() == 0) {
System.out.println("所有扫描任务结束");
//唤醒主线程
latch.countDown();
}
});
}
该递归过程如下:
FileScanner类的总代码:1 . 若传入文件路径为空则返回。
2 . 路径不为空,则线程池调用submit()方法提交任务。
3 . 线程池内部使用回调函数将当前目录下的所有内容保存到指定终端。
4 . 使用listFiles()方法获取当前这一级目录下file对象,之后遍历这些对象,若是文件夹则dirNum和threadCount调用incrementAndGet(),接着继续递归这个路径,若是文件则只进行fileNum的getAndIncrement()方法。
5 . 每当一个线程执行结束都会使用decrementAndGet()方法,若线程数为0,则唤醒主线程(调用latch对象的countDown()方法)
package task;
import callBack.FileScannerCallBaack;
import lombok.Getter;
import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Getter
public class FileScanner {
//扫描文件个数
private AtomicInteger fileNum = new AtomicInteger();
//扫描文件夹的个数
private AtomicInteger dirNum = new AtomicInteger(1);
//所有扫描文件的子线程个数,只有当子线程个数为0时,主线程再继续执行
private AtomicInteger threadCount = new AtomicInteger();
//最后一个线程执行完,调用countDown方法唤醒线程
private CountDownLatch latch = new CountDownLatch(1);
//获取当前电脑可用的CPU个数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//使用线程池创建对象
private ThreadPoolExecutor pool = new ThreadPoolExecutor(CPU_COUNT,CPU_COUNT * 2,10, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),new ThreadPoolExecutor.AbortPolicy());
//文件扫描回调对象
private FileScannerCallBaack callBack;
public FileScanner(FileScannerCallBaack callBack) {
this.callBack = callBack;
}
public void scan(File filePath) {
System.out.println("开始文件扫描任务,根目录为:" +filePath);
long start = System.nanoTime();
scanInternal(filePath);
threadCount.incrementAndGet();
try {
latch.await();
} catch (InterruptedException e) {
System.err.println("扫描任务中断,根目录为:" +filePath);
}finally {
System.out.println("关闭线程池......");
//当前所有子线程都执行完毕就正常关闭,若需中断任务需要立刻停止所有还在扫描的子线程
pool.shutdown();
}
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() +"个文件夹");
}
private void scanInternal(File filePath) {
if(filePath == null) return;
pool.submit(() -> {
//使用回调函数,将当前目录下的所有内容保存到终端
this.callBack.callback(filePath);
//获取当前这一级目录下的file对象
File[] files = filePath.listFiles();
for(File file : files) {
if(file.isDirectory()) {
dirNum.incrementAndGet();
threadCount.incrementAndGet();
scanInternal(file);
}else {
fileNum.incrementAndGet();
}
}
System.out.println(Thread.currentThread().getName() + "扫描:" +filePath +"任务结束");
threadCount.decrementAndGet();
if(threadCount.get() == 0) {
System.out.println("所有扫描任务结束");
//唤醒主线程
latch.countDown();
}
});
}
}
FileSearch类
search(String dir, String content)方法检索FileSeacher类主要是根据用户选择的文件夹路径和用户输入的内容从数据库中查找出指定位置的内容并返回
public static Listsearch(String dir,String content) { List result = new ArrayList<>(); Connection connection = null; PreparedStatement ps = null; ResultSet rs = null; try { connection = DBUtil.getConnection(); //现根据用户选择的文件夹查询 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.out.println("从数据库搜索用户查找内容出现错误,请检查SQL语句"); e.printStackTrace(); }finally { DBUtil.close(ps,rs); } return result; } }
FileSearch类只有这一个方法,其目的就是从数据库中找到文件夹路径和用户输入的内容并返回,其中用户选择的检索文件夹路径一定不为空,用户搜索框中的内容可以为空,若为空则显示当前选择的文件夹路径下的所有内容
该方法的具体操作流程如下:
FileSearch类总代码1 . 创建FileMeta类List集合,创建SQLite数据库连接、PreparedStatement对象和ResultSet对象
2 . 编写sql语句
注 :此时若传入的content若不为空,则要在sql语句加上支持用户输入的文件全名称,拼音全名称以及拼音首字母的模糊查询
3 . 使用PrepareStement对象执行sql语句
4 . 使用ResultSet对象获取执行完sql语句查询到的数据并将其保存到FileMeta对象中,最后保存到List集合中。
注 :将返回的数据封装到FileMeta对象时若meta是文件则保存大小,反之不保存
5 . 关闭PrepareStement和ResultSet资源对象
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 {
public static List search(String dir,String content) {
List result = new ArrayList<>();
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
connection = DBUtil.getConnection();
//现根据用户选择的文件夹查询
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.out.println("从数据库搜索用户查找内容出现错误,请检查SQL语句");
e.printStackTrace();
}finally {
DBUtil.close(ps,rs);
}
return result;
}
}
app包
这个包主要是简历用户直接操作的界面以及界面信息对象
Controller类Controller类的成员属性该类的作用主要是初始化用户的操控界面
@FXML
private GridPane rootPane;
@FXML
private TextField searchField;
@FXML
private TableView fileTable;
@FXML
private Label srcDirectory;
private Thread scanThread;
nitialize(URL location, ResourceBundle resources)方法初始化界面1 . rootPane变量主要是设置界面窗口的面板
2 . searchFiled变量代表的是是文件搜索框
3 . fileTable变量代表的是搜索文件后显示的内容
4 . srcDirectory变量代表的是用户选择文件后显示的路径
5 . fileMetas对应数据库中的一行行数据
6 . scanThread代表扫描目录的线程
public void initialize(URL location, ResourceBundle resources) {
//在界面初始化之前初始化数据库
DBInit.init();
// 添加搜索框监听器,内容改变时执行监听事件
searchField.textProperty().addListener(new ChangeListener() {
public void changed(ObservableValue extends String> observable, String oldValue, String newValue) {
freshTable();
}
});
}
方法简介:
choose(Event event)方法获取选择的文件夹点击运行项目,界面初始化时加载的一个方法
该方法的执行流程如下:
1 . 在界面初始化时初始化数据库
2 . 添加搜索框监听器,当搜索框里面的内容改变时执行监听事件(刷新显示的内容)
public void choose() {
// 选择文件目录
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();
}
scanThread = new Thread(() -> {
fileScanner.scan(file);
//刷新界面,展示刚才扫描的文件信息
freshTable();
});
scanThread.start();
}
该方法主要是当用户选择了要检索的文件夹之后告诉用户之前选择的是哪个文件夹和当用户在之前任务没执行完再次选择路径的中断操作,路径显示其具体表现如下:
该方法的具体操作流程如下:
freshTable()方法刷新表格数据1 . 当用户选取了文件之后获取目录路径并显示
2 . 获取到要扫描的路径之后进行文件扫描工作
3 . 若当前任务还没结束,则中断当前任务
4 . 开启新的线程扫描新选择的目录(调用FileScanner类对象的scan()方法)并刷新界面,展示刚才扫描到的文件信息(Controller类的freshTable()方法)
private void freshTable(){
ObservableList metas = fileTable.getItems();
metas.clear();
String dir = srcDirectory.getText();
if(dir != null && dir.trim().length() != 0) {
//取出数据库中已保存的内容展示到界面上
//获取用户正在搜索框中输入的内容
String content = searchField.getText();
//根据选择的路径和用户的输入(为空就展示所有内容),将数据库中的指定内容刷新到界面中
List filesFromDB = FileSearch.search(dir,content);
metas.addAll(filesFromDB);
}
}
该方法主要作用是刷新表格数据并将其展现在界面上,其具体流程如下:
FileMeta类1 . 创建FileMeta对象ObservableList集合接受当前表格中的数据,使用clear()方法清楚当前表格数据
2 . 获取当前选择的文件路径,若路径为空则不显示,否则拿到用户输入框中的内容配合文件路径将数据库中的指定内容刷新到界面中
FileMeta类的成员属性该类的一个对象就是数据表中的一行
private String name;
private String path;
private Boolean isDirectory;
private Long size;
private Date lastModified;
private String pinYIN;
private String pinYinFtrst;
//文件类型
private String isDirectoryText;
//文件大小
private String sizeText;
//修改时间
private String lastModifiedText;
FileMeta类的属性完全按照磁盘中文件的相关属性定义的:
setSize(Long size)方法显示带单位的文件大小1 . name :文件名称
2 . path : 文件路径
3 . isDirectory : 是否是文件夹
4 . lastModified : 最后修改时间
5 . size : 文件大小(若是文件夹则不显示)
6 . pinYin : 若文件名包含中文,则显示拼音全拼
7 . pinYinFirst : 拼音首字母
8 . isDirectoryText : 文件类型(中文显示)
9 . sizeText : 文件大小(包含单位的显示)
10 . lastModifiedText : 上次修改时间(中文显示)
public void setSize(Long size) {
this.size = size;
this.sizeText = Util.parseSize(size);
}
setDirectory(Boolean directory)方法设置文件类型通过Util类的parseSize()方法将sizeText属性设置为带单位的文件大小
public void setDirectory(Boolean directory) {
isDirectory = directory;
this.isDirectoryText = Util.parseFileType(directory);
}
setLastModified(Date lastModified)通过Util类的paseDate()方法返回文件类型设置lastModifiedText属性
public void setLastModified(Date lastModified) {
this.lastModified = lastModified;
this.lastModifiedText = Util.parseDate(lastModified);
}
FileMeta类总代码通过Util类的parseDate()方法设置lastModifiedText属性
package app;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import util.Util;
import java.util.Date;
@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 pinYinFtrst;
//文件类型
private String isDirectoryText;
//文件大小
private String sizeText;
//修改时间
private String lastModifiedText;
public void setSize(Long size) {
this.size = size;
this.sizeText = Util.parseSize(size);
}
public void setDirectory(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;
}
}
因为这个类的成员属性比较多若要写出每一个属性的Getter和Setter方法代码量比较大所以这里使用了Lombok的注解实现了相关方法
callback包这个包主要实现回调函数
FileScannerCallBack类该类是文件信息扫描的回调接口,主要实现了callback抽象方法
package callBack;
import java.io.File;
public interface FileScannerCallBaack {
void callback(File dir);
}
FileSave2DB类
callback(File dir)方法该类实现了FileScannerCallBack接口覆写了callBack方法,主要作用是将文件信息保存到数据库
@Override
public void callback(File dir) {
//列举出当前dir路径下的所有文件对象
File[] files = dir.listFiles();
//边界条件
if(files != null && files.length != 0) {
//1.将dir下所有文件信息保存到内存中--视图1
List locals = new ArrayList<>();
for(File file : files) {
FileMeta meta = new FileMeta();
if(dir.isDirectory()) {
setCommonFiled(file.getName(), file.getParent(),true, file.lastModified(),meta);
}else {
//是个文件有大小
setCommonFiled(file.getName(), file.getParent(), false, file.lastModified(), meta);
meta.setSize(file.length());
}
locals.add(meta);
}
//2.从数据库中查询出当前路径下的所有文件信息--视图2
List dbFiles = query(dir);
//3.对比两个视图
//本地有,数据库没有,插入
for(FileMeta meta : locals) {
if(!dbFiles.contains(meta)) {
save(meta);
}
}
//数据库有,本地没有,删除
for(FileMeta meta : dbFiles) {
if(!locals.contains(meta)) {
delete(meta);
}
}
}
//若files == null || files.length == 0
//该文件夹下没有文件或者不是文件夹
}
该方法主要作用是更新数据库文件信息,流程如下:
delete(FileMeta meta)方法删除数据库中指定的记录1 . 获取当前路径下所有文件对象
2 . 若对象数组长度为0,则啥也不干
3 . 若文件数组长度不为0,则建立一个FileMeta对象数组,遍历这个文件对象数组分别设置文件对象属性将其添加到这个FileMeta对象数组里
4 . 获取当前路径下数据库的FileMeta对象数组
5 . 若本地FileMeta对象数组没有,但是数据库里有,则做删除操作
6 . 若这个本地FileMeta对象数组里有但是数据库里没有,则做插入操作
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);
}
}
该方法的操作流程如下:
save(FileMeta meta)方法将指定的文件对象保存到数据库中1 . 获取数据库连接
2 . 执行sql语句
执行删除操作的时候若删除的是个文件夹,则还需要删除文件本身,即sql语句中加上" or path = ?“和” or path like ?"
3 . 关闭PreparedStatement对象资源
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()) {
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);
}
}
该方法的操作流程如下:
query(File dir)方法查询数据库中指定路径下的文件信息1 . 获取数据库连接
2 . 执行sql语句
执行插入操作时要判断文件名是否带有中文(使用PinYinUtil类的containsChinese()方法),若带有中文则还需要插入文件的英文名全拼和英文名首字母
3 . 关闭PreparedStatement对象资源
private Listquery(File dir) { Connection connection = null; PreparedStatement ps = null; ResultSet rs = null; List dbFiles = new ArrayList<>(); try { connection = DBUtil.getConnection(); String sql = "select name,path,is_directory,size,last_modified from file_meta" + " 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 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; }
该方法的操作流程如下:
setCommonFiled方法创建文件对象1 . 获取数据库连接,创建FileMeta对象集合
2 . 执行sql语句
3 . 根据查询到的每条数据创建FileMeta对象并将其存入FileMeta对象集合中
4 . 关闭PreparedStatement和ResultSet对象资源
5 . 返回FileMeta对象集合
private void setCommonFiled(String name,String path,boolean isDirectory,Long lastModified,FileMeta meta) {
meta.setName(name);
meta.setPath(path);
meta.setDirectory(isDirectory);
meta.setLastModified(new Date((lastModified)));
}
总结该方法的作用就是创建普通文件对象
这个项目是我学Java以来做的第一个项目,虽然是个小项目,但我也花了很多时间在上面。在做项目的过程中让我学到了很多东西,这些是平常做练习和写算法题学不到的,比如:规划好每个类之间的关系,Maven项目怎么导入依赖等等,总而言之,这次做项目的经历让我受益匪浅,激发了我对Java编程的热爱,让我了解到编程也不仅仅有让人捉摸不透的算法,还有有趣的框架,以后我会更加勤奋的学习下去…