Go基于反射实现任意结构json值的替换

最近面临一个应用场景,需要对一个倒手转发的json进行数据替换,查阅了众多的库,大部分都不支持这个功能,从历史项目里捞出了jsonpath这个库,但其只能根据jsonpath的schema进行Get操作,无法实现inplace回写,后来又找到sjson,这是个根据jsonpath回写的库,没有用反射,自己实现的parser,但是在数组、对象混合嵌套的结构中,没办法正确写入,因为在使用gjson提取值之后,已经丢失了原始路径信息。在经过一番折腾之后,还是决定自己写一个替换器,虽然反射效率会比较低,但是好歹能实现。

需求

先抛弃jsonpath的思路,第一版先从简单的开始,期望替换库具备以下功能

  • 可以根据给定的key名,替换所有该key名下的value
  • 可以根据给定的key值类型,替换所有该类型的value
  • 以上三种方式的组合
  • 可以控制替换的深度

举例来说,
case1:对于下面这个json,

{
    "name1": "Bob",
    "name2": "Alice"
}

可以通过指定key=name2,将Alice增加一个前缀

{
    "name1": "Bob",
    "name2": "hello Alice"
}

case2:对于下面的json,指定替换字符串类型,并控制替换深度为2

{
    "name1": "Bob",
    "tag1": {
        "name2": "Dave",
        "tag2": {
            "name3": "Alice"
        }
    }
}

替换后为

{
    "name1": "hello Bob",
    "tag1": {
        "name2": "hello Dave",
        "tag2": {
            "name3": "Alice"
        }
    }
}

思路

由于json结构未知,无法直接Unmarshal为确定的结构,所以将json文档Unmarshal为map[string]interface{}进行解析。
由于json的深度未知,遂通过递归的方式,逐级遍历完成替换。

实现

数据结构

声明一个结构replacer,用于表示替换过程,其结构如下:

type replacer struct {
	maxDepth int                           // 最大递归替换深度,如果为0,表示不限制递归深度
	kind     reflect.Kind                  // 匹配的字段类型,如果未reflect.Invalid,表示不关心类型
	keyword  []string                      // 匹配的Key名,如果未指定即为nil,则仅做类型匹配
	replacer func(interface{}) interface{} // 替换器

	currDepth int // 当前递归深度
}

前三个字段,对应了需求中的约束条件,如果kind和keyword都为默认值,则所有值都将被替换;replacer是自定义替换函数,符合约束条件的值将会通过replacer调用传递给使用者,由使用者完成自定替换过程。currDepth为递归深度控制变量。

预处理

func (r replacer) Replacing(content []byte) ([]byte, error) {
	// 校验是否合法json
	if !json.Valid(content) {
		return nil, errors.New("invalid json")
	}

	// 反序列化json,兼容root为对象或数组的场景
	var holder interface{}
	if content[0] == '{' {
		holder = map[string]interface{}(nil)
	} else if content[0] == '[' {
		holder = []interface{}(nil)
	}

	err := json.Unmarshal(content, &holder)
	if err != nil {
		return nil, err
	}

	// 遍历/替换过程
	return json.Marshal(r.replacingElements("", holder))
}

输入的json为未知结构,所以首先对json合法性进行判断,将json反序列化为interface based结构。
替换的关键逻辑由replacingElements方法实现,并将替换后的json内容序列化为字符串返回。

替换逻辑

先来看一下replacingElements的伪代码

func replacingElements(key string, value interface{})interface{}{
    // 1.判断深度,达到递归深度则结束递归

    // 2.获取value的类型信息

    // 3.如果是值类型(字符串、数字、布尔),判断是否为指定的kind,且key是否为期望的key,如果是,则替换并回写
    // 4.如果是数组类型,遍历数组成员,对成员进行replaceElements递归调用
    // 5.如果是对象类型,遍历对象下的成员,对成员进行replaceElements递归调用
    // 6.如果没有指定的kind,则默认将所有kv都进行替换

    // 7.返回替换后的文档
}
深度判断
func (r replacer) deepEnough() bool {
	// 如果maxDepth为0,表示不限制递归深度,总返回false
	// 如果maxDepth大于0,且当前深度没有达到maxDepth,则返回false
	// 否则返回true
	return r.maxDepth != 0 && r.currDepth > r.maxDepth
}
是否为待替换key的判定
func (r replacer) needReplace(key string) bool {
	// 如果使用者没有指定keyword,那么总是返回true,表示不需要区分key
	if r.keyword == nil || len(r.keyword) == 0 {
		return true
	}
	// 查看当前key是否是使用者指定的key集合中的一个
	for _, v := range r.keyword {
		if v == key {
			return true
		}
	}
	return false
}
Number类型值处理

json中Number可以表达所有的数字,包括整形、无符号数、浮点数等。而在go中,对Number类型的反射,总是解释为float64类型,如果需要匹配使用者期望的kind,则需要对类型进行转化,实现如下

