Android开发Android开发经验谈Android开发

关于入参校验,有比用if else判断的更好方式

2018-11-20  本文已影响30人  生活简单些
validation.jpg

  很多时候面对业务性质地项目,功能实现容易,但是维护起来不容易,当代码量达到某个级别,如果没有一些基本地策略很难维护好现有地代码质量。其中,参数乱传导致运行期间某名奇怪的错误和崩溃会让你头疼。其实,不管任何项目都会涉及到传参行为,然后会涉及到参数校验问题,很多时候是写个方法统一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:


  1. @NotNull:不能为null;
  2. @Len:长度必须为指定的;
  3. @NotBlank:字符串不能为空白;
  4. @Min:最小不能小于指定数字;
  5. @Max:最大不能大于指定数字;
  6. @Range:数字范围必须在指定的最小和指定的最大之间;
  7. 其实,可以根据业务自由扩充,比如@Email,@Phone,@IPV4等等

关于如何实现,以下举几个例子:

  1. 首先,定义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()方法默认返回模版错误提示内容,但具体场景可以自定义,即:返回一个固定场景的提示语,如:“用户名不能为空”。

  1. 对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();
}
  1. 定义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等等,一共七个。

  1. 对外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会校验提醒你错在哪边,无需对方开发同事停下手头工作帮你看你的问题,是不是这种工作方式也算增加了工作流的并发呢?
关于实现你可以参考这里

上一篇下一篇

猜你喜欢

热点阅读