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

《关于入参校验,有比用if else判断更好的方式》 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:

  1. @NotNull:不能为null, 作用于String和别的非数字对象,包括集合;
  2. @Len:长度必须为指定范围,作用于数字字段;
  3. @NotBlank:字符串不能为空白,作用于字符串;
  4. @NotEmpty:集合不能为空,用于集合;
  5. @Min:最小不能小于指定数字,作用于数字;
  6. @Max:最大不能大于指定数字,作用于数字;
  7. @Size:数字范围必须在指定的最小和指定的最大之间;
  8. 其实,可以根据业务自由扩充,比如@Email,@Phone,@IPV4等等

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

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

  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";

    /**
     * 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会校验提醒你错在哪边,无需对方开发同事停下手头工作帮你看你的问题。
关于实现你可以参考这里,有更好的建议欢迎指点!

    原文作者:生活简单些
    原文地址: https://www.jianshu.com/p/7b4ce9f8c60f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