书接上回,本回书解决核心引擎的第二个问题:数据映射和数据校验。
我们把这个部分叫做数据转换模块。
输入数据的结构、属性名等,是接口发布方确定的。
出于安全、效率、调用方影响等方面的考虑,可能和自身系统中的结构和属性名不一致。
输入数据的格式可能有三种:
我们将对输入的数据进行校验,转换成自身系统的数据格式。
配置文件是数据转换模块的核心。
对于我方来说,数据的结构和格式是相对稳定的。
举个例子,发布方系统中注册用户数据格式如下。
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文件,可以保存在数据库中。
系统启动时,解析配置信息保存在内存中或者Memecached/redis中。
同时提供更新配置信息的方法。可以在不重启服务器的情况下更新配置。
提供通过接口名(即配置文件中的method)获取配置信息的方法。
解析代码如下所示。
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;
}
对于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;
}
给调用者提供范例数据,也是友好的做法。
代码如下。
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;
}
在本回第2节,我们预估了传入数据的格式,将有3种:Java对象、JSON格式和XML格式。
尽管格式不同,但数据结构和属性名都是按照配置文件的要求提供的。
转换的目标需要是Java对象。
通过反射创建实例、赋值。充分利用缓存,反射具备充分的高效率。
如果没有转换目标的Java BO类定义呢?
是否可以考虑用Map表示一个类的实例,Map中的key-Value表示属性的名字和值?
No!不要这样的方案,这种松散的Map对象带来各种不确定性。在追求稳定可控的情形下,不适宜出现。需要考虑的细节太多,各种长度的数字、各种表达方法的日期等等,需要花费的精力太多。
可以考虑这样一种折中方案:根据配置文件生成Java代码,手工调整后,编译得到目标class文件。
转换逻辑:
转换逻辑基本一致,只是从数据来源取值的方式不同。
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;
}
在数据转换的过程中,可以读出属性的验证信息,进行格式验证。
核心代码如下所示。
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的验证方法,执行数据校验。
除了 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‘
}
}
原文地址:http://blog.csdn.net/stationxp/article/details/45804495