文章目錄
- 功能背景
- 功能需要
- 前端開發
- 組件選用
- 組件嵌套和參數綁定
- 上傳邏輯示例
- 后端開發
- 接收邏輯
- 解析邏輯
- 省流
- 純手動實現(不建議)
功能背景
開發一個配置文件解析功能,需要兼容老版本的配置文件。
功能需要
- 前端:兩個配置文件分別上傳
- 后端:配置文件解析、分版本匹配、配置文件映射到實體類
前端開發
組件選用
選用element-plus的el-pload組件進行上傳控制,核心組件代碼為:
<el-uploadv-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 | stringname: stringstatus?: 'ready' | 'uploading' | 'success' | 'fail'size?: numberpercentage?: numberraw?: Fileresponse?: anyurl?: stringtype?: 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() {@Overridepublic 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);}@Overridepublic 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> {@Overridepublic 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");}
}