关于入参校验,有比用if else判断的更好方式
很多时候面对业务性质地项目,功能实现容易,但是维护起来不容易,当代码量达到某个级别,如果没有一些基本地策略很难维护好现有地代码质量。其中,参数乱传导致运行期间某名奇怪的错误和崩溃会让你头疼。其实,不管任何项目都会涉及到传参行为,然后会涉及到参数校验问题,很多时候是写个方法统一if else 判断各个参数是否合法,甚至有人在代码内部在使用入参的时候才开始判断是否为空等工作,效率之低,问题之大,面对此现象如何化解呢?
Android项目当然也逃不过,早期本人跟同事们也是这么干的,类似如下:
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;
}
看起来还可以,参数问题也的确能帮助排查问题,就是累人,而且还不可以自定义错误提示信息。
其实,Java早就有Bean Validation 2.0的标准,在Hibernate项目中得到了充分利用,其中定义了很多常见的Annotation,如@NotNull,@Min, @Max, @Size 等等。但是,在Android中想要使用他们,需要写注解解析器。所以,今天我们就是来写注解解释器的。
首先,让我们看看使用Demo:
class People {
@NotNull
@NotEmpty
List<Child> children = new ArrayList<>();
@NotNull
Child[] array;
}
class Child {
@NotNull
@Len(len = 4)
@NotBlank
String name;
@Min(min = 10)
@Range(min = 1, max = 20)
int age;
@Max(min = 200)
int height = 190;
Child(String name, int age) {
this.name = name;
this.age = age;
}
}
// 通过注解校验器校验java bean
People people = new People();
people.array = new Child[]{new Child("hell", 18)};
people.children.add(new Child("hhee", 10));
boolean valid = Validator.validate(v.getContext(), people);
if (valid){
// do something like: start new Activity, new Service or call some api
Toast.makeText(MainActivity.this, "params are valid", Toast.LENGTH_SHORT).show();
}
显而易见,以上Demo使用了七个Annotation:
- @NotNull:不能为null;
- @Len:长度必须为指定的;
- @NotBlank:字符串不能为空白;
- @Min:最小不能小于指定数字;
- @Max:最大不能大于指定数字;
- @Range:数字范围必须在指定的最小和指定的最大之间;
- 其实,可以根据业务自由扩充,比如@Email,@Phone,@IPV4等等
关于如何实现,以下举几个例子:
- 首先,定义Annotation,每个Annotation内部都提供了message()用于定义校验不通过的提示文案:
// @NotNull
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "%s不能为null";
}
// @Range
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Range {
int min();
int 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";
/**
* Validate fields under object.
*/
public static boolean validate(Context context, Object object) {
if (object == null) {
return false;
}
// ignore String and Number
if (object instanceof String || object instanceof Number) {
throw new IllegalArgumentException("String or Number instance is not supported");
}
// check 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;
}
// check 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;
}
// check 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
if (!(value instanceof String) && !(value instanceof 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 Range) {
validator = new RangeValidator((Range) 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());
Toast.makeText(context, validator.getMessage(), Toast.LENGTH_SHORT).show();
return false;
}
}
return true;
}
}
当然,对外入口API只有一个:
boolean valid = Validator.validate(context, object);
在随后的开发工作中,它将适用于团队合作的代码中,如每个页面或者Service的入参校验,每个自定义View的初始化入参校验,甚至每个封装的library对外公开的api入参校验等等。
首先,请求参数类对象打开就能看到每个字段的各自要求(不能为空,长度限制等),然后自己就有意识地传正确地参数给对方;
其次,即便不小心传错参数,Validator会校验提醒你错在哪边,无需对方开发同事停下手头工作帮你看你的问题,是不是这种工作方式也算增加了工作流的并发呢?
关于实现你可以参考这里