前言

最近有一个遇到一个很头疼的问题 在找“学习资料” 但是遇到了Win搜索速度缓慢 检索一个20G的磁盘需要很久很久 这就很难受 找一个文件我就要耐心等待它去慢慢的检索 于是突发奇想 如果我将电脑本地的文件全部存储到数据库中 利用数据库的索引以及查找机制 是不是能够大量的提升检索效率呢


使用工具

· 因为比较而言是熟系Java开发的 所以我选择使用Java语言
· 如果要制作成一个Win端口使用的工具 那么就需要使用到一个窗口端 窗口段可以使用Java FX
· 如果要实现在一个新的电脑上使用 那么肯定需要到数据库 不过MySQL是比较大的 总不能为了使用一个检索工具我再去下载一个MySQL 然后咔咔一顿配置 这个最多使用于程序员 这显然是不符合预期的 所以 我们选择使用SQLite 这是一款非常轻量的数据库 并且可以配套在软件中使用 无需多余的配置
· exe4j 为了能够将Java程序打包成exe文件 可是费了不少心 这款软件可以将Java写的程序打包出的Jar包封装为exe文件 并且也可以将JDK以及所需的依赖都封装进去


使用技术

Servlet、JDBC、JavaFX、IO、Java多线程


实现功能

准备实现一个基于多线程的文件检索工具 并且能够做到指定目录查询 查询信息是文件的名字文件的路径 文件的大小 文件的最后修改时间 有一个联系作者按钮 使用者使用过程中有任何不懂 有任何bug都可以及时的联系作者 还要封装为一个.exe文件 为了可以让用户点击即可使用(实现为绿色版)

关于使用的技术比较OUT

有些老铁认为使用Servlet 或者 JDBC是比较low的 有时候还会被鄙视到 其实这个问题很简单 学习到了新的技术 Spring 、MyBaits的确能够做到更高效的开发 但是我们反之一想 比较MyBatis 与JDBC来说 哪个技术更难呢? MyBatis本身就是为了简化JDBC开发 并且MyBatis底层也是使用JDBC实现的 我个人感觉 JDBC使用起来比较MyBatis是更难得 再之 如果难的你使用的很熟练 那么使用简单的那不是手到擒来吗?


JDBC

我们先来实现数据库连接部分 方便我们后续使用数据库可以方便快捷

构建数据库

因为我们使用的是SQLite 所以SQL语法上比较MySQL是有细微区别的

create table if not exists file_meta (
    id INTEGER primary key autoincrement,
    name varchar(50) not null,
    path varchar(512) not null,
    is_directory boolean not null,
    pinyin varchar(100) not null,
    pinyin_first varchar(50) not null,
    size BIGINT not null,
    last_modified timestamp not null
);

我们构造以上数据库 其中涉及到多个属性 我们来分析一下这些属性
· id - - 这是文件的编号 方便我们管理数据库
· name - - 文件的名称
· path - - 文件的路径
· is_directory - - 当前文件是否是目录
· pinyin - - 当前文件的全拼(因为我们也要实现文件的拼音搜索)
· pinyin_first - - 当前文件的拼音缩写(例如 个人博客 -> grbk)
· size - - 当前文件的大小
· last_modified - - 文件最后修改的时间


FileMeta

在进行添加文件 或者 查询文件的时候 必不可少的是需要一个类来代表一条数据
当然我们也需要匹配到数据库中的属性

private int id;
    private String name;
    private String path;
    private boolean isDirectory;


    //此处两个拼音是根据name来的 name可能每次都会变化 所以直接提供两个方法调用即可
//    private String pinyin;
//    private String pinyinFirst;
    private long size;
    //此处的size直接来表示也是不合理的 long表示的是字节 这个不方便观看
    private long lastModified;
    //此处的最后修改时间也要进行一定的修改

size处理

因为size中存储的是字节 而这个字节直接显示到屏幕上用户肯定是看不懂的 所以我们需要特殊的将这些处理尾 byte kb mb gb

