feat:设新增系统文件搜索功能

This commit is contained in:
czhqwer
2025-04-01 01:43:24 +08:00
parent 81aa02e638
commit eabdb649f1
6 changed files with 452 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
package cn.czh.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 任务队列大小
executor.setThreadNamePrefix("Async-Task-Executor-");
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,42 @@
package cn.czh.controller;
import cn.czh.base.Result;
import cn.czh.service.IFileSearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@Slf4j
@RequestMapping("/fileSearch")
@RestController
public class FileSearchController {
@Resource
private IFileSearchService fileSearchService;
/**
* 获取系统磁盘列表
*/
@GetMapping("/getDrives")
public Result<?> getDrives() {
return Result.success(fileSearchService.getDrives());
}
/**
* 根据指定的磁盘列表构建文件索引
*/
@PostMapping("/buildIndex")
public Result<?> buildIndex(@RequestBody List<String> drives) {
return Result.success(fileSearchService.buildIndex(drives));
}
/**
* 根据文件名称模糊查询
*/
@GetMapping("/search")
public Result<?> search(@RequestParam String keyword) {
return Result.success(fileSearchService.searchFiles(keyword));
}
}

View File

@@ -0,0 +1,25 @@
package cn.czh.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 文件索引构建结果类
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class IndexResult {
private long indexTime; // 索引构建耗时(毫秒)
private List<String> indexedDrives; // 已索引的磁盘列表
private String memoryUsage; // 内存占用MB格式化为字符串
private long fileCount; // 索引文件总数
}

View File

@@ -0,0 +1,15 @@
package cn.czh.service;
import cn.czh.dto.IndexResult;
import java.util.List;
public interface IFileSearchService {
List<String> getDrives();
IndexResult buildIndex(List<String> drives);
List<String> searchFiles(String keyword);
}

View File

@@ -0,0 +1,79 @@
package cn.czh.service.impl;
import cn.czh.dto.IndexResult;
import cn.czh.service.IFileSearchService;
import cn.czh.utils.FileSearchUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class FileSearchServiceImpl implements IFileSearchService {
@Resource
private FileSearchUtil fileSearchUtil;
@PreDestroy
public void destroy() {
log.info("关闭 FileSearchUtil 资源...");
fileSearchUtil.shutdown();
}
@Override
public List<String> getDrives() {
List<File> availableDrives = fileSearchUtil.getAvailableDrives();
return availableDrives.isEmpty() ? Collections.emptyList() : availableDrives.stream().map(File::getAbsolutePath).collect(Collectors.toList());
}
@Override
public IndexResult buildIndex(List<String> driveNames) {
List<File> availableDrives = fileSearchUtil.getAvailableDrives();
if (availableDrives.isEmpty()) {
throw new RuntimeException("未检测到可用磁盘!");
}
List<File> selectedDrives = new ArrayList<>();
Set<String> selectedNames = new HashSet<>(); // 用于去重
for (String driveName : driveNames) {
if (driveName.isEmpty()) continue;
boolean found = false;
for (File drive : availableDrives) {
String normalizedDriveName = driveName.endsWith(File.separator) ? driveName : driveName + File.separator;
if (drive.getAbsolutePath().equalsIgnoreCase(driveName) ||
drive.getAbsolutePath().equalsIgnoreCase(normalizedDriveName)) {
if (selectedNames.add(drive.getAbsolutePath().toLowerCase())) {
selectedDrives.add(drive);
}
found = true;
break;
}
}
if (!found) {
log.warn("警告:无效的磁盘名 '{}',将被忽略", driveName);
}
}
if (selectedDrives.isEmpty()) {
throw new RuntimeException("未选择任何有效磁盘!");
}
long indexTime = fileSearchUtil.buildIndex(selectedDrives);
return new IndexResult(
indexTime,
selectedDrives.stream().map(File::getAbsolutePath).collect(Collectors.toList()),
String.format("%.2f", fileSearchUtil.getMemoryUsageMB()),
fileSearchUtil.getIndexedFileCount()
);
}
@Override
public List<String> searchFiles(String keyword) {
return fileSearchUtil.search(keyword);
}
}

