接口參數(shù)校驗(yàn)之JSR303+AOP

前言

參數(shù)校驗(yàn)箭启,相信每個(gè)后端開發(fā)人員都有接觸。一般情況下前后端都會(huì)對(duì)數(shù)據(jù)進(jìn)行雙重校驗(yàn)蛉迹,保證正確性傅寡。比如某個(gè)參數(shù)不可為null,手機(jī)號(hào)格式不對(duì),等等荐操。
本文的重點(diǎn)在接口級(jí)別校驗(yàn)芜抒,起始于工作中的微服務(wù)接口參數(shù)檢查。大腦最省力原則告訴我托启,能不一個(gè)一個(gè)手動(dòng)寫宅倒,咱就堅(jiān)持抽象出來(lái)。
通常采用的是JSR303規(guī)范來(lái)做校驗(yàn)屯耸,Hibernate validator是JSR303規(guī)范的一種很好的實(shí)現(xiàn)拐迁。

依賴引入(Maven)

略(......)見諒

接口返回

抽象,那么執(zhí)行接口方法之前要check參數(shù)疗绣,不符合場(chǎng)景直接返回結(jié)果唠亚。首先,我們抽象一個(gè)接口返回包裝類持痰。相信這個(gè)已經(jīng)不稀奇灶搜,即使沒有參數(shù)校驗(yàn),大家N年前就已經(jīng)做了工窍。

public class MessageReturn<T> {

    /** 狀態(tài):默認(rèn)0 成功割卖;其他為失敗 */
    private int status;
    /** 響應(yīng)結(jié)果*/
    private T result;
    /** 相應(yīng)描述*/
    private String message;

    // 余下部分已省略
}

自定義注解

AOP切點(diǎn)

/**
 * 參數(shù)校驗(yàn)注解類
 *
 * @author  wangzhuhua
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Order(AspectOrderConstant.Validator)
@Documented
@Inherited
public @interface HValidate {

    /**默認(rèn)錯(cuò)誤碼*/
    // 這個(gè)default 是上面的MessageReturn
    int errorCode() default MessageUtil.BUSY;
}

校驗(yàn)分組

這里自定義了分組校驗(yàn)規(guī)則

/**
 * 參數(shù)校驗(yàn)分組注解類
 *
 * @author  wangzhuhua
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@Constraint(validatedBy = HValidateGroup.HValidateGroupValidator.class)
@Documented
public @interface HValidateGroup {

    //默認(rèn)錯(cuò)誤消息
    String message() default "";

    //分組
    Class<?>[] groups() default { };

    //負(fù)載
    Class<? extends Payload>[] payload() default { };

    //校驗(yàn)分組
    Class<?>[] value() default {};

    class HValidateGroupValidator implements ConstraintValidator<HValidateGroup, Object> {

        private Class<?>[] groups;

        private ValidatorFactory validatorFactory;

        public void initialize(HValidateGroup hValidateGroup) {
            this.groups = hValidateGroup.value();

            validatorFactory = ValidatorInterceptor.ValidatorFactory;
        }

        public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
            if (object == null) {
                return true;
            }

            Set<ConstraintViolation<Object>> validResult = validatorFactory.getValidator().validate(object, groups);
            if(validResult.size() == 0){
                return true;
            }

            Iterator<ConstraintViolation<Object>> iterator = validResult.iterator();
            while(iterator.hasNext()){
                ConstraintViolation violation = iterator.next();
                constraintValidatorContext.disableDefaultConstraintViolation();
                constraintValidatorContext.buildConstraintViolationWithTemplate(violation.getMessageTemplate())
                        .addPropertyNode(violation.getPropertyPath().toString())
                        .addConstraintViolation();
            }

            return false;
        }
    }
}

AOP攔截校驗(yàn)

/**
 * 參數(shù)校驗(yàn)配置
 *
 * @author wangzhuhua
 **/
@Component
@Aspect
public class ValidatorInterceptor {

    /**
     * true:快速校驗(yàn)?zāi)J剑琭alse:全部校驗(yàn)?zāi)J?     */
    @Value("${hibernate.validator.failFast:false}")
    private boolean validateModel;
    /**
     * 解決組合切點(diǎn)參數(shù)注入只適用于切點(diǎn)級(jí)別高的效應(yīng)
     */
    ThreadLocal<HValidate> currentAnnatation = new ThreadLocal<>();
    /**
     * 校驗(yàn)器工廠
     */
    private ValidatorFactory validatorFactory;

