系統之間在進行接口調用時,往往是有入參傳遞的,入參是接口業務邏輯實現的先決條件,有時入參的缺失或錯誤會導致業務邏輯的異常,大量的異常捕獲無疑增加了接口實現的複雜度,也讓代碼顯得雍腫冗長,因此提前對入參進行驗證是有必要的,可以提前處理入參數據的異常,並封裝好異常轉化成結果對象返回給調用方,也讓業務邏輯解耦變得獨立。
爲什麼要使用aop方式
入參驗證的方式有多種,傳統的方式是在接口實現中代碼注入,即在接口實現的業務邏輯處理之前,通過硬編碼的方式對入參進行有效性驗證,簡單粗暴,也直接有效。但代碼注入帶來的問題是代碼的重用,不同的接口不同的入參,都需要編寫不同的入參驗證邏輯,造成了代碼的重複使用,而這些驗證邏輯大部分是可以複用的,並且代碼注入方式雖然從一定程度上對業務邏輯進行了解耦,但依然需要在接口實現中注入代碼,從一定程度上不夠獨立。因此,從代碼重用和業務完全解耦上看,aop註解方式驗參更加有效。
怎麼實現aop方式
spring框架的aop是一種面向切面編程,說的簡單點就是將非核心業務的公共邏輯從業務層面抽離開來封裝成可重用的模塊,實現對業務邏輯的高度解耦,減少系統的重複代碼。
aop方式需要首先在spring配置中定義aop映射,使得服務能夠依賴註解有效切入。然後在服務調用前先定義好註解接口類及註解驗證方法,通過在業務接口實現方法上增加註解來實現aop。服務調用時會根據接口方法的註解在切面通知方法中進行參數驗證,驗證失敗則拋出異常,服務中止,業務邏輯完全獨立,如下:
@Override
@ParamValid(className = "orderRequestVoValidator")
public GenericResult<OrderResponseVo> orderRequest(OrderRequestVo vo) {
log.info("質押收單請求開始,vo:{}", GsonUtils.toJson(vo));
GenericResult<OrderResponseVo> result = tradeOrderService.orderRequest(vo);
log.info("質押收單請求完成,result:{}", GsonUtils.toJson(result));
return result;
}
一行註解搞定入參驗證,代碼瞬間簡單大氣,註解的實現接下來分析。使用註解,必然是需要先定義註解,註解可以根據系統的需要定義多種驗證方法,比如像是否需要驗證token,是否有調用次數限制。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamValid {
String className();
/* 是否限制調用次數 */
boolean isLimit() default false;
/* 方法訪問次數 */
long limitNum() default 10l;
/* 方法訪問限制時效 */
int limitTime() default 300;
}
因爲註解是採用aop方式實現,就一定會有aop通知方法來實現對參數的驗證。在通知方法中,就需要根據註解接口的方法來驗證參數了。
怎麼驗證參數
參數驗證有多種方式,可以驗證參數非空、參數類型、參數格式、賦值範圍等,最直接的方法就是在註解通知方法中依次驗證,但驗證邏輯就會很長,並且不同的參數需要驗證的類型不盡相同,在同一個方法中顯然很難做到靈活驗證,因此,就需要將參數驗證類型進行配置化管理。
在spring配置文件中,定義spring入參驗證配置,對需要驗證的入參配置驗證類型(非空、數值、金額、範圍等),並自定義spring標籤,配置多種驗證類型,如下所示:
<?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:validator="http://com.jd.assetPledge/schema/validator"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://com.jd.assetPledge/schema/validator http://com.jd.assetPledge/schema/validator/validator.xsd"
default-lazy-init="false" default-autowire="byName">
<validator:validator id="orderRequestVoValidator" class="com.jd.assetPledge.trade.export.vo.OrderRequestVo">
<validator:property name="pin">
<validator:NotNull message="請填寫pin"/>
</validator:property>
<validator:property name="assetNo">
<validator:NotNull message="請填寫資產編號"/>
</validator:property>
<validator:property name="assetType">
<validator:NotNull message="請填寫資產類型"/>
</validator:property>
<validator:property name="channelType">
<validator:NotNull message="請填寫渠道號"/>
</validator:property>
<validator:property name="sourceType">
<validator:NotNull message="請填寫移動來源"/>
</validator:property>
<validator:property name="token">
<validator:Token message="系統token爲空或錯誤"/>
</validator:property>
</validator:validator>
這樣就能實現對入參不同參數的驗證類型靈活配置,也能對同一入參對象配置多個驗證模塊,以滿足同一入參對象在不同接口中的多種驗證類型需求(比如同一入參的同一屬性,在A接口必傳,卻在B接口可空)。當然,所有這些驗證標籤都是在spring中自定義的,我們可以根據業務的需要增加各種類型驗證。
標籤定義好後,就是實現標籤的驗證邏輯了。首先我們需要定義一個統一的參數驗證接口,然後根據自定義的標籤一一實現接口邏輯,再根據spring的反射機制,將自定義標籤綁定到對應的接口實現類上,接口實現類如下:
public class NotNullValidator extends ValidatorImpl implements Validator {
public NotNullValidator() {
}
public boolean isValid(Object object) {
if(object instanceof String) {
int length = ((String)object).length();
return length > 0;
} else {
return object != null;
}
}
}
當需要增加驗證類型時,無需修改代碼,只需要自定義一套標籤並增加一個對應的驗證實現類即可,非常方便靈活。
定義好整套驗證實現類後,就可以在上面的註解通知方法中來統一調用了。在通知方法中,獲取入參對象,根據spring配置文件的定義,匹配到自定義標籤集合。根據標籤集合的驗證類型調用不同的驗證實現類,對入參的每個屬性進行驗證。直接上代碼:
@Component
@Aspect
public class ParamValidAspect {
private static Logger log = LoggerFactory.getLogger(ParamValidAspect.class);
@Autowired
private CacheRpc cacheRpc;
@Resource(name = "checkService")
private CheckService checkService;
@Value("${paramValid.token}")
private String token;
@Value("${paramValid.isLimit}")
private boolean isLimit;
@Value("${paramValid.limitNum}")
private long limitNum;
@Value("${paramValid.limitTime}")
private int limitTime;
@Pointcut("execution(* *(..)) && @annotation(com.jd.assetPledge.trade.web.validator.ParamValid)")
public void paramValidPointcut() {
log.info("paramValid aspect pointcut initialize successful...");
}
@Around("paramValidPointcut()")
public Object aroundParamValidReturn(ProceedingJoinPoint pjp) throws Throwable {
Method method = getMethod(pjp);
Class[] parameterTypes = method.getParameterTypes();
Object[] args = pjp.getArgs();
if (parameterTypes.length < 1 || args.length < 1) {
return pjp.proceed();
}
try {
ParamValid paramValid = method.getAnnotation(ParamValid.class);
String objName = paramValid.className();
WebApplicationContext applicationContext = ContextLoader.getCurrentWebApplicationContext();
Object obj = args[0];
/* 判斷入參是否正確 */
String checkMsgTip = checkService.check(obj, (ValidatorBean) applicationContext.getBean(objName));
if(StringUtils.isNotBlank(checkMsgTip)) {
return getResObj(method,ResultInfoEnum.REQUEST_PARAMS_ERROR, checkMsgTip);
}
/* 判斷調用次數限制 */
if(paramValid.isLimit() || isLimit) {
if(paramValid.isLimit()) {
limitNum = paramValid.limitNum();
limitTime = paramValid.limitTime();
}
BaseRequestVo base = (BaseRequestVo)args[0];
long count = cacheRpc.countKey(base.getPin() + method.getName(), limitTime);
if(count > limitNum) {
return getResObj(method,ResultInfoEnum.REQUEST_LIMIT_ERROR, null);
}
}
} catch (Exception e) {
return getResObj(method,ResultInfoEnum.UNKNOW_ERROR, null);
}
return pjp.proceed();
}
private Object getResObj(Method method,ResultInfoEnum enumType, String checkMsgTip) {
Class<?> returnType = method.getReturnType();
Class[] classArgs = new Class[2];
classArgs[0] = String.class;
classArgs[1] = String.class;
try {
Constructor constructor = returnType.getConstructor(classArgs);
String code = enumType.getErrorCode();
String message = enumType.getErrorMsg(checkMsgTip);
return constructor.newInstance(code, message);
} catch (Exception e) {
log.error("exception", e);
}
return null;
}
private Method getMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
Signature sig = pjp.getSignature();
MethodSignature msig = (MethodSignature) sig;
return getClass(pjp).getMethod(msig.getName(), msig.getParameterTypes());
}
private Class<? extends Object> getClass(ProceedingJoinPoint pjp)
throws NoSuchMethodException {
return pjp.getTarget().getClass();
}
}
public class CheckService {
private static final Logger logger = LoggerFactory.getLogger(CheckService.class);
public CheckService() {
}
public <T> String check(T t, ValidatorBean nameValidator) throws IllegalAccessException {
long t1 = System.currentTimeMillis();
logger.info("參數驗證開始-----------------------");
Map filedValidatorListMap = nameValidator.getFiledValidatorListMap();
ArrayList fieldList = new ArrayList();
DynamicUtil.getClassAllField(t.getClass(), fieldList);
StringBuffer checkMsgTip = new StringBuffer("");
Iterator i$ = fieldList.iterator();
while(true) {
Field field;
List validatorList;
do {
if(!i$.hasNext()) {
logger.info("參數驗證結束---------------------------時間{}", Long.valueOf(System.currentTimeMillis() - t1));
return checkMsgTip.toString();
}
field = (Field)i$.next();
String fieldName = field.getName();
field.setAccessible(true);
validatorList = (List)filedValidatorListMap.get(fieldName);
} while(validatorList == null);
Iterator i$1 = validatorList.iterator();
while(i$1.hasNext()) {
Validator validator = (Validator)i$1.next();
ValidatorImpl validator1 = (ValidatorImpl)validator;
boolean checkResult = validator.isValid(field.get(t));
if(!checkResult) {
logger.info(nameValidator.getName() + ":" + "field=" + validator1.getValidatorField() + ",value=" + field.get(t) + ",validatorType=" + validator1.getValidatorType() + "結果:" + checkResult + ",消息" + ((ValidatorImpl)validator).getMessage());
checkMsgTip.append(((ValidatorImpl)validator).getMessage() + "\n");
}
}
}
}
}
驗證完成後,如果驗證失敗集合不爲空,則使用公共返回對象封裝實例化,返回調用方相關的錯誤代碼與提示信息。
到此,接口入參的註解aop驗證方法介紹完畢,當然,上面所貼的代碼並非完整的驗證代碼,諸如spring標籤定義、接口反射等邏輯就不在這裏展示了,這裏主要是想表達下自己的見解和原理。從上面的分析可以看出,註解aop驗證入參,最大的好處就是讓參數驗證與業務邏輯高度解耦,用專業的方法幹專業的事,然後就是實現了驗證方式的靈活配置,這個能讓我們對代碼的傷害降到最低。