1. 为什么我们需要重新封装System.JSON?
在Delphi开发中处理JSON数据已经成为日常需求,但官方提供的System.JSON单元虽然功能完整,API设计却显得过于底层和繁琐。想象一下,每次访问一个JSON属性都需要写GetValue('name').Value这样的链式调用,不仅代码冗长,还容易因为拼写错误导致运行时异常。
我曾在实际项目中遇到过这样的场景:需要从一个API返回的JSON中提取多层嵌套的数据,使用原生System.JSON的代码就像下面这样:
delphi复制var
LJsonObj: TJSONObject;
begin
LJsonObj := TJSONObject.ParseJSONValue(ResponseText) as TJSONObject;
try
UserName := LJsonObj.GetValue('data').GetValue('user').GetValue('name').Value;
UserAge := StrToInt(LJsonObj.GetValue('data').GetValue('user').GetValue('age').Value);
finally
LJsonObj.Free;
end;
end;
这样的代码不仅难以阅读和维护,还隐藏着多个潜在崩溃点——任何一级GetValue如果找不到对应的键都会返回nil,接着调用Value属性就会引发访问冲突。而一个设计良好的封装库应该能让我们这样写:
delphi复制var
LJson: TSimpleJson;
begin
LJson := TSimpleJson.Parse(ResponseText);
try
UserName := LJson['data.user.name'].AsString;
UserAge := LJson['data.user.age'].AsInteger;
finally
LJson.Free;
end;
end;
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心API设计与实现原理
2.1 链式属性访问设计
实现类似LJson['data.user.name']这样的链式访问,关键在于重载TSimpleJson类的[]操作符。核心代码如下:
delphi复制function TSimpleJson.GetItem(const Path: string): TSimpleJsonValue;
var
LNames: TArray<string>;
LCurrent: TJSONValue;
I: Integer;
begin
LNames := Path.Split(['.']);
LCurrent := FJsonValue; // FJsonValue是内部封装的TJSONValue
for I := 0 to High(LNames) do
begin
if not (LCurrent is TJSONObject) then
Exit(nil); // 或者抛出特定异常
LCurrent := (LCurrent as TJSONObject).GetValue(LNames[I]);
if LCurrent = nil then
Exit(nil);
end;
Result := TSimpleJsonValue.Create(LCurrent);
end;
这种实现方式有几个关键点:
- 使用Split方法将路径字符串按点号分割
- 逐级向下查找JSON节点
- 在任何一级查找失败时返回nil或抛出异常(取决于设计选择)
- 最终返回一个封装过的TSimpleJsonValue对象
2.2 类型安全转换处理
原生System.JSON中,所有值都以字符串形式存储,需要开发者手动转换。我们的封装库应该提供类型安全的访问方式:
delphi复制type
TSimpleJsonValue = class
private
FValue: TJSONValue;
public
function AsString: string;
function AsInteger: Integer;
function AsBoolean: Boolean;
function AsFloat: Double;
function AsDateTime: TDateTime;
// ...其他类型转换方法
end;
每个转换方法内部都需要处理各种边界情况:
- 值为null时的默认值处理
- 字符串到数字的转换验证
- 日期时间格式的解析
- 类型不匹配时的异常处理
例如AsInteger的实现:
delphi复制function TSimpleJsonValue.AsInteger: Integer;
begin
if FValue = nil then
Exit(0); // 或者抛出异常
if FValue is TJSONNumber then
Exit((FValue as TJSONNumber).AsInt)
else if FValue is TJSONString then
begin
if not TryStrToInt((FValue as TJSONString).Value, Result) then
raise EJsonConvertError.Create('不是有效的整数');
end
else
raise EJsonConvertError.Create('类型不匹配');
end;
3. 高级功能实现技巧
3.1 流畅的JSON构建接口
除了解析,构建JSON也应该更直观。我们可以设计流畅接口(Fluent Interface)来实现:
delphi复制var
LJson: TSimpleJson;
begin
LJson := TSimpleJson.Create;
try
LJson
.Add('name', '张三')
.Add('age', 30)
.BeginObject('address')
.Add('city', '北京')
.Add('street', '朝阳区')
.EndObject
.BeginArray('hobbies')
.Add('游泳')
.Add('阅读')
.EndArray;
ShowMessage(LJson.ToString);
finally
LJson.Free;
end;
end;
实现这种流畅接口的关键在于每个方法都返回TSimpleJson实例本身:
delphi复制function TSimpleJson.Add(const Name: string; Value: Integer): TSimpleJson;
begin
(FJsonValue as TJSONObject).AddPair(Name, TJSONNumber.Create(Value));
Result := Self;
end;
3.2 枚举和数组处理
对于JSON数组,我们可以提供更便捷的访问方式:
delphi复制var
LJson: TSimpleJson;
I: Integer;
begin
LJson := TSimpleJson.Parse('[1, 2, 3, 4, 5]');
try
for I := 0 to LJson.Count - 1 do
ShowMessage(LJson[I].AsString);
finally
LJson.Free;
end;
end;
实现数组访问需要重载另一个版本的[]操作符:
delphi复制function TSimpleJson.GetItem(Index: Integer): TSimpleJsonValue;
begin
if not (FJsonValue is TJSONArray) then
raise EJsonError.Create('不是JSON数组');
if (Index < 0) or (Index >= (FJsonValue as TJSONArray).Count) then
raise EJsonError.Create('索引越界');
Result := TSimpleJsonValue.Create((FJsonValue as TJSONArray).Items[Index]);
end;
4. 性能优化与内存管理
4.1 对象池技术
频繁创建和销毁JSON对象会产生内存碎片。我们可以实现对象池来优化:
delphi复制type
TJsonObjectPool = class
private
FPool: TStack<TJSONObject>;
public
function Acquire: TJSONObject;
procedure Release(AObject: TJSONObject);
end;
// 使用时
var
LObj: TJSONObject;
begin
LObj := FObjectPool.Acquire;
try
// 使用LObj
finally
FObjectPool.Release(LObj);
end;
end;
4.2 惰性解析策略
对于大型JSON文档,我们可以实现按需解析:
delphi复制type
TLazyJson = class
private
FJsonText: string;
FParsed: Boolean;
FJsonValue: TJSONValue;
function GetValue(const Path: string): string;
public
constructor Create(const AJsonText: string);
destructor Destroy; override;
property Values[const Path: string]: string read GetValue;
end;
function TLazyJson.GetValue(const Path: string): string;
var
LNames: TArray<string>;
LCurrent: TJSONValue;
I: Integer;
begin
if not FParsed then
begin
FJsonValue := TJSONObject.ParseJSONValue(FJsonText);
FParsed := True;
end;
// 正常解析逻辑...
end;
5. 实际应用中的经验分享
5.1 日期时间处理的最佳实践
JSON标准没有定义日期格式,导致不同系统使用不同格式。我建议:
- 在库内部统一使用ISO 8601格式:"2023-07-20T15:30:00Z"
- 提供配置选项允许自定义格式
- 实现时区自动转换
delphi复制function TSimpleJsonValue.AsDateTime: TDateTime;
var
LDateStr: string;
begin
if FValue = nil then
Exit(0);
LDateStr := FValue.Value;
// 尝试解析ISO格式
if TryISO8601ToDate(LDateStr, Result) then
Exit;
// 尝试解析其他常见格式
if TryStrToDateTime(LDateStr, Result) then
Exit;
raise EJsonConvertError.Create('日期格式无效');
end;
5.2 处理不规则JSON数据
实际API常常返回不规范的JSON,我们的库应该能处理:
- 数字有时是字符串形式:"age": "30"
- 布尔值有时是字符串:"active": "true"
- 空值有时是空字符串:"address": ""
delphi复制function TSimpleJsonValue.AsInteger: Integer;
begin
// ...其他检查
if FValue is TJSONString then
begin
if (FValue as TJSONString).Value = '' then
Exit(0); // 空字符串视为0
if not TryStrToInt((FValue as TJSONString).Value, Result) then
raise EJsonConvertError.Create('不是有效的整数');
end;
end;
6. 单元测试策略
为确保封装库的可靠性,应该实现全面的单元测试:
delphi复制procedure TestTSimpleJson.TestBasicTypes;
var
LJson: TSimpleJson;
begin
LJson := TSimpleJson.Parse('{"name":"John","age":30,"active":true}');
try
CheckEquals('John', LJson['name'].AsString);
CheckEquals(30, LJson['age'].AsInteger);
CheckTrue(LJson['active'].AsBoolean);
finally
LJson.Free;
end;
end;
procedure TestTSimpleJson.TestNestedObjects;
var
LJson: TSimpleJson;
begin
LJson := TSimpleJson.Parse('{"user":{"name":"John","address":{"city":"New York"}}}');
try
CheckEquals('New York', LJson['user.address.city'].AsString);
finally
LJson.Free;
end;
end;
测试应覆盖:
- 各种数据类型转换
- 嵌套对象访问
- 数组操作
- 错误处理(如访问不存在的属性)
- 边界情况(如空值、空字符串)
7. 与第三方库的对比分析
与流行的Delphi JSON库(如SuperObject、dwsJSON)相比,我们的封装有以下特点:
| 特性 | System.JSON | SuperObject | dwsJSON | 本封装库 |
|---|---|---|---|---|
| 链式属性访问 | 不支持 | 支持 | 支持 | 支持 |
| 类型安全转换 | 有限 | 支持 | 支持 | 支持 |
| 流畅构建接口 | 不支持 | 不支持 | 不支持 | 支持 |
| 内存管理 | 手动 | 自动引用计数 | 自动 | 可配置 |
| 性能 | 高 | 中等 | 高 | 高 |
| 依赖 | 内置 | 第三方 | 第三方 | 基于System.JSON |
选择建议:
- 如果项目已经大量使用System.JSON,我们的封装提供平滑升级路径
- 如果需要最高性能,原生System.JSON或我们的封装是最佳选择
- 如果需要最小内存占用,dwsJSON的自动内存管理可能更合适
8. 实际项目集成建议
将封装库集成到现有项目时,建议:
- 逐步替换:先在新代码中使用,逐步替换旧代码
- 适配层:为已有接口创建适配层,而不是直接替换
- 性能监控:特别关注内存使用和解析速度
- 团队培训:确保所有开发者了解新的API约定
例如,替换原有JSON处理代码可以这样进行:
delphi复制// 旧代码
procedure ProcessUserJson(const AJsonText: string);
var
LJson: TJSONObject;
begin
LJson := TJSONObject.ParseJSONValue(AJsonText) as TJSONObject;
try
User.Name := LJson.GetValue('name').Value;
User.Age := StrToInt(LJson.GetValue('age').Value);
finally
LJson.Free;
end;
end;
// 新代码
procedure ProcessUserJson(const AJsonText: string);
var
LJson: TSimpleJson;
begin
LJson := TSimpleJson.Parse(AJsonText);
try
User.Name := LJson['name'].AsString;
User.Age := LJson['age'].AsInteger;
finally
LJson.Free;
end;
end;
9. 扩展性设计
好的封装库应该允许扩展:
- 自定义类型转换器:
delphi复制type
IJsonTypeConverter = interface
function CanConvert(ATypeInfo: PTypeInfo): Boolean;
function ToJson(AValue: TValue): TJSONValue;
function FromJson(AJson: TJSONValue; ATypeInfo: PTypeInfo): TValue;
end;
procedure TSimpleJson.RegisterConverter(AConverter: IJsonTypeConverter);
begin
FConverters.Add(AConverter);
end;
- 支持泛型方法:
delphi复制function TSimpleJsonValue.AsType<T>: T;
begin
// 使用RTTI进行转换
end;
// 使用示例
var
LDate: TDateTime;
begin
LDate := LJson['date'].AsType<TDateTime>;
end;
- 插件式架构:
delphi复制type
IJsonPlugin = interface
procedure ProcessBeforeParse(var AJsonText: string);
procedure ProcessAfterParse(AJsonValue: TJSONValue);
end;
10. 错误处理与调试支持
完善的错误处理应包括:
- 详细的异常信息:
delphi复制type
EJsonError = class(Exception);
EJsonPathError = class(EJsonError)
private
FPath: string;
public
constructor Create(const AMsg, APath: string);
property Path: string read FPath;
end;
- 调试视图:
delphi复制function TSimpleJson.ToDebugString: string;
begin
Result := FJsonValue.ToString;
// 添加格式化和语法高亮
end;
- 验证工具:
delphi复制class function TSimpleJson.IsValidJson(const AJsonText: string): Boolean;
begin
Result := False;
try
TJSONObject.ParseJSONValue(AJsonText).Free;
Result := True;
except
on E: Exception do
Exit(False);
end;
end;
在实现这些功能时,我发现最有价值的是为常见错误场景提供明确的错误信息。例如,当访问不存在的路径时,错误信息应该显示完整的访问路径,而不仅仅是"值不存在"这样模糊的描述。
