validation.png
很多时候面对业务性质地项目,功能实现容易,但是维护起来不容易,当代码量达到某个级别,如果没有一些基本地策略很难维护好现有地代码质量。其中,参数乱传导致运行期间某名奇怪的错误和崩溃会让你头疼。其实,不管任何项目都会涉及到传参行为,然后会涉及到参数校验问题,很多时候是写个方法统一if else 判断各个参数是否合法,甚至有人在代码内部在使用入参的时候才开始判断是否为空等工作,效率之低,问题之大,面对此现象如何化解呢?
曾经,有幸参与过Android 系统App定制开发,在Android App源码中发现一个现象:基本所有的Activity都定义了一个static startActivity()作为每个Activity打开的入口方法,此法虽然不算什么,但是它收口了每个Activity的启动入口,再加些参数校验似乎很OK,就像下面一样,其实早期团队也是这么做的:
public static void startActivity(Context context, String name, String email, String phone, int age){
if (ValidateUtils.isEmpty(name)
&& ValidateUtils.validateEmail(email)
&& ValidateUtils.validatePhone(phone)
&& ValidateUtils.isLargeThanZero(age)) {
Intent intent = new Intent(context, NextActivitity.class);
intent.putExtra(EXTRA_NAME, name);
intent.putExtra(EXTRA_EMAIL, email);
intent.putExtra(EXTRA_PHONE, phone);
intent.putExtra(EXTRA_AGE, age);
context.startActivity(intent);
}
// 举一个validate方法的例子
public static boolean validatePhone(String phoneNumber) {
return TextUtils.isEmpty(phoneNumber) && phoneNumber.length() != 11;
}
看起来还可以,参数问题也的确能帮助排查问题,就是需要写很多字段判断,比较累人,而且还不可以自定义错误提示信息,如果换一种方式呢:
SecondActivityParams params = new SecondActivityParams();
params.name = "张三";
params.childrenArray = new SecondActivityParams.Child[] {new SecondActivityParams.Child("hello world", 10)};
params.childrenList.add(new SecondActivityParams.Child("hello world", 8));
SecondActivityParams.Child child = new SecondActivityParams.Child("hello world", 8);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, -5);
child.setToys(new SecondActivityParams.Toy("toy1", 999F, calendar.getTimeInMillis()));
params.childrenMap.put(1, child);
params.startActivity(v.getContext(), params);
这个例子中你可能注意到了,startActivity的发起者是IntentParams的子类对象(其实方法定义在IntentParams里的,它是一个基类),当然这个例子参数类型很多也较复杂,如果再用上面的方式做校验,工作量会显得很大。
单看intentParams.startActivity(v.getContext(), params)
好像没有看到校验,其实都用注解标注在类的定义上了。
其实,Java早就有Bean Validation 2.0的标准,在Hibernate项目中得到了充分利用,其中定义了很多常见的Annotation,如@NotNull,@Min, @Max, @Size 等等。但是,在Android中想要使用他们,需要写注解解析器。所以,今天我们就是来写注解解释器的。
在介绍注解解释器之前先让大家了解下IntentParams以及上面的SecondActivityParams是如何定义的:
public class IntentParams implements Serializable {
public static final String EXTRA_INTENT_PARAMS = "intentParams";
private Class<?> clazz;
private String action;
private Bundle bundle;
protected IntentParams(@NonNull Class<?> clazz){
this.clazz = clazz;
}
protected IntentParams(@NonNull String action){
this.action = action;
}
public void setBundle(Bundle bundle){
this.bundle = bundle;
}
public static IntentParams with(@NonNull Class<?> clazz){
return new IntentParams(clazz);
}
public static IntentParams with(@NonNull String action){
return new IntentParams(action);
}
private Intent toIntent(Context context){
Intent intent = new Intent();
if (clazz != null){
intent.setClass(context, clazz);
intent.putExtra(EXTRA_INTENT_PARAMS, this);
} else if (!TextUtils.isEmpty(action)){
intent.setAction(action);
intent.putExtra(EXTRA_INTENT_PARAMS, this);
}
return intent;
}
private Bundle getBundle() {
return bundle;
}
/**
* startActivity and validate the intent params.
*/
public void startActivity(@NonNull Context context, @NonNull IntentParams params){
if(Validator.validate(context, params)){
if (params.getBundle() != null){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
context.startActivity(params.toIntent(context), params.getBundle());
} else {
context.startActivity(params.toIntent(context));
}
} else {
context.startActivity(params.toIntent(context));
}
}
}
// 除了startActivity(Context, IntentParams) 还提供了startService(Context, IntentParams), sendBroadcast(Context, IntentParams)等基础API。
}
这是一个基类,默认提供构造最基础只有class或者action的构造方法,所有提供类似startActivity(Context, IntentParams)的API都会进行对IntentParams进行校验,如果不带参数的跳转用这种方式
IntentParams.with(SecondActivity.class).startActivity(context)
即可.
class Man implements Serializable{
@NotBlank
String name = "zhang";
@NotEmpty(message = "childrenMap 为空")
Map<Integer, Child> childrenMap = new HashMap<>();
@NotEmpty(message = "childrenArray 为空")
Child[] childrenArray;
@NotEmpty(message = "childrenList 为空")
List<Child> childrenList = new ArrayList<>();
static class Child implements Serializable {
@Len(min = 4, max = 20)
String name;
@Size(min = 1, max = 20)
int age;
@Min(min = 50)
int height = 190;
@NotNull(message = "玩具可以没有,但是不可以没有放玩具的箱子")
List<Toy> toys;
Child(String name, int age) {
this.name = name;
this.age = age;
this.toys = new ArrayList<>();
}
void setToys(Toy... toys){
this.toys = Arrays.asList(toys);
}
}
static class Toy implements Serializable{
@NotBlank(message = "不知道是什么玩具就乱买")
String toyName;
@Max(max = 100, message = "价格太贵了")
float toyPrice;
@Min(min = 1451567252308L, message = "玩具太旧了")
long boughtDate;
Toy(String toyName, float toyPrice, long boughtDate){
this.toyName = toyName;
this.toyPrice = toyPrice;
this.boughtDate = boughtDate;
}
}
}
显而易见,以上Demo使用了七个Annotation:
- @NotNull:不能为null, 作用于String和别的非数字对象,包括集合;
- @Len:长度必须为指定范围,作用于数字字段;
- @NotBlank:字符串不能为空白,作用于字符串;
- @NotEmpty:集合不能为空,用于集合;
- @Min:最小不能小于指定数字,作用于数字;
- @Max:最大不能大于指定数字,作用于数字;
- @Size:数字范围必须在指定的最小和指定的最大之间;
- 其实,可以根据业务自由扩充,比如@Email,@Phone,@IPV4等等
关于如何实现,以下举几个例子:
- 首先,定义Annotation,每个Annotation内部都提供了message()用于定义校验不通过的提示文案:
// @NotNull
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "%s不能为null";
}
// @Size
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Size {
long min();
long max();
String message() default "%s不在范围%d和%d之间";
}
message()方法默认返回模版错误提示内容,但具体场景可以自定义,即:返回一个固定场景的提示语,如:“用户名不能为空”。
- 对Annotation的解析器进行抽象:
public abstract class ConstraintValidator<A extends Annotation> {
protected A annotation;
protected String fieldName;
public abstract boolean isValid(Object value);
ConstraintValidator(A annotation, String fieldName){
this.annotation = annotation;
this.fieldName = fieldName;
}
public abstract String getMessage();
}
- 定义Annotation解析器:
class NotNullValidator extends ConstraintValidator<NotNull> {
NotNullValidator(NotNull annotation, String fieldName) {
super(annotation, fieldName);
}
/**
* 先尝试取默认内容(根据是否包含%),否则取用户自定义内容
*/
@Override
public String getMessage() {
String message = annotation.message();
if (message.contains("%")) {
return String.format(annotation.message(), fieldName);
} else {
return message;
}
}
@Override
public boolean isValid(Object value) {
return value != null;
}
}
每个Annotation对应一个解析器,同样类似的还有LenValidator、MaxValidator、MinValidator等等,一共七个。
- 对外Annotation校验器, 尝试所有支持的Annotation对指定的对象进行校验,如果校验通过则返回true,否则返回false并弹Toast,Toast内容为getMessage()返回内容:
public class Validator {
private static final String TAG = "Validator";
/**
* ignore String, Number and Boolean
*/
public static boolean validate(Context context, Object object){
if (object != null
&& !(object instanceof String)
&& !(object instanceof Number)
&& !(object instanceof Boolean)) {
return doValidate(context, object);
}
return true;
}
/**
* validate fields under object.
*/
private static boolean doValidate(Context context, Object object) {
if (object == null) {
return true;
}
// validate fields in java.util.List
if (object instanceof List) {
List list = (List) object;
for (Object item : list) {
boolean valid = validate(context, item);
if (!valid) {
return false;
}
}
return true;
}
// validate values in map
if (object instanceof Map){
Map map = (Map) object;
// validate values
for (Object next : map.values()){
boolean valid = doValidate(context, next);
if (!valid) {
return false;
}
}
return true;
}
// validate fields in array
if (object.getClass().isArray()) {
Object[] array = (Object[]) object;
for (Object item : array) {
boolean valid = validate(context, item);
if (!valid) {
return false;
}
}
return true;
}
// validate fields of object
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
// ignore static and final
int modifiers = field.getModifiers();
boolean isStatic = Modifier.isStatic(modifiers);
boolean isFinal = Modifier.isFinal(modifiers);
if (isStatic || isFinal) {
continue;
}
try {
field.setAccessible(true);
Object value = field.get(object);
Annotation[] annotations = field.getAnnotations();
if (annotations.length > 0) {
for (Annotation annotation : annotations) {
// validate object its self
boolean valid = validateWithAnnotation(context, annotation, value, field.getName());
if (!valid) {
return false;
}
}
}
// validate fields of object, but make sure it's not String or Number
boolean valid = validate(context, value);
if (!valid) {
return false;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
return false;
}
}
return true;
}
/**
* validate value with annotations that we support.
*
* @param context Android context
* @param annotation annotation to validate with
* @param object object to validate
* @return return true when the object passes validation
* or the annotation we don't support
*/
private static <A extends Annotation> boolean validateWithAnnotation(Context context,
A annotation,
Object object,
String fieldName) {
ConstraintValidator validator = null;
if (annotation instanceof NotNull) {
validator = new NotNullValidator((NotNull) annotation, fieldName);
} else if (annotation instanceof NotEmpty) {
validator = new NotEmptyValidator((NotEmpty) annotation, fieldName);
} else if (annotation instanceof Min) {
validator = new MinValidator((Min) annotation, fieldName);
} else if (annotation instanceof Max) {
validator = new MaxValidator((Max) annotation, fieldName);
} else if (annotation instanceof Size) {
validator = new SizeValidator((Size) annotation, fieldName);
} else if (annotation instanceof Len){
validator = new LenValidator((Len) annotation, fieldName);
} else if (annotation instanceof NotBlank){
validator = new NotBlankValidator((NotBlank) annotation, fieldName);
}
if (validator != null) {
boolean valid = validator.isValid(object);
if (!valid) {
Log.e(TAG, validator.getMessage(object));
Toast.makeText(context, validator.getMessage(object), Toast.LENGTH_SHORT).show();
return false;
}
}
return true;
}
}
关于代码实现,思路是很easy的,只要定义对应的Annotation再定义对应的Validator,然后加入到判断规则里即可,这就意味着想扩展额外的校验非常容易。
最后做个总结吧,这种方式做校验好处有很多,首先,请求参数类对象打开就能看到每个字段的各自要求(不能为空,长度限制等),然后自己就有意识地传正确地参数给对方;
其次,即便不小心传错参数,Validator会校验提醒你错在哪边,无需对方开发同事停下手头工作帮你看你的问题。
关于实现你可以参考这里,有更好的建议欢迎指点!