目录

在前面的一些文章中我们有讲到,通过拦截器我们可以做很多的事情,包括接口统一的 参数校验、 登录校验、权限校验等,也可以做一些HTTP响应体写入逻辑,本篇我们也就是讲解下,使用拦截器对开放的接口做公共参数校验功能实现。

SpringBoot实战 - 拦截器实现统一参数校验

下面我以我们实际开发中所遇到的问题,来举例说明。

需求描述

在对外开放接口的时候,我们的调用端是很多的,比如:APP/PC/公众号H5/小程序 等等,当线上环境某一个用户出现问题时,如果这个问题仅仅是后端还好,但是如果是前后端需要配合解决的错误,我们就需要更多的调用客户端的一些信息,这个时候你去问客户 APP 什么版本、什么手机这显然是不妥的,所以我们应该收集更多的调用信息,以便我们做后续业务处理、日志记录等等的一些操作,通常需要对客户端统一收集的信息比如 调用来源、APP版本号、API的版本号、安全验证信息 等等。

解决方式

我们将这些信息放入头信息(HTTP HEAD中),下面给出在参数命名的例子:

X-Token 用户的登录token(用于获取用户登录信息)
Api-Version api的版本号
App-Version app版本号
Call-Source 调用来源(IOS、ANDROID、PC、WECHAT、WEB)
Authorization 安全校验参数

然后我们将该些信息在拦截器中做统一的参数校验,下面我们给出响应的代码,给大家一个写法上的参考。

我们为这些请求头参数变量定义常量类。

定义请求头参数常量类

package cn.notemi.demo.constant;

import cn.notemi.demo.enums.ApiStyleEnum;
import cn.notemi.demo.enums.CallSourceEnum;

/**
 * @desc Header的key罗列
 */
public class HeaderConstants {

    /**
     * 用户的登录token
     */
    public static final String X_TOKEN = "X-Token";

    /**
     * api的版本号
     */
    public static final String API_VERSION = "Api-Version";

    /**
     * app版本号
     */
    public static final String APP_VERSION = "App-Version";

    /**
     * 调用来源 {@link CallSourceEnum}
     */
    public static final String CALL_SOURCE = "Call-Source";

    /**
     * API的返回格式 {@link ApiStyleEnum}
     */
    public static final String API_STYLE = "Api-Style";
}

对于调用的来源我们可以定义一个枚举类:

package cn.notemi.demo.enums;

/**
 * @desc 调用来源枚举类
 */
public enum CallSourceEnum {
    /**
     * WEB网站
     */
    WEB,
    /**
     * PC客户端
     */
    PC,
    /**
     * 微信公众号
     */
    WECHAT,
    /**
     * IOS平台
     **/
    IOS,
    /**
     * 安卓平台
     */
    ANDROID;

    public static boolean isValid(String name) {
        for (CallSourceEnum callSource : CallSourceEnum.values()) {
            if (callSource.name().equals(name)) {
                return true;
            }
        }
        return false;
    }

}

定义拦截器

package cn.notemi.demo.intercepter;

import cn.notemi.demo.constant.HeaderConstants;
import cn.notemi.demo.enums.CallSourceEnum;
import cn.notemi.demo.enums.ResultCode;
import cn.notemi.demo.exceptions.BusinessException;
import cn.notemi.demo.util.StringUtil;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @desc 统一参数校验:HEADER头参数校验
 */
public class HeaderParamsCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            String callSource = request.getHeader(HeaderConstants.CALL_SOURCE);
            String apiVersion = request.getHeader(HeaderConstants.API_VERSION);
            String appVersion = request.getHeader(HeaderConstants.APP_VERSION);

            if (StringUtil.isAnyBlank(callSource, apiVersion)) {
                throw new BusinessException(ResultCode.PARAM_NOT_COMPLETE);
            }

            try {
                Double.valueOf(apiVersion);
            } catch (NumberFormatException e) {
                throw new BusinessException(ResultCode.PARAM_IS_INVALID);
            }

            if ((CallSourceEnum.ANDROID.name().equals(callSource) || CallSourceEnum.IOS.name().equals(callSource)) && StringUtil.isEmpty(appVersion)) {
                throw new BusinessException(ResultCode.PARAM_NOT_COMPLETE);
            }

            if (!CallSourceEnum.isValid(callSource)) {
                throw new BusinessException(ResultCode.PARAM_IS_INVALID);
            }

        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // nothing to do
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // nothing to do
    }

}

说明

上述校验逻辑其实很简单,如果校验不通过直接以一个异常抛出,交由统一的异常处理器去处理后续事情,对于app版本信息可能只有 Android、iOS 才会有所以我们根据这种特殊情况做了下判断。

拦截器配置类

package cn.notemi.demo.config;

import cn.notemi.demo.intercepter.HeaderParamsCheckInterceptor;
import cn.notemi.demo.intercepter.LoginAuthInterceptor;
import cn.notemi.demo.intercepter.ResponseResultInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {

    @Bean
    public HeaderParamsCheckInterceptor headerParamsCheckInterceptor() {
        return new HeaderParamsCheckInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        //请求头参数拦截
        registry.addInterceptor(headerParamsCheckInterceptor());

    }

}

最后

至此,所有端传不同的信息,然后将这些信息整合到日志中,就能更清晰的定位问题了。

读完文章你会发现,对拦截器的使用上基本上没有什么创新,只是在使用例子也就是业务的逻辑上代码的变化,其实是的,拦截器使用起来还是蛮简单的,但是该在什么地方去使用它,这个还是要过考虑下的,所以在这篇文章中我们主要是以实际开发中所用到的问题做举例,这可能引出你对拦截器更好使用的一些想法,希望你在以后的使用中能够更好的利用拦截器的特性,写出更美观、更好维护的代码。

GitHub地址:https://github.com/FlickerMi/notemi-demo