Java筆記 #06# 自定義簡易參數校驗框架——EasyValidator
索引
- 一、校驗效果演示
- 二、校驗器定義示例
- 定義一個最簡單的校驗器
- 正則校驗器
- 三、EasyValidator的實現
- 四、更好的應用姿勢——配合註解和面向切面
“參數校驗”屬於比較無聊但是又非常硬性的需求。。。
最原始的方式就是在方法頭手動逐個校驗,但是這樣寫不太好看,而且容易造成大量重復代碼,擴展起來也不是很方便。
我簡單看了一下已有的Spring Validation,粗看下去不太合胃口。想想寫一個似乎也不難,於是嘗試自定義了一個簡單的玩具版本。
一、校驗效果演示
MyTest.java:
publicclass 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