一、背景
在我們?nèi)粘=涌陂_發(fā)過程中,可能要面對(duì)一些稍微復(fù)雜一些的業(yè)務(wù)邏輯代碼的編寫,在執(zhí)行真正的業(yè)務(wù)邏輯前,往往要進(jìn)行一系列的前期校驗(yàn)工作,校驗(yàn)可以分為參數(shù)合法性校驗(yàn)和業(yè)務(wù)數(shù)據(jù)校驗(yàn)。
參數(shù)合法性校驗(yàn)比如最常見的校驗(yàn)參數(shù)值非空校驗(yàn)、格式校驗(yàn)、最大值最小值校驗(yàn)等,可以通過Hibernate Validator框架實(shí)現(xiàn),本文不具體講解。業(yè)務(wù)數(shù)據(jù)校驗(yàn)通常與實(shí)際業(yè)務(wù)相關(guān),比如提交訂單接口,我們可能需要校驗(yàn)商品是否合法、庫(kù)存是否足夠、客戶余額是否足夠、還有其他的一些風(fēng)控校驗(yàn)。我們的代碼可能看起來像是這樣的:
public ApiResult<OrderSubmitVo> submitOrder(OrderSubmitDto orderSubmitDto) {
// 業(yè)務(wù)校驗(yàn)1
// 業(yè)務(wù)校驗(yàn)2
// 業(yè)務(wù)校驗(yàn)3
// 業(yè)務(wù)校驗(yàn)n...
// 執(zhí)行真正的業(yè)務(wù)邏輯
return ApiResult.success();
}
二、問題
- 實(shí)現(xiàn)不夠優(yōu)雅
上述代碼在版本迭代的過程中,還可能陸陸續(xù)續(xù)增加/修改一些校驗(yàn)邏輯,如果業(yè)務(wù)邏輯校驗(yàn)的代碼都耦合在核心業(yè)務(wù)邏輯中,這樣實(shí)現(xiàn)其實(shí)是不夠優(yōu)雅,不符合設(shè)計(jì)原則的單一職責(zé)原則和開閉原則。
- 校驗(yàn)代碼無法復(fù)用
如果某個(gè)業(yè)務(wù)校驗(yàn)代碼需要在其他業(yè)務(wù)中也會(huì)用到,那我們則需要將相同的代碼復(fù)制一份至業(yè)務(wù)代碼中,比如校驗(yàn)用戶狀態(tài),在很多業(yè)務(wù)校驗(yàn)中都需要校驗(yàn),如果校驗(yàn)邏輯有些許更改的話,那么所有涉及到的地方都要同步修改,這樣不利于系統(tǒng)維護(hù)。
- 校驗(yàn)邏輯無法按照順序依賴執(zhí)行,并且校驗(yàn)過程中產(chǎn)生的數(shù)據(jù)后續(xù)獲取不便
如果我們將上述代碼中的各個(gè)校驗(yàn)邏輯封裝成獨(dú)立的子方法,那有可能存在業(yè)務(wù)校驗(yàn)2要依賴于業(yè)務(wù)校驗(yàn)1的數(shù)據(jù)結(jié)果,并且在業(yè)務(wù)校驗(yàn)過程中產(chǎn)生的數(shù)據(jù)在后續(xù)執(zhí)行真正的業(yè)務(wù)邏輯的時(shí)候是需要用得到的。
三、校驗(yàn)工具實(shí)現(xiàn)思路
我們要寫的校驗(yàn)工具至少要解決上面所說的三個(gè)問題
業(yè)務(wù)校驗(yàn)代碼與核心業(yè)務(wù)邏輯代碼解耦
同一個(gè)校驗(yàn)器可以用于多個(gè)業(yè)務(wù),提高代碼的復(fù)用性和可維護(hù)性
校驗(yàn)代碼可以按照指定順序執(zhí)行,并且校驗(yàn)過程中產(chǎn)生的數(shù)據(jù)可以后續(xù)傳遞
在用zuul來做網(wǎng)關(guān)服務(wù)的時(shí)候,我獲得了一些靈感,
zuul中的filterType用來區(qū)分請(qǐng)求路由到目標(biāo)之前、處理目標(biāo)請(qǐng)求、目標(biāo)請(qǐng)求返回后的類型,filterOrder用來指定過濾器的執(zhí)行順序,RequestContext為請(qǐng)求上下文,RequestContext繼承自ConcurrentHashMap,且與ThreadLocal綁定保證線程安全,請(qǐng)求上下文中的數(shù)據(jù)在一次請(qǐng)求的所有過濾器中可以獲取,很好的完成了數(shù)據(jù)傳遞。
首先我們需要定義一個(gè)校驗(yàn)器注解,注解中指定業(yè)務(wù)類型和執(zhí)行順序,在校驗(yàn)器上加上該注解表明這是一個(gè)校驗(yàn)器。定義一個(gè)校驗(yàn)器上下文,在業(yè)務(wù)校驗(yàn)執(zhí)行過程中產(chǎn)生的數(shù)據(jù)可以通過上下文進(jìn)行傳遞。定義一個(gè)校驗(yàn)器基類,校驗(yàn)器繼承基類,并實(shí)現(xiàn)其中的具體校驗(yàn)方法。定義一個(gè)校驗(yàn)器的統(tǒng)一執(zhí)行器,執(zhí)行器可以根據(jù)業(yè)務(wù)類型找出所有帶有校驗(yàn)器注解并且是指定業(yè)務(wù)類型的校驗(yàn)器列表,根據(jù)校驗(yàn)器注解中的執(zhí)行順序排序后,遍歷所有校驗(yàn)器列表調(diào)用校驗(yàn)方法。如果校驗(yàn)過程中校驗(yàn)失敗,則拋出校驗(yàn)異常中斷業(yè)務(wù)執(zhí)行。
以上為大概的實(shí)現(xiàn)思路,具體的實(shí)現(xiàn)代碼如下:
四、show me your code
- Validator.java
import java.lang.annotation.*;
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/10/23 13:58
* @description: 業(yè)務(wù)校驗(yàn)注解
*/
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Validator {
/**
* 業(yè)務(wù)類型,同一個(gè)校驗(yàn)器可以指定多個(gè)業(yè)務(wù)類型
*
* @return
*/
String[] validateTypes();
/**
* 執(zhí)行順序,數(shù)值越小越先執(zhí)行
*
* @return
*/
int validateOrder();
}
Validator校驗(yàn)注解,在校驗(yàn)器的類上加上該注解則表明為業(yè)務(wù)校驗(yàn)器,validateTypes表示業(yè)務(wù)類型,同一個(gè)校驗(yàn)器可以指定多個(gè)業(yè)務(wù)類型,多個(gè)業(yè)務(wù)類型可以復(fù)用同一個(gè)校驗(yàn)器,validateOrder表示執(zhí)行順序,數(shù)值越小越先被執(zhí)行。
- ValidatorContext.java
import java.util.concurrent.ConcurrentHashMap;
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/9/11 14:56
* @description: 校驗(yàn)器上下文,與當(dāng)前線程綁定
*/
public class ValidatorContext extends ConcurrentHashMap<String, Object> {
/**
* 請(qǐng)求對(duì)象
*/
public Object requestDto;
protected static final ThreadLocal<? extends ValidatorContext> threadLocal = ThreadLocal.withInitial(() -> new ValidatorContext());
/**
* 獲取當(dāng)前線程的上下文
*
* @return
*/
public static ValidatorContext getCurrentContext() {
ValidatorContext context = threadLocal.get();
return context;
}
/**
* 設(shè)值
*
* @param key
* @param value
*/
public void set(String key, Object value) {
if (value != null) put(key, value);
else remove(key);
}
/**
* 獲取String值
*
* @param key
* @return
*/
public String getString(String key) {
return (String) get(key);
}
/**
* 獲取Integer值
*
* @param key
* @return
*/
public Integer getInteger(String key) {
return (Integer) get(key);
}
/**
* 獲取Boolean值
*
* @param key
* @return
*/
public Boolean getBoolean(String key) {
return (Boolean) get(key);
}
/**
* 獲取對(duì)象
*
* @param key
* @param <T>
* @return
*/
public <T> T getClazz(String key) {
return (T) get(key);
}
/**
* 獲取Long值
*
* @param key
* @return
*/
public Long getLong(String key) {
return (Long) get(key);
}
public <T> T getRequestDto() {
return (T) requestDto;
}
public void setRequestDto(Object requestDto) {
this.requestDto = requestDto;
}
ValidatorContext為請(qǐng)求上下文,與當(dāng)前請(qǐng)求線程綁定,繼承自ConcurrentHashMap,requestDto屬性為接口請(qǐng)求入?yún)?duì)象,提供get/set方法使得在上下文中能更加便捷的獲取請(qǐng)求入?yún)?shù)據(jù)。
- ValidatorTemplate.java
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/10/23 11:51
* @description: 校驗(yàn)器模板,業(yè)務(wù)校驗(yàn)器需繼承模板類
*/
@Slf4j
@Component
public abstract class ValidatorTemplate {
/**
* 校驗(yàn)方法
*/
public void validate() {
try {
validateInner();
} catch (ValidateException e) {
log.error("業(yè)務(wù)校驗(yàn)失敗", e);
throw e;
} catch (Exception e) {
log.error("業(yè)務(wù)校驗(yàn)異常", e);
ValidateException validateException = new ValidateException(ResultEnum.VALIDATE_ERROR);
throw validateException;
}
}
/**
* 校驗(yàn)方法,由子類具體實(shí)現(xiàn)
*
* @throws ValidateException
*/
protected abstract void validateInner() throws ValidateException;
}
校驗(yàn)器抽象類,具體的校驗(yàn)器需要繼承該類,并且實(shí)現(xiàn)具體的validateInner校驗(yàn)方法。
- ValidatorTemplateProxy.java
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/10/25 18:03
* @description: ValidatorTemplate代理類
*/
@Data
@AllArgsConstructor
public class ValidatorTemplateProxy extends ValidatorTemplate implements Comparable<ValidatorTemplateProxy> {
private ValidatorTemplate validatorTemplate;
private String validateType;
private int validateOrder;
@Override
public int compareTo(ValidatorTemplateProxy o) {
return Integer.compare(this.getValidateOrder(), o.getValidateOrder());
}
@Override
protected void validateInner() throws ValidateException {
validatorTemplate.validateInner();
}
}
ValidatorTemplate類的代理類,實(shí)現(xiàn)了Comparable排序接口,便于校驗(yàn)器按照validateOrder屬性排序,并且將校驗(yàn)器中的注解轉(zhuǎn)化為代理類中的兩個(gè)屬性字段,方便執(zhí)行過程中的統(tǒng)一日志打印。
- ValidateProcessor.java
import java.lang.annotation.Annotation;
import java.util.*;
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/10/25 18:02
* @description: 執(zhí)行器
*/
@Slf4j
@Component
public class ValidateProcessor {
/**
* 執(zhí)行業(yè)務(wù)類型對(duì)應(yīng)的校驗(yàn)器
*
* @param validateType
*/
public void validate(String validateType) {
if (StringUtils.isEmpty(validateType)) {
throw new IllegalArgumentException("validateType cannot be null");
}
long start = System.currentTimeMillis();
log.info("start validate,validateType={},ValidatorContext={}", validateType, ValidatorContext.getCurrentContext().toString());
List<ValidatorTemplateProxy> validatorList = getValidatorList(validateType);
if (CollectionUtils.isEmpty(validatorList)) {
log.info("validatorList is empty");
return;
}
ValidatorTemplateProxy validateProcessorProxy;
for (ValidatorTemplateProxy validatorTemplate : validatorList) {
validateProcessorProxy = validatorTemplate;
log.info("{} is running", validateProcessorProxy.getValidatorTemplate().getClass().getSimpleName());
validatorTemplate.validate();
}
log.info("end validate,validateType={},ValidatorContext={},time consuming {} ms", validateType,
ValidatorContext.getCurrentContext().toString(), (System.currentTimeMillis() - start));
}
/**
* 根據(jù)Validator注解的validateType獲取所有帶有該注解的校驗(yàn)器
*
* @param validateType
* @return
*/
private List<ValidatorTemplateProxy> getValidatorList(String validateType) {
List<ValidatorTemplateProxy> validatorTemplateList = new LinkedList<>();
Map<String, Object> map = SpringUtil.getApplicationContext().getBeansWithAnnotation(Validator.class);
String[] validateTypes;
int validateOrder;
Annotation annotation;
for (Map.Entry<String, Object> item : map.entrySet()) {
annotation = item.getValue().getClass().getAnnotation(Validator.class);
validateTypes = ((Validator) annotation).validateTypes();
validateOrder = ((Validator) annotation).validateOrder();
if (item.getValue() instanceof ValidatorTemplate) {
if (Arrays.asList(validateTypes).contains(validateType)) {
validatorTemplateList.add(new ValidatorTemplateProxy((ValidatorTemplate) item.getValue(), validateType, validateOrder));
}
} else {
log.info("{}not extend from ValidatorTemplate", item.getKey());
}
}
Collections.sort(validatorTemplateList);
return validatorTemplateList;
}
}
業(yè)務(wù)校驗(yàn)的執(zhí)行器,getValidatorList方法根據(jù)validateType值獲取所有帶有該validateType值的校驗(yàn)器,并將其封裝成ValidatorTemplateProxy代理類,然后再做排序。validate為統(tǒng)一的業(yè)務(wù)校驗(yàn)方法。
- ValidateException.java
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/4/4 6:34 PM
* @description: 校驗(yàn)異常
*/
public class ValidateException extends RuntimeException {
// 異常碼
private Integer code;
public ValidateException() {
}
public ValidateException(String message) {
super(message);
}
public ValidateException(ResultEnum resultEnum) {
super(resultEnum.getMsg());
this.code = resultEnum.getCode();
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
ValidateException為校驗(yàn)失敗時(shí),拋出的業(yè)務(wù)校驗(yàn)異常類。
- ValidateTypeConstant.java
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/10/30 15:16
* @description:
*/
public class ValidateTypeConstant {
/**
* 提交訂單校驗(yàn)
*/
public static final String ORDER_SUBMIT = "order_submit";
}
ValidateTypeConstant為定義validateType業(yè)務(wù)校驗(yàn)類型的常量類。
五、使用樣例
以訂單提交為例,我們首先定義了兩個(gè)個(gè)基本的校驗(yàn)器,下單商品信息校驗(yàn)器、客戶狀態(tài)校驗(yàn)器,均為偽代碼實(shí)現(xiàn)。
- OrderSubmitProductValidator.java
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/10/30 15:34
* @description: 商品狀態(tài)以及庫(kù)存校驗(yàn)
*/
@Component
@Slf4j
@Validator(validateTypes = ValidateTypeConstant.ORDER_SUBMIT, validateOrder = 1)
public class OrderSubmitProductValidator extends ValidatorTemplate {
@Override
protected void validateInner() throws ValidateException {
ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
OrderSubmitDto orderSubmitDto = validatorContext.getRequestDto();
// 獲取商品信息并校驗(yàn)商品狀態(tài)
List<ProductShelfVo> productShelfVoList = new ArrayList<>();
if (0 == 1) {
throw new ValidateException("商品已下架");
}
// 將商品信息設(shè)置至上下文中
validatorContext.set("productShelfVoList", productShelfVoList);
}
}
- OrderSubmitCustomerValidator.java
/**
* @author: 會(huì)跳舞的機(jī)器人
* @date: 2019/10/30 19:24
* @description:
*/
@Component
@Slf4j
@Validator(validateTypes = ValidateTypeConstant.ORDER_SUBMIT, validateOrder = 2)
public class OrderSubmitCustomerValidator extends ValidatorTemplate {
@Override
protected void validateInner() throws ValidateException {
ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
String customerNo = validatorContext.getString("customerNo");
if (StringUtils.isEmpty(customerNo)) {
throw new IllegalArgumentException("客戶編號(hào)為空");
}
// 獲取客戶信息并校驗(yàn)客戶狀態(tài)
CustomerVo customer = new CustomerVo();
if (0 == 1) {
throw new ValidateException("客戶限制交易");
}
}
}
在提交訂單的業(yè)務(wù)邏輯的代碼中使用:
/**
* 提交訂單
*
* @param orderSubmitDto
* @return
*/
public ApiResult<OrderSubmitVo> submitOrder(OrderSubmitDto orderSubmitDto) {
// 業(yè)務(wù)校驗(yàn)
ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
validatorContext.setRequestDto(orderSubmitDto);
validateProcessor.validate(ValidateTypeConstant.ORDER_SUBMIT);
// 從上下文中獲取下單商品信息
List<ProductShelfVo> productShelfVoList = validatorContext.getClazz("productShelfVoList");
// 后續(xù)業(yè)務(wù)邏輯處理
return ApiResult.success();
}
通過使用上述封裝的校驗(yàn)工具后,業(yè)務(wù)代碼與校驗(yàn)代碼解耦,后續(xù)要增加/修改業(yè)務(wù)校驗(yàn)邏輯時(shí)候,我們只需要增加/修改相應(yīng)的校驗(yàn)器即可,不必改動(dòng)到主業(yè)務(wù)邏輯。為了我們能更簡(jiǎn)單和方便找到某個(gè)業(yè)務(wù)邏輯對(duì)應(yīng)所有的校驗(yàn)器,我們?cè)诿r?yàn)器的時(shí)候可以加上業(yè)務(wù)類型的前綴。
六、總結(jié)
1、在開發(fā)過程中,我們遇到一些“煩人”問題的時(shí)候,要想辦法解決它,而不是忽略不管它,通過解決問題可以提高我們的技術(shù)能力。
2、要善于從其他優(yōu)秀的技術(shù)框架學(xué)習(xí)其實(shí)現(xiàn)思路。
3、以上校驗(yàn)工具只是一個(gè)簡(jiǎn)單實(shí)現(xiàn),解決的問題只是筆者在開發(fā)過程中遇到的問題,可能并不一定具有通用性。
如果文章對(duì)你有幫助的話,給文章點(diǎn)個(gè)贊吧。
如果有寫得不正確的地方,歡迎指出。
文章首發(fā)公眾號(hào):會(huì)跳舞的機(jī)器人,歡迎掃碼關(guān)注。