如何做一个简单的开放接口(3)-核心引擎(下)

1、要实现的功能

书接上回,本回书解决核心引擎的第二个问题:数据映射和数据校验。

我们把这个部分叫做数据转换模块。

2、输入数据的格式

输入数据的结构、属性名等,是接口发布方确定的。
出于安全、效率、调用方影响等方面的考虑,可能和自身系统中的结构和属性名不一致。

输入数据的格式可能有三种:

  1. 反序列化后得到的Java对象。
  2. JSON格式。
  3. XML格式。

我们将对输入的数据进行校验,转换成自身系统的数据格式。

3、配置

配置文件是数据转换模块的核心。

对于我方来说,数据的结构和格式是相对稳定的。
举个例子,发布方系统中注册用户数据格式如下。

class User{
    String id ;
    String loginName ;
    String pwd ;
    List<User> friends ;
}

在发布第三方接口,采集用户数据的时候,发布方确定的接口编号为YH001,传入参数的类型是 List,yh类数据格式如下:

class yh{ // 用户
    String bh ; // 编号
    String dlm ; // 登录名
    String mm ; // 密码,原始密码经2次md5加密处理后的字符串
    List<String> hylb ; // 好友列表,编号组成的List
}

两者之间如何映射呢?

发布方内部实现方法为 List<Error> collectUserInfo(List<User> users) 我们定义如下所示的配置文件,依照这个配置文件,系统将输入的yh转换为User。

{
    method:'YH001',
    meta:{
        serviceClass:'cn.hailong.user.openapi.UserInfoService',
        serviceMethod:'collectUserInfo'
    },
    params:{
        yhlb:[{
            _meta:{
                _type:'BO', 
                _clz:'cn.hailong.user.domain.User',
                _validate:{
                    notNull : true
                }
            },
            bh:{
                _property : 'id',
                _label : '用户编号',
                _type:'String',
                _validate :{
                    notEmpty : 'true',
                    maxLength : 32
                }
            },
            dlm:{
                _property : 'loginName',
                _label : '登录名',
                _type:'String',
                _validate :{
                    notEmpty : 'true',
                    maxLength : 50
                }
            },
            mm:{
                _property : 'pwd',
                _label : '密码,原始密码md5运算两次得到的字符串',
                _type:'String',
                _validate :{
                    notEmpty : 'true',
                    maxLength : 50
                }
            },
            hylb:{
                _property : 'friends',
                _label : '好友列表',
                _type:'List',
                _clz:'java.lang.String'
            },
        }]
    }
}

以上配置信息可以保存为JSON文件,可以保存在数据库中。

4、解析配置

4.1、配置类基本原则

系统启动时,解析配置信息保存在内存中或者Memecached/redis中。

同时提供更新配置信息的方法。可以在不重启服务器的情况下更新配置。

提供通过接口名(即配置文件中的method)获取配置信息的方法。

4.2、代码

解析代码如下所示。

    protected ApiConfigNode parse(JSONObject joConfig){
        if(joConfig==null){
            return null;
        }

        String key = null;
        Object obj = null;
        JSONObject jo = null;
        String str = null;
        ApiConfigNode child = null;

        ApiConfigNode ret = new ApiConfigNode();

        for(Map.Entry<String, Object> entry : joConfig.entrySet()){
            key = entry.getKey();
            obj = entry.getValue();

            if(key.equals(ApiConfigNode.META_KEY)){
                jo = (JSONObject)obj;
                Map<String, Object> objMap = jo;
                ret.fillMeta(objMap);
            }else if(key.equals(ApiConfigNode.VALIDATE_KEY)){
                jo = (JSONObject)obj;
                Map<String, Object> objMap = jo;
                ret.fillValidate(objMap);
            }else if(key.startsWith("_")){
                str = StringUtil.safe2String(obj);
                ret.setMeta(key, str);
            }else if(obj instanceof JSONObject){
                jo = (JSONObject)obj;
                child = parse(jo);
                ret.setProperty(key, child);
            }else if(obj instanceof String){
                logger.error(String.format("----str,should not happen------key:%s,value:%s", key,obj));
            }else{
                logger.error(String.format("----how can this happen------key:%s,value:%s", key,obj));
            }
        }
        return ret;
    }

4.3、根据配置信息生成描述文档

对于Open Api 要明确告知调用者,需要传递的数据格式,数据校验要求。

这些信息已经包含在配置信息中,我们可以根据生成说明性文档。

这样既保持了文档准确,又保证了更新及时。

