Java工具类——通过配置XML验证Map
背景
在JavaWeb项目中,接收前端过来的参数时通常是使用我们的实体类进行接收的。但是呢,我们不能去决定已经搭建好的框架是怎么样的,在我接触的框架中有一种就是通过Map来接收前端过来的所有参数,框架中没有实体类的说法,从接收参数,验证参数到参数至持久层整个过程都是通过Map来传递数据。
而在开发的过程中,减少了实体类的存在,有时是感觉挺方便的,比如:一个系统中有100多个表,这里我们可以减少工作量(虽然对应表的实体可以代码生成),因为我们开发过程中是需要返回多个表关联后的结果的,这里可能我们需要创建DTO
,这些步骤确实是挺烦人的。但是,前端过来的参数我们需不需要验证呢?客户的输入不管有意或者是无意,我认为都应该让系统的容错能力更强悍一些。所以,在验证前端过来的参数时,使用了Map
就着实让人头痛。每个需要强制验证的参数都需要get
,然后判断类型,强制转型,判断参数符不符合期望值边界等。
所以,我就考虑了,实体类可以通过Spring MVC
中Hibernate
的Validation
使用注解的方式进行参数校验,那么,少了实体类,我是不是可以通过配置XML的方式来达到类似有实体类的效果。网上找了类似关键词的工具类,发现没有我所期望的,所以就动手来了一个。
大致的想法
在Web
开发时,有许多if-else
语句的出现都是在为了验证前端参数合不合法真的是挺无奈的,而且有些代码虽然长起来类似但是呢要去重构成一个公用的方法好像有些困难,时常问自己,要怎么去搞,Java
不是JavaScript
,语句没那么灵活。
于是想着通过XML
配置试试,大致就是通过配置好的XML
代替我们的实体类,并且有个入口将XML
中的实体映射,并传入待验证的Map
,验证之后传出一个数组,如果验证通过数组为空,不通过则是我们XML
中配置的对应错误语句。
如何设计XML格式
动手在这之前,需要想好我们大致的XML
结构是怎么样的。这里,我的想法是,在我们一般遇到的参数主要就是Integer
,String
,Double
,Date
,List
了(这里居然没有考虑Boolean
,算了,之后再做补充也行)。所以基于以上,设计的结构大致如下:
<?xml version="1.0" encoding="UTF-8"?>
<map-verify
xmlns="https://www.lger.cn/verify"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.lger.cn/verify map-verify-util.xsd"
>
<!--
在长度的方面分别使用了
lt; lte; gt; gte; eq;分别代表<; <=; >; >=; =
-->
<Entity name="User01">
<!--castErrMsg为当验证过程中类型解析错误时返回提示-->
<String name="username" castErrMsg="username必须为字符串类型">
<length>
<lt errMsg="当前值不能小于2">2</lt>
</length>
<notNull errMsg="当前值不能为空"/>
<notBlank errMsg="当前值不能为空(去掉首尾空格)"/>
<pattern>
<!--如果不匹配则发出错误-->
<value errMsg="号码D[0-8]{4}">[0-8]{4}</value>
<value errMsg="号码D[0-7]{4}">[0-7]{4}</value>
</pattern>
</String>
<Integer name="age" castErrMsg="必须为整型">
<lt errMsg="岁数必须大于2">2</lt>
<gt>150</gt>
<notNull/>
</Integer>
<List name="details">
<notEmpty/>
<size>
<gte>0</gte>
</size>
<!-- 遍历List内容,遍历的Entity映射为name为User02的实体 -->
<forEach entity="User02" />
</List>
<Date name="birthday">
<lt>1900-07-21</lt>
<notNull/>
</Date>
<Double name="money">
<lt>1</lt>
<gt>150</gt>
<notNull/>
</Double>
</Entity>
<Entity name="User02">
<String name="username">
<length>
<lt errMsg="当前值不能小于2">2</lt>
<gt errMsg="当前值不能大于10">10</gt>
</length>
<notBlank errMsg="当前值不能为空(去掉空格)"/>
</String>
</Entity>
</map-verify>
需要写出一个具有一定格式的XML
那么还需要写出一个约束文件了验证是否XML
格式写的正确。这里还小学了一下XSD
,这里就不贴出代码了。
以上的XML
大致的说了下怎么使用其替代实体类。写到了这里其实有时还觉得使用实体类可能更方便,何必使用Map
呢?但是,别人框架已经写了,参数只能接收到Map
那我只能屈服了。
类结构设计
首先,每一个Entity
就是一个实体对象,这里我认为每个Entity都应该包含有一个验证方法和一个初始化方法,因为在进行XML
解析时就调用init
,在进行Map
验证时就调用verify
方法这样,那些String
节点也是类似的,解析初始化时就把XML
中配置的信息保存起来,等到验证时就通过之前保存的信息进行判断即可,不必重新解析了。
这里以解析一个IntegerEntity
为例,首先是其父类,不论是XML
中哪个节点,都是实现VerifyEntity
接口,代码如下:
public interface VerifyEntity {
/**
* 一个实体初始化,当实体被创建时将由创建方主动调用
* @param currentEle 被创建的实体节点
* @param factory 实体创建工厂,可以通过此工厂创建Entity
*/
void init(Element currentEle, EntityFactory factory);
/**
* 验证当前节点是否匹配XML的配置
* @param value 需要验证的值
* @return 异常字符串集
*/
String[] verify(Object value);
/**
* 初始化完毕后调用,传入包含所有Entity的Map
* @param entityMap entityMap
*/
void finished(Map<String, VerifyEntity> entityMap);
}
首先解析XML
时当获取到Entity
节点下的子节点将会通过一个工厂类创建子节点的对应实现VerifyEntity
,之后调用init
方法对当前的子节点进行解析。这里先看下IntegerEntity
的源码:
public class IntegerEntity implements VerifyEntity {
private AbstractEquation<Integer> integerEquation;
private boolean notNull = false;
private String notNullErrMsg;
private String castErrMsg;
@Override
public void init(Element currentEle, EntityFactory factory) {
// 开始解析当前节点<Integer/>
String name = currentEle.attributeValue("name");
// 获取节点属性castErrMsg,看是否存在castErrMsg
this.castErrMsg = currentEle.attributeValue("castErrMsg");
if (Util.isEmpty(this.castErrMsg)) {
this.castErrMsg = name + ": this is not integer type.";
} else {
this.castErrMsg = name + ": " + this.castErrMsg;
}
// 获取子节点notNull
Element element = currentEle.element("notNull");
if (element != null) {
this.notNull = true;
this.notNullErrMsg = element.attributeValue("errMsg");
if (Util.isEmpty(this.notNullErrMsg)) {
this.notNullErrMsg = name + ": this is not null";
} else {
this.notNullErrMsg = name + ": " + this.notNullErrMsg;
}
}
// 这里是新建一个抽象的Equation类,主要是因为lt和lte等等的这些节点在其他实体中也有,为了代码复用所以使用了抽象类来定义
//这里实现后与上面解析notNull代码差异不大
integerEquation = new AbstractEquation<Integer>(currentEle, name) {
@Override
Integer valueOf(String value) {
try {
return Integer.valueOf(value);
}catch (NumberFormatException e) {
throw new ConvertException(castErrMsg);
}
}
@Override
boolean lessThan(Integer value, Integer lt) {
return value < lt;
}
@Override
boolean greaterThan(Integer value, Integer gt) {
return value > gt;
}
@Override
boolean lessThanOrEquals(Integer value, Integer lte) {
return value <= lte;
}
@Override
boolean greaterThanOrEquals(Integer value, Integer gte) {
return value <= gte;
}
@Override
boolean equals(Integer value, Integer eq) {
return value.equals(eq);
}
};
}
@Override
public String[] verify(Object value) {
//正式验证参数是否合法
if (value == null) {
//如果之前解析包含notNull,则这里为true,那么将返回解析的notNullErrMsg
if (this.notNull) {
return new String[]{this.notNullErrMsg};
}
return null;
}
try {
//使用上面实现的抽象类进行验证
return integerEquation.verify(Integer.valueOf(value.toString()));
}catch (NumberFormatException e) {
return new String[]{this.castErrMsg};
}
}
@Override
public void finished(Map<String, VerifyEntity> entityMap) {
//这里是在Entity解析完毕后调用,并将保存Entity的Map传入
}
}
其实根据以上的代码可以看出,就是XML
的节点对应着一个VerifyEntity
实现,每一个实现都保存着其中定义的<notNull/>
等信息。
实现后运行效果
这里我们的测试代码如下,其中demo.xml
为设计XML
所示的代码:
//初始化MapVerify,将定义好xml以数组方式传入
final String[] xmls = {"demo.xml"};
MapVerify.init(xmls);
final Map<String, Object> map = new HashMap<>(5);
map.put("username", "12312371237123778");
map.put("age", 1);
map.put("birthday", "2000-07-21");
map.put("money", 149);
List<Map<String, Object>> list = new ArrayList<>(2);
Map<String, Object> map1 = new HashMap<>(1);
map1.put("username", "abcab");
Map<String, Object> map2 = new HashMap<>(1);
map2.put("username", "cc");
list.add(map1);
list.add(map2);
map.put("details", list);
System.out.println(Arrays.toString(MapVerify.verifyByReturnArr("User01", map)));
运行的结果如下:
[age: 岁数必须大于2]
总结
此想法是半年前的了,现在利用了空余的时间实现了自己的想法,趁热打铁写下博客,希望有这个需求的小伙伴能节省自己的时间(注:这里Boolean的判断没有实现,如果有需要就需要自己动手了:-)
)。以后也希望自己如果有什么小想法尽量的抽时间去做,不论做的好不好。