1. 程式人生 > >Java筆記 #06# 自定義簡易參數校驗框架——EasyValidator

Java筆記 #06# 自定義簡易參數校驗框架——EasyValidator

ide lse logger 需求 diy eve rdquo names page

索引

  • 一、校驗效果演示
  • 二、校驗器定義示例
    • 定義一個最簡單的校驗器
    • 正則校驗器
  • 三、EasyValidator的實現
  • 四、更好的應用姿勢——配合註解和面向切面

“參數校驗”屬於比較無聊但是又非常硬性的需求。。。

最原始的方式就是在方法頭手動逐個校驗,但是這樣寫不太好看,而且容易造成大量重復代碼,擴展起來也不是很方便。

我簡單看了一下已有的Spring Validation,粗看下去不太合胃口。想想寫一個似乎也不難,於是嘗試自定義了一個簡單的玩具版本。

一、校驗效果演示

MyTest.java:

public
class MyTest { /** * 創建一次,應用N次。 */ private static final EasyValidator EASY_VALIDATOR = new EasyValidator(); public static void main(String[] args) { // 允許自定義各種各樣的校驗器,只需實現Validator接口即可 EASY_VALIDATOR.addValidator(new RegexpValidator()); EASY_VALIDATOR.addValidator(
new TypeValidator()); // 示例↓ int type = 3; String username = " "; String password = "32"; // 通過拋異常的方式傳遞具體錯誤信息 EASY_VALIDATOR.check(type, "type"); EASY_VALIDATOR.check(username, "username.regexp"); EASY_VALIDATOR.check(password,
"password.regexp"); } } /* ouput example - 1: Exception in thread "main" org.sample.exception.ValidateException: type越界了! ouput example - 2: Exception in thread "main" org.sample.exception.ValidateException: 用戶名不能為空 */

通過addValidator方法讓校驗器生效。

二、校驗器定義示例

僅需實現Validator接口就可以隨心所欲地定義各式各樣的參數校驗器。

Validator接口僅包含三個非常簡單的方法:

技術分享圖片
public interface Validator {

    /**
     * 用於標識和查找校驗器
     */
    String getName();

    void validate(Object o);

    /**
     * 便於多級查找校驗方法,如果不支持該功能,
     * 直接內部調void validate(Object o);方法即可
     */
    void validate(Object o, String validatorName);
}
Validator.java

定義一個最簡單的校驗器

validate方法說明:校驗不通過拋異常就ok了,在異常裏傳遞具體錯誤信息。

定義不拋異常的校驗器。。是沒意義的。在“傳Result”和“傳異常”間我也權衡了一下,網上說“傳異常”沒“傳Result”性能好,但就寫代碼的角度來看,還是傳異常幹凈得多,並且更簡單,性能方面的差距估計也是微乎其微,so。。。。

public class TypeValidator implements Validator {

    private static final String NAME = "type";

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public void validate(Object o) {
        int i = Integer.parseInt(String.valueOf(o));
        // 通過拋異常的方式傳遞具體錯誤信息
        if (i < 0 || i > 10) throw new ValidateException("type越界了!");
        // 沒拋出異常代表沒問題!
    }

    @Override
    public void validate(Object o, String validatorName) {
        validate(o);
    }
}

正則校驗器

同樣可以定義一個復雜些的校驗器↓

原始版本來自Spring筆記#02#利用切面和註解校驗方法參數

這是一個專門用來校驗字符串的校驗器,思路就是用.properties文件存鍵值對(“名”-正則)然後讀到HashMap構造Pattern。根據“名”查找對應的Pattern進行正則校驗。順便提一下自定義的兩種異常:ValidatorException和ValidateException,兩個異常都繼承自運行時異常,這樣不至於把代碼搞亂,前者屬於程序員的錯誤,後者則用於拋出校驗失敗的具體信息。

public class RegexpValidator implements Validator {

    private static final String NAME = "regexp";

    private static final String FILE_NAME = File.separator + "easy-validator.properties";

    private static final Map<String, Pattern> MAP = new HashMap<>();

    static {
        // 把.properties文件裏的鍵值對讀到內存裏
        Properties prop = new Properties();
        try (InputStream input = ServiceMethodAspect.class.getClassLoader().getResourceAsStream(FILE_NAME)){
            if (input == null) {
                throw new RuntimeException("Sorry, unable to find " + FILE_NAME);
            }
            prop.load(input);
            Enumeration<?> e = prop.propertyNames();
            while (e.hasMoreElements()) {
                String key = (String) e.nextElement();
                String value = prop.getProperty(key);
                MAP.put(key, Pattern.compile(value));
            }
        } catch (IOException e) {
            throw new RuntimeException("An exception occurred while reading " + FILE_NAME, e);
        }
    }

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public void validate(Object o) {
        throw new ValidatorException("validate(Object o)方法未定義");
    }

    @Override
    public void validate(Object o, String validatorName) {
        String str = String.valueOf(o);
        if (StringUtils.isBlank(str)) {
            throw new ValidateException(PresentationUtils.english2chinese(validatorName) + "不能為空");
        } else {
            Pattern pattern = MAP.get(validatorName);
            // 程序員要確保格式已經定義
            if (pattern == null) throw new ValidatorException(validatorName + "校驗器未定義");
            // 格式檢查
            if (!pattern.matcher(str).matches()) {
                throw new ValidateException(PresentationUtils.english2chinese(validatorName) + "格式不正確");
            }
        }
    }
}

