山东省环保厅官方网站建设项目/百度前三推广
- 本案例包括4个核心类:
AopUtils:抽取出来的公共方法
@ValidateGroup
@ValidateField
ValidateAspectJHandler - 功能包括:长度、值范围、正则匹配、非空校验
- 以下是设计思路、最终代码、测试结果
- 后续扩展只需要修改@ValidateField 和 ValidateAspectJHandler
1.演示: 最终使用方法
以注册功能为例
@RestController
@RequestMapping("validater")
public class ValidateController {@ValidateGroup(fields = {@ValidateField(index = 0,notNull = true,maxLen = 10,code = "param1-error",message = "param1校验错误"),@ValidateField(index = 1,notNull = true,fieldName = "passWord",minLen = 6,code = "passWord-erro",message = "密码校验错误"),@ValidateField(index = 1,notNull = true,fieldName = "age",minVal = 0,code = "age-error",message = "年龄不能小于0"),@ValidateField(index = 1,notNull = true,fieldName = "tall",minVal = 0,maxVal = 250.9,code ="tall-error",message = "身高范围出错"),@ValidateField(index = 1,notNull = true,fieldName = "phone",regStr = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$",code = "phone-error",message = "手机号错误")})@PostMapping("post")public String postValidater(@RequestParam String param1, RegisterDto dto){System.out.println("成功通过校验");System.out.println("第一个参数是:" + param1);System.out.println("第二个参数是"+dto.toString());return "succeed";}
}
其中:请求Dto包含name、passWord、phone等字段;
利用AspectJ对接口方法直接进行代理校验
2.流程分析
- AspectJ代理注解
@ValidateGroup
标注的方法,而这个Group注解中的属性就是@ValidateField []
- 获取到
@ValidateField
数组后,遍历。通过比对注解的参数 与 dto或者param中对应名字的参数来进行校验 - 如果校验成功就放行,校验失败就抛异常终止
3.公共方法抽取AopUtils
经过流程分析可知:至少需要以下几个方法(并可以抽取为公共组件)
public Method getMethod(ProceedingJoinPoint pjp)
:获取被AOP拦截的方法Method对象public Annotation getAnnotationByMethod(Method method, Class annoClass)
:获取目标方法对象的指定注解对象public Object getFieldFromDtoByFieldName(Object dto , String fieldName)
从dto中,获取指定属性名的属性值
如下工具类也可以做成全静态方法
public class AopUtils {private volatile static AopUtils aopUtils;private AopUtils() {
}public static AopUtils getInstance() {if (aopUtils == null) {synchronized (AopUtils.class) {if (aopUtils == null) {aopUtils = new AopUtils();}}}return aopUtils;
}/*** 获取目标类的指定方法*/
public Method getMethodByClassAndName(Class c, String methodName) {Method[] methods = c.getDeclaredMethods();for (Method method : methods) {if (method.getName().equals(methodName)) {return method;}}return null;
}/*** 获取目标方法的指定注解* 相当于 method.getAnnotation(xxxx.class);*/
public Annotation getAnnotationByMethod(Method method, Class annoClass) {Annotation all[] = method.getAnnotations();for (Annotation annotation : all) {if (annotation.annotationType() == annoClass) {return annotation;}}return null;
}/*** 获取被拦截方法的对象* 配合使用,最终用于在Aspectj中获取被拦截方法上的注解* 例如:AopUtils.getMethod(pjp).getDeclaredAnnotation(被aop拦截的注解.class)*/
public Method getMethod(ProceedingJoinPoint pjp) {//获取参数的签名MethodSignature msig = (MethodSignature) pjp.getSignature();// MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象 如果在实现类的方法上,应该使用反射获取当前对象的方法对象Object target = pjp.getTarget();//获取连接点所在的目标对象(被代理的对象)而不是父类or接口//方法名 + 方法形参 ————》获取指定的方法对象(重载)String methodName = msig.getName();Class[] parameterTypes = msig.getParameterTypes();Method method = null;try {method = target.getClass().getMethod(methodName, parameterTypes);} catch (NoSuchMethodException e) {//log.error(...);}return method;
}/*** 从dto中,获取指定属性名的属性值;*/
public Object getFieldFromDtoByFieldName(Object dto , String fieldName) throws NoSuchFieldException, IllegalAccessException {Class<?> dtoClazz = dto.getClass();Field field = dtoClazz.getDeclaredField(fieldName);field.setAccessible(true);return field.get(dto);
}// 这个其实还有另一种写法
// private Method getMethod(ProceedingJoinPoint joinPoint) {
// try {
// Class[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
// return joinPoint.getTarget().getClass().getMethod(joinPoint.getSignature().getName(), parameterTypes);
// } catch (NoSuchMethodException e) {
// e.printStackTrace();
// }
// return null;
// }
}
4.注解及设计原理
4.1ValidateGroup
这个注解用于被AspectJ拦截,其属性是一个数组,用于参数校验
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface ValidateGroup {ValidateFiled[] fields();}
4.2ValidateField
- 当且仅当只有一个参数的时候可以不用指定index
- index默认为0,例如
public void register(@RequestParam String param1 , @RequestBody Dto dto){}
中应该设置index = 1 ,这是由于joinPoint.getArgs()
获取的形参是一个数组,需要用index指定位置 - 所有参数都有默认值(不进行校验)
如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface ValidateField {/*** 参数索引位置:接口中有多个参数时用index指定需要校验的参数* 默认:0号索引,当且仅当接口方法只有一个参数*/int index() default 0;/*** 默认:如果参数是基本数据类型或String,就不用指定该参数* 如果参数是对象,要验证对象里面某个属性,就用该参数指定属性名*/String fieldName() default "";/*** 错误码,用于日志记录*/String code() default "";/*** 错误提示语,用于日志记录*/String message() default "";/*** 正则验证*/String regStr() default "";/*** 非空校验,为true表示不能为空,false表示能够为空*/boolean notNull() default false;/*** 字符串最大长度*/int maxLen() default 0x7fffffff;/*** 字符串最小长度*/int minLen() default 0;/*** 最大值,用于验证数值类型数据*/double maxVal() default 0x1.fffffffffffffP+1023;/*** 最小值,用于验证数值类型数据*/double minVal() default 0x0.0000000000001P-1022;}
5.ValidateAspectJHandler
5.1大体结构
@Component
@Aspect
public class ValidateAspectHandler {private AopUtils aopUtils = AopUtils.getInstance();/*** 使用AOP对使用了ValidateGroup的方法进行代理校验*/@Around("@annotation(ValidateGroup)")public Object validateAround(ProceedingJoinPoint joinPoint) throws Throwable {
//1.获取当前方法对象method
//2.根据method对象获取对应的ValidateGroup对象
//3.调用封装方法,将validateGroup.fields() 与 joinPoint.getArgs()形参数组传入,进行校验
//4.如果为true,则执行下一步return joinPoint.proceed();}/*** 封装方法:验证参数是否合法*/private boolean validateField(ValidateFiled[] validateFields, Object[] args) {for (ValidateFiled validateFiled : validateFields) {//1.每次循环都是一个校验逻辑//2.index仍然是指定对第几号元素进行校验//3.fieldName如果不指定,那就是对基本数据类型即@RequestParam进行校验;如果指定,则对Dto即@RequestBody进行校验}
5.2具体实现
@Component
@Aspect
public class ValidateAspctJHandler {// private static org.slf4j.Logger logger = LoggerFactory.getLogger("ValidateAspctJHandler");private AopUtils aopUtils = AopUtils.getInstance();/*** 使用AOP对使用了ValidateGroup的方法进行代理校验*///相当于用@Around + return joinPoint.proceed(); //使用@Before的时候不能用ProceedingJoinPoint 只能用JoinPoint@Before("@annotation(ValidateGroup)")public void validateAround(JoinPoint joinPoint) throws Throwable {//获取被代理的方法对象Method method = aopUtils.getMethod(joinPoint);//获取被代理的方法对象对应的@ValidateGroup对象ValidateGroup validateGroup = (ValidateGroup)aopUtils.getAnnotationByMethod(method, ValidateGroup.class);//获取被代理方法的参数数组(这是参数值,而不是 method.getParameterTypes()返回的是Class[] )Object[] args = joinPoint.getArgs();/** args和validateGroup中包含了全部需要校验的信息,因此可以封装为一个方法* 在这个方法中,如果校验失败则用throws抛异常的方式终止*/validateAllFields(validateGroup.fields(), args);}/*** 验证参数是否合法* ValidateField[]中每一条都是一个校验规则,每一条都对应一个属性* Object[] args中是所有的请求参数值,需要从args[validateFiled.index()中确定是对谁进行校验*/private void validateAllFields(ValidateField[] validateFields, Object[] args) throws NoSuchFieldException, IllegalAccessException {//遍历:对每个@ValidateField进行校验for (ValidateField validateFiled : validateFields) {Object arg;//1.当fieldName为默认值""的时候,此时是对@RequestParam即基本数据类型orString进行校验if ("".equals(validateFiled.fieldName())) {//arg是基本数据类型orStringarg = args[validateFiled.index()];//2.如果fieldName设置了,那就是对dto中的某个属性进行校验} else {//获取第index号参数dto指定的属性值arg = aopUtils.getFieldFromDtoByFieldName(args[validateFiled.index()], validateFiled.fieldName());}//3.以下是校验流程,需要同时考虑是对dto属性or基本数据类型orString//3.1判断参数是否为空if (validateFiled.notNull()) {if (arg == null || arg.equals("")) {
// logger.error(validateFiled.code() + ":" + validateFiled.message());throw new RuntimeException(validateFiled.code() + ":" + validateFiled.message());//如果该参数能够为空,并且当参数为空时,就不用判断后面的了 ,直接返回} }else {if (arg == null || arg.equals("")) {return;}}//3.2判断字符串最大长度 如果设置为一个负数则不校验 默认为最大int值if (validateFiled.maxLen() >= 0) {if (arg.toString().length() > validateFiled.maxLen()) {
// logger.error(validateFiled.code() + ":" + validateFiled.message());throw new RuntimeException(validateFiled.code() + ":" + validateFiled.message());}}//3.3判断字符串最小长度 如果设置为一个负数则不校验 默认为0if (validateFiled.minLen() >= 0) {if (arg.toString().length() < validateFiled.minLen()) {
// logger.error(validateFiled.code() + ":" + validateFiled.message());throw new RuntimeException(validateFiled.code() + ":" + validateFiled.message());}}//3.4判断数值最大值 当不是默认值0x1.fffffffffffffP+1023的时候进行判断if (validateFiled.maxVal() != 0x1.fffffffffffffP+1023) {if (Double.parseDouble(arg.toString()) > validateFiled.maxVal()) {
// logger.error(validateFiled.code() + ":" + validateFiled.message());throw new RuntimeException(validateFiled.code() + ":" + validateFiled.message());}}//3.5判断数值最小值 当不是默认值0x0.0000000000001P-1022的时候进行判断if (validateFiled.minVal() != 0x0.0000000000001P-1022) {if (Double.parseDouble(arg.toString()) < validateFiled.minVal()) {
// logger.error(validateFiled.code() + ":" + validateFiled.message());throw new RuntimeException(validateFiled.code() + ":" + validateFiled.message());}}//3.6判断正则 若未设置正则校验则跳过if (!"".equals(validateFiled.regStr())) {if (arg instanceof String || arg instanceof Integer || arg instanceof BigDecimal || arg instanceof Double) {if (!(arg.toString()).matches(validateFiled.regStr())) {
// logger.error(validateFiled.code() + ":" + validateFiled.message());throw new RuntimeException(validateFiled.code() + ":" + validateFiled.message());}}}}return;}
}
5.3接口测试
@RestController
@RequestMapping("validater")
public class ValidateController {@ValidateGroup(fields = {//如果是index=0也可以省略不写@ValidateField(index = 0,notNull = true,maxLen = 10,code = "param1-error",message = "param1校验错误"),@ValidateField(index = 1,notNull = true,fieldName = "passWord",minLen = 6,code = "passWord-erro",message = "密码校验错误"),@ValidateField(index = 1,notNull = true,fieldName = "age",minVal = 0,code = "age-error",message = "年龄不能小于0"),@ValidateField(index = 1,notNull = true,fieldName = "tall",minVal = 0,maxVal = 250.9,code ="tall-error",message = "身高范围出错"),@ValidateField(index = 1,notNull = true,fieldName = "phone",regStr = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$",code = "phone-error",message = "手机号错误")})@PostMapping("post")public String postValidater(@RequestParam String param1, RegisterDto dto){System.out.println("成功通过校验");System.out.println("第一个参数是:" + param1);System.out.println("第二个参数是"+dto.toString());return "succeed";}
}
如果参数错误,则会直接抛异常终止请求
如果参数都正确就可以通过校验,如下: