首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
2,346 阅读
2
大模型“无脑复读”?3分钟搞懂原因+解决办法
967 阅读
3
Spring AI 无法获取大模型深度思考内容?解决方案来了
782 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
419 阅读
5
EasyExcel 实战:导出带图片的 Excel 完整方案
419 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
AI大模型&智能体
登录
Search
标签搜索
Spring AI
java虚拟机
JVM
Spring AI Alibaba
Java
保姆级教程
SpringBoot
Spring
MCP
WebFlux
龙虾
Agent Skills
大模型
OpenClaw
人工介入
Nginx
Ubuntu
Apache POI
自定义starter
Mybatis
Luca Ju
累计撰写
55
篇文章
累计收到
4
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
AI大模型&智能体
页面
关于这个博客
搜索到
7
篇与
的结果
2026-05-26
做共享目录实时同步,踩过这些坑
项目背景:我们有一个知识库平台,之前都是用户在平台上上传文档,然后执行解析向量数据库,用于后续执行RAG现在客户希望可以文档上传到共享目录,系统就可以自动把这些文件同步到知识库。问题:为什么不用轮询?最直白的方案是定时扫目录,对比前后两次的文件列表差异。这个方案写起来简单,但有几个硬伤:延迟高:扫描周期设 5 分钟,用户上传后平均要等 2.5 分钟才能感知到IO 浪费:目录下文件多的时候,每次全量扫一遍磁盘开销不小无法感知"修改":如果只是文件内容变了但大小没变,很难准确判断所以决定用 java.nio.file.WatchService 做事件驱动监听。WatchService 的基本用法WatchService 本质上是对操作系统底层文件事件通知的封装。Linux 下用的是 inotify,Windows 下是 ReadDirectoryChangesW。注册一个目录的监听很简单:WatchService watchService = FileSystems.getDefault().newWatchService(); Path dir = Paths.get("/share"); WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);然后在一个独立线程里 take() 阻塞等待事件:WatchKey key = watchService.take(); for (WatchEvent<?> event : key.pollEvents()) { // 处理事件 } key.reset(); // 必须 reset,否则后续事件收不到坑一:WatchService 不会递归监听子目录这是最容易被忽略的一点。dir.register(...) 只监听这一层目录,子目录下的文件变更完全收不到。我们的共享目录结构是 /share/知识库名/文件,层级不固定,用户还可能随时新建子目录。所以必须自己实现递归注册。方案是在启动时把整个目录树 walk 一遍,每个子目录都注册上:Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { register(dir); // 每个目录都注册监听 return FileVisitResult.CONTINUE; } });同时监听 ENTRY_CREATE 事件,当发现新建的是目录时,立刻给它补上注册:if (kind == ENTRY_CREATE && Files.isDirectory(child)) { registerAll(child); }坑二:目录创建时,里面的文件可能"漏事件"这是个很隐蔽的竞态条件。假设用户通过 cp -r 拷贝一整个目录进来,操作系统产生的事件顺序可能是:ENTRY_CREATE —— 新目录 /share/A/BENTRY_CREATE —— 目录下的文件 file1.txtENTRY_CREATE —— 目录下的文件 file2.txt但如果我们的处理逻辑在第 1 步给 /share/A/B 注册监听时稍微慢了一点,第 2、3 步的事件可能已经被操作系统发出,而我们还没注册上,于是漏掉了。解决办法是在注册新目录后,主动扫一遍目录里已有的文件,补发创建事件:if (Files.isDirectory(child)) { registerAll(child); emitCreateForExistingFiles(child); // 兜底补发 }private void emitCreateForExistingFiles(Path dir) { try (Stream<Path> pathStream = Files.walk(dir)) { pathStream.filter(Files::isRegularFile).forEach(eventHandler::onCreate); } }坑三:事件防抖文件写入往往不是原子操作。比如用户拷贝一个大文件,或者用户频繁修改文件,操作系统可能产生一连串的 MODIFY 事件。如果每个事件都触发一次上传,既浪费资源又容易出错。我们在事件处理器里加了基于时间窗口的去抖:String debounceKey = eventType + "|" + fullPath; long now = System.currentTimeMillis(); Long last = debounceMap.get(debounceKey); if (last != null && now - last < windowMillis) { return; // 同一个文件同类型事件,5 秒内只处理一次 } debounceMap.put(debounceKey, now);这里用 ConcurrentHashMap 保证线程安全,key 的设计是"事件类型 + 文件路径",这样创建和修改事件互不干扰。坑四:应用重启期间的事件丢失WatchService 是内存态的,应用一重启,期间发生的所有文件变更都感知不到了。这对于生产环境是不可接受的。我们的补偿方案是快照比对:每次补偿扫描时,把当前目录下所有文件的路径、大小、最后修改时间文件hash记录下来,存到 db下次扫描时加载上次的快照,和当前状态做 diff新出现的文件 → 补发 CREATE大小或修改时间变化的 → 补发 MODIFY快照里有但现在没了的 → 补发 DELETE快照存 MySQL 而不是本地文件,是为了在集群部署时也能共享状态(虽然当前是单实例,但预留了扩展空间)。补偿扫描有两个触发时机:启动时立即执行一次:覆盖重启期间的漏事件定时执行:默认 5 分钟一次,作为兜底@Scheduled(fixedDelayString = "${doc.share-sync.reconcile-delay-ms:300000}") public void scheduledReconcile() { reconcile("scheduled"); }整体架构整个同步模块拆成了四个组件,职责很清晰:组件职责ShareDirWatcher基于 WatchService 的实时监听,递归注册子目录ShareFileEventHandler事件去抖、路径解析(从文件路径反查知识库ID)、文件处理ShareDirReconcileJob停机补偿扫描,快照比对补发事件ShareFileSnapshotStore快照的持久化存储(MySQL)配置也做了开关控制,方便分阶段上线:doc: share-sync: enabled: true dry-run: true # 第一阶段只打日志,不真写 root-dir: /share settle-seconds: 5 # 防抖窗口 reconcile-delay-ms: 300000一些没写的细节OVERFLOW 事件:WatchService 可能因事件队列满而丢失事件,此时会收到一个 OVERFLOW 事件。我们的处理是直接跳过,依赖定时补偿扫描来修复。删除事件的路径:ENTRY_DELETE 事件发生时,文件已经没了,ev.context() 只能拿到文件名,不能判断它是不是目录。所以删除事件统一走 onDelete,由下游根据数据库记录判断之前是什么。重复上传风险: 监听程序和定时补偿程序同时发现新创建了文件,会出现重复上传的问题。处理方案是添加数据库唯一索引,直接从数据库兜底总结WatchService 来做文件监听是我目前实现的方案,递归注册、新目录兜底扫描、事件去抖、停机补偿,这几块缺一不可。第一阶段先跑 dry-run,把日志打全、路径解析逻辑验证清楚,第二阶段再放开真实写入,这样上线心里比较有底。
2026年05月26日
11 阅读
0 评论
1 点赞
2025-08-18
从零开始:Java 服务部署与 HTTPS 访问全攻略
前言在现代 Web 应用开发中,将 Java 服务部署到服务器并通过 HTTPS 安全访问是一项基础且重要的技能。本文将详细记录从环境准备到最终实现 HTTPS 访问的完整过程,适合新手开发者参考学习。一、JDK 环境安装Java 服务运行依赖 JDK 环境,我们选择安装开源免费的 OpenJDK 17 版本。1. 更新系统索引包首先需要更新 Ubuntu 系统的软件包索引,确保能获取到最新的软件版本信息:sudo apt update2. 安装 OpenJDK 17执行以下命令安装 OpenJDK 17:sudo apt install openjdk-17-jdk安装过程中系统会自动处理依赖关系,无需额外操作。3. 验证 JDK 安装结果安装完成后,通过以下命令验证是否安装成功:java -version如果安装成功,会显示类似如下的版本信息:openjdk version "17.0.x" 20xx-xx-xx OpenJDK Runtime Environment (build 17.0.x+xx-Ubuntu-1ubuntux) OpenJDK 64-Bit Server VM (build 17.0.x+xx-Ubuntu-1ubuntux, mixed mode, sharing)4. 配置 JAVA_HOME 环境变量为了让系统和其他工具能正确识别 JDK 位置,需要配置 JAVA_HOME 环境变量:echo "export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> ~/.bashrc source ~/.bashrc执行完成后,JAVA_HOME 环境变量将在每次登录时自动生效。二、Java 服务打包与部署完成 JDK 环境配置后,接下来需要将开发好的 Java 服务打包并上传到服务器。1. 服务打包使用 Maven 工具对 Java 项目进行打包,推荐通过 IDEA 的 Maven 插件操作:在 IDEA 右侧的 Maven 面板中找到项目展开 Lifecycle 选项双击 package 命令执行打包操作打包成功后,会在项目的 target 目录下生成对应的 JAR 文件2. 上传服务到服务器将打包好的 JAR 文件通过 SFTP 工具(如 Termius)或 scp 命令上传到服务器的指定目录,例如/opt/java/services/目录。3. 启动 Java 服务通过以下命令启动 Java 服务:nohup java -jar /opt/java/services/your-service.jar &其中&符号表示让服务在后台运行。如果需要更完善的服务管理,建议配置 systemd 服务。三、Nginx 配置实现 HTTPS 访问为了实现 HTTPS 访问并优化请求转发,我们需要配置 Nginx 作为反向代理服务器。1. 创建或编辑 Nginx 配置文件执行以下命令创建新的 Nginx 配置文件:sudo nano /etc/nginx/sites-available/your_domain.conf如果你已有配置文件,直接编辑对应文件即可。2. 配置 HTTPS 与反向代理在配置文件中添加以下内容,实现 HTTP 到 HTTPS 的跳转以及请求转发:server { listen 80; server_name yourdomain.com; # 替换为你的实际域名 # 将所有HTTP请求强制跳转至HTTPS return 301 https://$host$request_uri; } server { listen 443 ssl; server_name yourdomain.com; # 替换为你的实际域名 # SSL证书配置(请替换为你的证书实际路径) ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # SSL安全优化配置 ssl_protocols TLSv1.2 TLSv1.3; # 支持的TLS协议版本 ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_session_cache shared:SSL:10m; # SSL会话缓存设置 ssl_session_timeout 10m; # SSL会话超时时间 # 将所有 /app 开头的请求转发到Java服务的9999端口 location /app/ { proxy_pass http://127.0.0.1:9999; # 传递原始请求信息(HTTPS环境关键配置) proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 重要:传递HTTPS协议信息 # 超时设置(根据实际服务需求调整) proxy_connect_timeout 300s; proxy_read_timeout 300s; } # 其他路径处理 location / { # 可以根据需求配置为返回404或指向其他服务 return 404; } }3. 启用 Nginx 配置创建符号链接将配置文件添加到 sites-enabled 目录,使 Nginx 能够识别该配置:sudo ln -s /etc/nginx/sites-available/your_domain.conf /etc/nginx/sites-enabled/4. 检查并重启 Nginx配置完成后,先检查配置文件是否有语法错误:sudo nginx -t如果显示 "nginx: configuration file /etc/nginx/nginx.conf test is successful",说明配置无误,可以重启 Nginx 使配置生效:sudo systemctl restart nginx四、验证 HTTPS 访问完成以上所有步骤后,通过浏览器访问https://yourdomain.com/app/user/login(替换为你的实际域名和接口路径),如果能正常访问并获得服务响应,说明整个部署过程成功完成。常见问题排查如果无法访问 HTTPS 服务,先检查服务器防火墙是否开放 443 端口证书路径错误会导致 Nginx 启动失败,需确保证书文件存在且权限正确Java 服务未启动或端口被占用会导致 502 错误,可通过netstat -tlnp查看端口占用情况配置文件修改后未重启 Nginx 会导致配置不生效,记得每次修改后重启服务通过本文的步骤,你已经成功实现了 Java 服务的部署和 HTTPS 访问配置。在实际生产环境中,还可以根据需求添加服务监控、日志管理等功能,进一步提升服务的稳定性和可维护性。
2025年08月18日
44 阅读
0 评论
0 点赞
2023-05-25
java使用POI导出Excel单元格为数字类型
第一版开发的时候,所有的单元格都是文本类型,由于需求方需要导出Excel可以直接使用函数计算,所以需要改动一下,将导出的Excel设置为数值类型。创建XSSFCellStyle// 此处设置数据格式 XSSFDataFormat df = wb.createDataFormat(); // 创建单元格样式 XSSFCellStyle numberStyle = wb.createCellStyle(); numberStyle.setFillForegroundColor((short) 1); //设置要添加表背景颜色 numberStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); //solid 填充 numberStyle.setAlignment(XSSFCellStyle.ALIGN_CENTER); //文字水平居中 numberStyle.setVerticalAlignment(XSSFCellStyle.VERTICAL_CENTER);//文字垂直居中 numberStyle.setBorderBottom(BorderStyle.THIN); //底边框加黑 numberStyle.setBorderLeft(BorderStyle.THIN); //左边框加黑 numberStyle.setBorderRight(BorderStyle.THIN); // 有边框加黑 numberStyle.setBorderTop(BorderStyle.THIN); //上边框加黑 // 注意#,##0.00_ 后面有一个空格 numberStyle.setDataFormat(df.getFormat("#,##0.00_ ")); //为单元格添加背景样式 sheet=setTitleStyle(sheet,rowNum,colNum,style1);创建单元格,并且设置XSSFCellStyle。private static XSSFSheet setTitleStyle(XSSFSheet sheet,int rowNum,int colNum,XSSFCellStyle style){ for (int i = 0; i < rowNum; i++) { //需要行表格 Row row = sheet.createRow(i); //创建行 row.setHeight((short)600); for (int j = 0; j < colNum; j++) {//需要列 row.createCell(j).setCellStyle(style); } } return sheet; }在为单元格赋值的时候,将数据转换为Double类型。
2023年05月25日
36 阅读
0 评论
0 点赞
2023-04-14
使用Apache POI 设置单元格中文字方向
前几天遇到了一个需求,需要使用Apache POI导出Excel,并且还需要实现单元格合并和文字竖向展示的功能。最终结果是这个样子介绍一下合并单元格和文字竖向展示的实现方法。1、合并单元格只要知道需要合并单元格的行号和列号就可以//创建工作簿 XSSFWorkbook workBook = new XSSFWorkbook(); //创建一个sheet XSSFSheet sheet = workBook.createSheet(); CellRangeAddress balanceCell = new CellRangeAddress(0, 4, 1, 1); sheet.addMergedRegion(balanceCell);2、文字竖向显示获取到需要竖向显示的单元格,然后设置单元格样式,设置Rotation属性 //创建工作簿 XSSFWorkbook workBook = new XSSFWorkbook(); //创建一个sheet XSSFSheet sheet = workBook.createSheet(); CellStyle directionStyle = workBook.createCellStyle(); directionStyle.setRotation((short)255); XSSFRow row = sheet.getRow(0); XSSFCell cell = row.getCell(4); cell.setCellStyle(directionStyle);
2023年04月14日
37 阅读
0 评论
0 点赞
2020-05-25
MyBatis分页助手报错java.util.ArrayList cannot be cast to com.github.pagehelper.Page
最近在使用MyBatis时遇到了这问题原本可以正常使用分页,但是当我添加了一个查询条件时突然报出了这个错误,后来终于找到了原因。因为PageHelper.startPage(pageNum,pageSize)方法 只会对靠近的第一个查询语句进行分页。结果我新添加的查询条件又进行了一个select查询,所以会产生这个错误如:PageHelper.startPage(pageNum, pageSize, true);XXXMapper.xxxxPage<> page= (Page<>)TestMapper.query( );改为:XXXMapper.xxxxPageHelper.startPage(pageNum, pageSize, true);Page<> page= (Page<>)TestMapper.query( ); 或者:PageHelper.startPage(pageNum, pageSize, true);Page<> page= (Page<>)TestMapper.query( );XXXMapper.xxxx这样就不会报这个错了。 还是因为自己对这个分页助手不够了解呀。。。
2020年05月25日
22 阅读
0 评论
0 点赞
1
2