func (r replacer) convertArbitraryNumber(n interface{}, expect reflect.Kind) interface{} {
	// 获取以float64表达的json Number类型值
	rn, ok := n.(float64)
	if !ok {
		return n
	}

	// 将以float64表达的Number转换为目标类型,目标类型由使用者指定
	switch expect {
	case reflect.Float32:
		return float32(rn) // precision lost, cut off
	case reflect.Int:
		return int(rn) // precision lost
	case reflect.Int8:
		return int8(rn) // cut off
	case reflect.Int16:
		return int16(rn) // cut off
	case reflect.Int32:
		return int32(rn) // cut off
	case reflect.Int64:
		return int64(rn) // precision lost
	case reflect.Uint:
		return uint(rn) // cut off, sign lost
	case reflect.Uint8:
		return uint8(rn) // cut off, sign lost
	case reflect.Uint16:
		return uint16(rn) // cut off, sign lost
	case reflect.Uint32:
		return uint32(rn) // cut off, sign lost
	case reflect.Uint64:
		return uint64(rn) // precision lost, sign lost
	}
	return rn
}

需要注意float64转换为指定类型时,可能会有精度丢失、符号丢失的问题,所以最好是对替换目标值的类型心里有数。

替换过程

这里是json替换的核心逻辑

func (r replacer) replacingElements(key string, value interface{}) interface{} {
	// 递归深度判断
	if r.deepEnough() {
		return value
	}

	// 反射获取value的类型信息
	vt := reflect.TypeOf(value)
	k := vt.Kind()

	// 根据kind分别处理
	switch k {
	case r.kind: // 是期望的值类型
		if r.needReplace(key) {
			// 当前key是目标key集合中的内容
			return r.replacer(value)
		}
	case reflect.Slice:
		// 获得slice的value
		val := reflect.ValueOf(value)
		r.currDepth++ // increace depth
		for i := 0; i < val.Len(); i++ {
			// 遍历slice
			elem := val.Index(i)
			if elem.CanInterface() {
				// 将成员递归处理
				relem := r.replacingElements(key, elem.Interface())
				if elem.CanSet() {
					elem.Set(reflect.ValueOf(relem))
				}
			}
		}
	case reflect.Map:
		val, ok := value.(map[string]interface{})
		if ok {
			r.currDepth++ // increace depth
			for mkey, mval := range val {
				// 将成员递归处理
				val[mkey] = r.replacingElements(mkey, mval)
			}
		}
	default:
		if k == reflect.Float64 && (r.kind >= reflect.Int && r.kind <= reflect.Float32) {
			// for arbitrary numberic types, adjust underlying type to expected
			if r.needReplace(key) {
				// 这里调用convertArbitraryNumber对float64进行了转换,确保使用者在对value进行断言时
				// 可以获得自己在replacer.kind成员上指定的类型值
				return r.replacer(r.convertArbitraryNumber(value, r.kind))
			}
		} else if r.kind == reflect.Invalid {
			// 对于任意类型的值,不判断value的类型
			if r.needReplace(key) {
				return r.replacer(value)
			}
		}
	}
	return value
}

接口化

这里借鉴了我之前在《redis分布式锁与redsync库源码分析》中提到的可变参Option的技巧


func NewReplacer(replaceFunc func(interface{}) interface{}, options ...Option) IReplacer {
	r := new(replacer)
	for _, v := range options {
		v.Apply(r)
	}
	r.replacer = replaceFunc
	return r
}

type Option interface {
	Apply(*replacer)
}

type OptionFunc func(*replacer)

func (f OptionFunc) Apply(r *replacer) {
	f(r)
}

func WithMaxDepth(n int) Option {
	return OptionFunc(func(r *replacer) {
		r.maxDepth = n
	})
}

func WithKind(k reflect.Kind) Option {
	return OptionFunc(func(r *replacer) {
		r.kind = k
	})
}

func WithKeyword(k string) Option {
	return OptionFunc(func(r *replacer) {
		if r.keyword == nil {
			r.keyword = make([]string, 0, 1)
		}
		r.keyword = append(r.keyword, k)
	})

}

func WithKeywords(k []string) Option {
	return OptionFunc(func(r *replacer) {
		if r.keyword == nil {
			r.keyword = make([]string, 0, 1)
		}
		r.keyword = append(r.keyword, k...)
	})
}

type IReplacer interface {
	Replacing([]byte) ([]byte, error)
}

使用起来就很简单,示例如下:

replaceFunc := func(i interface{}) interface{} {
	val, ok := i.(string)
	if ok {
		return "hello " + val
	}
	return i
},
r := NewReplacer(replaceFunc, WithKind(reflect.String), WithKeyword("name"))
ret := r.Replacing([]byte(`{"name1":"Bob","name2":"Alice"}`))
fmt.Println(string(ret))	// {"name1":"hello Bob","name2":"hello Alice"}

这样,一个基于反射的json值替换器就实现完了。

结语

这个是一次对反射功能的实践,目的是加深对go反射的理解。从性能角度来讲,大概率是不如gjson/sjson这种基于文本parser的实现的。不过重点在于对反射原则的理解,能不能取用您自己看着办吧。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值