IntelliJ IDE 插件开发 | (八)PSI 进阶:构建自定义代码分析与重构工具
1. PSI 深度解析:理解代码的骨骼与血脉
当你用 IntelliJ IDEA 写代码时,有没有想过它为什么能准确识别UserController是个类、saveUser是个方法?这背后就是 PSI(Program Structure Interface)在发挥作用。简单来说,PSI 就像代码的 X 光机,能把 Java、XML 等文件解析成结构化的语法树。
举个例子,当你看到这段 MyBatis Mapper 接口:
public interface UserMapper { @Select("SELECT * FROM user WHERE id=#{id}") User findById(@Param("id") Long id); }PSI 会将其拆解为:
- PsiJavaFile(顶层文件)
- PsiClass(UserMapper 接口)
- PsiMethod(findById 方法)
- PsiAnnotation(@Select)
- PsiParameter(id 参数)
- PsiMethod(findById 方法)
- PsiClass(UserMapper 接口)
通过PsiViewer工具(在 IDEA 设置中启用),你能直观看到这种树形结构。我曾用这个工具排查过一个诡异问题:某次插件总把@Transactional注解识别成普通注释,最后发现是忘记在plugin.xml添加 Java 依赖。
2. 构建 Spring Data JPA 智能分析器
假设我们要开发一个插件,自动检查 Repository 接口与实体类的映射关系。核心步骤如下:
2.1 扫描 JPA 仓库接口
首先需要识别 Spring Data JPA 的 Repository 接口:
fun isJpaRepository(clazz: PsiClass): Boolean { return clazz.interfaceTypes.any { it.canonicalText == "org.springframework.data.jpa.repository.JpaRepository" } }2.2 提取实体类型信息
通过泛型参数获取关联的实体类:
val entityType = clazz.extendsListTypes .firstOrNull { it.canonicalText.startsWith("JpaRepository") } ?.parameters?.get(0)?.resolve() as? PsiClass2.3 验证映射完整性
检查实体类是否正确定义了 JPA 注解:
fun validateEntityMapping(entity: PsiClass): List<ProblemDescriptor> { val problems = mutableListOf<ProblemDescriptor>() if (!entity.hasAnnotation("javax.persistence.Entity")) { problems += createProblem(entity, "实体类缺少 @Entity 注解") } // 检查字段注解... return problems }我曾遇到一个典型场景:用户修改了实体类字段名但忘记更新@Column注解名称,通过这种检查可以自动生成快速修复(Quick Fix)建议。
3. 实现自动化重构引擎
PSI 修改操作需要遵循"先获取修改对象,再在 WriteCommandAction 中执行"的原则。比如批量重命名 MyBatis Mapper 方法及其对应的 XML 节点:
3.1 创建安全重构操作
fun renameMapperMethod(method: PsiMethod, newName: String) { val project = method.project WriteCommandAction.runWriteCommandAction(project) { // 重命名Java方法 method.setName(newName) // 查找对应的XML节点 val mapperXml = findMapperXml(method) mapperXml?.accept(object : XmlRecursiveElementVisitor() { override fun visitXmlAttribute(attr: XmlAttribute) { if (attr.name == "id" && attr.value == method.name) { attr.valueElement?.replace( createXmlAttributeValue(project, newName) ) } } }) } }3.2 处理级联修改
当修改实体类字段时,需要同步更新所有相关 SQL:
fun renameEntityField(field: PsiField, newName: String) { val usages = ReferencesSearch.search(field).findAll() WriteCommandAction.runWriteCommandAction(field.project) { field.setName(newName) usages.forEach { ref -> when (val element = ref.element) { is PsiJavaToken -> handleJavaUsage(element) is XmlAttributeValue -> handleXmlUsage(element) } } } }有个实际案例:某次重命名user_name为username时,插件自动更新了 23 个 XML 文件和 15 个 Java 文件中的引用,比手动操作节省了 15 分钟。
4. 高级 PSI 遍历技巧
4.1 使用递归访问者模式
深度遍历 PSI 树时,推荐使用PsiRecursiveElementVisitor。比如收集所有 Lombok 注解:
class LombokAnnotationCollector : PsiRecursiveElementVisitor() { val annotations = mutableListOf<PsiAnnotation>() override fun visitAnnotation(annotation: PsiAnnotation) { if (annotation.qualifiedName?.startsWith("lombok") == true) { annotations.add(annotation) } super.visitAnnotation(annotation) } } // 使用方式 val collector = LombokAnnotationCollector() psiFile.accept(collector)4.2 优化遍历性能
对于大型项目,需要注意:
- 使用
PsiElementVisitor而非直接操作 AST - 对
PsiReference使用ReferencesSearch而非手动查找 - 缓存常用 PSI 查询结果
我曾优化过一个扫描 Spring Bean 的插件,通过引入缓存机制将分析时间从 12 秒降到了 1.3 秒。
5. 调试与问题排查实战
开发复杂 PSI 插件时,这些调试技巧很实用:
5.1 使用 PSI Viewer
在 IDEA 设置中启用idea.is.internal=true后,通过Tools > View PSI Structure可以实时查看当前文件的 PSI 结构。
5.2 断点技巧
- 在
PsiElement的accept方法设断点 - 使用
PsiElement的text属性检查内容 - 观察
PsiElement的children数组
5.3 常见问题处理
- PSI 与文件不同步:调用
PsiDocumentManager.getInstance(project).commitDocument(document) - 修改不生效:确保在
WriteCommandAction中执行写操作 - NPE 异常:检查
PsiElement是否有效(isValid)
记得有次插件在保存文件时崩溃,最后发现是忘记检查PsiFile的isWritable属性。现在我的代码里总会加上这样的防御性检查:
if (!psiFile.isValid || !psiFile.isWritable) { throw IllegalStateException("文件不可修改") }开发 PSI 插件就像在教 IDEA 理解特定领域的"方言",需要耐心但回报丰厚。当你看到自己开发的插件能帮团队减少重复劳动时,那种成就感绝对值得投入。