public String getSizeNext(){
        double curSize = size;
        String[] units = {"byte","KB","MB","GB"};
        for(int level = 0; level < units.length; level++){
            if(curSize < 1024){
                return String.format("%.2f " + units[level],new BigDecimal(curSize));
            }
            curSize /= 1024;
        }
        return String.format("%.2f GB",new BigDecimal(curSize));
    }

文件最后修改时间

因为最后修改时间中存储的是long 这是一个时间戳 这样显示出来也是不合理的 所以我们需要浅浅的处理一下

public String getLastModifiedNext(){
        DateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dataFormat.format(lastModified);
    }

equals重写

因为我们会对于数据库中的文件名称进行一共匹配 所以我们也必然是需要重写equals方法的

@Override
    public boolean equals(Object o){
        if(this == o){
            return true;
        }
        if(o == null){
            return false;
        }
        if(getClass() != o.getClass()){
            return false;
        }
        FileMeta fileMeta = (FileMeta) o;
        return name.equals(fileMeta.name) && path.equals(fileMeta.path) &&
                isDirectory == fileMeta.isDirectory;
    }

其他方法

还有我们一些其他的方法 get set 因为比较多 可以直接使用快捷键Ait+insert生成 也可以加入@Data注解(Lombox神级辅助)

public String getPinyin(){
        return PinyinUtil.get(this.name,true);
    }

    public String getPinyinFirst(){
        return PinyinUtil.get(this.name,false);
    }

    public String getSizeNext(){
        double curSize = size;
        String[] units = {"byte","KB","MB","GB"};
        for(int level = 0; level < units.length; level++){
            if(curSize < 1024){
                return String.format("%.2f " + units[level],new BigDecimal(curSize));
            }
            curSize /= 1024;
        }
        return String.format("%.2f GB",new BigDecimal(curSize));
    }

    public String getLastModifiedNext(){
        DateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dataFormat.format(lastModified);
    }

    public void setName (String name) {
        this.name = name;
    }

    public FileMeta (String name , String path , boolean isDirectory , long size , long lastModified) {
        this.name = name;
        this.path = path;
        this.isDirectory = isDirectory;
        this.size = size;
        this.lastModified = lastModified;
    }

    public FileMeta(File f){
        this(f.getName(),f.getParent(),f.isDirectory(),f.length(),f.lastModified());
    }

    @Override
    public boolean equals(Object o){
        if(this == o){
            return true;
        }
        if(o == null){
            return false;
        }
        if(getClass() != o.getClass()){
            return false;
        }
        FileMeta fileMeta = (FileMeta) o;
        return name.equals(fileMeta.name) && path.equals(fileMeta.path) &&
                isDirectory == fileMeta.isDirectory;
    }


    public void setPath (String path) {
        this.path = path;
    }

    public void setDirectory (boolean directory) {
        isDirectory = directory;
    }

    public void setSize (long size) {
        this.size = size;
    }

    public void setLastModified (long lastModified) {
        this.lastModified = lastModified;
    }

    public int getId () {
        return id;
    }

    public String getName () {
        return name;
    }

    public String getPath () {
        return path;
    }

    public boolean isDirectory () {
        return isDirectory;
    }

    public long getSize () {
        return size;
    }

    public long getLastModified () {
        return lastModified;
    }

dao

数据库源头

因为JDBC是对于所有数据库的封装 所以我们可以使用JDBC来操作SQLite 是没任何毛病的

//建立连接 并且设置URL
    public static DataSource getDataSource(){
        if (dataSource == null) {
            synchronized (DBUtil.class) {
                if (dataSource == null) {
                    dataSource = new SQLiteDataSource();
                    File file = new File("your_file_path");
                    String filePath = file.getAbsolutePath();
                    System.out.println(filePath);
                    ((SQLiteDataSource) dataSource).setUrl("jdbc:sqlite://" + filePath);
                }
            }
        }
        return dataSource;
    }