    static ValidatorFactory ValidatorFactory;

    /**
     * 構(gòu)建校驗(yàn)工廠
     */
    @PostConstruct
    public void initValidator() {
        validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
//                .messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("MyMessages")))
                .failFast(validateModel)
                .buildValidatorFactory();

        ValidatorInterceptor.ValidatorFactory = validatorFactory;
    }

    /**
     * 切點(diǎn)
     */
    @Pointcut("@within(com.sstc.hmis.util.validator.HValidate)")
    public void pointcutWithin() {
    }

    /**
     * 切點(diǎn)
     */
    @Pointcut("@annotation(com.sstc.hmis.util.validator.HValidate)")
    public void pointcutAnnotation() {
    }

    /**
     *
     * @param proceedingJoinPoint
     * @param hValidate
     * @return
     */
    @Around(value = "pointcutWithin() && @within(hValidate)")
    public Object pointcutWithin(ProceedingJoinPoint proceedingJoinPoint, HValidate hValidate) throws Throwable {
        setCurrentAnnatation(hValidate);
        return process(proceedingJoinPoint);
    }

    /**
     *
     * @param proceedingJoinPoint
     * @param hValidate
     * @return
     */
    @Around(value = "pointcutAnnotation() && @annotation(hValidate)")
    public Object pointcutAnnotation(ProceedingJoinPoint proceedingJoinPoint, HValidate hValidate) throws Throwable {
        setCurrentAnnatation(hValidate);
        return process(proceedingJoinPoint);
    }

    /**
     * 校驗(yàn)切點(diǎn)
     *
     * @param proceedingJoinPoint 切點(diǎn)對(duì)象
     * @return 接口響應(yīng)
     * @throws Throwable
     */
    @Around("pointcutWithin()")
    public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 獲得的方法
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();

        // 獲得切入目標(biāo)對(duì)象
        Object target = proceedingJoinPoint.getThis();
        // 獲得切入方法參數(shù)
        Object[] args = proceedingJoinPoint.getArgs();
        // 執(zhí)行驗(yàn)證
        Set<ConstraintViolation<Object>> validResult = validateParameters(target, method, args);

        // 參數(shù)不合法返回
        if (validResult.size() > 0) {
            return processConstraintViolations(validResult);
        }

        return proceedingJoinPoint.proceed();
    }

    /**
     * 校驗(yàn)方法參數(shù)
     *
     * @param target 校驗(yàn)方法所在對(duì)象
     * @param method 將校驗(yàn)的方法
     * @param args   參數(shù)
     * @return 校驗(yàn)結(jié)果
     */
    public Set<ConstraintViolation<Object>> validateParameters(Object target, Method method, Object[] args) {
        // 校驗(yàn)結(jié)果
        Set<ConstraintViolation<Object>> result = new HashSet<>();
        // 先驗(yàn)證默認(rèn)Validator校驗(yàn)范圍
        Validator validator = validatorFactory.getValidator();
        ExecutableValidator executableValidator = validator.forExecutables();
        Set<ConstraintViolation<Object>> validResult = executableValidator.validateParameters(target, method, args);
        result.addAll(validResult);

        return result;
    }

    /**
     * 驗(yàn)證信息轉(zhuǎn)MessageReturn
     *
     * @param violations 驗(yàn)證信息
     * @return 返回消息
     */
    public MessageReturn<Object> processConstraintViolations(Set<ConstraintViolation<Object>> violations) {
        // 僅取第一條錯(cuò)誤
        StringBuilder sb = new StringBuilder();
        Iterator<ConstraintViolation<Object>> iterator = violations.iterator();
        if (iterator.hasNext()) {
            sb.append(iterator.next().getMessage()).append(";");
        }

        // 構(gòu)建校驗(yàn)錯(cuò)誤提示信息(使用已知消息提示)
        MessageReturn<Object> messageReturn = new MessageReturn<Object>();
        MessageReturn.setStatus(getCurrentAnnatation().errorCode());
        MessageReturn.setMessage(sb.toString());

        return MessageReturn;
    }

    /**
     * 獲取 校驗(yàn)器工廠
     *
     * @return validatorFactory 校驗(yàn)器工廠
     */
    public ValidatorFactory getValidatorFactory() {
        return this.validatorFactory;
    }

    /**
     * 設(shè)置 校驗(yàn)器工廠
     *
     * @param validatorFactory 校驗(yàn)器工廠
     */
    public void setValidatorFactory(ValidatorFactory validatorFactory) {
        this.validatorFactory = validatorFactory;
    }

    /**
     * 獲取 true:快速校驗(yàn)?zāi)J交汲琭alse:全部校驗(yàn)?zāi)J?     *
     * @return validateModel true:快速校驗(yàn)?zāi)J脚羲荩琭alse:全部校驗(yàn)?zāi)J?     */
    public boolean isValidateModel() {
        return this.validateModel;
    }

    /**
     * 設(shè)置 true:快速校驗(yàn)?zāi)J剑琭alse:全部校驗(yàn)?zāi)J?     *
     * @param validateModel true:快速校驗(yàn)?zāi)J窖吐兀琭alse:全部校驗(yàn)?zāi)J?     */
    public void setValidateModel(boolean validateModel) {
        this.validateModel = validateModel;
    }

    /**
     * 獲取 注解對(duì)象
     *
     * @return currentAnnatation 注解對(duì)象
     */
    public HValidate getCurrentAnnatation() {
        return this.currentAnnatation.get();
    }

    /**
     * 設(shè)置 注解對(duì)象
     *
     * @param hValidate 注解對(duì)象
     */
    public void setCurrentAnnatation(HValidate hValidate) {
        this.currentAnnatation.set(hValidate);
    }
}

