简介:这是一款面向 JIRA 普通用户的批量建单插件,基于 Java 开发,允许从 CSV 文件一次导入并创建多个问题,同时保留 JIRA 自身的字段校验与权限控制。与一般导入器不同,它会强制校验必填字段与业务约束,适合需要批量录入测试用例、运维工单或表单数据的团队。压缩包共 93 个文件,大小约 11.99MB,以 32 个 Java 源码文件为核心,辅以 14 个 CSV 样例模板、11 个 Velocity 页面模板、6 个前端 js/css、6 个 XML 配置和 2 个 psd 图标源文件,从后端接口到页面样式均有覆盖。包内含有完整项目目录、README 与测试数据,读者可沿着代码了解 CSV 如何被解析并映射到 JIRA 问题字段,也可自行调整模板或样式来适配个性化的批量录入流程,降低从零开发 JIRA 插件的学习门槛。目前已有 725 人学习下载,对从事 JIRA 二次开发或需要用脚本化方式管理工单的技术人员有直接参考价值。
1. 批量创建 JIRA 问题:一个 CSV 文件加一个插件,普通用户也能一次建几十条
某测试团队在提测前收到一份 200 行的缺陷清单,需要逐条录进 JIRA,但没人给测试组开管理员权限,批量导入数据只能等管理员抽空处理,一等就是大半天。这个名为 bulk-create-issues-for-jira 的插件,解决的就是这种场景:JIRA 普通用户不需要任何后台权限,只需要上传一个 CSV 文件,就能一次创建多个问题。反直觉的是,插件全程不绕过任何权限校验,项目不可见、字段不可用,JIRA 拒绝的理由和用户在页面上建单完全一致。它更像给普通用户配了一个“手速放大器”,适合 QA、运营、客服这类每天批量录入工单的团队。
2. 插件全景:从 CSV 上传到 JIRA REST API 的完整数据链路
2.1 功能边界:批量建单插件只管四件事
这个插件表面看是一个页面入口,实际拆开是四个职责清晰的模块。
| 模块 | 核心职责 | 刻意不做的事 |
|---|---|---|
| CSV 读取 | 去 BOM、识别编码、按列建立索引 | 不负责业务数据是否合法 |
| 字段映射 | 把 CSV 列名翻译成 JIRA 字段 key | 不支持字段联动、级联、公式 |
| 批量提交 | 逐条调用创建问题的 REST API | 不做整批事务回滚 |
| 结果报告 | 按原始行号反馈成功与失败原因 | 不自动重试 400 类业务错误 |
把“不做什么”写清楚,是因为批量导入类工具最容易引发误解。使用者天然会以为:插件能绕过权限、能自动补必填字段、能更新已有问题,甚至能批量删除。实际上这个插件只做一件事——把用户在界面上逐条点击“创建问题”的操作,换成程序按 CSV 行循环提交。权限校验发生的位置和页面完全一样,项目是否能建、问题类型是否可用、字段是否必填,全部由 JIRA 后端决定。所以插件不会成为权限后门,它需要的是一个已经能建单的普通用户身份,再把这个身份的手速放大几十倍。
典型操作流程是这样:登录 JIRA 后进入插件页面,选择目标项目,上传 CSV 文件,插件解析前 20 行做映射预览;用户确认“CSV 某一列对应 JIRA 哪个字段”后进入提交阶段;提交完成后结果报告按行号列出成功与失败原因。整个流程里唯一的新增动作是“上传 CSV 和确认映射”,其余和用户手工建单的权限判断路径是重叠的。
2.2 技术选型:Servlet 入口 + 公开 REST API
JIRA 插件开发一般有两种入口:Servlet 和 REST 端点。这个插件选择 Servlet,主要原因是页面要走 multipart 文件上传、映射预览、进度刷新这一套交互,Servlet 在 JIRA 插件框架里集成起来更直接,不需要绕注解扫描。
| 选型 | 用在哪 | 为什么这么选 |
|---|---|---|
| Java 8+ | 全部后端逻辑 | JIRA Server 插件的主流基线 |
| Maven | 工程构建 | JIRA 插件 SDK 默认就是 Maven |
| OpenCSV | CSV 解析 | 引号、转义、字段内换行处理比手写 split 稳定 |
| Jackson | JSON 序列化 | 避免手拼 JSON 字符串导致转义问题 |
REST API/rest/api/2/issue | 创建问题 | 与页面同一条通道,不依赖内部私有接口 |
这里有一个关键决定:创建问题不直接用 JIRA 内部的 IssueService,而是请求公开的 REST API。理由有两条。第一,REST API 是稳定的对外契约,大版本升级后基本不变,插件生命周期更长;内部 Service 接口经常随版本重构,一旦插件里调用的方法签名变了,整个插件可能在升级后无法加载。第二,REST API 会完整走一遍权限校验和字段校验,和用户在界面上操作结果等价,不容易出现“程序能建但页面不能建”的权限黑洞。
关于身份上下文要说明一下:插件发起 REST API 调用时,必须使用当前登录用户的身份,而不是配置一个管理员账号在后台创建。如果用一个固定管理员 token 去建单,等于给所有普通用户开了一个提权通道,这是批量导入工具最容易踩的红线。实际实现时,Servlet 从当前会话拿到用户凭证,再用带凭证的 HTTP 客户端请求同机 REST 接口;嫌麻烦也可以直接注入 IssueService 走内部接口,效果一样,只是升级兼容性略差。
2.3 工程目录结构与主流程
工程结构按功能分包,避免把所有逻辑塞进一个 Servlet:
src/main/java/com/example/bulkcreate ├── servlet │ └── BulkCreateServlet.java # 上传与导入入口 ├── csv │ ├── CsvParser.java # CSV 解析、BOM、编码 │ └── CsvColumnMapping.java # 列名到 JIRA 字段的映射表 ├── mapping │ ├── FieldMapper.java # 字段值转换接口 │ ├── SimpleFieldMapper.java # 摘要/描述等透传字段 │ ├── SelectFieldMapper.java # 单选字段包装成 {"name": ...} │ └── DateFieldMapper.java # 日期格式归一化 ├── jira │ └── JiraClient.java # 封装创建问题 REST 调用 ├── model │ ├── ImportRow.java # 单行数据 + 原始行号 │ └── ImportReport.java # 成功/失败结果 └── config └── PluginConfig.java # 字段映射与项目配置主流程按四步推进,每步都有对应的页面状态。上传阶段只做两件事:把 CSV 存成临时文件,解析出表头;预览阶段读取前 20 行数据,根据配置自动匹配列名,用户要在这里确认项目 key 和问题类型;提交阶段按批次循环调用 REST API,并把每行结果写进 ImportReport;报告阶段把成功 key、失败行号和原因按原始行号回显。初次接入最常见的问题集中在预览阶段——目标项目没有出现在下拉框里,几乎都是因为当前登录用户对这个项目没有创建问题的权限,这是 JIRA 行为,不是插件 Bug。
3. 核心模块拆解:CSV 解析、字段映射与批量提交
3.1 CSV 解析模块:先解决 BOM 和编码,再谈映射
实战里最容易翻车的是编码,不是解析。中文 Windows 的 Excel 导出的 CSV 默认是 GBK 编码,Mac 和 Linux 上又常常是 UTF-8 带 BOM。如果程序固定按 UTF-8 读,GBK 文件第一行表头就会乱成一团,严重时全部列名对不上映射。所以解析器的第一步是识别并跳过 BOM,再保留一个按 GBK 重试的兜底路径。
public List<ImportRow> parse(InputStream in) throws IOException { PushbackInputStream pb = new PushbackInputStream(in, 3); byte[] head = new byte[3]; int read = pb.read(head); boolean hasBom = read == 3 && (head[0] & 0xFF) == 0xEF && (head[1] & 0xFF) == 0xBB && (head[2] & 0xFF) == 0xBF; if (!hasBom) { // 不是 BOM 就把读走的字节还回去,保证后续解析不丢内容 pb.unread(head, 0, read); } Reader reader = new InputStreamReader(pb, StandardCharsets.UTF_8); try (CSVReader csv = new CSVReaderBuilder(reader).build()) { // 第一行作为表头,trim 后建立 列名 -> 下标 索引 String[] header = csv.readNext(); for (int i = 0; i < header.length; i++) { header[i] = header[i].trim().toLowerCase(); } String[] line; while ((line = csv.readNext()) != null) { if (isBlank(line)) { continue; // 跳过空行,避免空行变成一条空白问题 } rows.add(new ImportRow(line, rowIndex)); } } }这段代码做三件事。用 PushbackInputStream 预读三个字节判断是不是 UTF-8 BOM;如果不是,把字节 unread 回去,保证后续 CSV 解析不受影响;表头 trim 后统一转小写,建立列名到数组下标的索引。表头 trim 必须在建立索引之前完成,否则索引 key 还带着空格,后面映射怎么都对不上。转小写是为了匹配“Summary”和“summary”这种大小写差异,用户从不同系统导出的表头往往风格不一致。
说说为什么不直接用 CSV 库的编码参数。很多 CSV 库不主动处理 BOM,尤其是国内环境常见的 Excel 导出文件,带 BOM 会让第一列列名多一个不可见字符,映射接口失效得毫无提示。所以去 BOM 放在解析器最前面,不依赖 CSV 库。整体解析过程中如果出现大量替换字符(形如“锟斤拷”),常见做法是把文件按 GBK 再读一遍,把两种解析结果并排展示给用户选择,而不是自动切换编码。自动猜编码在数据量大时偶尔翻车,让用户选一次最直接。
另外,CSV 字段里允许换行和逗号,这是 Excel 导入场景的基本盘。处理这类内容不建议用 line.split(","),正确做法是用 OpenCSV 这类库,它能根据引号和转义符正确切分带逗号、带换行的字段。如果自己写正则,遇到描述里带引号的日志会切得支离破碎。
3.2 字段映射模块:把 CSV 行翻译成 JIRA 的 fields 对象
CSV 的列名和 JIRA 字段名通常对不上,比如表头写“严重级别”,JIRA 里是 priority;表头写“发生日期”,对应的是某个自定义字段。映射模块维护一张配置表,把 CSV 列名翻译成 JIRA 字段 key,再用不同的 FieldMapper 处理不同类型的值。
{ "project": "WEB", "columns": { "标题": { "field": "summary" }, "严重级": { "field": "priority", "type": "select" }, "发生日期": { "field": "customfield_10021", "type": "date" }, "模块": { "field": "components", "type": "array-select" } } }这个配置的意思是:CSV 里“标题”列写给 summary;“严重级”列写给 priority,按单选字段处理;“发生日期”列写给自定义字段 customfield_10021,按日期字段处理。不同字段类型,最终给 REST API 的 payload 结构完全不一样。单选字段要包一层 name,数组字段要包一层数组,日期字段要先做格式归一化。
public class SelectFieldMapper implements FieldMapper { @Override public Object toJiraValue(String rawValue) { // JIRA 单选字段要求包 {"name": ...},直接传字符串会报 400 return Collections.singletonMap("name", rawValue.trim()); } }单选字段是批量导入最容易报错的地方。JIRA REST API 接受的值是 {"name": "High"},直接传字符串会被拒绝。数组字段(组件、标签、多选)则在此基础上再套一层 List,每个元素都是一个 {"name": ...}。SimpleFieldMapper 负责摘要、描述这类透传字段,但透传不等于不过滤——描述里可能带换行和制表符,最后统一交给 Jackson 序列化时处理,绝不手写字符串拼接。
还有一点要说明白:映射配置是按“项目 + 问题类型”的维度存的,因为不同项目的必填字段不一样。某个项目创建 Bug 时必填“影响版本”,映射配置就要给这个项目单独加一行 version 的映射。预览页上,用户确认完映射再提交,遇到必填字段缺失的行,在提交前就能标出来,而不是等提交后一条条报错。这种“先校验再提交”的顺序,比把校验堆在 REST API 的报错里要省事得多。
3.3 批量提交模块:失败隔离是批量导入的灵魂
批量创建没有现成的批量事务接口,最稳妥的实现就是循环调用单条 REST API。这里的核心设计是失败隔离:第 10 行失败,不影响第 11 行继续提交。
public void submit(List<ImportRow> rows, ImportReport report) { for (int i = 0; i < rows.size(); i++) { ImportRow row = rows.get(i); try { HttpResponse resp = jiraClient.createIssue(row.buildPayload()); if (resp.isSuccess()) { report.addSuccess(row, resp.getKey()); } else { // 400 这类业务错误,重试多少次都是同样的结果 report.addFailure(row, resp.getErrorMessage()); } } catch (TimeoutException e) { // 网络超时可以重试一次,先记进待重试列表 report.addRetryable(row); } catch (Exception e) { report.addFailure(row, e.getMessage()); } } }这里把超时和业务拒绝分开处理,因为两者的恢复策略完全不同。超时重试一次就够了,重试前等几百毫秒,避免把 JIRA 负载打上去;400 类业务错误重试多少次都一样,直接记录到失败列表。report 会保留原始 CSV 行号,结果页能精确显示“第 37 行失败:优先级字段不允许为空”,而不是笼统的“部分失败”。
批量提交还有一个隐含的配额问题。JIRA 在高并发创建时响应会明显变慢,常见做法是控制单位时间内的提交量。这里默认一批 50 条,配合循环里的短暂 sleep,相当于给 JIRA 留出处理余量。sleep 时间不需要很长,几百毫秒对几百行的数据总量影响不大,但对 JIRA 的 CPU 和数据库压力是质变。如果 JIRA 实例负载高,把批次调小到 20;实例配置好,可以放宽到 100,这个参数抽到配置里,不要写死在代码中。
失败隔离还带来一个额外好处:导入可以重跑。报告里给出失败行号,用户修好 CSV 对应行后,把失败行重新导入一遍,成功行不会重复创建。这比“要么全部成功要么全部失败”的强事务设计实用得多,因为 JIRA REST API 本来就没有跨行的回滚能力,强行做事务只会让一次翻车拖垮整批数据。
4. 从源码到运行:编译、安装与参数配置
4.1 环境准备:JDK 版本与插件描述文件
JIRA 插件本质上是一个 Maven 工程,基础环境需要 Java 8 或 11(跟着当前 JIRA Server 的主版本要求走),Maven 3.6+。插件描述文件 atlassian-plugin.xml 负责把 Servlet 注册到指定路径:
<atlassian-plugin key="com.example.bulk-create-issues" name="Bulk Create Issues"> <plugin-info> <description>Allow normal users to create issues from CSV file</description> <vendor name="Example Team"/> </plugin-info> <servlet key="bulk-create-servlet" class="com.example.bulkcreate.servlet.BulkCreateServlet"> <url-pattern>/bulk-create</url-pattern> </servlet> </atlassian-plugin>servlet 在 JIRA 插件里默认对登录用户可见,但代码里要显式校验当前用户,避免匿名请求进入。大多数插件会在 servlet 类上加 @RequireJiraUser 注解,JIRA 在请求进入之前先做登录校验。没有这个注解,插件页面可能被匿名访问到,虽然 JIRA 后端 API 会二次拦截,但页面本身应该先挡住。这里的“普通用户可用”指的是不要求管理员权限,但必须登录 JIRA,登录身份就是后续所有 API 调用的身份。
依赖方面,pom.xml 里要声明 JIRA 插件 SDK 提供的系统依赖范围。特别注意 jira.version 这个属性,它决定编译时引用哪个版本的 JIRA API 类。如果在一个老版本 JIRA 实例上部署用新版本 API 编译的插件,加载阶段就可能因为类不存在直接失败;反过来在新版 JIRA 上跑旧版编译的插件,又可能出现接口行为不一致。这个坑不比业务代码少,第一次接入最好拉一个本地测试实例验证。
页面前端不用复杂框架,一个 JSP 模板加 WebResource 声明就够。Servlet 接收 multipart 上传后,直接把解析预览渲染成页面,提交阶段用轻量 JSON 接口回报进度。把前端资源声明在 atlassian-plugin.xml 的 web-resource 节点里,JIRA 会自动带上上下文路径,避免手写资源路径导致 404。二次开发时不用担心前端工程化,这个项目的页面层就是普通 Java Web 那套。
4.2 编译打包与安装
编译命令很简单:
mvn clean package -DskipTests # 产物在 target/ 目录下,文件名形如 bulk-create-issues-for-jira-<version>.jarjar 生成后,进入 JIRA 管理后台的应用插件管理页,上传这个 jar。JIRA 会解析 atlassian-plugin.xml 并注册 servlet,导航里出现插件入口。新装插件一般热加载,不需要重启 JIRA。
注意:同一个实例反复上传新 jar 后,如果页面样式或接口表现异常,先重启 JIRA 再排查。
本地联调阶段可以不用手工上传。JIRA 插件 SDK 提供的 atlas-run 命令会拉起一个带测试数据的本地 JIRA 实例,代码改动自动同步,适合反复验证:
atlas-run --product jira --version 8.x这段命令里的 8.x 要替换成当前目标 JIRA 的主版本号。atlas-run 启动的实例用的是 H2 内存数据库,重启后数据不保留,但用来验证插件加载、CSV 导入流程完全够用。真正要在真实数据上验证,还是得部署到测试 JIRA 实例,因为本地实例的数据和权限配置与生产差异很大。
4.3 一次完整导入的参数清单与操作步骤
准备一个最简单的 CSV,第一行表头,后面每行一条问题:
summary,issuetype,priority,description 登录页白屏,Bug,High,打开登录页后页面空白 导出无数据,Bug,Medium,导出按钮点击后无文件导入时插件页面上的关键参数如下:
| 参数 | 说明 | 示例 |
|---|---|---|
| 目标项目 | 无权限的项目直接过滤,不会出现 | WEB |
| 问题类型 | 默认取 CSV 里 issuetype 列 | Bug |
| CSV 文件 | UTF-8 或带 BOM,GBK 也能兜底 | defect.csv |
| 映射预览 | 确认每列对应 JIRA 字段 | summary → 摘要 |
确认映射后点提交,结果报告分三块:成功列表(含新问题 key)、失败列表(行号 + 原因)、待重试列表(超时行)。200 条数据的实际耗时取决于 JIRA 响应速度,通常几分钟内完成。如果结果全是失败,先去检查目标项目 key 和问题类型在这个项目里是否可用,这两个字段错了会全军覆没。还有一个实用的检验习惯:第一次导入先取 5 行样本跑一遍,确认映射和字段没问题,再放全量。样本导入的成功率,基本就是全量的上限。
5. 避坑指南:CSV 批量建单最容易翻车的五个场景
CSV 批量建单翻车,十有八九不是 JIRA 配置问题,而是 CSV 文件本身和 REST API 的交互细节问题。下面五条是从几轮接入中反复遇到的真实场景里拎出来的,按“现象 → 原因 → 解决”三条线展开,每一条都能直接在导入时对上号。
5.1 Excel 导出 CSV 后中文乱码
现象:上传后页面预览里表头和正文全是乱码,“登录页”变成“鐧诲綍椤”之类的替换字符。
原因:中文 Windows 的 Excel 默认按 GBK 编码保存 CSV,插件按 UTF-8 解码,字节流被错误映射到 Unicode 字符集。
解决:解析器先去 BOM;没有 BOM 且出现明显乱码时,把文件按 GBK 再解析一次,让用户二选一。同时页面提示“建议另存为 CSV UTF-8 格式”。程序侧的兜底必须做,不能只靠提示,因为用户手里带着历史数据的 CSV 不会专门去改编码。判断乱码要在解析早期做,不要等几千行数据全部解析完再发现表头不对。
5.2 一提交就整批失败,看不到是哪行错
现象:100 条数据提交后一个都没建成,报错只有一句“createMeta failed: field not found”。
原因:字段映射配置里某个自定义字段 key 写错,或者必填字段没填,而提交程序在第一行失败时就中断了循环。
解决:把 try-catch 放在单条循环内,逐条记录失败原因,结果页显示“第 23 行失败:customfield_10001 该字段必填”而不是一句笼统提示。这在代码层面就是 3.3 里那个设计,不要因为图省事把整批包在一个 try 里。另外一个排查技巧:先在 JIRA 界面上手工建一条问题,看这个项目的必填字段到底有哪些,再回来核对映射配置。页面上能建成功,REST API 基本也能成功,两边字段校验规则是同源的。
5.3 日期字段填了 2024/1/5 被拒绝
现象:CSV 里日期写成 2024/1/5,提交时 JIRA 报“Invalid date format”。
原因:JIRA REST API 的日期字段要求 ISO 格式 yyyy-MM-dd,Excel 里常见的斜杠格式不在识别范围内。
解决:在 DateFieldMapper 里按 yyyy-MM-dd、yyyy/M/d、yyyy/M/dd、M/d/yyyy 多种格式尝试解析,最终统一输出 ISO 格式。格式列表要放在配置里,而不是写死在代码里,这样后面遇到新的日期写法,加一条配置就行,不用动代码重新打包。解析失败时不要静默吞掉,要把原始值和期望格式一起写进失败原因,方便用户回看。
5.4 表头带前导空格,映射预览匹配不上
现象:CSV 第一列显示名是“ summary”(前面有一个空格),自动映射时这一列永远匹配不到 JIRA 的 summary 字段。
原因:表头从别处复制粘贴过来,自带不可见空格;解析时如果没做 trim,列索引里的 key 就带着空格。
解决:在建立列索引之前统一 trim 表头,同时大小写不敏感匹配。这里有一个顺序坑:先建索引再 trim 是没用的,索引里的 key 还是原值,后面 mapping 按 trim 后的名字去找必然失配。检查时注意这类问题不要依赖用户在 Excel 里清理,Excel 里看起来干净的表头未必没有空格,程序侧兜底才可靠。
5.5 描述字段含多行文本导致 JSON 报错
现象:description 列里用户复制了三行日志,提交时报 400,错误信息是 JSON parse error。
原因:CSV 字段里的换行符进入字符串后,拼 JSON 时没有转义,把 JSON 结构截断了。
解决:所有 payload 用 Jackson 序列化,不要手写字符串拼接。同时 CSV 解析器要能正确识别带引号的字段,OpenCSV 默认支持引号和双引号转义,这一步不要自己用 split(",") 处理,否则带逗号的描述也会被切断。一个直观的验证方法:把失败的行单独复制成一小份 CSV,用接口调试工具手动提交一次,看到的错误信息会比插件结果页更完整,适合定位是解析层问题还是字段值问题。对照这份清单,导入前先跑 5 行样本,把编码、表头、必填字段、日期格式都过一遍,剩下的导入过程基本不会再出幺蛾子。失败行报告本身就是最好的排查入口,看到哪行错、错在哪个字段,比猜原因省太多时间。
6. 二次开发实战:加一个可配置的多格式日期映射器
6.1 扩展点:FieldMapper 是一个 Java 接口
想加新字段类型,不需要动 Servlet 和提交模块,实现 FieldMapper 接口后在映射工厂注册即可:
public interface FieldMapper { Object toJiraValue(String rawValue); boolean supports(String jiraFieldKey); }supports 告诉映射工厂这个 mapper 负责哪些 JIRA 字段,toJiraValue 把 CSV 字符串转成 REST API 接受的 Java 对象。改造日期支持时,把原来只认 yyyy-MM-dd 的实现替换成多格式版本:
public class MultiFormatDateMapper implements FieldMapper { @Override public boolean supports(String fieldKey) { return fieldKey.endsWith("date") || fieldKey.startsWith("customfield_"); } @Override public Object toJiraValue(String rawValue) { for (String pattern : Arrays.asList("yyyy-MM-dd", "yyyy/M/d", "yyyy/M/dd", "M/d/yyyy")) { try { return LocalDate.parse(rawValue.trim(), DateTimeFormatter.ofPattern(pattern)) .format(DateTimeFormatter.ISO_LOCAL_DATE); } catch (DateTimeParseException ignored) { // 继续尝试下一种格式,全部失败再抛异常 } } throw new MappingException("无法识别日期: " + rawValue); } }提示:日期格式列表放在配置文件里,后续遇到新格式只需加一行,不用重新打包。
改造完成后做两件事。第一是单测,把 2024-01-05、2024/1/5、1/5/2024 各写一个断言,确认输出都是 2024-01-05。第二是手动回归,用前面那份缺陷清单 CSV 再跑一次,清单里混着斜杠日期,跑通就说明替换没有破坏原有流程。从那以后我每次改字段映射,都会先在本地实例上跑一遍真实 CSV 再动手改代码,改成后还要用同一份文件回归一次,省掉了不少上线后才发现的对不齐问题。如果你正好在折腾 JIRA 批量导入,这份工程可以直接拿起来跑,把映射配置换成你项目里的真实字段就行。希望帮到你。
本文还有配套的精品资源,点击获取