其中值得注意的是 数据库的URL将其设置为了先获取当前文件路径 然后再当前文件路径中使用数据库 这么做的原因是什么呢? 是因为我们要做成将文件发送给任何人 都可以进行使用的

获取连接

 //获取连接
    public static Connection getConnection() throws SQLException {
        return getDataSource().getConnection();
    }

获取数据库的连接 JDBC的老步骤了

关闭资源连接

这一步很重要 数据库每次连接完都有很多资源 Connetion、Statement、ResultSet 这些资源 如果我们不在程序中手动释放的话 根据JVM的垃圾回收机制 是不会对于这些进行回收的 所以一定一定 使用完要记得释放 (也算是一个内存泄露)

 //关闭资源
    public static void close(Connection connection , Statement statement , ResultSet resultSet){
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

其中我们设置了三类的释放 没有对于单一的资源做释放方法 我们对于只需要释放任意一个资源或者两个资源怎么办呢 我们可以直接在调用释放的时候 传入一个null即可 其中也对于如果为null的话 也是不会做处理的 也保证了不会造成空引用访问


FileDao

初始化

首位我们要让我们写的SQL能够设置到数据库中 总不能让用户使用之前自己先去做初始化吧 这是不可取的 所以我们需要借助IO流将SQL取到放入SQLite中去执行

//将代码初始化到SQLiet
    public void initDB() {
        // 1) 先能够读取到 db.sql 中的 SQL 语句.
        // 2) 根据 SQL 语句调用 jdbc 执行操作.
        Connection connection = null;
        Statement statement = null;
        try {
            connection = DBUtil.getConnection();
            statement = connection.createStatement();
            String[] sqls = getInitSQL();
            for (String sql : sqls) {
                System.out.println("[initDB] sql: " + sql);
                statement.executeUpdate(sql);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

提一嘴 我们此处使用的是IO流进行获取的 所以我们也要使用到IO技术 并且我们如果想要去执行一段SQL语句 使用的不再是以前的PreparedStatement类 而是使用的是Statement类 即使后序使用查询或者插入也是没关系的 因为PreparedStatement是Statement的子类
关于Win搜索太慢我自己写了一个Everything-LMLPHP

插入文件/目录到数据库中

 //插入文件/目录到数据库中
    public void add(List<FileMeta> lists){
        Connection connection = null;
        PreparedStatement statement = null;
        try{
            connection = DBUtil.getConnection();
            //关闭连接的自动提交功能
            connection.setAutoCommit(false);
            String sql = "insert into file_meta values(null,?,?,?,?,?,?,?)";
             statement = connection.prepareStatement(sql);
            for(FileMeta fileMeta : lists){
                statement.setString(1,fileMeta.getName());
                statement.setString(2,fileMeta.getPath());
                statement.setBoolean(3,fileMeta.isDirectory());
                statement.setString(4, fileMeta.getPinyin());
                statement.setString(5, fileMeta.getPinyinFirst());
                statement.setLong(6,fileMeta.getSize());
                statement.setTimestamp(7,new Timestamp(fileMeta.getLastModified()));
                statement.addBatch();
                System.out.println("[insert] 插入" + fileMeta.getPath() + File.separator + fileMeta.getName());
            }
            statement.executeBatch();
            //执行结束后告知数据库添加完毕
            connection.commit();
        } catch (SQLException e) {
            try {
                if(connection != null){
                    connection.rollback();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }finally {
            DBUtil.close(connection,statement,null);
        }

其中我们使用到了一点SQL中的事务功能 因为我们数据插入是多条的 不能是一条一条的去执行sql语句 因为每次执行也是需要资源的 所以此处我们选择使用事务来进行插入数据库 以提升效率

查询数据

1. 给定路径查找文件

//给定路径查询这个路径对应的结果
    public List<FileMeta> searchByPath(String targetPath){
        List<FileMeta> fileMetas = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try{
            connection = DBUtil.getConnection();
            String sql = "select name, path, is_directory, size, last_modified from file_meta"
                    + " where path = ?";
            statement = connection.prepareStatement(sql);
            statement.setString(1,targetPath);
            resultSet = statement.executeQuery();
            while(resultSet.next()){
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModified = resultSet.getTimestamp("last_modified");
                FileMeta fileMeta = new FileMeta(name,path,isDirectory,size,lastModified.getTime());
                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally{
            DBUtil.close(connection,statement,resultSet);
        }
        return fileMetas;
    }

因为我们查询到的数据大概率不是单条的数据 所以我们使用List来接收

2. 实现按照关键字查找

 //按照特定关键字进行查询
    public List<FileMeta> searchByPattern(String pattern){
        List<FileMeta> fileMetas = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try{
            connection  = DBUtil.getConnection();
            String sql = "select name, path, is_directory, size, last_modified from file_meta"
                    + " where name like ? or pinyin like ? or pinyin_first like ?"
                    + " order by path, name";
            statement = connection.prepareStatement(sql);
            statement.setString(1,"%" + pattern + "%");
            statement.setString(2,"%" + pattern + "%");
            statement.setString(3,"%" + pattern + "%");
            resultSet = statement.executeQuery();
            while(resultSet.next()){
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModified = resultSet.getTimestamp("last_modified");
                FileMeta fileMeta = new FileMeta(name,path,isDirectory,size,lastModified.getTime());
                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection,statement,resultSet);
        }
        return fileMetas;
    }

其中的SQL语句是比较麻烦的 因为要考虑到很多 我们不单单要匹配 名称 还要匹配拼音(全拼、缩写) 以及路径 所以这里的SQL语句一定要注意

删除数据

因为用户的文件是会修改的 修改或者删除 我们都需要同步到数据库中 所以我们删除数据功能也是必不可少的喽

//删除数据
    public void delete(List<FileMeta> fileMetas){
        Connection connection = null;
        PreparedStatement statement = null;
        try{
            connection = DBUtil.getConnection();
            connection.setAutoCommit(false);
            for(FileMeta fileMeta : fileMetas){
                String sql = null;
                if(!fileMeta.isDirectory()){
                    //删除文件操作
                    sql = "delete from file_meta where name = ? and path = ?";
                }else {
                    //删除目录操作
                    sql = "delete from file_meta where (name = ? and path = ?) or (path like ?)";
                }
                statement = connection.prepareStatement(sql);
                if(!fileMeta.isDirectory()){
                    statement.setString(1, fileMeta.getName());
                    statement.setString(2,fileMeta.getPinyin());
                }else {
                    statement.setString(1, fileMeta.getName());
                    statement.setString(2, fileMeta.getPath());
                    statement.setString(3, fileMeta.getPath()
                            + File.separator + fileMeta.getName() + File.separator + "%");
                }
                statement.executeUpdate();
                System.out.println("[delete] : " +  fileMeta.getPath() + fileMeta.getName());
                statement.close();
            }
            connection.commit();
        } catch (SQLException e) {
            try {
                connection.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }finally {
            DBUtil.close(connection,null,null);
        }
    }

其中需要注意的点就是我们要区分当前要删除的是一个文件还是一个目录 如果是文件的话好说 直接删除即可 但是如果是一个目录 那么其中所有的文件肯定已经消失了 我们也没有继续保存管理的意义了 所以此时需要遍历一次数据库 将当前目录下的所有文件都删除即可


特殊处理

我们发现 其中在构建数据库中使用到了拼音搜索 其中如果我们去手动实现一个这个方法是不现实的 其中涉及到了太多的语义 所以此时 我们选择来引入一个第三方库 pinyin4j

<!-- https://mvnrepository.com/artifact/com.belerweb/pinyin4j -->
        <dependency>
            <groupId>com.belerweb</groupId>
            <artifactId>pinyin4j</artifactId>
            <version>2.5.1</version>
        </dependency>

这是这个库的Maven
这个库可以实现一些一个字符的拼音 当然其中我们对于中英混杂的还需要特殊处理 因为这个类在处理过程中如果遇到字母跟数字是会抛出异常的 所以我们需要额外的多做一些处理

方法实现

    //获取拼音 当前有两个参数 第一个参数为要拼音的字母 第二个参数为true时返回全拼 为false返回首字母
    public static String get(String str , boolean fullSpell){
        //如果当前传入的字符串为空 那么直接返回空
        if(str == null || str.trim().length() == 0){
            return null;
        }
        //创建一个HanyuPinyin类 设置一下将U:设置为v
        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setVCharType(HanyuPinyinVCharType.WITH_V);
        //使用一个StringBuidler保存结果
        StringBuilder string = new StringBuilder();
        //因为每次只能处理一个字母 所以进行循环
        for(int i = 0;i < str.length(); i++){
            char ch = str.charAt(i);
            String[] ret = null;
            try {
                //取出一个字母的拼音 注意 此处返回的是String[] 因为存在多音字
                ret = PinyinHelper.toHanyuPinyinStringArray(ch,format);
            } catch (BadHanyuPinyinOutputFormatCombination e) {
                e.printStackTrace();
            }
            //如果没有拼音 1 2 3 a b
            if(ret == null || ret.length == 0){
                string.append(ret);
            }else if(fullSpell){
                //如果返回的是全拼
                string.append(ret[0]);
            }else{
                //如果返回的是首字母
                string.append(ret[0].charAt(0));
            }
        }
        return string.toString();
    }

测试

我们可以选择去测试文件中对这个方法进行简单的测试

public class TestPinyin {
    public static void main (String[] args) {
        String[] strings  = PinyinHelper.toHanyuPinyinStringArray('绿');
        System.out.println(Arrays.toString(strings));
    }
}

附上效果图关于Win搜索太慢我自己写了一个Everything-LMLPHP

服务

因为我们的所有信息都是需要使用服务调用的 (这是Spring的思想)所以我们来实现一下服务

初始化

刚开始服务我们肯定是要先去扫描用户给定的目录去存储到数据库中的

    public void init(String basePath){
        fileDao.initDB();
        //fileManager.scanAll(new File(basePath));
        t = new Thread(()->{
            while(!t.isInterrupted()){
                fileManager.scanAll(new File(basePath));
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    System.out.println("用户有新的输入 马上去执行新任务");
                    break;
                }
            }
        });
        t.start();
        System.out.println("[SearchService] 初始化完成");
    }

FileDao这个类中我们已经实现了初始化 可以看到的是 我们还额外多加了一个线程 这个线程的功能是 当用户有输入的时候 我们直接去调用查询 这样可以实现用户输入内容我们进行实时查询的效果 并且也做了一个Sleep的操作 这是为啥呢? 因为我们的程序在运行的过程中 用户可能对于文件进行了修改 删除 重命名 我们是需要感知到这个东西的 但是我想了很多的方案

  1. 可以搞一个单独的线程 这个线程进行周期性的扫描这个指定的路径(这个思路扫描周期不太好确定 因为如果当前数据库中文件很多 如果设定较短的时间进行扫描的话 就会导致有的文件扫描不上 无法感知到修改 如果设置的太长 也不合理 有的文件被修改后 就会无法感知到)
  2. 让操作系统来感知文件的变动 Java中也提供了相关的类 WatchService API 这个类一旦有变化就会通知程序 (但是这个也不合理 因为这个只能监视指定的目录)
  3. 也有一些第三方库 甚至可以感受到子目录 孙子目录的变动 (这个方法也是使用的周期性的扫描 如此一来 其实使用的还是思路1的思想)
  4. 我们也可以参考一下everthing是如何实现的这个 它使用的是NTFS这个文件系统的特性 这个文件系统内置了一个日志的功能 会记录用户对文件的所有变动(但是这个方法其实是有些死板的 因为这个机制只有在win状态下才可以使用 换到其他系统就不行了 )
    其实经过以上的思路分析 最后我决定还是使用思路1 上述的代码也是使用思路1来实现的

服务方法

public void shutdown(){
        if(t != null){
            t.interrupt();
        }
}
public List<FileMeta> search(String pattern){
    return fileDao.searchByPattern(pattern);
}

shutdown 使用这个方法 可以让扫描线程停下来
search 提供了一个查找的方法


操作

单线程扫描

我们来实现一个目录的扫描功能 其中我们可以使用一下多叉树遍历的思想 如果当前是一个目录 那么进入目录中继续扫描(有孩子节点) 如果是一个文件(子节点)就直接加入即可

 private void scanAllByOneThread(File basePath){
        if(!basePath.isDirectory()){
            return ;
        }
        if(basePath == null || basePath.length() == 0){
            return ;
        }
        File[] files = basePath.listFiles();
        for(File file : files){
            if(file.isDirectory()){
                scanAllByOneThread(file);
            }
        }
    }

其中我们使用的是单线程的思想 当然因为我们已经熟系多线程了 所以可以思考一下 只要满足可以对文件进行合理的划分 就可以实现这个多线程的去扫描 毕竟现在CPU是多核的 我们还是要利用起来的

多线程扫描

    //线程池
    private static ExecutorService executorService = Executors.newFixedThreadPool(8);
	private AtomicInteger tackCount = new AtomicInteger(0);
	
    public void scanAllByThreadPool(File basePath){
        if(!basePath.isDirectory()){
            return ;
        }
        //计数器自增
        tackCount.getAndIncrement();
        //如果是一个目录的话 将创建为任务 放置到线程池中
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                //此处使用try 主要是为了应对scan中如果抛出异常了 那么就无法执行到下面的--操作
                //放到finally中 就能确保执行到
                try {
                    scan(basePath);
                }finally{
                    tackCount.getAndDecrement();//--
                    //System.out.println("!!!!!!!!!!!!!!!!!" + tackCount.get());
                    if(tackCount.get() == 0){
                        //当计数器为0了 就要通知主线程停表了
                        countDownLatch.countDown();
                    }
                }
            }
        });
        File[] files = basePath.listFiles();
        if(files == null || files.length == 0){
            return ;
        }
        //循环遍历
        for(File file : files){
            if(file.isDirectory()){
                //如果是目录则进行扫描
                scanAllByThreadPool(file);
            }
        }
    }

其中也有细节需要处理的 我们来考虑一下 如果当前主线程跑完了 剩余的都交给其他线程去完成了 那么可能会出现什么情况 其他线程还在跑 主线程已经结束了 这样是极其不合理的 这样会导致线程异常 此时我们也可以借用一下 信号量 + 终点线的思想 只有所有的线程都结束了 才会结束这个进程 还有要注意的是 如果我们直接使用一个Integer 或者int 因为我们使用的是多线程 我们还要考虑到ABA问题 所以此处我们直接使用AtomicInteger 类 这个类是线程安全的 可以保证线程安全 也不会产生ABA问题的

扫描文件

此处的扫描文件需要做的工作是很多的 其中需要找出未存在数据库的文件 以及已经被用户删除的文件 在数据库中删掉

 private void scan(File path){
        System.out.println("[fileManager] 扫描路径 :" + path.getAbsolutePath());
        List<FileMeta> scanned = new ArrayList<>();
        //将所有的目录列出
        File[] files = path.listFiles();
        if(files != null){
            for(File f : files){
                scanned.add(new FileMeta(f));
            }
        }
        //查看要删除的文件
        List<FileMeta> saved = fileDao.searchByPath(path.getPath());
        List<FileMeta> forDelete = new ArrayList<>();
        for(FileMeta fileMeta : saved){
            if(!scanned.contains(fileMeta)){
                forDelete.add(fileMeta);
            }
        }
        fileDao.delete(forDelete);

        //查看新增加的文件
        List<FileMeta> forAdd = new ArrayList<>();
        for(FileMeta fileMeta : scanned){
            if(!saved.contains(fileMeta)){
                forAdd.add(fileMeta);
            }
        }
        fileDao.add(forAdd);
    }

csanAll

这个类会将所有的功能进行一个调用 可以说 刚刚准备的所有功能都是为了在这里使用的

public void scanAll(File basePath){
        System.out.println("[FileManager] scanAll 开始");
        long start = System.currentTimeMillis();
        //scanAllByOneThread(basePath);
        countDownLatch = new CountDownLatch(1);

        scanAllByThreadPool(basePath);
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("[FileManager] scanAll 结束" + (end - start) + "ms");
    }

其中 扫描时间也计算出来加入到日志中 终点线的思想 countDownLatch = new CountDownLatch(1); 这边这么做的原因 是因为每次都会指定一个线程 如果已经到达终点了 其他的线程就再次冲线就不会有效了 所以此处需要每次设置一次countDownLatch.await(); 如果已经冲线了 就去通知一下


JavaFX

我们选择使用JavaFX实现 JavaFX是一种基于Java的富客户端应用程序平台,它允许开发者使用Java语言创建跨平台的GUI应用程序。JavaFX被设计为可扩展的,它提供了丰富的图形、文本、媒体和Web应用程序的工具集

fxml

如果要实现特定的功能 就要实现对应的多个标签 大概要做一个这样的界面关于Win搜索太慢我自己写了一个Everything-LMLPHP

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.*?>

<?import javafx.scene.control.*?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<GridPane fx:controller="gui.GUIController" fx:id="gridPane" vgap="10" alignment="center" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8">
  <children>
    <Button fx:id="button" onMouseClicked="#choose" prefHeight="40"
            text="选择目录" GridPane.rowIndex="0" GridPane.columnIndex="0"
            style="-fx-min-height: 30; -fx-min-width: 80;-fx-background-color: rgba(124,196,255,1); border-radius:50px">
    </Button>
    <Label fx:id="label" text="当前未选择目录" GridPane.rowIndex="0" GridPane.columnIndex="0">
      <GridPane.margin>
        <Insets left="100"></Insets>
      </GridPane.margin>
    </Label>
    <Button fx:id="QQ" prefHeight="40" text="联系作者" GridPane.rowIndex="0" GridPane.columnIndex="0"
            style="-fx-min-height: 30; -fx-min-width: 80;-fx-background-color: rgba(124,196,255,1);">
      <GridPane.margin>
        <Insets left="800"></Insets>
      </GridPane.margin>
    </Button>
    <TextField fx:id="textField" prefWidth="700" GridPane.rowIndex="1" GridPane.columnIndex="0"></TextField>
    <TableView fx:id="tableView" prefWidth="800" prefHeight="700" GridPane.rowIndex="2" GridPane.columnIndex="0">
        <columns>
          <TableColumn prefWidth="220" text="文件名">
            <cellValueFactory>
              <PropertyValueFactory property="name"></PropertyValueFactory>
            </cellValueFactory>
          </TableColumn>
          <TableColumn prefWidth="300" text="路径">
            <cellValueFactory>
              <PropertyValueFactory property="path"></PropertyValueFactory>
            </cellValueFactory>
          </TableColumn>
          <TableColumn prefWidth="100" text="大小">
            <cellValueFactory>
              <PropertyValueFactory property="sizeNext"></PropertyValueFactory>
            </cellValueFactory>
          </TableColumn>
          <TableColumn prefWidth="180" text="修改时间">
            <cellValueFactory>
              <PropertyValueFactory property="LastModifiedNext"></PropertyValueFactory>
            </cellValueFactory>
          </TableColumn>
        </columns>
    </TableView>
  </children>
</GridPane>

实现对应功能

我们准备一个类来对于这个界面的获取 以及功能的实现

public class GUIController implements Initializable {
    @FXML
    private GridPane gridPane;

    @FXML
    private Button button;

    @FXML
    private Label label;

    @FXML
    private TextField textField;

    @FXML
    private TableView<FileMeta> tableView;

    @FXML
    private Button QQ;

    private SearchService searchService = null;
    @Override
    public void initialize (URL location , ResourceBundle resources) {
        init();
        textField.textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                // newValue 输入框改完之后, 新的值是啥.
                freshTable(newValue);
            }
        });
    }

    private void init(){
        QQ.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                openQQ();

            }
        });
//        Stage stage = new Stage();
//
//        stage.setHeight(500);
//        stage.setWidth(500);
//
//        stage.show();
    }

    private void freshTable(String query){
        if(searchService == null){
            System.out.println("searchService 尚未初始化 不能查询!");
            return ;
        }

        ObservableList<FileMeta> fileMetas = tableView.getItems();
        //清空之前的内容
        fileMetas.clear();
        List<FileMeta> results = searchService.search(query);
        //把查询结果添加到TavleViem中
        fileMetas.addAll(results);
    }

    public void choose(MouseEvent mouseEvent){
        DirectoryChooser directoryChooser = new DirectoryChooser();
        Window window = gridPane.getScene().getWindow();
        File file = directoryChooser.showDialog(window);
        if(file == null){
            System.out.println("当前用户选择的路径为空");
            return ;
        }
        label.setText(file.getAbsolutePath());

        if(searchService != null){
            //如果不为空 则代表正在扫描 如果重新选择了 就要停止扫描 执行用户新的路径选择
            searchService.shutdown();
        }
        searchService = new SearchService();
        searchService.init(file.getAbsolutePath());
    }

    public void openQQ() {
        // 通过Desktop类来打开默认浏览器访问QQ网站
        try {
            URI uri = new URI("https://wpa.qq.com/msgrd?v=3&uin=2064555556&site=qq&menu=yes");
            Desktop.getDesktop().browse(uri);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }
 }

其中使用到了一个FXML注解 这个注解的功能是获取对应类的 为后期实现功能

textField.textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                // newValue 输入框改完之后, 新的值是啥.
                freshTable(newValue);
            }
});

