项目背景
基于win10自带的搜索系统,创建一个自己的搜索项目,类似于Everything。
Everything简介:
Everything是voidtools(中文主页)开发的一款文件搜索工具,官网描述为“基于名称实时定位文件和目录(Locate files and folders by name instantly)”。它体积小巧,界面简洁易用,快速建立索引,快速搜索,同时占用极低的系统资源,实时跟踪文件变化,并且还可以通过http或ftp形式分享搜索。
everything是利用操作系统记录的文件操作日志来进行搜索,特点是效率高,速度快,但是具有一定的局限性,只能用于NTFS文件系统。
技术栈
- 所用技术:JavaFX,多线程,SQLite,pinyin4j
- 所用环境:IDEA,Maven
项目功能
- 指定搜索目录,显示该目录内的内容。
- 使用多线程进行文件的比较,比较数据库和本地的差异,则更新数据库。
- 可以根据文件名称进行文件的查询。
- 支持汉语拼音询以及首字母查询。
项目展示
目录搜索
按照文件名搜索
按照拼音首字母搜索
技术栈介绍
JavaFX:
- JavaFX是一个强大的图形和多媒体处理工具包集合,它允许开发者来设计、创建、测试、调试和部署富客户端程序,并且和Java一样跨平台。
- 内部有大量的UI控件和CSS支持。
- 提供有可视化的布局设计工具Scene Builder,可以用于拖拽式的界面设计。
- 有丰富的API,可以进行多种特效设计。
SQLite:
- 轻量级数据库
- 无需安装配置及管理
- 支持多种主流开发语言
- 采用动态数据类型
- SQLite通过数据库级上的独占性和共享锁来实现独立事务处理
pinyin4j:
- 使用方便,转换只需要使用PinyinHelper类的静态工具方法即可。
- 支持多音字的输出。
- 支持拼音声调的输出。
系统开发设计
数据库设计
设计目的:
- 保存文件的基本信息:名称,修改时间,大小,路径
- 当文件名为中文时,还需要保存拼音及其首字母方便后续查询
数据库代码:
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)
);
数据库初始化
读取本地的SQL文件,并且进行数据库初始化的建表操作
使用IO流来读取数据并执行,需要考虑多行代码的读取以及SQL标注的问题。
public static String[] readSQL() {
try {
InputStream is = DBInit.class.getClassLoader().getResourceAsStream("ini.sql");
BufferedReader br = new BufferedReader(new InputStreamReader(is,"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 (IOException e) {
e.printStackTrace();
throw new RuntimeException("读取时错误",e);
}
}
public static void init(){
Connection connection = null;
Statement statement = null;
try {
connection = DBUtil.getConnection();
statement = connection.createStatement();
String[] sqls = readSQL();
for (String sql :sqls){
//System.out.println("打印SQL语句"+sql);
statement.executeUpdate(sql);
}
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("初始化数据库表失败",e);
}finally {
DBUtil.close(connection,statement);
}
}
拼音工具类
由于拼音工具类为第三方库提供,因此我们在引入之后要进行设置,配置汉字的字符范围,以及拼音的输出格式。
public class Pinyin {
private static final String CHINESE_PATTERN ="[\\u4E00-\\u9FA5]";
public static final HanyuPinyinOutputFormat FORMAT = new HanyuPinyinOutputFormat();
static {
//拼音小写
FORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE);
//不带音调
FORMAT.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
//设置带V
FORMAT.setVCharType(HanyuPinyinVCharType.WITH_V);
}
public static boolean containsChinese(String name){
return name.matches(".*" + CHINESE_PATTERN + ".*");
}
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;
}
//多音字
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 {
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;
}
public static String[] compose(String[][] pinyin){
if (pinyin == null ||pinyin.length == 0){
return null;
}else if (pinyin.length == 1){
return pinyin[0];
}else {
for (int i = 1;i<pinyin.length;i++){
pinyin[0] = compose(pinyin[0],pinyin[i]);
}
return pinyin[0];
}
}
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()]);
}
//多音字的拼接
public static String[] compose(String[] pinyins1,String[] pinyins2){
String[] result = new String[pinyins1.length * pinyins2.length];
for (int i = 0;i<pinyins1.length;i++){
for (int j = 0;j<pinyins2.length;j++){
result[i*pinyins2.length+j] = pinyins1[i]+pinyins2[j];
}
}
return result;
}
}
事件监听
实现了javaFX的Initializable接口,进行界面初始化,进行事件监听,当内容改变时进行反馈
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();
}
});
}
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() {
ScanCallback callback = new FileSave();
FileScanner scanner = new FileScanner(callback);
try {
System.out.println("执行文件扫描");
scanner.scan(path);
scanner.waitFinish();
System.out.println("任务执行完毕,刷新表格");
freshTable();
} catch (Exception 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);
}
}
}
多线程扫描
选择多线程进行大量的任务,使用线程池来提高工作效率。
在线程执行时,待执行任务数+1, 执行完后,待执行任务数-1,开启子任务时,每个子任务都执
行任务数+1操作,这样在最后可以判断出是否所有线程执行完毕,当任务完成,调用线程等待方法,使线程阻塞等待,关闭线程池。计数器使用线程安全的incrementAndGet(),decrementAndGet()方法。
public class FileScanner {
private ExecutorService pool = Executors.newFixedThreadPool(4);
private ScanCallback callback;
private volatile AtomicInteger count = new AtomicInteger();
private Object lock = new Object();
public FileScanner(ScanCallback callback) {
this.callback = callback;
}
public void scan(String path){
count.incrementAndGet();
doscan(new File(path));
}
public void doscan(File dir) {
pool.execute(new Runnable() {
@Override
public void run() {
try {
callback.callback(dir);
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
if (child.isDirectory()) {
System.out.println("当前任务数" + count.get());
count.incrementAndGet();
doscan(child);
}
}
}
}finally {
int r = count.decrementAndGet();
if (r == 0){
synchronized (lock){
lock.notify();
}
}
}
}
});
}
public void waitFinish()throws InterruptedException{
try {
synchronized (lock){
lock.wait();
}
}finally {
System.out.println("关闭线程池");
pool.shutdownNow();
}
}
}
信息保存
将扫描到的信息进行保存到数据库中。
本地有,数据库没有,插入该文件信息;数据库有,本地没有,删除数据库中该文件信息。如果该文件是文件夹,还需要删除所有在该文件夹下的文件。
public class FileSave implements ScanCallback {
public void callback(File dir){
File[] children = dir.listFiles();
List<FileMeta> locals = new ArrayList<>();
if (children != null){
for (File child : children){
locals.add(new FileMeta(child));
}
}
List<FileMeta> metas = query(dir);
for (FileMeta meta : metas){
if (!locals.contains(meta)){
delete(meta);
}
}
for (FileMeta meta : locals){
if (!metas.contains(meta)){
save(meta);
}
}
}
public 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.getDirectory()){
sql += " or path=?"+
" or path like ?";
}
ps = connection.prepareStatement(sql);
ps.setString(1,meta.getName());
ps.setString(2,meta.getPath());
ps.setBoolean(3,meta.getDirectory());
if (meta.getDirectory()){
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 (Exception e){
e.printStackTrace();
throw new RuntimeException("删除文件出错",e);
}finally {
DBUtil.close(connection,ps);
}
}
private List<FileMeta> query(File dir){
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
List<FileMeta> metas = 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();
while (rs.next()){
String name = rs.getString("name");
String path = rs.getString("path");
Boolean isDirectory = rs.getBoolean("is_directory");
Long size = rs.getLong("size");
Timestamp lastModified = rs.getTimestamp("last_modified");
FileMeta
meta = new FileMeta(name,path,isDirectory,size,new java.util.Date(lastModified.getTime()));
System.out.printf("查询文件信息: name = %s, path = %s,is_directory = %s,size = %s,last_modified = %s\n",name,path,String.valueOf(isDirectory),String.valueOf(size),Util.parseDate(new java.util.Date(lastModified.getTime())));
metas.add(meta);
}
return metas;
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("查询出错",e);
}finally {
DBUtil.close(connection,ps,rs);
}
}
private void save(FileMeta meta){
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,meta.getName());
statement.setString(2,meta.getPath());
statement.setBoolean(3,meta.getDirectory());
statement.setLong(4,meta.getSize());
statement.setString(5,meta.getLastModifiedText());
String pinyin = null;
String pinyin_first = null;
if (Pinyin.containsChinese(meta.getName())){
String[] pinyins = Pinyin.get(meta.getName());
pinyin = pinyins[0];
pinyin_first = pinyins[1];
}
statement.setString(6,meta.getPinyin());
statement.setString(7,meta.getPinyinFirst());
System.out.println("执行文件保存操作:"+ sql);
statement.executeUpdate();
}catch (SQLException e){
throw new RuntimeException("文件保存失败",e);
}finally {
DBUtil.close(connection,statement);
}
}
}
文件搜索
根据输入值进行匹配,部分字符,拼音,拼音首字母,只要有匹配成功就显示文件信息。
public static List<FileMeta> search(String dir, String content) {
List<FileMeta> metas = new ArrayList<>();
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
connection = DBUtil.getConnection();
String sql = "SELECT name, path, is_directory, size, 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 + "%");
}
rs = ps.executeQuery();
while (rs.next()){
String name = rs.getString("name");
String path = rs.getString("path");
Boolean isDirectory = rs.getBoolean("is_directory");
Long size = rs.getLong("size");
Timestamp lastModified = rs.getTimestamp("last_modified");
FileMeta meta = new FileMeta(name,path,isDirectory,size,new java.util.Date(lastModified.getTime()));
metas.add(meta);
}
}catch (Exception e){
throw new RuntimeException("文件查询失败路径" +dir+"内容"+ content,e);
}finally {
DBUtil.close(connection,ps,rs);
}
return metas;
}
项目总结
项目优点:
使用了多线程的方法来进行文件的遍历,提高了搜索的效率。
项目缺点:
使用起来不是很方便,搜索方面比较僵硬