加速企业数据导入:基于YAML、Java和Vue的轻量级服务
在现代企业系统中,快速高效地导入大量历史或初始数据常常是一项艰巨的任务。传统的做法往往需要繁琐的代码或复杂的配置,可能将整个过程拖延数日。本文将探讨一种创新方法,利用Java和Vue.js技术栈构建一个轻量级、面向YAML的服务,旨在将数据加载时间缩短至短短几分钟,从而显著简化技术专家与平台及其内容的交互方式。
新JMatrixPlatform的开发秉持“一切皆代码”的原则,却也暴露出一个关键问题:缺乏便捷的一次性数据上传工具。与旧系统数据导入轻而易举不同,新架构要求为每个操作编写Java代码,这使得整个过程既费力又缺乏灵活性。分析师因此失去了独立管理数据的能力,完全依赖于开发人员。
我们考虑了几种标准方法,但每种都存在显著缺点:
- 通过批量服务增强REST API: 这将需要将Excel数据复杂地转换为JSON数组,包括在Excel公式中转义引号,这本身就是一项不小的挑战。使用Postman等外部工具会使过程进一步复杂化。
- 为每个对象使用
curl + json: 这将需要额外的脚本来获取令牌,并且不具备错误恢复能力,会在第一个错误记录处中断导入。 - 通过SQL直接导入: 这种方式被排除,因为所有数据交互逻辑都在应用程序层面实现。直接干预数据库,绕过平台的业务逻辑,可能导致数据不一致并损害系统完整性。
- 为每个实体开发原生Excel导入功能: 对于一次性导入而言,这被认为在经济上不可行且耗时。
因此,对一个快速、灵活且能赋能技术专家独立管理数据上传的工具的需求变得显而易见。
解决方案:面向YAML的批量上传服务
认识到现有方法的局限性,作者认为需要一个极简服务,它应具有一个单一入口点(endpoint),能够接受结构化数据以执行各种操作(创建、修改、删除)。之所以选择YAML而非JSON,是因为其简洁性,以及通过简单的Excel公式或LLM(大型语言模型)即可轻松手动生成或自动化创建的便利性。
一个YAML请求示例展示了其简洁性和可读性:
#request
- createObject:
type: ru.commons.matrix.schema.type.ATPPerson
policy: ru.commons.matrix.schema.policy.ALCPerson
- createObject:
type: ru.commons.matrix.schema.type.ATPPerson1
policy: ru.commons.matrix.schema.policy.ALCPerson
- createObject:
type: ru.commons.matrix.schema.type.ATPPerson
policy: ru.commons.matrix.schema.policy.ALCPerson
以及相应的响应,包含执行状态、对象ID或错误消息:
#response
---
- createObject:
status: 200
message: null
oid: "f4ba679e-9253-4a83-a390-44daf7ac7756"
- createObject:
status: 500
message: "Admin type ru.commons.matrix.schema.type.ATPPerson1 not found. Enter a correct name or contact the administrator."
- createObject:
status: 200
message: null
oid: "d9c98e74-bd17-4b3c-ae07-d9c307151c74"
这种格式允许轻松生成命令,例如,通过Excel使用简单公式:
="- createObject:
id: "&K2&"
type: "&I2&"
policy: "&J2&"
code: "&B2&"
title: '"&C2&"'"
这种方法并非一个成熟的DSL(领域特定语言),而是利用现有的REST API DTO(数据传输对象)并将其封装在操作命令中,从而确保了灵活性和可重用性。
基于Java和Spring框架的技术实现
该服务的核心是JQLController,它负责处理传入的YAML请求。
@RestController
@RequestMapping("jql")
@RequiredArgsConstructor
public class JQLController {
/**
* 执行一批命令。
*
* 使用LinkedHashMap是为了保留输入和输出命令的顺序
* - 输入命令按照它们在YAML中出现的顺序处理
* - 响应也按照请求的相同顺序返回
* - 这对于执行顺序很重要的场景(例如:创建 → 连接)至关重要。
* Jackson默认使用LinkedHashMap,但明确指定可以防止未来意外的实现更改。
*/
@PostMapping(consumes = "application/yaml", produces = "application/yaml")
public ResponseEntity<List<LinkedHashMap<String, JQLResponseData>>> promote(@JPathContextVariable JContext ctx,
@RequestBody List<LinkedHashMap<String, Object>> commands) {
List<LinkedHashMap<String, JQLResponseData>> results = new ArrayList<>(commands.size());
for (Map<String, Object> command : commands) {
Map<JQLEnum, IJQLDTO> parsed = fromYaml(command);
//命令中的错误不会导致整个批次失败
//run方法已考虑到这一点,因此此处不需要try-catch
results.add(run(ctx, parsed));
}
return ResponseEntity.ok(results);
}
private static Map<JQLEnum, IJQLDTO> fromYaml(Map<String, Object> commands) {
Map<JQLEnum, IJQLDTO> command = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : commands.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
JQLEnum jqlEnum = JQLEnum.valueOf(key);
Class<? extends IJQLDTO> dtoClass = jqlEnum.getDTOClass();
IJQLDTO dto = JObjectJSON.MAPPER.convertValue(value, dtoClass);
command.put(jqlEnum, dto);
}
return command;
}
/**
* 执行命令并以相同格式返回结果。
*
* 输入:{ "createObject": { "type": "...", "policy": "..." } }
* 输出:{ "createObject": { "status": 200, "id": "..." } }
*
* 或者
*
* 输出:{ "createObject": { "status": 500, "message": "..." } }
*/
private static LinkedHashMap<String, JQLResponseData> run(JContext ctx, Map<JQLEnum, IJQLDTO> commands) {
LinkedHashMap<String, JQLResponseData> response = new LinkedHashMap<>();
if (commands.isEmpty()) {
return response;
}
try {
ctx.getTxUpdate().executeWithoutResult(tx -> {
for (Map.Entry<JQLEnum, IJQLDTO> entry : commands.entrySet()) {
response.put(entry.getKey().name(), entry.getKey().execute(ctx, entry.getValue()));
}
});
} catch (JMatrixLocalizedError ex) {
commands.keySet().forEach(el -> {
response.put(el.name(), new JQLResponseData(500, ex.getLocalizedMessage(ctx.getLocale())));
});
} catch (Exception ex) {
commands.keySet().forEach(el -> {
response.put(el.name(), new JQLResponseData(500, ex.getMessage()));
});
}
return response;
}
}
JQLController使用Spring注解处理application/yaml内容类型的POST请求。一个关键特性是使用LinkedHashMap来保留命令顺序,这对于操作序列很重要的场景(例如,在链接对象之前创建对象)至关重要。fromYaml方法负责将YAML对象反序列化为相应的DTO,利用JQLEnum确定命令类型和DTO类。run方法在事务中执行命令,处理错误并形成结构化的YAML响应。
Spring对YAML支持的配置
由于Spring框架本身不支持application/yaml作为内容类型,因此需要额外的HttpMessageConverter配置。
@Configuration
public class YamlConfig {
@Bean
public YamlHttpMessageConverter yamlHttpMessageConverter() {
YAMLFactory factory = new YAMLFactory();
//.disable(YAMLGenerator.Feature.SPLIT_LINES)
//.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
YAMLMapper mapper = new YAMLMapper(factory);
return new YamlHttpMessageConverter(mapper);
}
public static class YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public YamlHttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper,
MediaType.parseMediaType("application/yaml"),
MediaType.parseMediaType("application/x-yaml"),
MediaType.parseMediaType("text/yaml"));
}
}
}
YamlConfig定义了一个YamlHttpMessageConverter bean,它扩展了AbstractJackson2HttpMessageConverter并注册了一个YAMLMapper来处理YAML格式(application/yaml、application/x-yaml、text/yaml)。这使得Spring能够自动在控制器对象中编组和解组YAML数据。
DTO结构和命令枚举
为了标准化响应,开发了一个基础DTO JQLResponseData:
@Getter
public class JQLResponseData {
private int status = 200;
private String message = null;
public JQLResponseData() {
}
public JQLResponseData(int status, String message) {
this.status = status;
this.message = message;
}
}
这个类为执行状态和错误消息提供了一致的格式。
最初,为了MVP(最小可行产品),命令集通过enum JQLEnum实现,这简化了开发。然而,在更成熟的系统中,这可以被更灵活的命令类注册机制所取代。
public enum JQLEnum {
createObject {
@Override
JQLResponseData execute(JContext ctx, IJQLDTO value) {
JDTODomainObject dto = (JDTODomainObject) value;
JDomainObject object;
if (dto.getId() == null) {
object = new JDomainObject();
} else {
object = new JDomainObject(dto.getId());
}
dto.unmap(object);
object.create(ctx, JModel.getRequiredAdminByName(dto.getType()), JModel.getRequiredAdminByName(dto.getPolicy()));
return new CreateDomainRS(object.getId());
}
@Override
public Class<? extends IJQLDTO> getDTOClass() {
return JDTODomainObject.class;
}
},
deleteObject {
@Override
JQLResponseData execute(JContext ctx, IJQLDTO value) {
DeleteDomainRQ dto = (DeleteDomainRQ) value;
new JDomainObject(dto.getId()).delete(ctx);
return new JQLResponseData();
}
@Override
public Class<? extends IJQLDTO> getDTOClass() {
return DeleteDomainRQ.class;
}
},
//etc.
abstract JQLResponseData execute(JContext ctx, IJQLDTO value);
public abstract Class<? extends IJQLDTO> getDTOClass();
}
每个JQLEnum元素都封装了执行特定命令(execute)的逻辑,并提供了一个方法来检索相应的DTO类(getDTOClass)。这使得可以集中管理可用的操作及其处理过程。
基于Vue.js的用户界面
为了与YAML服务交互,我们使用Vue.js开发了一个简单的Web界面。它包含两个区域:一个用于输入YAML请求,另一个用于显示结果。ace-builds库提供了便捷的YAML代码编辑功能,并支持语法高亮。
<template>
<div class="jql-base-div">
<div ref="refRequests" class="jql-requests-div" @keyup.alt.enter="handleAltEnter"></div>
<div ref="refResults" class="jql-response-div"></div>
</div>
</template>
<script setup>
import { useJServices } from '@/composables/useJServices';
const { serviceFetch } = useJServices()
import ace from 'ace-builds';
import 'ace-builds/src-noconflict/mode-yaml';
import 'ace-builds/src-noconflict/theme-chrome';
import { onMounted, ref } from 'vue';
ace.config.set('basePath', '/ace')
ace.config.set('workerPath', '/ace')
ace.config.set('themePath', '/ace')
const props = defineProps({
routeParams: Object,
routeQuery: Object,
requestBody: Object,
metaComponent: Object
})
const refRequests = ref(null)
const refResults = ref(null)
let aceEditorRequests = null
let aceEditorResponse = null
onMounted(() => {
document.title = 'JMatrix: JQL'
aceEditorRequests = ace.edit(refRequests.value)
aceEditorRequests.setTheme("ace/theme/chrome")
aceEditorRequests.session.setMode("ace/mode/yaml")
aceEditorRequests.setOptions({
fontSize: "13px",
showPrintMargin: false,
r
该界面提供了一个交互式环境,用于测试和执行YAML命令,使开发人员和分析师能够快速验证操作结果并调整请求。ace-builds的使用显著提升了处理YAML结构的便捷性。
优势与未来发展
YAML服务的实现将历史数据加载所需的时间从数天大幅缩短至数分钟。这一解决方案不仅提高了售前定制的效率,还通过为分析师提供独立数据工作的工具,而无需开发人员直接介入,显著扩展了他们的能力。
这种方法的关键优势包括:
- 速度与效率: 简化格式和批量处理带来了快速的数据加载。
- 灵活性: 无需复杂编码即可从多种来源(Excel、LLM)轻松生成请求。
- 分析师自主性: 减少了分析师在日常数据导入操作中对开发人员的依赖。
- 架构整洁性: 利用现有DTO并在应用程序层面保留业务逻辑。
未来的发展方向包括:
- 将
JQLEnum重构为更动态的命令注册机制,例如使用反射或配置,以简化新操作的添加。 - 扩展UI,增加YAML模式验证功能和更复杂的响应处理。
- 与版本控制系统集成,以跟踪导入数据的变化。
这种方法展示了深思熟虑的架构和恰当的技术选择如何解决复杂的数据导入挑战,显著提升团队生产力和系统灵活性。
核心要点
- 问题: 在“代码优先”的企业系统中,传统的(REST API、SQL、原生工具)数据导入方法对于一次性历史数据加载效率低下,通常需要数天的工作。
- 解决方案: 使用Java(Spring框架)和Vue.js构建一个轻量级、面向YAML的服务,用于批量命令处理。
- 选择YAML: YAML因其简洁性、易于生成(从Excel、LLM)以及对技术专家的可读性,优于JSON。
- 架构: 该服务使用
JQLController处理YAML请求,YamlHttpMessageConverter实现Spring集成,标准化DTO(JQLResponseData),并使用枚举进行命令执行。 - 成果: 将数据导入时间从数天缩短至数分钟,提高了分析师的自主性,并在保持业务逻辑完整性的同时增强了系统灵活性。
— Editorial Team
暂无评论。