文章目录
- 功能背景
- 功能需要
- 前端开发
- 组件选用
- 组件嵌套和参数绑定
- 上传逻辑示例
- 后端开发
- 接收逻辑
- 解析逻辑
- 省流
- 纯手动实现(不建议)
功能背景
开发一个配置文件解析功能,需要兼容老版本的配置文件。
功能需要
- 前端:两个配置文件分别上传
- 后端:配置文件解析、分版本匹配、配置文件映射到实体类
前端开发
组件选用
选用element-plus的el-pload组件进行上传控制,核心组件代码为:
<el-upload
v-model:file-list="fileListModel"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:limit="1"
:on-exceed="handleExceed"
:auto-upload="false"
class="upload-location"
accept=".yml,.yaml"
/>
核心参数说明:
属性 | 作用 |
---|---|
v-model:file-list |
绑定上传文件列表 |
:on-remove |
文件移除时触发的回调函数,移除文件是组件自发的,此处绑的是你想在触发该逻辑时做的操作 |
:before-remove |
文件移除前触发的函数,此处我绑定了一个确认弹框 |
:limit="1" |
限制最多上传 1 个文件 |
:on-exceed |
超出文件数量限制时的回调,我绑了个提示框 |
:auto-upload="false" |
不自动上传,手动触发上传,因为我要一次提交两个不同的配置文件 |
class="upload-location" |
设置样式类 |
accept=".yml,.yaml" |
限制可上传的文件类型,此处我设置的是yaml类型 |
组件嵌套和参数绑定
此处我选择以el-upload组件为核心,将涉及的提示操作等封装成一个自定义组件。在父组件中使用两次该子组件,
并通过defineModel 实现父组件和子组件间值的双向绑定。
el-upload需要绑定的类型为UploadFile数组,即
子组件:const fileListModel = defineModel<UploadFile[]>("fileList");
父组件:const fileList = ref<any[]>([]);
绑定:v-model:file-list="fileList"
UploadFile参数为:
export interface UploadFile {
uid: number | string
name: string
status?: 'ready' | 'uploading' | 'success' | 'fail'
size?: number
percentage?: number
raw?: File
response?: any
url?: string
type?: string
}
其中raw为我们需要向后端传递的数据部分。
上传逻辑示例
const formData = new FormData()
//实际需要进行判空,此处只写核心部分
formData.append('file1', fileList1.value[0].raw)
formData.append('file2', fileList2.value[0].raw)
axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(res => {
console.log('上传成功:', res.data)
}).catch(err => {
console.error('上传失败:', err)
})
后端开发
接收逻辑
@PostMapping("/upload")
public R uploadTask(
@RequestParam(value = "file1", required = false) MultipartFile file1,
@RequestParam(value = "file2", required = true) MultipartFile file2,
@RequestParam(value = "groupId", required = true) Integer groupId) {
if (file2 == null || groupId == null) {
return R.fail("IMPORT_NOT_EXIST_PARAMS");
}
if (file1 != null) {
return paramService.importTaskParams(file1, file2, groupId);
} else {
return paramService.importTaskParams(file2, groupId);
}
}
解析逻辑
单纯的解析yaml文件并映射比较简单,无非将传入的文件内容使用YAMLMapper解析一下,但本任务有一个要求:
- 兼容之前的yml配置文件写法
注:
之前的配置文件使用了 Spring Boot 提供的“松散绑定(Relaxed Binding)”机制,这是直接上传yaml文件并解析无法直接办到的,由此延伸出几点需求:
- 支持单字符串向数组类型的映射(即支持a,b,c,d写法);
- 支持单字符串向枚举类型忽略大小写的映射;
- 中划线和驼峰类型双兼容(即既能解析中划线写法的配置文件,又能解析驼峰写法的配置文件)
- 16进制单字符向char类型的映射
省流
手动调用 Spring Boot 提供的绑定工具,不必自己实现。
高效实现:
try ( InputStream inputStream = multipartFile.getInputStream()){
// 1. 读取 YAML 文件
Yaml yaml = new Yaml();
//假设没有上层需要排除
Map<String, Object> yamlMap = yaml.load(inputStream);
// 2. 平铺嵌套结构(嵌套结构必须用)
Map<String, Object> flatMap = flattenMap(yamlMap, null);
// 3. 构造 PropertySource(Spring Boot Binder 需要它)
PropertySource<?> propertySource = new MapPropertySource("customYaml", flatMap);
StandardEnvironment env = new StandardEnvironment();
env.getPropertySources().addFirst(propertySource);
// 4. 使用 Binder 绑定
Binder binder = Binder.get(env);
return binder.bind("", Bindable.of(YourConfig.class))
.orElseThrow(() -> new RuntimeException("Binding failed"));
} catch (Exception e) {
e.printStackTrace();
}
辅助方法:
// 将嵌套结构扁平化成 "a.b.c" 格式,只有这样才能处理子级别的中划线映射驼峰
private Map<String, Object> flattenMap(Map<String, Object> source, String parentKey) {
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, Object> entry : source.entrySet()) {
String key = (parentKey != null ? parentKey + "." : "") + entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
result.putAll(flattenMap((Map<String, Object>) value, key));
} else {
result.put(key, value);
}
}
return result;
}
纯手动实现(不建议)
- 单字符串向数组类型的映射
- 重写set方法,使得数组类型属性可接受非数组类型参数。
- 单字符串向枚举类型忽略大小写的映射
- 手写模糊获取方法,并在set方法中调用 。
- 中划线和驼峰类型双兼容
- 重构映射逻辑,在映射之前加一段处理,即将原本的中划线名字修改为驼峰,这样就可和实体类中的属性实现匹配,具体实现为:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class FlexibleObjectMapper extends ObjectMapper {
public FlexibleObjectMapper() {
// 注册自定义模块用于字段名转换
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
if (deserializer instanceof BeanDeserializer) {
return new FlexibleCaseDeserializer((BeanDeserializer) deserializer);
}
return deserializer;
}
});
this.registerModule(module);
}
private static class FlexibleCaseDeserializer extends BeanDeserializer {
public FlexibleCaseDeserializer(BeanDeserializer base) {
super(base);
}
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
//在原逻辑之前加一段处理,即将原本的中划线名字修改为驼峰,这样就可和实体类中的属性实现匹配
JsonNode tree = p.getCodec().readTree(p);
if (tree.isObject()) {
ObjectNode objNode = (ObjectNode) tree;
Map<String, JsonNode> newFields = new HashMap<>();
objNode.fields().forEachRemaining(entry -> {
String fieldName = entry.getKey();
String camelCase = toCamelCase(fieldName);
newFields.put(camelCase, entry.getValue());
});
objNode.removeAll();
newFields.forEach(objNode::set);
JsonParser newParser = objNode.traverse(p.getCodec());
newParser.nextToken(); // advance to START_OBJECT
//回到原逻辑
return super.deserialize(newParser, ctxt);
}
return super.deserialize(p, ctxt);
}
private String toCamelCase(String s) {
if (!s.contains("_")) return s;
StringBuilder sb = new StringBuilder();
boolean upper = false;
for (char c : s.toCharArray()) {
if (c == '_') {
upper = true;
} else {
sb.append(upper ? Character.toUpperCase(c) : c);
upper = false;
}
}
return sb.toString();
}
}
}
- 16进制单字符向char类型的映射
- 自定义反序列化逻辑,指定格式字符串向字符的处理。
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.IOException;
public class CharDeserializer extends JsonDeserializer<Character> {
@Override
public Character deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException {
String text = jsonParser.getText().trim();
// 支持 \u 或 u 开头的十六进制 Unicode 字符
if (text.startsWith("\\u") || text.startsWith("u")) {
text = text.substring(text.indexOf('u') + 1);
try {
int code = Integer.parseInt(text, 16);
return (char) code;
} catch (NumberFormatException e) {
throw new IOException("Invalid hexadecimal character: " + text);
}
}
throw new IOException("Empty character string");
}
}