1. 程式人生 > >在系統中使用Bean Validation驗證引數

在系統中使用Bean Validation驗證引數

為什麼要使用Bean Validation?

 當我們實現某個介面時,都需要對入引數進行校驗。例如下面的程式碼
public String queryValueByKey(String parmTemplateCode, String conditionName, String conditionKey, String resultName) {
        checkNotNull(parmTemplateCode, "parmTemplateCode not null");
        checkNotNull(conditionName, "conditionName not null");
        checkNotNull(conditionKey, "conditionKey not null");
        checkNotNull(resultName, "resultName not null");


該方法輸入的四個引數都是必填項。用程式碼進行引數驗證帶來幾個問題

  • 需要寫大量的程式碼來進行引數驗證。
  • 需要通過註釋來直到每個入參的約束是什麼。
  • 每個程式設計師做引數驗證的方式不一樣,引數驗證不通過丟擲的異常也不一樣。

什麼是Bean Validation?

Bean Validation是一個通過配置註解來驗證引數的框架,它包含兩部分Bean Validation API和Hibernate Validator。

  • Bean Validation API是Java定義的一個驗證引數的規範。
  • Hibernate Validator是Bean Validation API的一個實現。

快速開始

引入POM

<!-- Bean Validation start -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.1.1.Final</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>el-api</artifactId>
    <version>2.2</version>
</dependency>
<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>javax.el</artifactId>
    <version>2.2.4</version>
</dependency>
<dependency>
    <groupId>org.jboss.logging</groupId>
    <artifactId>jboss-logging</artifactId>
    <version>3.1.3.GA</version>
</dependency>
<dependency>
    <groupId>com.fasterxml</groupId>
    <artifactId>classmate</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.13</version>
</dependency>
<!-- Bean Validation end -->

例項程式碼如下,可以驗證Bean,也可以驗證方法引數

import java.lang.reflect.Method;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import javax.validation.executable.ExecutableValidator;

public class BeanValidatorTest {

    public static void main(String[] args) {
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        //驗證Bean引數,並返回驗證結果資訊
        Car car = new Car();
        Set<ConstraintViolation<Car>> validators = validator.validate(car);
        for (ConstraintViolation<Car> constraintViolation : validators) {
            System.out.println(constraintViolation.getMessage());
        }

        // 驗證方法引數
        Method method = null;
        try {
            method = Car.class.getMethod("drive", int.class);
        } catch (SecurityException e) {
        } catch (NoSuchMethodException e) {
        }
        Object[] parameterValues = { 80 };
        ExecutableValidator executableValidator = validator.forExecutables();
        Set<ConstraintViolation<Car>> methodValidators = executableValidator.validateParameters(car,
            method, parameterValues);
        for (ConstraintViolation<Car> constraintViolation : methodValidators) {
            System.out.println(constraintViolation.getMessage());
        }
    }

    public static class Car {

        private String name;

        @NotNull(message = "車主不能為空")
        public String getRentalStation() {
            return name;
        }

        public void drive(@Max(75) int speedInMph) {

        }

    }
}

執行程式碼後,輸出如下:

車主不能為空
最大不能超過75

使用程式碼驗證方法引數

Validation驗證不成功可能返回多個驗證錯誤資訊,我們可以包裝下,當有錯誤時直接返回第一個錯誤的異常。

import static com.google.common.collect.Iterables.getFirst;

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

/**
 * 物件驗證器
 * 
 * @author tengfei.fangtf
 * @version $Id: BeanValidator.java, v 0.1 Dec 30, 2015 11:33:40 PM tengfei.fangtf Exp $
 */
public class BeanValidator {

    /**
     * 驗證某個bean的引數
     * 
     * @param object 被校驗的引數
     * @throws ValidationException 如果引數校驗不成功則丟擲此異常
     */
    public static <T> void validate(T object) {
        //獲得驗證器
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        //執行驗證
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(object);
        //如果有驗證資訊,則將第一個取出來包裝成異常返回
        ConstraintViolation<T> constraintViolation = getFirst(constraintViolations, null);
        if (constraintViolation != null) {
            throw new ValidationException(constraintViolation);
        }
    }

}

我們可以在每個方法的第一行呼叫BeanValidator.validate來驗證引數,測試程式碼如下,

import static junit.framework.Assert.assertEquals;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;

import org.junit.Test;

/**
 * 
 * @author tengfei.fangtf
 * @version $Id: BeanValidatorTest.java, v 0.1 Dec 30, 2015 11:33:56 PM tengfei.fangtf Exp $
 */
public class BeanValidatorTest {

    @Test
    public void test() {
        try {
            BeanValidator.validate(new Car());
        } catch (Exception e) {
            assertEquals("rentalStation 車主不能為空", e.getMessage());
        }
    }

    public static class Car {

        private String name;

        @NotNull(message = "車主不能為空")
        public String getRentalStation() {
            return name;
        }

        public void drive(@Max(75) int speedInMph) {

        }

    }

}

使用攔截器驗證方法引數

我們在對外暴露的介面的入參中使用Bean Validation API配置引數約束,如下XXXService介面

public interface XXXService {

GetObjectResponse getObject(GetObjectRequest getObjectRequest);

}

在getObject的GetObjectRequest引數中配置註解來約束引數。

public class GetObjectRequest {

    @Valid
    @NotNull
    private ObjectKey      objectKey;

    @Size(max = 9)
    private Map&lt;String, Object&gt; parameters;

    @AssertTrue
    public boolean isEntityNameOrCodeAtLeastOneIsNotBlank() {
        return isNotBlank(entityName) || isNotBlank(entityCode);
    }
//程式碼省略
}

編寫引數驗證攔截器,當方法被呼叫時,觸發Validator驗證器執行驗證,如果不通過則丟擲ParameterValidationException。

import static com.google.common.collect.Iterables.getFirst;

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xx.ParameterValidationException;

/**
 * 引數驗證攔截器,基於JSR-303 BeanValidation
 *
 * @author tengfei.fangtf
 *
 * @version $Id: TitanValidateInterceptor.java, v 0.1 Nov 23, 2015 11:13:55 PM tengfei.fangtf Exp $
 */
public class TitanValidateInterceptor implements MethodInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(TitanValidateInterceptor.class);

    private final Validator     validator;

    public TitanValidateInterceptor() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Validate arguments");
        }
        //獲取引數,並檢查是否應該驗證
        Object[] arguments = invocation.getArguments();
        for (Object argument : arguments) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Validate argument: {}", argument);
            }
            Set<ConstraintViolation<Object>> constraintViolations = validator.validate(argument);
            ConstraintViolation<Object> constraintViolation = getFirst(constraintViolations, null);
            if (constraintViolation == null) {
                continue;
            }
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("ConstraintViolation: {}", constraintViolation);
            }
            throw new ParameterValidationException(constraintViolation.getPropertyPath() + " " + constraintViolation.getMessage());
        }
        return invocation.proceed();
    }

}

配置攔截器core-service.xml,攔截XXXService的所有方法。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:webflow="http://www.springframework.org/schema/webflow-config"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
         http://www.springframework.org/schema/webflow-config http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd"
	default-autowire="byName">

	<bean id="XXXService" class="org.springframework.aop.framework.ProxyFactoryBean">
		<property name="target">
			<bean class="com.XXXService" />
		</property>
		<property name="interceptorNames">
			<list>
				<value>validateInterceptor</value>
			</list>
		</property>
	</bean>

	<bean id="validateInterceptor"
		class="com.mybank.bkloanapply.common.validator.ValidateInterceptor" />
</beans>

參考資料


方 騰飛

花名清英,併發網(ifeve.com)創始人,暢銷書《Java併發程式設計的藝術》作者,螞蟻金服技術專家。目前工作於支付寶微貸事業部,關注網際網路金融,併發程式設計和敏捷實踐。微信公眾號aliqinying。