方法的入参校验,应该是编码过程中高频的操作了。这个费时费力又没有技术含量的步骤,有没有更优雅的解决方案?
看过很多篇关于入参校验的文章,感觉提供的解决方案还是不够便捷,索性就自己写一个吧。
常规做法一:写很多的if....,弊端就不用多说了
直接上代码
if (Objects.isNull(userType)) {
logger.warn( "test verify:{}, {}", "userType", ErrorCode.PARAMETER_CANNOT_NULL.getMessage() );
return ResultWrapper.fail( ErrorCode.PARAMETER_CANNOT_NULL, "userType" );
}
if (Objects.isNull(testDto.getUserId())) {
logger.warn( "test verify:{}, {}", "userId", ErrorCode.PARAMETER_CANNOT_NULL.getMessage() );
return ResultWrapper.fail( ErrorCode.PARAMETER_CANNOT_NULL, "userId" );
}
if (Objects.isNull(testAccountDto.getBusiness())) {
logger.warn( "test verify:{}, {}", "business", ErrorCode.PARAMETER_CANNOT_NULL.getMessage() );
return ResultWrapper.fail( ErrorCode.PARAMETER_CANNOT_NULL, "business" );
}
...
常规做法二:if 少了些,但返回的错误信息不够精确
if(demoDto==null || demoDto.getSid()==null || demoDto.getBusiness()==null){
logger.info("demoMethod校验参数不通过,demoDto:{}",JSONObject.toJSONString(demoDto));
return ResultUtil.buildFailResult("校验参数不通过");
}
到底哪个参数校验不通过,你倒是告诉我啊!!!
最惨的情况有可能把所有的字段都比对够一遍,才能知道具体哪个字段为空。字段少还好,如果是插入的场景,几十个字段,一一核查就要了命了。
常规做法三:属性上加注解
@NotNull(describe = "商家Id不能为null")
private Long sid;
@NotLessEqualsZero(describe = "业务线Id不能小于0")
private Long businessId;
...
校验逻辑
public static String checkObject(Object object) {
if (null == object) {
return "参数不能为null";
}
try {
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
String name = field.getName();
name = name.substring(0, 1).toUpperCase() + name.substring(1);
//检测是否为null
String notNullDescribe = checkNotNull(object, field, name);
if (null != notNullDescribe) {
return notNullDescribe;
}
//检测是否大于0
String notLessEqualsZeroDescribe = checkNotLessEqualZero(object, field, name);
if (null != notLessEqualsZeroDescribe) {
return notLessEqualsZeroDescribe;
}
//检测是否大于等于0
String notNotLessZeroDescribe = checkNotLessZero(object, field, name);
if (null != notNotLessZeroDescribe) {
return notNotLessZeroDescribe;
}
}
return null;
} catch (Exception e) {
logger.error("参数校验异常!params={}.", JSON.toJSONString(object), e);
return "参数校验异常";
}
}
在需要校验的地方调用校验
String errMsg = DtosUtil.checkObject(withdrawDto);
if (errMsg != null) {
return ResultUtil.buildFailResult(errMsg);
}
这种写法,看着还比较优雅,但是也不够灵活,还得手工处理返回结果。如果某些属性在某些场景不能为空,有些场景又可以为空,这就不好处理了。
举个例子,在插入的时候很多参数是不能为空的,但是在更新的时候大部分参数是都可以为空的。
在属性上使用注解,代码侵入性高。如果是对外提供的jar包,jar包内还需要额外引入其他jar包(一般都会引用三方现有的注解功能,如果是自定义注解就没有这个问题了)。
我期望的效果+解决方案
1、能够灵活的控制需要校验的参数;
2、校验失败时能够返回并且是自动返回具体哪些参数失败,失败的原因;
3、有些场景,校验失败可以继续执行,打个警告的日志即可;
4、校验失败的日志级别可以根据业务场景自己控制;
5、除了非空,其他的校验最好也能支持,容易扩展;
6、捎带把日志也打印了。
自定义注解
public @interface NotNull {
/**
* 待校验的参数/属性
* @return
*/
String fields();
/**
* 提示信息
* @return
*/
String message() default "not null";
/**
* 业务场景描述
*/
String businessDesc() default "";
/**
* 日志级别
*/
String logLevel() default "ERROR";
/**
* 校验失败,自动返回
* @return
*/
boolean autoReturn() default true;
}
切面处理
@Aspect
@Component
public class CheckAspect {
private static final Logger logger = LoggerFactory.getLogger(CheckAspect.class);
private final String BASE_TYPES = "void,int,byte,short,char,double,long";
private final String NUMBER_TYPES = "int,byte,short,double,long";
private final String CHAR_TYPES = "char";
@Pointcut("@annotation(com.daojia.dop.checkcenter.annotation.NotNull)")
public void notNull(){}
@Pointcut("@annotation(com.daojia.dop.checkcenter.annotation.Pattern)")
public void pattern(){}
@Around("notNull()||pattern())
public Object doAccessCheck(ProceedingJoinPoint point){
long start = System.currentTimeMillis();
boolean checkFlag = true;
boolean autoReturn = true;
//校验失败的字段信息
Map<String, String> checkFailMap = new HashMap<>();
// 校验失败返回内容格式:类名.方法名(): businessDesc [field:message]
StringBuilder returnMsg = new StringBuilder();
//切面信息
Class target = point.getTarget().getClass();
Signature signature = point.getSignature();
String fullSignature = signature.toLongString();
// 拿到类名,方法名,所有参数名,所有参数值,注解信息,返回值类型
String className = target.getSimpleName();
// 获取当前被切面的方法
MethodSignature methodSignature = (MethodSignature)signature;
Method pointMethod = methodSignature.getMethod();
// 方法名
String methodName = pointMethod.getName();
// 参数名称
String[] paramNames = methodSignature.getParameterNames();
// 日志级别
String logLevel = "ERROR";
if(paramNames.length < 1){ //无参方法
checkFlag = false;
checkFailMap.put(className + "." +methodName + "()", "Don't join in if the function don't have any arguments...");
}else{
// 参数值
Object[] paramValues = point.getArgs();
// 打印入参
logger.info("{}() - request={}", methodName, JSON.toJSONString(paramValues, SerializerFeature.WriteDateUseDateFormat));
// 参数名,参数值
Map<String, Object> paramKV = new HashMap<>();
// 提取1、2层参数,参数值 【只支持两层】
try{
for (int i = 0; i < paramNames.length; i++) {
Object tmp = paramValues[i];
// 方法参数的值-外层
paramKV.put(paramNames[i], tmp);
if(tmp != null && tmp.getClass().getName().indexOf(".") > 0 && !tmp.getClass().getName().startsWith("java.")){
// 方法参数对象内部各属性的值-内层
Field[] innerFields = tmp.getClass().getDeclaredFields();
for (Field innerField : innerFields) {
innerField.setAccessible(true);
String innerFieldName = innerField.getName();
paramKV.put(paramNames[i] + "." + innerFieldName, innerField.get(tmp));
}
}
if(tmp instanceof List){
//泛型类型
ParameterizedType p = (ParameterizedType)tmp.getClass().getGenericSuperclass();
//泛型类
Class c = (Class) p.getActualTypeArguments()[0];
}
}
}catch(Exception e){
logger.warn("Handle parameterName/parameterValue failure! ", e);
}
returnMsg.append(className).append(".").append(methodName).append("():");
// 校验参数
Annotation[] annotations = pointMethod.getAnnotations();
for (Annotation annotation : annotations) {
String annotationName = annotation.annotationType().getSimpleName();
switch (annotationName){
// 非空校验
case "NotNull" : {
NotNull notNull = (NotNull) annotation;
returnMsg.append(notNull.businessDesc()).append(" - ");
logLevel = notNull.logLevel();
autoReturn = notNull.autoReturn();
String fields = notNull.fields();
// fields格式校验
checkFlag = this.checkFormat(fields);
if(checkFlag){
String[] fieldsArray = notNull.fields().split("\\|");
for (String field : fieldsArray) {
if(field.indexOf(":") > 0 && !checkFailMap.keySet().contains(field.split(":")[0])){
String paramObj = field.split(":")[0];
String[] paramFields = field.split(":")[1].split(",");
if(paramKV.get(paramObj) == null){
checkFailMap.put(paramObj, notNull.message());
}
for (String paramField : paramFields) {
if(paramKV.get(paramObj + "." + paramField) == null){
checkFailMap.put(paramObj + "." + paramField, notNull.message());
}
}
continue;
}
if(paramKV.get(field) == null){
checkFailMap.put(field, notNull.message());
}
}
}
}
break;
// 正则校验... 其他校验
default:break;
}
}
}
// 根据校验结果处理
if(checkFailMap.size() > 0 || !checkFlag){
returnMsg = returnMsg.append(JSON.toJSONString(checkFailMap));
// 根据日志级别打印日志
if("ERROR".equals(logLevel.toUpperCase())){
logger.error(returnMsg.toString());
}else if("INFO".equals(logLevel.toUpperCase())){
logger.info(returnMsg.toString());
}else{
logger.warn(returnMsg.toString());
}
// 处理返回类型
Object returnObj = null;
try {
// 获取方法返回类型
String returnTypeStr = fullSignature.split(" ")[1];
if(!BASE_TYPES.contains(returnTypeStr)){
Class retCls = Class.forName(returnTypeStr);
if(retCls.isInterface()){
if(returnTypeStr.contains("List")){
returnObj = new ArrayList<>();
}
if(returnTypeStr.contains("Set")){
returnObj = new HashSet<>();
}
if(returnTypeStr.contains("Map")){
returnObj = new HashMap<>();
}
}else{
returnObj = retCls.newInstance();
}
if(returnTypeStr.toUpperCase().indexOf("RESULT") > 0){
Class retTypeCls = Class.forName(returnTypeStr);
Field[] retTypeFields = retTypeCls.getDeclaredFields();
for (Field retTypeField : retTypeFields) {
if (retTypeField.getName().equals("code") && retTypeField.getType().equals(int.class)){
Method setCode = retCls.getMethod("setCode", int.class);
if(setCode != null){
setCode.invoke(returnObj, -1);
}
}if (retTypeField.getName().equals("status") && retTypeField.getType().equals(int.class)){
Method setCode = retCls.getMethod("setStatus", int.class);
if(setCode != null){
setCode.invoke(returnObj, -1);
}
}else if (retTypeField.getName().equals("message") && retTypeField.getType().equals(String.class)){
Method setCode = retCls.getMethod("setMessage", String.class);
if(setCode != null){
setCode.invoke(returnObj, returnMsg.toString());
}
}else if (retTypeField.getName().equals("msg") && retTypeField.getType().equals(String.class)){
Method setCode = retCls.getMethod("setMsg", String.class);
if(setCode != null){
setCode.invoke(returnObj, returnMsg.toString());
}
}
}
}
return returnObj;
}else if(NUMBER_TYPES.contains(returnTypeStr)){
return 0;
}else if(CHAR_TYPES.contains(returnTypeStr)){
return '\u0000';
}
} catch (Exception e) {
logger.warn("Handle return type failure! ", e);
}
}else {
logger.info(returnMsg.append("check success!").toString());
try {
if(!autoReturn){
Object obj = point.proceed();
// 打印出参
logger.info("{}() - result={}, cost:{}ms", methodName, JSON.toJSONString(obj, SerializerFeature.WriteDateUseDateFormat), System.currentTimeMillis()-start);
return obj;
} catch (Throwable throwable) {
logger.error("check exception", throwable);
}
}
return null;
}
/**
* fields 格式校验
* @param fields
* @return
*/
public boolean checkFormat(String fields){
if(fields != null && fields.matches("\\w+(:\\w+(,\\w+)*)*(\\|\\w+(:\\w+(,\\w+)*)*)*")){
return true;
}else{
logger.error("The format of fields to check is illegal! reference: fields=\"person:age,name|student\"");
return false;
}
}
看看最终效果
方法多个参数‘|’分隔,类内的属性校验':'+','分隔
@NotNull(fields = "configDto:classificationId,classificationName,dataTypeId,dataTypeName," +
"acquireDataPattern,getDataMode,getDataTime,ruleConfigStatus,operator|test", businessDesc = "新增配置")
@Override
public Result addConfig(TestDto configDto, String test) {
return testService.addConfig(configDto) ? ResultWrapper.success() : ResultWrapper.fail();
}
运行效果
// 服务端日志
09:40:26.560 [] [DubboServerHandler-thread-200] INFO ***.aspect.CheckAspect - addConfig() - request=[{"operator":"tony"}]
09:40:26.560 [] [DubboServerHandler-thread-200] ERROR ***.aspect.CheckAspect - TestAgentImpl.addConfig():新增配置 - {"configDto.getDataMode":"not null","configDto.ruleConfigStatus":"not null","configDto.acquireDataPattern":"not null","configDto.classificationId":"not null","configDto.getDataTime":"not null","configDto.classificationName":"not null","configDto.dataTypeId":"not null","configDto.dataTypeName":"not null"}
// 调用方收到的信息
{ status: -1,
message: "TestAgentImpl.addConfig():新增配置 - {"configDto.getDataMode":"not null","configDto.ruleConfigStatus":"not null","configDto.acquireDataPattern":"not null","configDto.classificationId":"not null","configDto.getDataTime":"not null","configDto.classificationName":"not null","configDto.dataTypeId":"not null","configDto.dataTypeName":"not null"}",
data: "" }
如果需要扩展其他校验功能,新增注解,在切面里增加处理逻辑即可。