使用示例

接口定義

實(shí)際工作中使用到Spring Cloud丙挽,就用這個(gè)做個(gè)示例了

@RequestMapping(value = "/add", method = RequestMethod.POST)
MessageReturn<CustomObject> add(@RequestBody @HValidateGroup({CustomObjectGroup.Add.class }) CustomObject customObject);

接口實(shí)現(xiàn)類

@RestController
// 這里作為切點(diǎn)進(jìn)入
@HValidate(errorCode = 10101000)
// 下面這個(gè)是未知異常catch,請(qǐng)忽略
@ErrorHandler(errorCode = 20101099)
public class CustomServiceImpl implements CustomService {

    @Override
    // @HValidate(errorCode = 10101099)
    // 有需要的場(chǎng)景匀借,這里的級(jí)別更高颜阐,出現(xiàn)不同的錯(cuò)誤碼
    public MessageReturn<CustomObject> add(@RequestBody CustomObject customObject) {
        // do something
        return xxxx;
    }

校驗(yàn)規(guī)則

/**
 * Demo
 *
 * @author wangzhuhua
 **/
public class CustomObject {

    /** 標(biāo)識(shí) */
    @NotEmpty(message = "ID不可為空", groups = { CustomObjectGroup.Update.class })
    private String id;

    /** 名稱 */
    @NotEmpty(message = "名稱不能為空", groups = { CustomObjectGroup.Add.class,
            CustomObjectGroup.Update.class })
    private String name;
}

這樣拓展了Hibernate validator,用起來(lái)方便吓肋,即可分組凳怨,也可自定義校驗(yàn)規(guī)則。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末是鬼,一起剝皮案震驚了整個(gè)濱河市肤舞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌均蜜,老刑警劉巖李剖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異囤耳,居然都是意外死亡篙顺,警方通過查閱死者的電腦和手機(jī)偶芍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)慰安,“玉大人,你說我怎么就攤上這事聪铺』溃” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵铃剔,是天一觀的道長(zhǎng)撒桨。 經(jīng)常有香客問我,道長(zhǎng)键兜,這世上最難降的妖魔是什么凤类? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮普气,結(jié)果婚禮上谜疤,老公的妹妹穿的比我還像新娘。我一直安慰自己现诀,他們只是感情好夷磕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著仔沿,像睡著了一般坐桩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上封锉,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天绵跷,我揣著相機(jī)與錄音,去河邊找鬼成福。 笑死碾局,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的奴艾。 我是一名探鬼主播擦俐,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼握侧!你這毒婦竟也來(lái)了蚯瞧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤品擎,失蹤者是張志新(化名)和其女友劉穎埋合,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萄传,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡甚颂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蜜猾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片振诬。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蹭睡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赶么,到底是詐尸還是另有隱情肩豁,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布辫呻,位于F島的核電站清钥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏放闺。R本人自食惡果不足惜祟昭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怖侦。 院中可真熱鬧篡悟,春花似錦、人聲如沸匾寝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)旗吁。三九已至踩萎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間很钓,已是汗流浹背香府。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留码倦,地道東北人企孩。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像袁稽,于是被迫代替她去往敵國(guó)和親勿璃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355