参数校验与aop

在软件开发中,通常需要对参数进行校验,比如某些参数不能为空,而且不光前端要校验,后端也要校验。那么后端在哪里校验参数?是在控制层还是service层?参数校验有多种实现方式,采用哪一种校验方式呢? 一般来说,把无业务语义的校验放在action层,用validation做校验,比如数据类型,是否为空,数据长度、格式等,有业务语义的校验放在service层。当然实际中往往并没有那么清晰,当校验足够简单时,可能只在前端校验就行了,或者简单的在service层校验一下,而当校验较复杂时,则可能需要多层校验。

参数校验

Spring3支持JSR-303验证框架,JSR-303是JAVA EE6中的一项子规范,叫做BeanValidation,官方参考实现是hibernate Validator(与Hibernate ORM没有关系),主要用于对Java Bean中的字段值就行校验。

通过BeanValidation配合拦截器可以实现对参数的校验。如在Java Bean中属性上添加@NotNull或@NotBlank注解,表示该属性不能为空串或者null。然后在rest接口上添加注解:@BeanValid。接着实现一个拦截器,在拦截器的preHandle方法中,获取注解对应的方法及参数,进行validatation。

当然也可以不用拦截器,而直接用aop切面,在注解的方法前面进行切入,通过BeanValidation进行校验。实际上拦截器也是利用的切面原理实现的。

这里先以aop校验方式为例,直接看代码。
自定义BeanValid注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BeanValid {
    /* 是否必须校验 */
    boolean required() default true;
}

创建aop切面(注解式声明):

@Component
@Aspect
public class BeanAspect {
    @Before("@annotation({包名}.BeanValid)")
    public void checkBeanValid(JoinPoint joinPoint) throws Exception{
        List args = Arrays.asList(joinPoint.getArgs());
        try {
            args.stream().forEach((Object obj) -> {
                DataValidationUtil.validate(obj);
            });
        } catch (ValidException e) {
            throw new BusinessException(ErrorCodeDefinition.INVALID_PARAMETER, "参数不合法," + e.getMessage());
        }
    }
} 
  

相关类:

public class DataValidationUtil {
    private static final Logger LOG = LoggerFactory.getLogger(DataValidationUtil.class);
    private static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    private static Validator validator = factory.getValidator();
    public static  void validate(T bean){
        StringBuffer sb = new StringBuffer();
        try {
            if (null == bean) {
                sb.append("校验的对象不能为null");
            } else {
                Set> constraintViolations = validator.validate(bean);
                for (ConstraintViolation violation : constraintViolations) {
                    sb.append(violation.getPropertyPath()).append(":").append(violation.getMessage()).append(" ,");
                }
            }
        } catch (RuntimeException e) {
            LOG.error("数据校验发生异常", e);
            sb.append("数据校验发生异常,请检查字段注解配置是否合法");
        }
        if(StringUtils.isNotEmpty(sb.toString())){
            LOG.info("校验异常入参:{}", JsonUtil.toString(bean));
            throw new ValidException(sb.toString());
        }
    }
}

public class ValidException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public ValidException(String message){
        super(message);
    }

    public ValidException() {
        super();
    }

    public ValidException(String message, Throwable cause) {
        super(message, cause);
    }
    public ValidException(Throwable cause) {
        super(cause);
    }
}

其中BusinessException为自定义业务异常,JsonUtil为自定义json转换工具,ErrorCodeDefinition为自定义错误码枚举类。

拦截器

前面说到可以以拦截器的方式实现参数校验。具体实现方式是:定义一个拦截器,需实现HandlerInterceptor接口。拦截器接口中有三个方法,preHandle,postHandle和afterCompletion。其中preHandle方法是在控制器rest接口之前被调用,该方法的第三个参数handle为接口方法句柄,通过接口方法句柄可以获得接口方法相关的信息,以此实现对参数的校验。
说到拦截器,不得不说一下过滤器,这两个概念很容易混淆。过滤器是在Servlet规范中声明的,是Servlet容器支持的,一般在web.xml中进行配置,关键字是filter。而拦截器是spring的组件,配置在Spring文件中,能使用Spring里的资源。如果使用springMVC拦截器,则声明方式如下:

 <mvc:interceptor>
     <mvc:mapping path="/**"/>
        <bean id="customInterceptor" class="xx.framework.interceptor.FrameworkInterceptor">
                    bean>
    mvc:interceptor>
mvc:interceptors>

过滤器是在servlet容器启动后,请求到达springMVC之前进行过滤,一般用于登录权限验证、资源访问权限控制、敏感词汇过滤,字符转换编码等操作。自定义过滤器要实现Filter接口并重写doFilter方法。过滤器在web.xml中进行配置,配置顺序和执行顺序是一致的。
拦截器是在请求到达springMVC的前端控制器DispatcherServlet之后,映射到具体的rest接口处理方法之前起作用的。