代码如下。

    public String fetchPropertyDesc(String key) {
        logger.debug("desc ,key " + key);
        if( StringUtils.isEmpty(key) ){
            // 返回自身的
            return this.fetchPropertyDesc();
        }else{
            String[] entrys = key.split("\\.");
            if(entrys==null || entrys.length==0){
                // 返回自身的
                return this.fetchPropertyDesc();
            }else{
                ApiConfigNode child = this.getProperty(entrys[0]);
                if(child==null){
                    return this.fetchPropertyDesc();
                }
                if(entrys.length==1){
                    key = "";
                }else{
                    key = key.replace(entrys[0]+".", "");
                }
                String ret = null;
                ret = child.fetchPropertyDesc(key);
                logger.debug("child key : " + key + ", desc : " + ret);
                return ret;
            }
        }
    }

    public String fetchPropertyDesc(){
        JSONArray ja = new JSONArray();
        JSONObject jo = null;
        String propName = null;
        ApiConfigNode prop = null;
        ApiConfigNodeType type = null;
        String validates = null;

        Map<String, ApiConfigNode> props = this.getProperties();

        for(Map.Entry<String, ApiConfigNode> propEntry : props.entrySet()){
            propName = propEntry.getKey();
            prop = propEntry.getValue();
            type = prop.getNodeType();
            if( type!=ApiConfigNodeType.STRING && 
                type!=ApiConfigNodeType.BIG_DECIMAL && 
                type!=ApiConfigNodeType.CALENDAR ){
                continue;
            }
            jo = new JSONObject();
            jo.put("name", propName);
            jo.put("label", prop.getLabel());
            jo.put("type", type.getCode());
            validates = prop.fetchValidateDesc();
            jo.put("validate", validates);
            ja.add(jo);
        }
        String ret = JSON.toJSONString(ja, true);
        logger.debug("[self] desc : " + ret);
        return ret;
    }

4.4、根据配置信息生成范例数据

给调用者提供范例数据,也是友好的做法。

代码如下。

    public JSONObject buildSampleData() {
        JSONObject ret = new JSONObject();

        Map<String, ApiConfigNode> props = this.getProperties();


        Object sampleData = null;
        ApiConfigNodeType type = null;
        ApiConfigNode prop = null;
        String label = null;
        Map<String, String> validates = null;

        for(Map.Entry<String, ApiConfigNode> entry : props.entrySet()){

            prop = entry.getValue();        // 当前节点的属性
            type = prop.getNodeType();      // 当前节点的属性类型
            validates = prop.getValidates();
            label = prop.getLabel();

            sampleData = null;
            switch(type){
            case STRING:
                String sampleStr = label;
                if(validates.containsKey("notEmpty")){
                    sampleStr += ",不能为空";
                }
                if(validates.containsKey("maxLength")){
                    sampleStr += ",最大长度"+validates.get("maxLength");
                }
                if(validates.containsKey("dictRange")){
                    sampleStr += ",码表"+validates.get("dictRange");;
                }
                if(validates.containsKey("length")){
                    sampleStr += ",长度为"+validates.get("length");;
                }
                sampleData = sampleStr ;
                break;
            case BIG_DECIMAL:
                sampleData = "123456.789";
                break;
            case CALENDAR:
                sampleData = dateFormat.format(new Date());
                break;
            case BO:
                sampleData = prop.buildSampleData();
                break;
            case LIST:
                List<Object> sampleList = new ArrayList<Object>();
                sampleList.add(prop.buildSampleData());
                sampleList.add(prop.buildSampleData());
                sampleData = sampleList;
                break;
            case UUID:// 不需要调用方提交,所以不体现在范例数据中
                break;
            case SEQUENCE:// 不需要调用方提交,所以不体现在范例数据中
                break;
            case EXPR:// 不需要调用方提交,所以不体现在范例数据中
                break;
            default:
                break;
            }
            ret.put(entry.getKey(), sampleData);
        }
        return ret;
    }

5、数据转换

在本回第2节,我们预估了传入数据的格式,将有3种:Java对象、JSON格式和XML格式。

尽管格式不同,但数据结构和属性名都是按照配置文件的要求提供的。

5.1、数据的目标

转换的目标需要是Java对象。
通过反射创建实例、赋值。充分利用缓存,反射具备充分的高效率。

如果没有转换目标的Java BO类定义呢?
是否可以考虑用Map表示一个类的实例,Map中的key-Value表示属性的名字和值?
No!不要这样的方案,这种松散的Map对象带来各种不确定性。在追求稳定可控的情形下,不适宜出现。需要考虑的细节太多,各种长度的数字、各种表达方法的日期等等,需要花费的精力太多。

可以考虑这样一种折中方案:根据配置文件生成Java代码,手工调整后,编译得到目标class文件。

5.2、转换逻辑

转换逻辑:

  1. 找到对应的配置信息对象。
  2. 配置信息中包含当前数据的类型信息,依据不同类型进行处理。
  3. 如果当前数据是BO,则按照遍历配置信息对象的所有属性,逐个到当前数据中找对应的值,如果属性也是BO,则遍历。

转换逻辑基本一致,只是从数据来源取值的方式不同。

