如何做一个简单的开放接口(3)-核心引擎(下)
2015-05-18 01:03
411 查看
1、要实现的功能
书接上回,本回书解决核心引擎的第二个问题:数据映射和数据校验。我们把这个部分叫做数据转换模块。
2、输入数据的格式
输入数据的结构、属性名等,是接口发布方确定的。出于安全、效率、调用方影响等方面的考虑,可能和自身系统中的结构和属性名不一致。
输入数据的格式可能有三种:
反序列化后得到的Java对象。
JSON格式。
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、转换逻辑
转换逻辑:找到对应的配置信息对象。
配置信息中包含当前数据的类型信息,依据不同类型进行处理。
如果当前数据是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' } }
相关文章推荐
- 如何做一个简单的开放接口(2)-核心引擎(上)
- 如何做一个简单的开放接口(1)-功能设计
- 如何做一个简单的开放接口(4)-常见Handler的参考实现
- 如何实现一个简单的工作流审批引擎
- 一个简单的游戏引擎核心状态机的C++实现
- 如何实现一个简单的工作流审批引擎——请看
- easyopen——一个简单易用的接口开放平台
- easyopen——一个简单易用的接口开放平台
- C++多态是如何实现的——一个简单明晰的例子告诉你!
- 如何使用反射确定一个属性是否实现了IList接口,如何确定元素量为空的集合的元素类型。
- WebService基础教程之一(概念,如何发布和调用一个简单的WebService)
- Egret 中一共封装了7个显示相关的核心类,一个接口
- javascript如何用递归写一个简单的树形结构
- 如何用phototype框架实现一个简单的ajax验证
- 如何基于微信开放接口开发企业的微信CRM
- 如何设计一个异步Web服务——接口部分
- 如何使用最简单的方法将一个已经存在的工程中使用 cocaPodfile
- 六十 如何写一个简单的守护(精灵)进程原型
- 18. 如何使用GameCenter制作一个简单的多人游戏教程:第一部分
- 如何创建一个简单的线程