This commit is contained in:
2026-08-28 23:01:52 +08:00
parent 969ff05815
commit da32a67d7f
107 changed files with 465 additions and 271 deletions
+122
View File
@@ -0,0 +1,122 @@
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { dirname, join, relative, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptDir = dirname(fileURLToPath(import.meta.url))
export const repositoryRoot = resolve(scriptDir, '..')
function filesBelow(root, predicate) {
if (!statSafe(root)?.isDirectory()) return []
const result = []
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (entry.name === 'target' || entry.name === '.git') continue
const path = join(root, entry.name)
if (entry.isDirectory()) result.push(...filesBelow(path, predicate))
else if (predicate(path)) result.push(path)
}
return result
}
function statSafe(path) {
try { return statSync(path) } catch { return null }
}
function packageName(source) {
return source.match(/\bpackage\s+([\w.]+)\s*;/)?.[1] || ''
}
function mapperInterfaces(javaFiles) {
return javaFiles.flatMap((path) => {
const source = readFileSync(path, 'utf8')
const names = [...source.matchAll(/\b(?:public\s+)?interface\s+(\w*Mapper)\b/g)].map((match) => match[1])
return names.map((name) => ({ path, source, packageName: packageName(source), name }))
})
}
function mapperScanAnnotation(source) {
const start = source.indexOf('@MapperScan')
if (start < 0) return ''
const end = source.indexOf('\n', start)
if (source[start + '@MapperScan'.length] !== '(') return source.slice(start, end < 0 ? undefined : end)
let depth = 0
for (let index = start + '@MapperScan'.length; index < source.length; index += 1) {
if (source[index] === '(') depth += 1
else if (source[index] === ')' && --depth === 0) return source.slice(start, index + 1)
}
return source.slice(start)
}
function moduleNames(root) {
const pom = readFileSync(join(root, 'pom.xml'), 'utf8')
return [...pom.matchAll(/<module>([^<]+)<\/module>/g)].map((match) => match[1].trim())
}
export function auditMapperScanning(root = repositoryRoot) {
const failures = []
let mapperCount = 0
let persistenceModuleCount = 0
for (const moduleName of moduleNames(root)) {
const moduleRoot = join(root, moduleName)
const javaRoot = join(moduleRoot, 'src', 'main', 'java')
const javaFiles = filesBelow(javaRoot, (path) => path.endsWith('.java'))
const mappers = mapperInterfaces(javaFiles)
if (!mappers.length) continue
persistenceModuleCount += 1
mapperCount += mappers.length
const mapperNames = new Map()
for (const mapper of mappers) {
const previous = mapperNames.get(mapper.name)
if (previous) {
failures.push(`${moduleName}: 根包扫描下 Mapper Bean 名称重复 ${mapper.name}(${relative(root, previous)}、${relative(root, mapper.path)})`)
} else mapperNames.set(mapper.name, mapper.path)
if (!/@Mapper\b/.test(mapper.source)) {
failures.push(`${moduleName}: ${mapper.packageName}.${mapper.name} 缺少 @Mapper(${relative(root, mapper.path)})`)
}
}
const applications = javaFiles.filter((path) => /@SpringBootApplication\b/.test(readFileSync(path, 'utf8')))
if (applications.length !== 1) {
failures.push(`${moduleName}: 期望且只能有一个 @SpringBootApplication,实际 ${applications.length} 个`)
continue
}
const applicationPath = applications[0]
const applicationSource = readFileSync(applicationPath, 'utf8')
const rootPackage = packageName(applicationSource)
const scan = mapperScanAnnotation(applicationSource)
if (!scan) failures.push(`${moduleName}: 启动类缺少统一 @MapperScan`)
else {
if (!scan.includes(`"${rootPackage}"`)) failures.push(`${moduleName}: @MapperScan 必须覆盖服务根包 ${rootPackage}`)
if (!/annotationClass\s*=\s*Mapper\.class/.test(scan)) failures.push(`${moduleName}: @MapperScan 必须使用 annotationClass = Mapper.class`)
}
for (const path of javaFiles) {
if (path === applicationPath) continue
if (/@MapperScan\b/.test(readFileSync(path, 'utf8'))) {
failures.push(`${moduleName}: 禁止分散维护 MapperScan(${relative(root, path)})`)
}
}
for (const mapper of mappers) {
if (!(mapper.packageName === rootPackage || mapper.packageName.startsWith(`${rootPackage}.`))) {
failures.push(`${moduleName}: ${mapper.packageName}.${mapper.name} 不在服务扫描根包 ${rootPackage} 内`)
}
}
}
return { failures, mapperCount, persistenceModuleCount }
}
function main() {
const result = auditMapperScanning(repositoryRoot)
if (result.failures.length) {
console.error(`Mapper 扫描合同失败,共 ${result.failures.length} 项:`)
result.failures.forEach((failure) => console.error(`- ${failure}`))
process.exitCode = 1
return
}
console.log(`Mapper 扫描合同通过:${result.persistenceModuleCount} 个持久化模块、${result.mapperCount} 个 Mapper 均使用 @Mapper,并由服务根包注解过滤扫描。`)
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main()