View File

@@ -0,0 +1,269 @@
package cn.czh.utils;
import cn.czh.base.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
/**
* 文件搜索工具类,使用 ForkJoinPool 实现高效并行文件索引和搜索
*/
@Component
public class FileSearchUtil {
private static final Logger log = LoggerFactory.getLogger(FileSearchUtil.class);
private final Map<String, ConcurrentLinkedQueue<String>> fileIndex = new ConcurrentHashMap<>();
private final ForkJoinPool forkJoinPool;
private volatile boolean isIndexed = false;
/**
* 构造函数,初始化线程池
*
* @param threadMultiplier 线程数倍乘因子,默认使用 CPU 核心数 * 2
*/
public FileSearchUtil(int threadMultiplier) {
this.forkJoinPool = new ForkJoinPool(
Math.max(4, Runtime.getRuntime().availableProcessors() * threadMultiplier));
}
/**
* 默认构造函数,使用 CPU 核心数 * 2 作为线程数
*/
public FileSearchUtil() {
this(2);
}
/**
* 获取系统中所有可用磁盘列表
*
* @return 磁盘根目录列表
*/
public List<File> getAvailableDrives() {
File[] roots = File.listRoots();
return roots != null ? Arrays.asList(roots) : Collections.emptyList();
}
/**
* 根据指定的磁盘列表构建文件索引
*
* @param selectedDrives 要索引的磁盘列表
* @return 索引构建所用时间(毫秒)
* @throws IllegalArgumentException 如果传入的磁盘列表为空或无效
*/
public long buildIndex(List<File> selectedDrives) {
if (isIndexed) {
return 0; // 已构建过索引,直接返回
}
if (selectedDrives == null || selectedDrives.isEmpty()) {
throw new BusinessException("所选驱动器列表不能为空");
}
long startTime = System.currentTimeMillis();
List<Future<?>> tasks = new ArrayList<>();
for (File drive : selectedDrives) {
if (drive != null && drive.exists() && drive.isDirectory()) {
tasks.add(forkJoinPool.submit(new IndexTask(drive)));
}
}
if (tasks.isEmpty()) {
throw new BusinessException("没有选择索引的有效驱动器");
}
// 等待所有任务完成
for (Future<?> task : tasks) {
try {
task.get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new BusinessException("索引构建中断", e);
}
}
isIndexed = true;
return System.currentTimeMillis() - startTime;
}
/**
* 搜索文件(模糊匹配)
*
* @param keyword 搜索关键词
* @return 匹配的文件路径列表
*/
public List<String> search(String keyword) {
if (!isIndexed) {
throw new BusinessException("请先构建索引");
}
if (keyword == null || keyword.trim().isEmpty()) {
return Collections.emptyList();
}
String searchKey = keyword.toLowerCase();
return fileIndex.entrySet().stream()
.filter(entry -> entry.getKey().contains(searchKey))
.flatMap(entry -> entry.getValue().stream())
.collect(Collectors.toList());
}
/**
* 获取索引中的文件总数
*
* @return 文件总数
*/
public long getIndexedFileCount() {
return fileIndex.values().stream()
.mapToLong(ConcurrentLinkedQueue::size)
.sum();
}
/**
* 获取索引占用内存估计值MB
*
* @return 内存占用MB
*/
public double getMemoryUsageMB() {
return fileIndex.size() * 16.0 / 1024 / 1024;
}
/**
* 关闭线程池,释放资源
*/
public void shutdown() {
if (!forkJoinPool.isShutdown()) {
forkJoinPool.shutdown();
log.info("ForkJoinPool 已关闭");
}
}
// 内部索引任务类
private class IndexTask extends RecursiveAction {
private final File directory;
public IndexTask(File directory) {
this.directory = directory;
}
@Override
protected void compute() {
try {
File[] files = directory.listFiles();
if (files == null) return;
List<IndexTask> subTasks = new ArrayList<>();
for (File file : files) {
if (file.isDirectory()) {
IndexTask subTask = new IndexTask(file);
subTasks.add(subTask);
} else {
String fileName = file.getName().toLowerCase();
fileIndex.computeIfAbsent(fileName, k -> new ConcurrentLinkedQueue<>())
.add(file.getAbsolutePath());
}
}
if (subTasks.size() < 4) {
for (IndexTask subTask : subTasks) {
subTask.compute();
}
} else {
invokeAll(subTasks);
}
} catch (Exception e) {
log.error("访问目录失败: {}", directory, e);
}
}
}
// 交互式多选磁盘和搜索
/*public static void main(String[] args) {
FileSearchUtil searchUtil = new FileSearchUtil();
Scanner scanner = new Scanner(System.in);
// 1. 获取并显示所有磁盘
List<File> drives = searchUtil.getAvailableDrives();
if (drives.isEmpty()) {
System.out.println("未检测到可用磁盘!");
searchUtil.shutdown();
return;
}
System.out.println("可用磁盘列表:");
for (int i = 0; i < drives.size(); i++) {
System.out.println((i + 1) + ". " + drives.get(i).getAbsolutePath());
}
// 2. 提示用户输入磁盘名(支持多选,用逗号分隔)
System.out.print("请输入要搜索的磁盘名(多个磁盘用逗号分隔,例如 C:,D:");
String input = scanner.nextLine().trim();
// 3. 解析用户输入并选择磁盘
String[] driveNames = input.split("\\s*,\\s*"); // 支持带空格的逗号分隔
List<File> selectedDrives = new ArrayList<>();
Set<String> selectedNames = new HashSet<>(); // 用于去重
for (String driveName : driveNames) {
if (driveName.isEmpty()) continue;
boolean found = false;
for (File drive : drives) {
String normalizedDriveName = driveName.endsWith(File.separator) ? driveName : driveName + File.separator;
if (drive.getAbsolutePath().equalsIgnoreCase(driveName) ||
drive.getAbsolutePath().equalsIgnoreCase(normalizedDriveName)) {
if (selectedNames.add(drive.getAbsolutePath().toLowerCase())) {
selectedDrives.add(drive);
}
found = true;
break;
}
}
if (!found) {
System.out.println("警告:无效的磁盘名 '" + driveName + "',将被忽略");
}
}
if (selectedDrives.isEmpty()) {
System.out.println("未选择任何有效磁盘!");
searchUtil.shutdown();
scanner.close();
return;
}
// 4. 构建指定磁盘的索引
long indexTime = searchUtil.buildIndex(selectedDrives);
System.out.println("索引建立完成,用时:" + indexTime + "ms");
System.out.println("已索引磁盘:" + selectedDrives.stream()
.map(File::getAbsolutePath)
.collect(Collectors.joining(", ")));
System.out.println("索引内存占用:" + String.format("%.2f", searchUtil.getMemoryUsageMB()) + "MB");
System.out.println("索引文件总数:" + searchUtil.getIndexedFileCount());
// 5. 提示用户输入搜索关键词
System.out.print("请输入搜索关键词:");
String keyword = scanner.nextLine().trim();
// 6. 执行搜索
long startTime = System.currentTimeMillis();
List<String> results = searchUtil.search(keyword);
long searchTime = System.currentTimeMillis() - startTime;
// 7. 输出结果
System.out.println("搜索耗时:" + searchTime + "ms");
if (!results.isEmpty()) {
System.out.println("找到 " + results.size() + " 个匹配文件:");
results.forEach(System.out::println);
} else {
System.out.println("未找到文件:" + keyword);
}
// 8. 清理资源
searchUtil.shutdown();
scanner.close();
}*/
}