项目:
仿照 everthing 设计文件搜索引擎,相对于 everthing 支持跨平台的使用,这是因为Java是跨平台的。
原因:
功能简单,主要是阶段性验收学习成果,以及练习多线程编程
使用的环境和工具:
🐟 选用数据库:SQLite (主要是小(几MB),轻量化,嵌入式的数据库,文件搜索系统在实现过程中只需要调用一张表)
🐟 选择环境语言:Java8 + JavaFX
🐟 项目管理工具:maven,方便进行第三方 jar 包的导入和管理,方便对当前项目的整个生命周期的掌握。
🐟 插件:lombok
实现功能:
🐳 选择文件夹多线程扫描该文件夹下的子文件,展示文件的名称、路径、大小和修改时间。
🐳 选择路径后,支持搜索相关文件内容,支持文件部分名、全拼或者是首字字母,支持模糊查询。
🐳 文件夹扫描完毕之后,显示搜索的所有文件以及文件夹个数,以及查询时间
分隔符🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀分隔符
分隔符🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀分隔符
准备阶段:
💧 依赖添加
💧 数据库创建和连接
💧 前端页面设计
第一天
💧 依赖添加
🙋 🙋 问题一:🙋 🙋
文件名有可能是中文的,所以想要实现文件名搜索,需要把汉字字符转换成字母,但是汉字是存在多音字的,所以需要一个工具类,帮我们把中文字符转换为字母字符串的形式,这样就可以支持模糊查找。
e.g. 七七冲鸭 ➡️ qiqichongya / qqcy
<!--汉语言拼音处理工具-->
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
工具类的代码设计:
package util;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
/**
* @Author qiqichongya
* @Date 2022/8/26 18:28
* @PackageName:util
* @ClassName: PinyinUtil
* @Description: 拼音工具类
* 将及汉语拼音的字符映射到字母字符串中
*/
public class PinyinUtil {
// 定义汉语拼的配置:全局变量,必须时全局唯一的且在定义时就要初始化
private static final HanyuPinyinOutputFormat FORMAT;
static {
// 当 PinyinUtil类加载时执行静态代码块,除了可以产生对象外,还可以进行一些配置相关的工作
FORMAT = new HanyuPinyinOutputFormat();
// 设置转换之后的英文字母都是英文小写字母
FORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE);
}
/**
* 传入任意的文件名称,就能将该文件名称转为字母字符串全拼和首字母小写字符串
* 若文件名是由中文和英文或数字组成,只需要转换中文即可。
* e.g. 七七chongya666 --> qiqichongya666 / qqchongya666
*
* @param fileName
* @return
*/
public static String[] getPinyinByFileName(String fileName) {
// 第一个字符串用来存放文件名全拼
// 第二个字符串用来存放首字母
String[] ret = new String[2];
// 全拼
StringBuilder allNameSpells = new StringBuilder();
// 首字母
StringBuilder firstCaseSpells = new StringBuilder();
for (char c : fileName.toCharArray()) {
try {
String[] pinYins = PinyinHelper.toHanyuPinyinStringArray(c, FORMAT);
if (pinYins == null || pinYins.length == 0) {
// 说明碰到非中文的字符,保留
allNameSpells.append(c);
firstCaseSpells.append(c);
} else {
// 碰到了中文字符
allNameSpells.append(pinYins[0]);
firstCaseSpells.append(pinYins[0].charAt(0));
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
allNameSpells.append(c);
firstCaseSpells.append(c);
e.printStackTrace();
}
}
ret[0] = allNameSpells.toString();
ret[1] = firstCaseSpells.toString();
return ret;
}
}
🙋 🙋 问题二:🙋 🙋
怎末导入SQLite数据库,以及连接操作?三步走战略:创建 + 连接 + 执行 +(关闭)
首先在maven中导入SQLite依赖:
<!-- SQLite数据库-->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.36.0.3</version>
</dependency>
创建SQLite数据源:
创建数据库连接
A、拿到SQLite数据源a、配置数据库的欢号窟码、曰期格式等
b、配置本和上数据库的路经和uKL
/** 对于SQLite来说,没有服务端和客户端,因此只需指定SQLite数据的地址就行了 **/
B、拿到数据库连接(从数据源拿到数据库连接)
C、执行 Statement 的查询或更新方法。
executeuery() : resultset对象,存储诸返回信息
executeupdata ():int 执行更新后的修改的行数
D、关闭连接释放资源 (statement 或 preparedstatement)
需要注意的点:
1. 因为配置数据源的URL是SQLite子类中独有的方法,所以要向下转型
2. 由于SQLite不像MySql那样由定义好的日期格式,所以这里需要手动定义一下日期格式。
package util;
import org.sqlite.SQLiteConfig;
import org.sqlite.SQLiteDataSource;
import javax.sql.DataSource;
import java.io.File;
import java.sql.*;
/**
* @Author qiqichongya
* @Date 2022/7/17 15:44
* @PackageName:util
* @ClassName: DataBaseUtil
* @Description: SQLite数据库的工具类,创建数据源,创建数据库的连接
* 只向外部提供SQLite数据库的连接即可,数据源不提供(封装在工具类的内部)
*/
public class DataBaseUtil {
/**
* 数据源
*/
private volatile static DataSource DATASOURCE;
/**
* 数据库连接
*/
private volatile static Connection CONNECTION;
/**
* 获取数据源
* 考虑到多线程场景下,SQLite要满足多个线程使用同一个数据库连接
* 这里采用double-check单例模式获取数据库连接对象
* @return
*/
public static DataSource getDataSource() {
if (DATASOURCE == null) {
// 考虑到多线程场景下,只能有一个线程能进入同步代码块
synchronized (DataBaseUtil.class) {
if (DATASOURCE == null) {
// 设置SQLite
SQLiteConfig config = new SQLiteConfig();
// 设置时间格式
config.setDateStringFormat(Util.DATE_PATTERN);
// 防止其他线程回复执行后多次创建单例对象,所以在同步代码中判断 DATASOURCE 为空后再实例化。
DATASOURCE = new SQLiteDataSource(config);
// 这里是因为配置数据源的URL是SQLite子类中独有的方法,所以要向下转型
((SQLiteDataSource) DATASOURCE).setUrl(getUrl());
}
}
}
return DATASOURCE;
}
/**
* 获取数据库连接
* 考虑到多线程场景下,SQLite要满足多个线程使用同一个数据库连接
* 这里采用double-check单例模式获取数据库连接对象
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
if (CONNECTION == null) {
// 考虑到多线程场景下,只能有一个线程能进入同步代码块
synchronized (DataBaseUtil.class) {
if (CONNECTION == null) {
// 获取数据源的数据库连接
CONNECTION = getDataSource().getConnection();
}
}
}
return CONNECTION;
}
/**
* 配置SQLite数据库的地址
* 对于SQLite数据库来说,没有服务端和客户端,因此只需要指定SQLite数据库的地址即可。
*
* @return
*/
private static String getUrl() {
// 大家一定记得改为自己电脑的路径!!
String path = "D:\\ideaworkpace\\search_java\\target";
String url = "jdbc:sqlite://" + path + File.separator + "search_java.db";
System.out.println("获取数据库的连接为 : " + url);
return url;
}
/**
* 数据库释放资源
* @param statement
*/
public static void close(Statement statement) {
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
/**
* 数据库释放资源
* @param preparedStatement
* @param resultSet
*/
public static void close(PreparedStatement preparedStatement, ResultSet resultSet) {
close(preparedStatement);
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) throws SQLException {
System.out.println(getConnection());
}
}
SQLite日期格式类:
package util;
/**
* @Author qiqichongya
* @Date 2022/7/17 15:53
* @PackageName:util
* @ClassName: Util
* @Description: TODO 通用的工具类
*/
public class Util {
/**
* SQLite没有默认的日期格式,需要手动配置一下
*/
public static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
}
🙋 🙋 问题三:🙋 🙋
简化操作,引入lombok开发包。这是现在基本每个项目必备的开发包。普通的Java类,都需要写一大堆的getter,setter,toString,hashCode,equals等等一大堆的代码,而使用这个库可以直接几个注解,注入这些代码。没有Lombok之前的代码特别冗余,加入lombok之后就会很简洁。
添加Lombok依赖:
<!--lombok库-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
分隔符🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀分隔符
分隔符🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀分隔符
第二天
💧 数据库创建和连接
🙋 🙋 问题四:🙋 🙋
当用户选择文件之后,需要展示文件的名称、路径、大小和修改时间。这就需要使用一张数据表来保存信息,需要保存信息有:
* 绿色:是要展示的信息 * 浅红色:是创建数据表要用到的信息
文件名:name
文件路径:path
文件大小:size
最后修改时间:last_modified
该文加是否是文件夹:is_directory
文件字母全拼:pinyin
文件全拼首字母:initials
创建数据表:
在resources路径下创建 init.sql文件。每次启动的时候重新创建 file_meta 表,
在表头 -- drop table if exists file_meta; 之后在代码中处理 “--” 注释。
-- 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),
initials varchar(50)
);
在使用文件输入流读入 init.sql 文件资源时有,有三种方式:
🐄 InputStream inputStream = new FileInputStream("init.sql");
这种方式需要注意的是,使用的是相对路径,但是这样写是找不到资源的,因为默认init.sql这个资源必须是要和工程目录在同一级下。如图1
-------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------
🐏InputStream inputStream = new FileInputStream("src/main/resources/init.sql");
相对路径写法。这样写也有弊端,那就是在打成 jar 包之后,依旧会报错找不到文件,因为 jar在 target 下,init.sql 在 src 下,它们就不在一个路径下。如图2
-------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------
🐇 InputStream inputStream = DataBaseUtil.class.getClassLoader(). getResourceAsStream("init.sql");
所有源文件打包之后会被编译到target目录下的classes文件下,所以采用类加载器的方式引入资源文件,JVM在加载类的时候用到ClassLoader类,getClassLoader()方法是获得编译后的classes目录,因为打包之后所有的resources文件都放到了该文件下。如图3
-------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------
第三种方法是处理相对路径的通用写法,体现了Java的可以移植性。
数据库初始化代码设计:
package util;
import java.io.FileInputStream;
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;
/**
* @Author qiqichongya
* @Date 2022/7/17 15:38
* @PackageName:util
* @ClassName: DataBaseInit
* @Description: 数据库初始化
*/
public class DataBaseInit {
/**
* 在页面初始化之前要先初始化数据库,先创建数据表
*/
public static void init() {
Connection connection = null;
Statement statement = null;
try {
// 拿到数据库连接
connection = DataBaseUtil.getConnection();
// 拿到 .sql 文件中的内容
List<String> sqls = readSQL();
// statement预编译SQL。
statement = connection.createStatement();
// 此时要执行多个sql,所以采用的是Statement,而不是PrepareStatement
for (String sql : sqls) {
// 执行SQL操作。
statement.executeUpdate(sql);
}
} catch (SQLException e) {
System.out.println("数据库初始化失败!");
e.printStackTrace();
} finally {
// 无论数据库是否初始化成功都要关闭释放资源
DataBaseUtil.close(statement);
}
}
/**
* 从resources路径下读取 .sql 文件,并加载到程序中
*
* @return
*/
public static List<String> readSQL() {
// 创建一个 List 对象 用来存放 .sql 文件中的内容。
List<String> list = new ArrayList<>();
// 要获取 .sql 文件中的内容需要通过文件 IO 操作来实现
// 绝对不能出现绝对路径
// 采用类加载器的方式引入资源文件,JVM在加载类的时候用到ClassLoader类,
// getClassLoader()方法是获得编译后的classes目录,因为打包之后所有的resources文件都放到了该文件下
InputStream inputStream = DataBaseUtil.class.getClassLoader().
getResourceAsStream("init.sql");
// 对于输入流,采用Scanner类来处理
Scanner scanner = new Scanner(inputStream);
// 按照 ";" 分隔,自定义分隔符
scanner.useDelimiter(";");
while (scanner.hasNext()) {
// 因为 nextLine 是默认碰到换行符自动分隔,所以在这里采用 next按照自定义的分隔符分隔。
String str = scanner.next();
if ("".equals(str) || "\n".equals(str)) {
continue;
}
if (str.contains("--")) {
// 用空字符串替换掉 --
str = str.replace("--", "");
}
list.add(str);
}
return list;
}
}
💧 前端页面设计
前端界面的设计:
在resources目录下创建一个.fxml 文件,采用JavaFX设计一个前端的简易界面,以供可视化操作。
前端可视化界面:
前端界面代码设计:
<?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="160" text="修改时间">
<cellValueFactory>
<PropertyValueFactory property="lastModifiedText"/>
</cellValueFactory>
</TableColumn>
</columns>
</TableView>
</children>
</GridPane>
小结:
前期的准备工作已经完成,接下来就是具体的实现功能,主要是集中在后端功能的完善和升级。
分隔符🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀分隔符
分隔符🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀分隔符
实现功能阶段:
第三天
问题五:
由于每次先前端页面显示文件信息,都要从数据库中拿到数据,所以为了我们需要专门设计一个类,用来和数据库打交道,最终程序中获取数据库的记录就通过该类来描述。
数据库对象类设计:
这个类就对应我们数据库中的表名,数据表中的一行记录就对应这个类的一个对象。则数据表的所有内容就对应 FileMeta 这个类的对象数组
特别注意的是:
1.在这种数据表到类的映射关系中,基本类型统一使用包装类,因为基本类型有默认值,在之后的处理中会比较麻烦。
2.因为 pinyin 和 pinYinFirst 这两个变量并不是每一个对象都需要,到但是其他的对象是每一个都会有的,所以在这里有参构造就排除了 pinyin 和 pinYinFirst。
package app;
import com.sun.xml.internal.ws.spi.db.DatabindingException;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.Date;
/**
* @Author qiqichongya
* @Date 2022/7/17 15:29
* @PackageName:app
* @ClassName: FileMeta
* @Description:
*
* 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)
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public class FileMeta {
private String name;
private String path;
private Boolean isDirectory;
private Long size;
private Date 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;
}
private String pinyin;
private String pinYinFirst;
}
连接控制前端的类设计:
前端界面创建好之后,我还需要在创建一个类来控制前端的一些操作,比如选择文件的butten按钮等。这里创建一个控制类:Controller 用来点击选择目录,最终界面上就会展示选择的文件夹下内容。这里需要注意一定要和前端页面对应,所以要先创建一个 app 包,在 app 包下创建Controller。
此外,app.fxml 中的 fx:id 的名称要和 app.Controller 类中的属性名称完全一致,这样的话界面中的内容才会正确的被 Controller 类所接收。
要实现的功能:
😸 在系统界面初始化之前,要先初始化数据库。
调用DataBaseInit.init();
😸 点击选择目录,最终界面上就会展示选择的文件夹下内容。
需要一个FileScanner类,用来文件扫描。
其中文件类型、大小、修改时间需要进行特殊处理再展示。
😸 显示当前选择的文件路径
把当前文件路径放入文本:this.srcDirectory.setText(path);
😸 刷新表格数据
创建一个 freshTable() 方法。
package app;
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 util.DataBaseInit;
import java.io.File;
import java.net.URL;
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;
@Override
public void initialize(URL location, ResourceBundle resources) {
// 在系统界面初始化之前,要先初始化数据库。
DataBaseInit.init();
// 添加搜索框监听器,内容改变时执行监听事件
searchField.textProperty().addListener(new ChangeListener<String>() {
@Override
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();
// 每次选择文件之后显示当前路径
this.srcDirectory.setText(path);
// 实例化文件扫描对象
FileScanner fileScanner = new FileScanner();
// 传入选择路径
fileScanner.scan(file);
// TODO
}
// 刷新表格数据
private void freshTable() {
ObservableList<FileMeta> metas = fileTable.getItems();
metas.clear();
// TODO
}
}
文件扫描类的设计:
思路:
😹 因为文件有文件夹和其他文件之分,说首先需要创建 fileNum 和 directoryNum 来分别记录当前扫描到的文件个数 和 当前扫描到的文件夹个数。
😹 再使用 List 集合来保存 所有扫描到的文件信息。
😹 创建一个 scan 扫描方法,传入文件路径用来。
先判断边界条件:判断传入路径中是否为空
再 File[ ] 类型的对象调用 listFiles() 方法获取当前目录下所有file对象。
这里需要递归,因为文件夹下可能还有文件夹
遍历 File[ ] 对象,判断是否是文件夹 isDirectory()
注意:
👹 此时最开始扫描的根路径没有统计,因此初始化文件夹的个数为1,表示从根目录下开始进行扫描任务。
👹 同时,该类只添加了 @Getter,没有 @setter 所以程序外部没有办法修改属性。
👹 设置最后修改时间时,lastModified 是一个长整型,需要转换为 Date 类型
package task;
import app.FileMeta;
import lombok.Getter;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Author qiqichongya
* @Date 2022/7/22 17:45
* @PackageName:task
* @ClassName: FileScanner
* @Description: 文件扫描类
*/
@Getter
public class FileScanner {
/**
* 当前扫描到的文件个数
*/
private int fileNum;
/**
* 当前扫描到的文件夹个数
*/
private int directoryNum;
/**
* 保存所有扫描到的文件信息
*/
List<FileMeta> fileMetas = new ArrayList<>();
/**
* 根据传入的文件夹进行扫描任务。
*
* @param filePath
*/
public void scan(File filePath) {
// 判断传入路径中是否为空
if (filePath == null) {
return;
}
// 获取当前目录下所有file对象。
File[] files = filePath.listFiles();
// 遍历files数组,根据files数组中的对象是否为文件夹继续递归操作。
for (File file : files) {
FileMeta meta = new FileMeta();
if (file.isDirectory()) {
// 是文件夹,就不设置大小了
directoryNum++;
setCommenFiled(file.getName(), file.getPath(), true, file.lastModified(), meta);
fileMetas.add(meta);
// 需要递归扫描
scan(file);
} else {
// 文件有大小
meta.setSize(file.length());
setCommenFiled(file.getName(), file.getPath(), false, file.lastModified(), meta);
fileMetas.add(meta);
// 不是文件夹
fileNum++;
}
}
}
/**
* 把扫描到的文件和文件夹通用的属性添加到FileMeta的集合中
* @param name
* @param path
* @param isDirectory
* @param lastModified
* @param meta
*/
private void setCommenFiled(String name, String path, boolean isDirectory, long lastModified, FileMeta meta) {
meta.setName(name);
meta.setPath(path);
meta.setIsDirectory(isDirectory);
// 设置最后修改时间,但是 lastModified 是一个长整型,此处需要转换为Date类型
meta.setLastModified(new Date(lastModified));
}
}
做完以上类的设计之后,运行程序会发现展示框中,文件类型、大小、修改时间这三个属性并没有显示,这是因为前端的 JavaFX 页面 和后端定义的属性名不一致导致的,在这里它们需要进行特殊处理再展示。比如:
文件类型:不能展示 true / false
文件大小:要根据文件的大小来展示不同的单位,如 B,KB,MM,GB等
修改时间:需要根据年月日时分秒格式化展示。
所以不能直接使用FileMeta中的属性值,需要做一些特殊的处理展示。首先需要改造下FileMeta类。
FileMeta类设计:
package app;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import util.Util;
import java.util.Date;
/**
* @Author qiqichongya
* @Date 2022/7/17 15:29
* @PackageName:app
* @ClassName: FileMeta
* @Description: 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)
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public class FileMeta {
private String name;
private String path;
private Boolean isDirectory;
private Long size;
private Date lastModified;
// 以下三个属性需要在界面中展示,将当前属性值做处理之后展示
// 这些属性名要和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;
}
private String pinyin;
private String pinYinFirst;
}
接下就是构造处理 文件类型、大小、修改时间的方法。此方法在通用工具类 Util 中构造。
通用工具类设计:
package util;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Author qiqichongya
* @Date 2022/7/17 15:53
* @PackageName:util
* @ClassName: Util
* @Description: TODO 通用的工具类
*/
public class Util {
/**
* SQLite没有默认的日期格式,需要手动配置一下
*/
public static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 根据传入的文件大小返回不同的单位
* 支持显示 B,KB,MM,GM
*
* @param size
* @return
*/
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);
}
}