首个 Java 知识库项目:从启动到保存验收结果
学过 Java 类、方法和集合,能看懂 JSON,就可以跟做这一单元。你会启动一个本机 HTTP 服务,用接口创建文档,再检查回答引用的是哪个版本、当前身份有没有权读取,最后保存自己的结果。暂时不要求读懂整个 Spring Security 或检索实现。
先下载项目源码,完整解压。已有网站源码时,项目位于 examples/knowledge-service。项目总览保留源码阅读路径、源码摘要与原有测试报告。
先知道这次会运行什么
默认实现是 H2 文件数据库 + 本地词项检索 + 原文摘录,返回模式为 local-lexical-extractive。英文按词项、中文主要按相邻双字切分;问题词项覆盖率至少 0.6 才返回引用,分数不是模型置信度。正文整体作为一个检索单元,答案直接取首条引用的原文。词项重合不保证真正回答了问题。
本单元不用 Docker、PostgreSQL 或模型密钥,也不要加 -Pai。既有 pgvector 实测使用真实数据库与三维确定性 fixture,证明存储、过滤和版本处理;真实模型调用、语义质量和生成回答未验收。源码的可选 AI 模式也没有聊天模型生成步骤。
2026-11-06 的教学命令实跑与源码核对范围见文末。以下“预期”是你运行时要比较的目标,不代表你电脑上已经通过。
1. 准备环境,找到正确目录
本页使用 PowerShell 命令,本次实跑环境为 Windows 上的 PowerShell 7.6.9;Windows PowerShell 5.1 未实跑。打开终端 A,进入含 pom.xml 的 knowledge-service 目录。仓库用户可用 Set-Location 'D:/zhuomian/qiuzhiwang_pro/examples/knowledge-service';下载包用户换成自己的解压目录。
java -version
mvn.cmd -version
Test-Path ./pom.xml预期:Java 为 21,Maven 为 3.9+,最后输出 True。还要核对 Maven 输出中的 Java version / runtime,避免它实际使用另一个 JDK。第一次启动需要联网下载 Maven 依赖。
2. 启动服务,隔离端口和数据库
项目默认配置是 127.0.0.1:8085、jdbc:h2:file:./data/knowledge;DB_CLOSE_ON_EXIT=FALSE;WRITE_DELAY=0。本教程改用 18085,每次生成一个独立临时目录。数据库 URL 用环境变量覆盖,端口用 Spring Boot 运行参数覆盖;不需要修改配置文件。
在终端 A 依次执行:
$ErrorActionPreference = 'Stop'
# 只探测端口是否空闲,不关闭任何已有服务。绑定失败就先停在这里。
$probe = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 18085)
try { $probe.Start() } finally { $probe.Stop() }
$taskDir = Join-Path ([System.IO.Path]::GetTempPath()) ('knowledge-first-run-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $taskDir | Out-Null
$dbPath = (Join-Path $taskDir 'knowledge').Replace('\', '/')
$env:SPRING_DATASOURCE_URL = "jdbc:h2:file:$dbPath;DB_CLOSE_ON_EXIT=FALSE;WRITE_DELAY=0"
$taskDir # 记下此目录,数据库在这里,不在项目的 data 目录。
mvn.cmd -B -ntp spring-boot:run '-Dspring-boot.run.arguments=--server.port=18085 --spring.profiles.active=default'终端保持运行。预期日志显示 Tomcat 使用 18085、Hikari 的 H2 URL 指向刚才的临时目录,最后出现 Started Application。URL 不对就先停止本次启动再核对,避免写入旧库。这里显式使用运行 profile default,不要同时启用 ai。
打开终端 B,后续接口命令都在同一个终端 B 顺序执行,不要中途关闭,否则变量会丢失:
$ErrorActionPreference = 'Stop'
$base = 'http://127.0.0.1:18085/api'
$health = Invoke-RestMethod "$base/health"
$health预期为 status = UP。浏览器也可打开 http://127.0.0.1:18085/ 查看控制台;本单元的可复核结果由下面的接口命令取得。网站页面不会自动启动或连接你的本机服务。
3. 登录教学身份,确认服务端认定的角色
这些固定密码仅限本机教学。空数据库启动时会初始化以下用户;不会自动导入样例文档。
| 用户 | 教学密码 | 租户 | 角色 | 本单元用途 |
|---|---|---|---|---|
| admin | demo-admin-local | alpha | ADMIN | 创建、更新并读取受限文档 |
| editor | demo-editor-local | alpha | EDITOR | 对照权限收紧后的可见性 |
| reader | demo-reader-local | alpha | READER | 对照只读权限 |
| other | demo-other-local | beta | ADMIN | 对照跨租户隔离 |
minRole 表示文档最低访问级别。身份来自服务端会话,不由请求中的 tenant/role 字段决定。定义一个只把登录结果放进内存的函数,再分别登录:
function Get-DemoHeaders([string]$Name) {
$body = @{ username = $Name; password = "demo-$Name-local" } | ConvertTo-Json -Compress
$login = Invoke-RestMethod "$base/auth/login" -Method Post -ContentType 'application/json' -Body $body
return @{ Authorization = "Bearer $($login.token)" }
}
$adminHeaders = Get-DemoHeaders 'admin'
$editorHeaders = Get-DemoHeaders 'editor'
$readerHeaders = Get-DemoHeaders 'reader'
$otherHeaders = Get-DemoHeaders 'other'
$identities = @(
Invoke-RestMethod "$base/me" -Headers $adminHeaders
Invoke-RestMethod "$base/me" -Headers $editorHeaders
Invoke-RestMethod "$base/me" -Headers $readerHeaders
Invoke-RestMethod "$base/me" -Headers $otherHeaders
)
$identities | Format-Table username, tenant, role预期与表中四行一致。不要输出 $login 或任一 *Headers 变量,不要开请求转录或把认证头存入成果文件。token 是两小时有效的登录凭据,退出会撤销;页面控制台与本终端是分别登录的会话。
4. 创建 v1,读回自己写入的文档
为了让词项命中稳定且避开不同 PowerShell 版本的中文请求编码差异,这次先写一段英文教材。Virtual threads 是检索词,不要求你已经学过虚拟线程。
$createBody = @{
title = 'Java threads'
content = 'Virtual threads are suitable for blocking I/O tasks.'
minRole = 'READER'
} | ConvertTo-Json -Compress
$doc = Invoke-RestMethod "$base/documents" -Method Post -Headers $adminHeaders -ContentType 'application/json' -Body $createBody
$doc | Select-Object id, version, title, minRole, content
$readBack = Invoke-RestMethod "$base/documents/$($doc.id)" -Headers $adminHeaders
$readBack创建的契约是 HTTP 201;响应 id 是本次随机 UUID,version = 1、minRole = READER,读回内容应与输入一致。不要复制别人的 UUID。创建请求不要传 expectedVersion。重复执行创建会新增文档;若想完整重做,先停止自己的实例,再从第 2 步生成新临时库。
5. 查询,核对引用和原文
$queryBody = @{ question = 'virtual threads'; topK = 3 } | ConvertTo-Json -Compress
$v1 = Invoke-RestMethod "$base/search" -Method Post -Headers $adminHeaders -ContentType 'application/json' -Body $queryBody
$readerV1 = Invoke-RestMethod "$base/search" -Method Post -Headers $readerHeaders -ContentType 'application/json' -Body $queryBody
$v1 | ConvertTo-Json -Depth 6预期两次都能命中同一份 v1:mode = local-lexical-extractive、refused = false,本次独立库只有一条引用。citations[0].documentId 等于 $doc.id,version = 1,score = 1.0,quote 和 answer 都是 Virtual threads are suitable for blocking I/O tasks.。标题和正文参与词项匹配,引用的 quote 是完整正文。
这里验证的是“有可访问的词项证据时返回原文”,没有调用模型改写或生成解释。查询允许 topK 为 1–10,省略时为 5。
6. 更新到 v2,同时收紧访问级别
用同一个文档 ID 改正文,并将最低级别改为 ADMIN。只新增版本,不创建另一个文档。
$updateBody = @{
title = 'Java threads v2'
content = 'Virtual threads do not accelerate CPU-bound computation.'
minRole = 'ADMIN'
expectedVersion = $doc.version
} | ConvertTo-Json -Compress
$updated = Invoke-RestMethod "$base/documents/$($doc.id)" -Method Put -Headers $adminHeaders -ContentType 'application/json' -Body $updateBody
$updated | Select-Object id, version, minRole, content
$v2 = Invoke-RestMethod "$base/search" -Method Post -Headers $adminHeaders -ContentType 'application/json' -Body $queryBody
$v2 | ConvertTo-Json -Depth 6预期更新 HTTP 200,ID 不变、version = 2、minRole = ADMIN。admin 再查时引用版本为 2,answer 和 quote 都变为 Virtual threads do not accelerate CPU-bound computation.,不再返回 v1 的 I/O 原文。
expectedVersion 表示“我基于哪个版本修改”。旧值重放会得到 409;不要遇到冲突就盲目加 1,应先读回当前文档再判断是否继续修改。
7. 无证据时明确拒答
$unknownBody = @{ question = 'quasarxyz'; topK = 3 } | ConvertTo-Json -Compress
$noEvidence = Invoke-RestMethod "$base/search" -Method Post -Headers $adminHeaders -ContentType 'application/json' -Body $unknownBody
$noEvidence | ConvertTo-Json -Depth 6预期 HTTP 200,refused = true、citations = [],answer 为“没有找到当前身份可访问且足够相关的证据,拒绝作答。”。拒答是成功处理查询后的业务结果。这个样例只说明一个不匹配词项会拒答,不能证明系统识别了所有没有答案的问题。
8. 用原来的会话做权限对照
不重新登录,继续用第 3 步得到的身份查询同一个问题:
$readerV2 = Invoke-RestMethod "$base/search" -Method Post -Headers $readerHeaders -ContentType 'application/json' -Body $queryBody
$editorV2 = Invoke-RestMethod "$base/search" -Method Post -Headers $editorHeaders -ContentType 'application/json' -Body $queryBody
$otherV2 = Invoke-RestMethod "$base/search" -Method Post -Headers $otherHeaders -ContentType 'application/json' -Body $queryBody
@($readerV2, $editorV2, $otherV2) | Select-Object mode, refused, citations预期三次都是 refused = true、空引用。reader 原来能读 v1,当前 v2 收紧后不能回退到旧版;editor 也不能读 ADMIN 文档;other 虽是 ADMIN,却属于 beta 租户。
再检查直接读文档、只读用户写入、匿名访问和旧版本更新的状态码。下面的小函数只返回状态码,不打印请求头或错误响应全文:
function Get-ApiStatus {
param([string]$Path, [string]$Method = 'Get', [hashtable]$Headers = @{}, [string]$Body)
$request = @{ Uri = "$base$Path"; Method = $Method; Headers = $Headers; UseBasicParsing = $true; ErrorAction = 'Stop' }
if ($PSBoundParameters.ContainsKey('Body')) {
$request.ContentType = 'application/json'
$request.Body = $Body
}
try { return [int](Invoke-WebRequest @request).StatusCode }
catch {
if ($null -eq $_.Exception.Response) { throw '请求未取得 HTTP 响应,请检查服务和地址。' }
return [int]$_.Exception.Response.StatusCode
}
}
$status = [ordered]@{
readerReadV2 = Get-ApiStatus -Path "/documents/$($doc.id)" -Headers $readerHeaders
editorReadV2 = Get-ApiStatus -Path "/documents/$($doc.id)" -Headers $editorHeaders
otherReadV2 = Get-ApiStatus -Path "/documents/$($doc.id)" -Headers $otherHeaders
readerCreate = Get-ApiStatus -Path '/documents' -Method Post -Headers $readerHeaders -Body $createBody
anonymousMe = Get-ApiStatus -Path '/me'
staleUpdate = Get-ApiStatus -Path "/documents/$($doc.id)" -Method Put -Headers $adminHeaders -Body $updateBody
}
[pscustomobject]$status | Format-List| 对照请求 | 预期状态 | 原因 |
|---|---|---|
| reader / editor 直接读取 v2 | 404 / 404 | 当前文档对其不可见 |
| beta 的 other 直接读取同一 ID | 404 | 跨租户不暴露文档 |
| reader 创建文档 | 403 | 只读身份不能写 |
匿名读取 /me | 401 | 没有有效会话 |
| admin 重放基于 v1 的更新 | 409 | 当前版本已经是 2 |
搜索被过滤时是 HTTP 200 加拒答;按 ID 读取不可见文档则是 404。minRole 是文档级别,本步骤未修改用户角色。
9. 保存一份可以复核的成果
保存业务响应和身份信息,不保存登录响应、密码、token 或请求认证头。以下文件放在新临时成果目录,复制到自己的学习笔记目录后可以长期保留:
$outDir = Join-Path ([System.IO.Path]::GetTempPath()) ('knowledge-first-run-result-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $outDir | Out-Null
$result = [ordered]@{
recordedAt = (Get-Date).ToString('o')
baseUrl = $base
health = $health
identities = $identities
documentV1 = $doc
documentV2 = $updated
question = 'virtual threads'
adminV1 = $v1
readerV1 = $readerV1
adminV2 = $v2
noEvidence = $noEvidence
readerV2 = $readerV2
editorV2 = $editorV2
otherTenantV2 = $otherV2
httpStatus = $status
}
$result | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $outDir 'result.json') -Encoding UTF8
@'
本次运行的是 H2 / 本地词项检索 / 原文摘录。
请对照 result.json 填写:v1 和 v2 的 ID 是否相同?引用版本和正文如何变化?
reader 为何从可读变为拒答?搜索拒答和直接读取 404 有何不同?
只勾选自己实际核对过的结果;真实模型、生成质量和生产性能未验收。
'@ | Set-Content (Join-Path $outDir 'README.txt') -Encoding UTF8
$outDir完成标志是能用自己的 result.json 指出:v1/v2 的同一 ID 和不同正文、未知词项的空引用、reader 权限收紧前后的差异,以及六个状态码。数字与预期不符就记录实际值和第一条错误,不手动改成“通过”。
10. 退出并停止自己的实例
先在终端 B 撤销四个会话并移除内存中的认证头:
foreach ($h in @($adminHeaders, $editorHeaders, $readerHeaders, $otherHeaders)) {
Invoke-RestMethod "$base/auth/logout" -Method Post -Headers $h | Out-Null
}
Remove-Variable adminHeaders, editorHeaders, readerHeaders, otherHeaders, h预期退出接口为 204。回终端 A 按 Ctrl+C 停止自己刚才启动的 Maven/Java 实例,再执行 Remove-Item Env:SPRING_DATASOURCE_URL 清除本终端的数据库覆盖。不要按进程名批量结束 Java,也不要停止其他端口的服务。数据库和成果目录可以先保留;临时目录可能被系统清理,需要保留的成果请另存。
下次若重用同一个数据库 URL,H2 会保留数据;重新执行第 2 步则是新库。重启持久化、删除墓碑、用户角色调整可作为下一轮练习,本教程的本次回放不把它们记成已测。
遇到失败时先查什么
| 现象 | 处理顺序 |
|---|---|
java / mvn.cmd 找不到,或 release 21 不支持 | 配好 JDK 21、Maven 3.9+ 的 PATH / JAVA_HOME,重开终端,再核对 mvn.cmd -version 的 runtime |
| 找不到 pom 或 Maven 提示无项目 | 切到解压后真正含 pom.xml 的目录,不在网站根目录运行 |
| Maven 下载失败 | 保留第一条下载错误,检查网络与本机 Maven 仓库/镜像配置;未启动成功就不继续接口步骤 |
| 18085 已占用 | 不停止已有服务;另选空闲端口,并同步修改启动参数与终端 B 的 $base。本次只验证了 18085 |
H2 文件被占用或日志仍指向 ./data/knowledge | 停止自己的实例,核对环境变量及临时路径,确保没有两个实例共用同一文件 |
| 连接被拒绝 | 检查终端 A 是否还在运行、是否已有 Started Application、URL 是否为 18085 |
| 登录失败或身份不符 | 核对教学密码、空库及 app.seed-demo;既有库初始化不会重置已有角色。本教程用新库避开历史状态 |
| 请求 400 | 核对字段拼写、合法枚举和 JSON;创建不传 expectedVersion,更新必须传当前版本;不额外加 tenant/role |
| 请求 401 | 会话可能过期或已退出,重新执行第 3 步;不要把 token 复制到地址栏 |
| v1 搜索拒答,或出现多条引用 | 核对正文/问题原样输入、登录身份和数据库 URL;是否重复创建或导入过样例?先重做独立空库,不降低阈值冒充成功 |
| 403 / 404 / 409 | 先对照第 8 步预期;admin 更新若意外冲突,读回当前版本再判断,不盲目重试 |
本页核验范围与下一步
2026-11-06 已读取项目 README、API.md、实际 controller、认证、检索实现和配置,并在原源码的临时副本中以 mvn.cmd spring-boot:run 实跑本页启动及第 2–10 步接口、成果保存和退出命令。使用 Java 21.0.12、Maven 3.9.16、PowerShell 7.6.9、18085 端口与临时 H2 文件库;v1/v2 原文、无证据拒答、权限搜索和六个状态码均与预期一致,退出后 admin 的旧会话请求为 401。已清理本次测试进程。没有执行整站构建、Maven 测试套件或 pgvector/真实模型测试。
本次教学回放不涵盖 Windows PowerShell 5.1、Linux/macOS、浏览器控制台操作、重启持久化、删除墓碑、用户角色调整、其他端口与故障注入;这些仅是源码说明或后续建议。独立记录位于仓库 docs/verification/first-run-guide.json 与 docs/verification/first-run-guide/README.md;原有源码摘要和生成实测片段仍在项目总览。本页不替换原有密封报告。
完成首轮后,对照七天练习扩展用例,用验收手册补上边界。沿 Java AI 路线继续读源码,资料入口补原理,再到求职入口整理自己的经历。仅运行和阅读就如实写学习实践;新增功能、修复与性能收益需要另外提供自己的证据。