在相應路徑下的easy-validator.properties中定義正則表達式:

username=^[a-zA-Z0-9_]{4,16}$
# 用戶名:4到16位大小寫字母、數字、下劃線
password=^[a-zA-Z0-9_]{6,16}$
# 密碼:6到16位大小寫字母、數字、下劃線

三、EasyValidator的實現

EasyValidator的實現像它的名字一樣簡單。每個校驗器由getName()的返回值標識(確保唯一),允許自定義多級校驗器,命名規則為“xx.次級校驗器名.頂級校驗器名”,詳細匹配過程參考代碼實現:

/**
 * 自定義的一個簡易參數校驗器
 */
public class EasyValidator {
    /**
     * 初始化後不再更新,否則在多線程環境下有風險
     */
    private static final Map<String, Validator> MAP = new HashMap<>();

    public void addValidator(Validator validator) {
        MAP.put(validator.getName(), validator);
    }

    public void check(Object object, String validatorName) {
        Validator validator = MAP.get(getLast(validatorName));
        if (validator != null) {
            validator.validate(object, removeLast(validatorName));
        } else {
            throw new ValidatorException("validator not found, validatorName=" + validatorName);
        }
    }

    /**
     * 獲取頂級校驗器名稱,如果只有一級就原樣返回
     */
    private static String getLast(String validatorName) {
        int index = StringUtils.lastIndexOf(validatorName, ‘.‘);
        if (index == -1) return validatorName;
        return StringUtils.substring(validatorName, index + 1);
    }

    /**
     * 去掉頂級校驗器名稱,以便向下傳遞,如果只有一級就原樣返回
     */
    private static String removeLast(String validatorName) {
        int index = StringUtils.lastIndexOf(validatorName, ‘.‘);
        if (index == -1) return validatorName;
        return StringUtils.substring(validatorName, 0, index);
    }
}

四、更好的應用姿勢——配合註解和面向切面

Example代碼(因為用到了Spring的黑科技,需要添加相應依賴):

@Aspect
@Configuration
public class ServiceMethodAspect {

    private static final Logger LOGGER = LogManager.getLogger();

    private static final ParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

    private static final EasyValidator EASY_VALIDATOR = new EasyValidator();

    static {
        EASY_VALIDATOR.addValidator(new RegexpValidator());
    }

    @Pointcut("execution(* org.sample.shop.common.service.impl.*.*(..))")
    public void serviceMethod() {}

    @Around("serviceMethod()")
    public ServiceResult work(ProceedingJoinPoint jp) {
        // 參數校驗
        Method method = ((MethodSignature) jp.getSignature()).getMethod();
        Parameter[] parameter = method.getParameters(); // 便於獲取註解對象
        String[] parameterName = DISCOVERER.getParameterNames(method);
        Object[] parameterValue = jp.getArgs(); 
        String currentValidatorName;
        try {
            // 逐個參數進行校驗,先嘗試獲取Validator註解值,註解不存在用參數名作為校驗器名
            for (int i = 0; i != parameter.length; ++i) {
                if (parameter[i].getAnnotation(Validator.class) != null) {
                    currentValidatorName = parameter[i].getAnnotation(Validator.class).value();
                } else {
                    currentValidatorName = parameterName[i];
                }
                LOGGER.info("check: object={}, validatorName={}", parameterValue[i], currentValidatorName); // 偶爾出現校驗異常有日誌可查 TODO 用戶信息
                EASY_VALIDATOR.check(parameterValue[i], currentValidatorName);
            }
            return (ServiceResult) jp.proceed();
        } catch (ValidateException e) {
            return ServiceResult.fail(e.getMessage());
        } catch (Throwable throwable) {
            // 錯誤日誌
            LOGGER.error(new DetailedInfo(jp.getArgs(), throwable.getMessage()), throwable);
            return ServiceResult.error();
        }
    }

    private class DetailedInfo {

        private Object[] args;

        private String message;

        DetailedInfo(Object[] args, String message) {
            this.args = args;
            this.message = message;
        }

        @Override
        public String toString() {
            return "DetailedInfo{" +
                    "args=" + Arrays.toString(args) +
                    ", message=‘" + message + ‘\‘‘ +
                    ‘}‘;
        }
    }
}

Example代碼之應用註解:

    @Override
    public ServiceResult<User> getUser(@Validator("username.regexp") String username, @Validator("password.regexp") String password) {
        return ServiceUtils.daoOperation(() -> {
            User user = userDAO.getUser(username, password);
            return user != null ? ServiceResult.ok(user) : new ServiceResult<>(LOGIN_FAIL);
        }, Connection.TRANSACTION_READ_COMMITTED);
    }

註解的實現依然和Spring筆記#02#利用切面和註解校驗方法參數裏的差不多,只不過換了個名字。。。

Java筆記 #06# 自定義簡易參數校驗框架——EasyValidator