以上方法我们来解释一下 这个方法实现的功能是一直获取到输入框中的内容 获取到数据框中的内容之后直接去查询

public void openQQ() {
        // 通过Desktop类来打开默认浏览器访问QQ网站
        try {
            URI uri = new URI("https://wpa.qq.com/msgrd?v=3&uin=2064555556&site=qq&menu=yes");
            Desktop.getDesktop().browse(uri);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
}

这个是额外添加的新功能 可以实现点击跳转到网页并且打开与作者聊天的QQ界面 其中参数就是url 如果要修改QQ只需要将uin修改为自己QQ即可

GUIClient

展示界面 我们还需要一个必要的方法来规定页面显示的大小 页面是否展示

public class GUIClient extends Application {
    @Override
    public void start (Stage primaryStage) throws Exception {
        Parent parent = FXMLLoader.load(GUIClient.class.getClassLoader().getResource("app.fxml"));
        primaryStage.setScene(new Scene(parent, 900, 800));
        primaryStage.setTitle("文件搜索工具-冰激凌");
        // 帷幕拉开. 把场景显示出来.
        primaryStage.show();
    }

    public static void main (String[] args) {
        launch(args);
    }
}

效果展示

文件检索效果

关于Win搜索太慢我自己写了一个Everything-LMLPHP

联系作者效果

关于Win搜索太慢我自己写了一个Everything-LMLPHP

代码获取

Gitee链接:点击跳转Gitee

总结

制作这个everything中遇到了不少的bug以及坎坷 但是这些困难 正是最好的学习的点 发现了很多自己的不足 经过了很多的努力 但是看这完成的软件 还是成就感满满的 也学习到了很多新的东西


关于打包exe文件的内容另外做了一篇博客点击跳转

06-20 05:43