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

来源:互联网 发布:微信视频制作软件 编辑:程序博客网 时间:2024/05/16 04:35

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'        }    }
0 1