返回

优雅的使用AOP实现参数校验

如题所述,本篇博文就是使用AOP方法实现参数校验,同时利用了自定义注解。第一次见到这个做法的时候觉得甚是巧妙,故此记录。

AOP

首先解释一下什么是AOP,AOP(Aspect Orient Programming)是spring框架中的一个重要部分。直译过来就是面向切面编程,这是一种编程思想,作为面向对象的一种补充。其本质就是在不改变源代码的情况下给程序或一组程序动态统一的添加额外功能。

用一个具体的例子来讲,假如我们有如下这么个类,用来实现简单的四则运算。

@NoArgsConstructor
@AllArgsConstructor
public class Operation {
	private int num1;
	private int num2;
  	public int add() {
		return num1 + num2;
	}
	public int sub() {
		return num1 - num2;
	}
	public int mul() {
		return num1 * num2;
	}
	public int div() {
		return num1 / num2;
	}
}

如果我们希望在每个方法运行前打印出一行日志信息,很容易想到的就是在操作类中的每个方法返回之前加上 logger.info(),虽然很简单,但是当源代码无法修改或者十分繁杂的时候,工程量将会呈几何倍数增加。此时会更希望将这个日志方法抽象为一个类,当每个方法执行时,能够自动的在方法返回之前打印出日志。如下图,AOP其实就是由切面对象和目标对象组成的代理对象。

AOP示意图
AOP示意图

自定义注解实现参数校验

假定有以下这个用户类( User),它有两个字段 namepassword,希望实现对用户名的非空判断和对密码的长度判断。

@Data
@NoArgsConstructor
public class User {
    private String name;
    private String password;
}

直观的做法就是在控制器方法上,对获得的用户对象或者是相应的请求参数进行if的条件判断。诚然,这十分有效,但如果换个角度,需要进行参数校验的类不止有User,且控制器方法也不止一个。那么还要一个一个的去添加if条件吗?甚至当控制器方法无法修改时呢?于是,使用一个类或者是一个注解抽象出这个功能就显得一劳永逸了。这也就是AOP强大的解耦能力。

注解的定义

顺着上面的思路,需要拦截的就是控制器方法,首先需要的就是一个用于标识方法的注解,当方法有这个注解时进入拦截器中执行参数校验过程。这个注解可以这么定义。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface GlobalInterceptor() {
	boolean checkParam() default false;  // 校验参数,默认为false
}

再思考这样的问题,对于控制器方法的每个参数,校验的规则一样吗?每个参数都需要校验吗?显然不是,因此需要再定义一个参数注解来实现更加自适应的功能。这个注解可以根据实际需要来实现。在这里实现了长短判断和是否必须的功能。如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface VerifyParam {
    int min() default -1;  // 最大长度
    int max() default -1;  // 最小长度
    boolean required() default false;  // 是否必须
}

切面类的实现

在写完了注解之后,接下来就该实现切面类来使用这个注解了。我们希望能对拥有这个注解的方法实现拦截,自然可以想到切点就是这个注解。而参数校验应该要在方法执行之前实现,因此可以定义如下一个切面类:

public class GlobalOperationAspect {
	@PointCut("@annotation(com.example.annotation.GlobalInterceptor)") // 注解的全类名
	private void requestInterceptor() {}
	
	@Before("requestInterceptor()")
	public void interceptorDo(JoinPoint point) throws Exception {
		// TODO
	}
}

之后需要自然是要获取方法的参数类型,参数值,并考虑参数是否具有之前定义的 VerifyParam注解来考虑对应的判断规则。但是从切面类中,我们只有一个连接点,此时要获取这些信息只能通过反射。

我们利用 point反射获取控制器对象的实例类,被拦截方法的参数值,方法名等。之后再考虑对被拦截的方法的参数依次校验。于是我们可以完善interceptorDo方法。

@Before("requestInterceptor()")
public void interceptorDo(JoinPoint point) throws Exception {
    try {
        Object target = point.getTarget(); // 获取被代理的目标对象
        Object[] args = point.getArgs(); // 获取被拦截方法的参数值
        String methodName = point.getSignature().getName(); // 获取方法名
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
        Method method = target.getClass().getMethod(methodName, parameterTypes);
        GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
        if (interceptor == null) {  // 此判断也可不做
            return;
        }
        if (interceptor.checkParam()) {  // 如果需要校验,才进行校验,否则可能添加了注解但是不校验
            validateParam(method, args);
        }
    } catch (Exception e) {
        throw e;
    }
}

接下来讲讲 validateParam方法。这个方法是实现被拦截方法参数校验的,自然而然我们需要得到参数值以及更为重要的参数注解 (从注解中才能获得校验规则)。依旧是通过反射来实现。

private void validateParam(Method method, Object[] args) throws Exception {
    Parameter[] parameters = method.getParameters(); // 参数列表信息
    for (int i = 0; i < parameters.length; ++i) {
        Parameter parameter = parameters[i];
        Object value = args[i];  // 参数值
        VerifyParam verifyParam = parameter.getAnnotation(VerifyParam.class);
        if (null == verifyParam) {
            continue;
        }
        checkValue(value, verifyParam);
    }
}
private void checkValue(Object value, VerifyParam verifyParam) {
    boolean isEmpty = value == null || value.toString().length() == 0;
    int length = value == null ? 0 : value.toString().length();
    // 校验是否为空
    if (isEmpty && verifyParam.required()) {
        throw new IllegalArgumentException();
    }
    // 校验长度
    if (!isEmpty && (verifyParam.min() != -1 && length < verifyParam.min() ||
            verifyParam.max() != -1 && length > verifyParam.max())) {
        throw new IllegalArgumentException();
    }
}

至此,切面类的实现也就完成了。

效果

最后当然是要试试效果了,附上简单的控制器方法。

    @RequestMapping("/test")
    @GlobalInterceptor(checkParam = true)
    public ResponseVO<User> access(@VerifyParam(required = true) String name,
                                   @VerifyParam(required = true, min = 8, max = 18) String password) {
        ResponseVO<User> responseVO = new ResponseVO<>();
        User user = new User();
        user.setName(name);
        user.setPassword(password);

        responseVO.setStatus("success");
        responseVO.setInfo("校验通过");
        responseVO.setData(user);
        return responseVO;
    }

当我们的控制器方法不加自定义的注解时,发送请求不符合规则请求的结果如下,

不拦截结果
不拦截结果

可以看见,请求参数被正常接收了,当我们加上参数时

拦截结果
拦截结果

响应发生了变化,不再接收不符合规则的参数了。(由于只抛出了异常,因此响应比较简单)。

写在最后

本文只是简单介绍了使用自定义注解和AOP功能来实现参数校验和拦截的功能,整体的思路如图:

自定义注解拦截大致过程
自定义注解拦截大致过程

在这个思路的基础上,可以实现更为复杂的校验,以及更为完善的响应。

写下这篇博文旨在能够加强一些印象,也是这种方法带来的惊艳所驱使。

你要相信流星划过会带给我们幸运,就像现实告诉你我要心存感激
Built with Hugo
Theme Stack designed by Jimmy