博主在平时写代码的时候,喜欢先对方法参数进行一遍非空验证,然后才开始处理业务逻辑。当然,这有两方面原因,第一是参加工作那会,带我的老司机写代码时教我们的,第二是我觉得这样能够保证程序更加严密,更加可靠,消耗更少的资源(比如添加了非空校验,可以阻止调用数据库)。
由于代码写的多,所以校验也就写的多,有时候一个方法写的校验比业务逻辑还多,会比较麻烦。看看下面这段代码,就是平时写的校验:
@Transactional
public void downloadFromBaseReagent(List<BaseReagentView> baseReagentViews, String orgId,
String userId) {
logger.info("【从平台产品库中下载产品信息到机构产品库】orgId = {} , userId = {} , baseReagentViews = {}", orgId,
userId, baseReagentViews);
if (ProString.isBlank(orgId)) {
logger.warn("【参数orgId为空】");
throw new IllegalArgumentException("从平台产品库中下载产品信息到机构产品库,参数orgId为空");
}
if (ProString.isBlank(userId)) {
logger.warn("【参数userId为空】");
throw new IllegalArgumentException("从平台产品库中下载产品信息到机构产品库,参数userId为空");
}
if (ProCollection.isEmpty(baseReagentViews)) {
logger.warn("【参数baseReagentViews为空】");
throw new IllegalArgumentException("从平台产品库中下载产品信息到机构产品库,参数baseReagentViews为空");
}
List<CoreReagentView> coreReagents = generateOrgReagent(baseReagentViews, orgId, userId);
coreReagentMapper.batchInsert(coreReagents);
}
上面这段代码,非空校验就写了很大一部分,所以从一年前博主就在思考通过一些工具类帮助程序员做这种非空校验,比如Spring MVC的@RequestParam,参考@RequestParam的思路,博主花了三个小时写了一个简单版(Beta-1.0.0),测试了一下,可行,先来写篇博客,然后再慢慢完善。
工具的实现是自定义注解+AOP,主要有两个类,下面给出源代码
自定义注解:ParamRequired.java
/**
* Copyright (c) 2015 - 2016 Eya Inc.
* All rights reserved.
*/
package com.cdelabcare.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* @create ll
* @createDate 2016年12月2日 上午11:20:34
* @update
* @updateDate
*/
// 作用域,这里限制只能作用在参数上
@Target(ElementType.METHOD)
// 注解的生命周期,这里限制在运行时有效
@Retention(RetentionPolicy.RUNTIME)
//用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
@Documented
public @interface ParamRequired {
/**
* 需要校验的参数索引,下表从0开始
* @Author : ll. create at 2016年12月2日 下午3:07:16
*/
int[] indexs();
/**
* 字符串不能为空字符串,即不能为"",默认true
* @Author : ll. create at 2016年12月2日 下午3:07:42
*/
boolean stringNotEmpty() default true;
/**
* {@code Collection}不能为空集合,默认为true
* @Author : ll. create at 2016年12月2日 下午3:08:03
*/
boolean collectionNotEmpty() default true;
/**
* {@code Map}不能为空,默认为true
* @Author : ll. create at 2016年12月2日 下午3:08:54
*/
boolean mapNotEmpty() default true;
}
AOP切面类:ParamRequiredAspect
import com.eya.common.annotation.ParamRequired;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.javassist.ClassClassPath;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.javassist.bytecode.CodeAttribute;
import org.apache.ibatis.javassist.bytecode.LocalVariableAttribute;
import org.apache.ibatis.javassist.bytecode.MethodInfo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Map;
/**
* 参数非空校验的切面类
* <p>扫描带有{@code ParamRequired}注解的方法
*
* @create ll
* @createDate 2016年12月2日 上午11:57:38
* @update
* @updateDate
*/
@Aspect
@Component
public class ParamRequiredAspect {
// shared param discoverer since it caches data internally
private static final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 拦截带@ParamRequired注解的方法,校验参数是否为空
*
* @Author : ll. create at 2016年12月2日 下午3:01:44
*/
@Before("@annotation(com.wyying.consult.common.annotation.ParamRequired)")
public void beforeDo(JoinPoint joinPoint) throws Exception {
//拦截的实体类
Object target = joinPoint.getTarget();
//拦截的方法名称
String methodName = joinPoint.getSignature().getName();
//拦截的放参数类型
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
//拦截的方法
Method method = target.getClass().getMethod(methodName, parameterTypes);
// 获取参数名称
//String[] paramNames = getFieldsName(this.getClass(), target.getClass().getName(), methodName);
String[] paramNames = getParameterNames(method);
//拦截的方法参数(值)
Object[] args = joinPoint.getArgs();
// 获取方法的注解
ParamRequired paramRequired = method.getAnnotation(ParamRequired.class);
// 获取方法注解的indexs属性的值
int[] indexs = paramRequired.indexs();
// 处理没有指定参数索引的情况:如果没有指定参数索引,则表示校验所有参数
if (ArrayUtils.isEmpty(indexs)) {
indexs = new int[args.length];
for (int i = 0, len = args.length; i < len; i++) {
indexs[i] = i;
}
}
// 注解的三个参数,分别为:String是否不能为空字符串,集合是否不能为空集合,Map是否不能为空
boolean stringNotEmpty = paramRequired.stringNotEmpty();
boolean collectionNotEmpty = paramRequired.collectionNotEmpty();
boolean mapNotEmpty = paramRequired.mapNotEmpty();
// 依次校验是否有空值
Object tmpValue = null;
int illegalIndex = -1;
for (int i : indexs) {
tmpValue = args[i];
if (tmpValue == null) {
illegalIndex = i;
}
if (tmpValue instanceof String) {
if (stringNotEmpty && StringUtils.isBlank((String) tmpValue)) {
illegalIndex = i;
}
} else if (tmpValue instanceof Collection<?>) {
if (collectionNotEmpty && CollectionUtils.isEmpty((Collection<?>) tmpValue)) {
illegalIndex = i;
}
} else if (tmpValue instanceof Map<?, ?>) {
if (mapNotEmpty && MapUtils.isEmpty((Map<?, ?>) tmpValue)) {
illegalIndex = i;
}
} else if (tmpValue instanceof Object[]) {
if (collectionNotEmpty && ArrayUtils.isEmpty((Object[]) tmpValue)) {
illegalIndex = i;
}
}
// 校验不通过,直接处理结果
if (illegalIndex != -1) {
resultHandler(target, methodName, paramNames, illegalIndex);
}
}
}
/**
* 处理校验结果
*
* @param obj 被拦截的对象
* @param methodName 方法名称
* @param paramNames 参数数组
* @param index 参数索引
* @Author : ll. create at 2016年12月2日 下午2:52:23
*/
private void resultHandler(Object obj, String methodName, String[] paramNames, int index) {
String msgTemplate = "调用%s.%s时,参数%s为空";
String msg = String.format(msgTemplate, obj.getClass().getName(), methodName, paramNames[index]);
throw new IllegalArgumentException(msg);
}
/**
* 获取方法的参数列表
* <p>用于替代getFieldsName方法</p>
*
* @param method {@link Method}
* @return 指定方法的参数列表
*/
public static String[] getParameterNames(Method method) {
return paramNameDiscoverer.getParameterNames(method);
}
}
通过上面2个类,就可以实现验证方法参数非空了,使用的时候,在要校验的方法上,加上注解@ParamRequired即可,那么将最上面的业务代码就可以改成如下这样:
@Transactional
@ParamRequired(indexs = { 0, 1, 2 })
public void downloadFromBaseReagent(List<BaseReagentView> baseReagentViews, String orgId,
String userId) {
logger.info("【从平台产品库中下载产品信息到机构产品库】orgId = {} , userId = {} , baseReagentViews = {}", orgId,
userId, baseReagentViews);
List<CoreReagentView> coreReagents = generateOrgReagent(baseReagentViews, orgId, userId);
coreReagentMapper.batchInsert(coreReagents);
}
如果在调用上述方法时,参数为空,则会抛出异常,输出的异常信息描述如下
调用com.eya.service.test.downloadFromBaseReagent时,参数baseReagentViews为空
修改之后,方法体变得简单多了,只需要专注于解决业务问题即可,太棒了。
注解除了indexs,还有其他三个参数,通过看源代码就能理解,这里不再累述。
希望该工具能够帮助和我一样对程序严谨性和健壮性有要求的朋友。如果朋友们有更好建议,欢迎指导和交流。
在最后,谈一下使用AOP带来的一个缺陷,如果从方法内部调用调用当前类的另一个方法,比如beanA中的方法method1和method2,都添加了@ParamRequired注解,然后method1中调用了method2,AOP不会对这样的调用进行拦截,这是和代理相关的问题,这里不详细介绍。解决这种问题有两种思路:
1、(Spring官方推荐,但不可取)在外部将调用method1和method2分开,即先调用method1,再调用method2来解决这种问题。但是这样会使我们的逻辑变得紊乱。
2、使用AopContext获取当前代理对象,然后调用method2,可以触发AOP拦截(详细介绍请上车:http://www.tuicool.com/articles/NFJNRf)