JSON的参考代码如下(逻辑未梳理,尚未优化,只是初步实现,仅供参考)。

    public Object buildBO(Object obj) {
        Object ret = null;
        JSONObject jo = null;
        ApiConfigNodeType type = this.getNodeType();
        switch(type){
        case STRING:
            if(obj==null){
                ret = null;
            }else{
                if(obj instanceof String){
                    ret = (String)obj;
                }else if (obj instanceof Number){
                    Number number = (Number)obj;
                    ret = number.doubleValue() + "";
                }else{
                    ret = StringUtil.safe2String(obj);
                }
            }
            break;
        case BIG_DECIMAL:
            if(obj==null ){
                ret = null;
            }else{
                if(obj instanceof Number){
                    Number num = (Number)obj;
                    ret = new BigDecimal(num.doubleValue());
                }else if(obj instanceof String){
                    String objString = (String)obj;
                    if(StringUtils.isEmpty(objString)){
                        ret = null;
                        break;
                    }
                    try{
                        ret = new BigDecimal((String)obj);
                    }catch(Throwable e){
                        String errMsg = String.format("格式错误:输入的 %s(%s) 数据 %s 应该是数字类型。",
                                this.getLabel(),this.getCode(),(String)obj);
                        throw new ApiException(errMsg);
                    }
                }else{
                    //FIXME
                }
            }
            break;
        case CALENDAR:
            if(obj==null){
                ret = null;
            }else{
                Date date = null;
                try {
                    date = dateFormat.parse((String)obj);
                } catch (ParseException e) {
                    throw new ApiException("传入数据日期格式不正确。",e);
                }
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(date);
                ret = calendar;
            }
            break;
        case BO:
            if(obj==null){
                ret = null;
            }else{
                jo = (JSONObject)obj;
                ret = buildBOInner(jo);
            }
            break;
        case LIST:
            if(obj==null){
                ret = null;
            }else{
                List<Object> list = new ArrayList<Object>();
                JSONArray ja = (JSONArray)obj;
                if(ja!=null && ja.size()>0){
                    ListIterator<Object> it = ja.listIterator();
                    Object item = null;
                    while(it.hasNext()){
                        jo = (JSONObject)it.next();
                        item = buildBOInner(jo);
                        list.add(item);
                    }
                }
                ret = list;
            }
            break;
        case UUID:
            ret = StringUtil.getUUID();
            break;
        case SEQUENCE: // 交给程序处理
            ret = null;
            break;
        case EXPR:
            ret = null;//FIXME
            break;
        default:
            break;
        }
        return ret;
    }

    /**
     * @param jo
     * @return
     */
    private Object buildBOInner(JSONObject jo){
        Object ret = null;

        Class<?> clz = this.getClzObj();
        ret = BeanUtil.newInstance(clz);

        Map<String, ApiConfigNode> props = this.getProperties();

        String propName = null;
        String propNameInBo = null;
        ApiConfigNode propConfig = null;
        Object propValue = null;
        for(Map.Entry<String, ApiConfigNode> entry : props.entrySet()){
            propName = entry.getKey();
            propConfig = entry.getValue();
            propValue = jo.get(propName);
            propNameInBo = propConfig.getPropertyName();

            propValue = propConfig.buildBO(propValue);
            try{
            ApiReflectUtil.setProperty(ret, propNameInBo, propValue);
            }catch(Throwable e){
                e.printStackTrace();
            }
        }

        return ret;
    }

6、数据校验

在数据转换的过程中,可以读出属性的验证信息,进行格式验证。

核心代码如下所示。

for (String methodName : validateMap.keySet()) {
    String validateValue = StringUtil.safe2String(validateMap.get(methodName));
    ReflectUtil.invoke(ApiAssert.inst, methodName, new Object[] {
            fieldValue, validateValue, field, label, errorMsg });
}

其中,methodName的取值可以是 notEmpty 或 maxLength 或者 dictRange。
validateValue德取值可以是 true 或 25 或 ‘USER_TYPE’ 。全部是从配置文件读出。

通过反射,调用Java的验证方法,执行数据校验。

7、表达式

除了 String、BO、LIST、UUID 等基本类型,还支持 Expr 表达式类型。

当_type设为 Expr 时,同时必须设置 _clz 和 _value 属性。

应用场景如下:

1、当前属性引用其他属性的数据。配置如下:

    nickName:{
        _type:'Expr',
        _value:'this.loginName',
        _clz:'java.lang.String',
        _property : 'nickName',
        _label : '昵称,默认为登录名',
        _validate : {
            notEmpty : 'true',
            maxLength : 50  
        }
    }

如果引用上级数据,可以通过parent.entId获得。
2、当前属性的值是计算得到的。配置如下:

    sum:{
        _type:'Expr',
        _value:'this.mathScore + this.chineseScore',
        _clz:'java.lang.Long',
        _property : 'nickName',
        _label : '昵称,默认为登录名',
        _validate : {
            notEmpty : 'true',
            maxLength : 10  
        }
    }

3、表达式中可以有自定义函数。配置如下:

    age:{
        _type:'Expr',
        _value:'calcAge(this.birthday)',
        _clz:'java.lang.Long',
        _property : 'nickName',
        _label : '昵称,默认为登录名',
        _validate : {
            notEmpty : 'true',
            maxLength : 10  
        }
    }

4、也可以在校验中使用表达式,表达式的返回值务必是布尔值。配置如下:

    linkman:{
        _validate : {
            expr:'this.tel!=null || this.mobile!=null'
        }
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值