下面将以一个完整的例子来说明如何使用拦截器:
先注册一个拦截器:

/* 
*   1.interceptors根目录下的拦截器对所有请求都起作用
*   2.此处注册的实际上是一个拦截器链,其中只有一个子拦截器
*/
<mvc:interceptors>
            <bean id="customInterceptor" class="cn.tonghao.framework.interceptor.FrameworkInterceptor">
            <property name="actions">
                <list>
                    <bean class="cn.tonghao.framework.interceptor.validation.PropertyAnalyticsInterceptor"/>
                list>
            property>
        bean>
   mvc:interceptors>

下面是拦截器类:

public class FrameworkInterceptor implements HandlerInterceptor{

    /**
     * 处理动作集
     */
    private List actions;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle) throws Exception {
        String requestUrl = request.getRequestURI().toString();
        if (handle instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handle;
            //获得方法返回类型
            Class methodReturnType = handlerMethod.getMethod().getReturnType();
            if (methodReturnType == null || methodReturnType.equals(String.class)) {
                return true;
            }
            //构建调用上下文
            InvokeContext context = new InvokeContext(request,response,handlerMethod);
            context.setJsonParameter(getJsonParameter(request));
            //将子拦截器(通过bean注入到actions属性中)设置到上下文调用对象中
            context.setActions(actions);
            context.setRequestUrl(requestUrl);
            //开始调用子拦截器
            context.startInvoke();
            return context.getExecuteResult();
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {

    }

    /**
     * 获取HttpServletRequest参数体
     * @param request
     * @return
     */
    private String getJsonParameter(HttpServletRequest request) throws IOException{
        String method = request.getMethod();
        if (method.equals("GET") || method.equals("DELETE")){
            return request.getQueryString();
        }
        StringBuilder buffer = new StringBuilder();
        String line;
        BufferedReader reader = request.getReader();
        while ((line = reader.readLine()) != null){
            buffer.append(line);
        }
        return buffer.toString();
    }

    public List getActions() {
        return actions;
    }

    public void setActions(List actions) {
        this.actions = actions;
    }
}

下面是调用上下文实现:

public class InvokeContext {

    private static Logger logger = LoggerFactory.getLogger(InvokeContext.class);
    /**
     * 请求
     */
    private HttpServletRequest request;

    /**
     * 响应
     */
    private HttpServletResponse response;

    /**
     * 方法句柄
     */
    private HandlerMethod handlerMethod;

    /**
     * 任务执行下标
     */
    private int index;

    /**
     * 任务链
     */
    private List actions;

    /**
     * 请求参数
     */
    private String JsonParameter;

    private String requestUrl;

    /**
     * 任务执行结果
     */
    private List executeResult = new ArrayList();


    public InvokeContext(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) {
        super();
        this.request = request;
        this.response = response;
        this.handlerMethod = handlerMethod;
    }

    /**
     * 启动任务链
     */
    public void startInvoke(){
        reset();
        invokeNext();
    }

    /**
     * 重置链
     */
    public void reset() {
        this.index = 0;
    }

    /**
     * 获取各个链的执行结果,不能有错误
     */
    public synchronized boolean getExecuteResult() {
        for (boolean bool : executeResult) {
            if (!bool) {
                return bool;
            }
        }
        return true;
    }


    public synchronized void invokeNext() {
        if (actions != null && !actions.isEmpty() && getExecuteResult()) {
            try {
                if (index <= actions.size() - 1) {
                    FilterAction action = actions.get(index);
                    index++;
                    action.invoke(this);
                    executeResult.add(Boolean.TRUE);
                }
            }catch (Exception e) {
                logger.error("执行interceptor时出错");
                executeResult.add(Boolean.FALSE);
            }
        }
    }


    public HttpServletRequest getRequest() {
        return request;
    }

    public void setRequest(HttpServletRequest request) {
        this.request = request;
    }

    public HttpServletResponse getResponse() {
        return response;
    }

    public void setResponse(HttpServletResponse response) {
        this.response = response;
    }

    public HandlerMethod getHandlerMethod() {
        return handlerMethod;
    }

    public void setHandlerMethod(HandlerMethod handlerMethod) {
        this.handlerMethod = handlerMethod;
    }

    public String getJsonParameter() {
        return JsonParameter;
    }

    public void setJsonParameter(String jsonParameter) {
        JsonParameter = jsonParameter;
    }

    public String getRequestUrl() {
        return requestUrl;
    }

    public void setRequestUrl(String requestUrl) {
        this.requestUrl = requestUrl;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    public List getActions() {
        return actions;
    }

    public void setActions(List actions) {
        this.actions = actions;
    }

    public void setExecuteResult(List executeResult) {
        this.executeResult = executeResult;
    }
}

调用上下文中通过action.invoke(this)来调用子拦截器,子拦截器实现如下:

public class PropertyAnalyticsInterceptor implements FilterAction{

    private static final Logger logger = LoggerFactory.getLogger(Logger.class);

    @Resource
    private Validator validator;

    /**
     * Jackson对象映射器
     */
    private ObjectMapper objectMapper;
    {
        //初始化
        objectMapper = new ObjectMapper();
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }

    @Override
    public void invoke(InvokeContext invokeContext) throws Exception {
        //解析请求
        String jsonData = invokeContext.getJsonParameter();
        //参数转化及根据JSR303规范进行属性验证
        BeanValid beanValid = invokeContext.getHandlerMethod().getMethodAnnotation(BeanValid.class);
        try{
            if (StringUtils.isNoneBlank(jsonData) && !"{}".equals(jsonData)) {
                if (beanValid != null) {
            //获取方法参数
                    MethodParameter[] methodParameters = invokeContext.getHandlerMethod().getMethodParameters();
                    for (MethodParameter methodParameter : methodParameters){
                        if (!methodParameter.hasParameterAnnotation(RequestBody.class)) {   
        //如果参数上没有请求参数绑定注解,则跳过当前参数                               
                            continue;
                        }
                        //过滤掉不需要校验的参数
                        if (filterParameter(methodParameter)) {
                            continue;
                        }
                        Object validObject = resolveArgument(jsonData, invokeContext.getRequest(), methodParameter);
                        validateObject(validObject);
                    }
                }
            }else{
                ObjectNotNull notNullAnno = null;
                MethodParameter[] methodParameters = invokeContext.getHandlerMethod().getMethodParameters();
                boolean forceRequired = true;
                for (MethodParameter methodParameter : methodParameters) {
                    notNullAnno = methodParameter.getParameterType().getAnnotation(ObjectNotNull.class);
                    if (notNullAnno != null) {
                        forceRequired = beanValid.required();
                        if (forceRequired) {
                            // 异常封装,参数基础验证失败
                            throw new BusinessException(ErrorDefinition.INVALID_PARAMETER, "参数不合法,参数不能为空");
                        }
                    }
                }
            }
        }catch (Exception e) {
            HttpServletRequest request = invokeContext.getRequest();
            String requestUri = request.getRequestURI();
            logger.error("property analytics error,request url:{}, data:{}, e:{}", requestUri, jsonData, e.getMessage());
            throw e;
        }
        invokeContext.invokeNext();
    }


    /**
     * 参数解析
     * @param jsonData json格式的参数
     * @param request 请求
     * @param parameter 方法参数描述
     * @return 参数对象
     */
    private Object resolveArgument(String jsonData, HttpServletRequest request, MethodParameter parameter)
            throws IOException{
        try{
            JsonNode node = objectMapper.readTree(jsonData);
            String path = parameter.getParameterName();
            if (node.has(path)) {
                return objectMapper.readValue(node.path(path).traverse(),parameter.getParameterType());
            }else{
                return objectMapper.readValue(jsonData,parameter.getParameterType());
            }
        }catch (Exception e) {
            throw new BusinessException(ErrorDefinition.INVALID_PARAMETER,"解析json对象出错:"+e.getMessage());
        }
    }

    /**
     * 参数校验过滤器
     */
    private boolean filterParameter(MethodParameter methodParameter) {
        boolean filtered = false;
        if (methodParameter != null && (methodParameter.getParameterAnnotation(RequestBody.class) == null)
            && (methodParameter.getParameterAnnotation(PathVariable.class) != null)) {
            filtered = true;
        }
        return filtered;
    }

    private void validateObject(Object validObject) {
        Validate.notNull(validObject);
        Set> violations = validator.validate(validObject);
        if (!violations.isEmpty()) {
            StringBuilder buffer = new StringBuilder();
            for (ConstraintViolation violation : violations) {
                buffer.append(violation.getPropertyPath()).append(":").append(violation.getMessage()).append(" ,");
            }
            buffer.deleteCharAt(buffer.length() - 1);
            // 异常封装,参数基础验证失败
            throw new BusinessException(ErrorDefinition.INVALID_PARAMETER, buffer.toString());
        }
    }
} 
  

相关类及变量说明:

FilterAction接口
public interface FilterAction {
    /**
     * 任务调用
     *
     * @param invokeContext 调用上下文
     * @throws Exception 任务执行异常
     */
    void invoke(InvokeContext invokeContext) throws Exception;
}

验证bean:validator在springMVC配置文件中配置(注意:spring容器可以调用springMVC子容器中的资源,但反之不行)


<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

其他相关类BusinessException为自定义业务异常,JsonUtil为自定义json转换工具,ErrorCodeDefinition为自定义错误码枚举类。
两种参数验证实现方式的比较:
aop方式较为简洁;拦截器方式实际上展示了一个拦截器系统的实现方式,通常情况下拦截器是以拦截器链的方式出现的。

你可能感兴趣的:(后端框架)