ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SonarQube插件开发:从5.5到7.x的PDF报告兼容实践

SonarQube插件开发:从5.5到7.x的PDF报告兼容实践 简介面向SonarQube插件开发者与代码质量团队的PDF报告生成插件源码覆盖5.5至7.x多个版本可解决代码分析结果难以直观共享、归档和跨项目传播的问题。工程以96个Java文件为逻辑主体承担报告生成、数据组装与SonarQube API对接等核心工作5个properties和3个xml用于扩展点注册与运行配置PNG/JPG图片、ttf字体及YML、LICENSE等资源共同构成完整项目全部121个文件打包为14.86MB的zip。已有307人学习下载。资源价值在于既可直接编译部署生成定制化PDF报告也可作为插件开发范本学习如何在多版本兼容前提下设计可维护的Java/Ruby混合架构源码中的目录结构与构建逻辑对理解SonarQube插件生命周期及报告自动化有明确参考意义。1. 为什么要在 SonarQube 5.5 到 7.x 上自己做 PDF 报告插件做过代码质量门禁的人都知道SonarQube 自带的 Web 界面和 REST API 能看趋势、查问题、导 CSV但客户和领导要的往往是「一份能直接归档的 PDF」。官方从 7.x 开始逐步把内置的 PDF 报告功能弱化社区插件大多停在某个旧版本新装的 7.x 实例根本找不到能用的现成插件。于是很多团队只能翻出老的 5.x 代码改一改或者从零写一个。这个标题的价值就在这里它要求你同时理解 SonarQube 的插件扩展点、版本间的 API 断代以及 PDF 生成这条完整的数据管线。适合读这篇文章的人是后台开发或 DevOps 工程师你已经知道 SonarQube 的基本用法但没写过插件或者写过一个内部工具但被版本兼容问题折磨过。我会以「源码设计」为线索从插件入口、敏感数据校验、PDF 渲染管线一直讲到跨版本回归验证。先给出一个反直觉的结论真正难的不是画 PDF而是处理好 SonarQube 5.5 到 7.x 之间 API 签名变更带来的 ClassNotFoundException。2. SonarQube 插件机制与版本兼容层从 5.5 到 7.x 的 API 变迁2.1 插件生命周期与扩展点为什么报告插件挂在 Batch 侧SonarQube 插件有四种运行容器Server、Batch、Compute Engine、Scanner。PDF 报告插件必须挂在 Server 侧因为只有 Server 才能访问聚合后的 Measure 和 Issue 数据。常见的做法是定义一个Plugin实现类在define方法里注册一个Page扩展点这个 Page 会在项目仪表盘的「更多」菜单里出现用户点击后触发 Servlet 生成 PDF。public final class PdfReportPlugin implements Plugin { Override public void define(Context context) { context.addExtensions( PdfReportPage.class, PdfReportServlet.class, PdfReportService.class ); } }这里有三个扩展点对应三类职责PdfReportPage负责 UI 入口PdfReportServlet负责 HTTP 下载PdfReportService负责装配数据并调用 PDF 引擎。参数说明addExtensions接受 Class 数组插件框架会自动按依赖关系实例化不需要手动注解Scoped默认是普通单例。2.2 5.5 到 6.7 的分水岭org.sonar.api.batch与org.sonar.api.ce的拆分5.5 时代批处理和 Compute Engine 部分 API 混用很多插件直接引用org.sonar.api.batch.measure.Metric来读数据。6.x 引入 Compute Engine 后Measure的获取方式从Resource变成了Componentorg.sonar.api.resources.Project也被org.sonar.api.ce.measure.Component取代。写兼容层时我一般先抽象一个ProjectDataProvider接口再按 SonarQube 版本提供不同实现public interface ProjectDataProvider { String projectKey(); String projectName(); MapString, Double metricValues(); } public class V55DataProvider implements ProjectDataProvider { private final Resource project; // 5.5 用 Resource 取 Measure } public class V7DataProvider implements ProjectDataProvider { private final Component project; // 7.x 用 Component.childrenMeasures() 取 Measure }选择 Provider 的判断逻辑不能放在Plugin.define里因为 define 阶段还没有版本号。正确位置是在 Servlet 或 Service 的构造函数里通过SonarQubeVersion判断。代码片段如下public PdfReportService(PdfReportPage page, SonarQubeVersion version) { this.version version; this.provider version.isGreaterThanOrEquals(6, 0) ? new V7DataProvider() : new V55DataProvider(); }这段代码的价值在于把版本差异收敛到一个边界上。SonarQubeVersion.isGreaterThanOrEquals(6, 0)在 5.5 和 7.x 里都存在签名稳定可以放心调用。2.3 7.x 的Plugin.Context变化别再硬编码getExtensions()到了 7.xPlugin.Context的内部实现从List改成了ExtensionInstaller驱动的集合直接反射getExtensions()会抛NoSuchMethodError。如果源码设计里依赖了这个方法就必须用addExtensions的 Class 数组重写。还有一个坑是org.sonar.api.resources.Qualifiers中PROJECT和VIEW的字符串常量6.3 之后取消了VIEW要用APP代替。下表总结了 5.5 → 7.x 的关键差异关注点5.5 行为6.x/7.x 行为兼容策略Measure 获取Resource.getMeasure(metric)Component.childrenMeasures()抽象 Provider项目类型Qualifiers.PROJECT与VIEW7.x 只有PROJECT与APP用字符串常量而非枚举Plugin 扩展context.addExtension(Class)正常推荐addExtensions批量注册统一用批量注册权限校验ResourcePermissions.verifyPermissionTemplates变化不大用org.sonar.api.security的PermissionChecker2.4 依赖打包的硬约束用maven-shade-plugin避免 PDF 库类冲突PDF 插件必然引入iText或OpenPDF这些库如果直接打进 war 类加载器会和 SonarQube 自带的类冲突。SonarQube 插件使用单例类加载器规则是「父类优先插件不能覆盖服务器类」。解决手段是做 shaded jar并把 PDF 库 relocate 到com.example.pdf名下。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId executions execution phasepackage/phase goalsgoalshade/goal/goals configuration relocations relocation patterncom.lowagie.text/pattern shadedPatterncom.example.pdf.lowagie.text/shadedPattern /relocation /relocations filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude /excludes /filter /filters /configuration /execution /executions /plugin注意参数relocations里的pattern必须是 PDF 库的根包名。如果不做 relocate插件上线后会间歇性出现NoClassDefFoundError且只在某些页面触发排查成本极高。3. 源码结构设计扩展点、权限校验与 PDF 渲染管线3.1 插件目录与模块划分接口、版本适配、前端资源分离源码设计不需要把所有类塞进一个模块。按可维护性我倾向于分成api-adapter版本适配、pdf-core渲染、server-plugin扩展点注册三个模块。目录结构如下sonarqube-pdf-plugin/ ├── api-adapter/ │ ├── src/main/java/org/example/adapter/ │ │ ├── ProjectDataProvider.java │ │ ├── V55DataProvider.java │ │ └── V7DataProvider.java ├── pdf-core/ │ ├── src/main/java/org/example/report/ │ │ ├── PdfRenderer.java │ │ ├── HtmlTemplateLoader.java │ │ └── ReportData.java └── server-plugin/ ├── src/main/java/org/example/plugin/ │ ├── PdfReportPlugin.java │ └── PdfReportServlet.java └── src/main/resources/static/ └── report.html这个结构的好处是pdf-core完全不知道 SonarQube 的类存在可以单独测试api-adapter依赖 SonarQube 但只放薄适配层版本升级时替换整个模块不会波及核心渲染逻辑。3.2 权限校验不能只靠按钮可见性报告里含项目名、Bug 数、漏洞详情这些数据敏感必须在 Servlet 里做二次校验。SonarQube 的Page扩展点可以在前端控制菜单显示但正常浏览器可以构造 URL 直接访问 Servlet。所以源码里要注入UserSession并调用hasComponentPermissionOverride protected void doGet(HttpServletRequest req, HttpServletResponse resp) { String projectKey req.getParameter(projectKey); UserSession session UserSession.get(); if (!session.hasComponentPermission(Permission.SCAN, projectKey)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } // 后续生成 PDF }这里Permission.SCAN是 SonarQube API 里最稳定的权限枚举之一。注意 5.5 里UserSession.get()已存在但 6.0 后hasComponentPermission的第一个参数类型从String改成了Permission写双版本兼容时要做一个PermissionResolver传入项目 key 并返回 boolean。3.3 PDF 渲染管线的三个步骤拉数据、填模板、输出流渲染管线我常用「JSON 中间层」设计先聚合数据到ReportData再序列化成 JSON 给模板最后用 OpenPDF 渲染。这样测试时不需要起 SonarQube直接喂 JSON 就能验证 PDF。public class PdfRenderer { public byte[] render(ReportData data) throws IOException { String html HtmlTemplateLoader.render(report.html, data); return ITextRenderer.fromHtml(html).pdf().build(); } }ITextRenderer来自名称为openpdf的库它的fromHtml能接受 HTML 字符串并输出 PDF 字节。参数说明HtmlTemplateLoader.render使用 Freemarker 或 Thymeleaf但为了减少依赖可以用String.format拼装简单表格。真实项目里我会用 Freemarker因为模板里要遍历 Measure 列表String.format会失控。3.4 数据聚合把 Measure 和 Issue 的异步结果对齐SonarQube 的指标计算是异步的ProjectDataProvider拿到的 Measure 可能携带空值尤其是「未分析」的项目。设计时需要约定如果某指标无值在 PDF 表格里显示—而不是抛出 NPE。聚合逻辑如下public ReportData aggregate(String projectKey) { ReportData data new ReportData(); for (MetricDefinition m : reportMetrics) { Double value provider.metricValue(projectKey, m.key()); data.addMetric(m.displayName(), value null ? null : round2(value)); } data.setIssueCount(provider.countIssuesBySeverity(projectKey)); return data; }round2用来处理浮点数精度避免出现 3.3000000000000003 这样的行。countIssuesBySeverity在 5.5 中可以用IssueQuery服务在 7.x 中推荐用IssueFinder但两者的返回结构差异不大适配层封装即可。4. 用 PDF 模板引擎输出可读报告从 HTML 到 PDF 的参数与控制4.1 为什么选择 HTML 转 PDF 而不是直接用 iText 画布直接用 iText 的DocumentPdfPTable写报告代码会变成一长串 setter 调用改个 Logo 位置就要重新编译。HTML 转 PDF 的方式把布局交给 CSS维护成本低很多。OpenPDF 支持的 CSS 子集有限但表格、字体、颜色、基本边框都没问题。模板头部我一般这样写!DOCTYPE html html head style body { font-family: Noto Sans CJK SC, sans-serif; font-size: 10pt; } table.metrics { width: 100%; border-collapse: collapse; } .bug { color: #d4333f; font-weight: bold; } /style /head body h2${projectName} - 质量报告/h2 table classmetrics trth指标/thth数值/th/tr #list metrics as m trtd${m.name}/tdtd${m.value}/td/tr /#list /table /body /html模板里的${projectName}和#list metrics as m是 Freemarker 语法。注意 OpenPDF 对 CSSborder-collapse支持不完整如果边框消失可以改成在每个td上直接写styleborder:1px solid #ccc这是兼容性最好的做法。4.2 分页与页眉页脚的三个控制参数PDF 报告如果超过一页默认没有页号归档时不专业。OpenPDF 提供PdfWriter事件机制来加页眉页脚但 HTML 转 PDF 时可以通过设置页面属性来实现ITextRenderer renderer new ITextRenderer(); renderer.setDocumentFromString(html); renderer.getSharedContext().setPrintBackgroundColor(true); renderer.getSharedContext().setReplacedElementFactory( new Base64ImageReplacementFactory(renderer.getSharedContext()));真正可控分页的是 CSSpage规则style page { size: A4; margin: 2cm 1.5cm; bottom-center { content: counter(page) / counter(pages); } } /style参数说明bottom-center里的counter(page)是 PagedMedia 规范的一部分OpenPDF 支持这个方式size: A4对应实际物理纸张如果报告要电子分发也可以设置成size: A4 landscape让表格更宽。4.3 中文与 Logo 图片的编码坑字体路径和 Base64 嵌入中文乱码是 PDF 插件最常见的问题。SonarQube 服务器通常是 Linux 环境系统里可能没有中文字体。两个办法一是把字体文件打进插件 jar二是使用服务器已安装的字体。我推荐前者因为可控。把字体放进src/main/resources/fonts/然后注册ITextFontResolver resolver renderer.getFontResolver(); resolver.addFont(fonts/NotoSansSC-Regular.ttf, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);BaseFont.IDENTITY_H表示 UTF-16 编码支持中文BaseFont.EMBEDDED表示把字体内嵌到 PDF 里避免打开 PDF 的机器没字体导致显示异常。Logo 图片不要用绝对路径引用服务器文件最好转成 Base64 字符串直接写入 HTMLString logoBase64 Base64.getEncoder().encodeToString(logoBytes); String imgTag img srcdata:image/png;base64, logoBase64 width120/;这样 PDF 渲染时不需要访问真实文件系统也不容易因为路径不存在而抛异常。4.4 报告内容模块Bug、漏洞、坏味道、重复率与覆盖率一份能被客户接受的报告至少要包含四类内容问题分布、严重级别汇总、质量门禁结果、项目元信息。下表是我常用的指标映射报告模块SonarQube 指标 key描述严重级别blocker_violations,critical_violations显示 Blocker/Critical 数量覆盖率coverage单位是百分比保留两位小数重复率duplicated_lines_density同样百分比处理质量门禁alert_statusOK或ERROR显示门禁名称在ProjectDataProvider里取这些指标时5.5 的Resource.getMeasure(coverage)返回对象带getValue()7.x 的Measure有getDoubleValue()要注意空指针。我一般写成Double value measure null ? null : measureValue(measure);其中measureValue内部判断 Version 再调用对应方法。5. 兼容性回归的 3 个验证技巧在 5.5 与 7.x 之间跑同一套测试5.1 用嵌入式 Runner 在本地同时起两个 SonarQube 实例插件源码设计完成后光靠 mvn test 不够必须在真实容器里验证。常见做法是用 Docker 分别启动sonarqube:5.5和sonarqube:7.9镜像两个实例端口不一样。启动后把插件 jar 复制到extensions/plugins/重启并运行一次扫描。docker run -d --name sonar55 -p 9001:9000 sonarqube:5.5 docker run -d --name sonar79 -p 9002:9000 sonarqube:7.9说明5.5 镜像里默认没有中文字体必须用docker cp把字体文件传进去并设置fc-cache。7.9 是 7.x 的最终版API 行为能覆盖整个 7 系。5.2 断言 PDF 中关键字符串而不是像素对比PDF 回归测试不建议做像素级对比环境差异会导致渲染差异。正确思路是解析 PDF 文本断言包含「项目名称」「Bug 数」「质量门禁 OK」。这一步可以用pdfbox的PDFTextStripper。PDDocument doc PDDocument.load(pdfBytes); String text new PDFTextStripper().getText(doc); assertTrue(text.contains(sample-project)); assertTrue(text.contains(质量门禁)); doc.close();参数说明PDFTextStripper.getText会按阅读顺序输出文本块如果模板里用了 CSSorder属性改变视觉顺序文本顺序可能与页面视觉不一致。解决方法是断言比文本块更小的粒度比如「Blocker: 3」而不是整个表格。5.3 模拟旧的 Scanner 版本触发 5.5 专属代码路径很多团队只升级 SonarQube不升级 Scanner导致 5.5 服务器上跑的 Scanner 版本很老。插件内部如果调用了org.sonar.api.batch.ScannerSide的类在 5.5 下正常工作但 7.x 把这类移到org.sonar.scanner会触发NoClassDefFoundError。回归技巧在 CI 里加一个 job用sonar-scanner-cli:3.0.3配合 5.5 实例扫描再在 7.x 实例上用新 Scanner 扫描确保两端都能生成 PDF。sonar-scanner -Dsonar.host.urlhttp://localhost:9001 \ -Dsonar.projectKeysample \ -Dsonar.sourcessrc \ -Dsonar.java.binariestarget/classes这段命令里-Dsonar.host.url指向本地 5.5 实例同样的命令把端口改成 9002 再跑一次即可验证 7.x。如果 5.5 实例出现Unsupported major.minor version说明插件编译用的 JDK 版本过高需要降级maven.compiler.source1.8/maven.compiler.source。最后一招在插件里加一个隐藏的系统属性开关-Dsonar.pdf.skiptrue一旦生成 PDF 导致扫描失败可以临时跳过 PDF 步骤排查问题。把这个开关写进PdfReportServlet的第一个判断里能省掉很多事故现场的抢救时间。本文还有配套的精品资源点击获取
返回列表