@ControllerAdvice配合RequestBodyAdviceAdapter/ResponseBodyAdviceAdapter使用学习

1,008 阅读2分钟

用途

  • 日常项目中。
  • 1、我们的记录请求日志,或者在返回的时候记录请求日志。
  • 2、我们的公共校验、比如签名校验、密钥校验、过期时间检验等
  • 3、统一异常处理等。

@ControllerAdvice与RequestBodyAdviceAdapter

  • @ControllerAdvice注解可以扫描针对Controller层的扩展组件。通过@Order注解可以使其支持顺序加载。

  • RequestBodyAdviceAdapter是RequestBodyAdvice适配器类,可以方便的扩展所需要的方法。

  • RequestBodyAdvice功能如下:

  • 允许在请求消息体在被读取及调用convert转换成实体之前做一些个人化操作,作用于含有@RequestBody注解的请求。实现此接口的类,需要在RequestMappingHandlerAdapter中配置或通过@ControllerAdvice注解配置。

代码如下

@Slf4j
public abstract class AbstractApiAdvice extends RequestBodyAdviceAdapter {

    @Autowired
    private AuthConfig authConfig;

    protected static final String HEAD_SIGN_PARAMETER = "sign";
    protected static final String HEAD_TIMESTAMP_PARAMETER = "timestamp";
    protected static final String HEAD_APP_KEY_PARAMETER = "appKey";

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    @ResponseBody
    @ExceptionHandler
    public RestResponse<?> exceptionHandle(BusinessException e) {
        return new RestResponse<>(e.getCode(), e.getMessage());
    }

    @ResponseBody
    @ExceptionHandler
    public RestResponse<?> exceptionHandle(Exception e) {
        log.error("发生未知异常", e);
        return new RestResponse<>(ErrorCode.OTHER_ERROR, e.getMessage());
    }

    @SuppressWarnings("NullableProblems")
    @Override
    public boolean supports(MethodParameter methodParameter
            , Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @SuppressWarnings("NullableProblems")
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter
            , Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {

        HttpHeaders headers = inputMessage.getHeaders();

        final String sign = headers.getFirst(HEAD_SIGN_PARAMETER);
        final String timestamp = headers.getFirst(HEAD_TIMESTAMP_PARAMETER);

        // 字段缺失校验
        if (StrUtil.isEmpty(timestamp)) {
            throw new BusinessException(ErrorCode.TIME_INVALID);
        }
        if (StrUtil.isEmpty(sign)) {
            throw new BusinessException(ErrorCode.SIGN_ERROR);
        }

        // 校验请求是否已经过期
        validTime(timestamp);

        final InnerApiInput innerApiInput = new InnerApiInput()
                .setBody(IoUtil.read(inputMessage.getBody(), CharsetUtil.UTF_8))
                .setSign(sign)
                .setToken(getToken(headers, parameter))
                .setTimestamp(timestamp);

        // 校验签名是否一致
        signCheckout(innerApiInput);

        return new HttpInputMessage() {
            @SuppressWarnings("NullableProblems")
            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream(innerApiInput.getBody().getBytes(StandardCharsets.UTF_8));
            }

            @SuppressWarnings("NullableProblems")
            @Override
            public HttpHeaders getHeaders() {
                return preHandleHttpHeader(headers);
            }
        };
    }

    /**
     * 获取当前请求对应的token值
     *
     * @param headers   HTTP 请求头
     * @param parameter MethodParameter
     * @return token值
     */
    protected abstract String getToken(HttpHeaders headers, MethodParameter parameter);

    /**
     * 获取过期时间
     * @return timestamp过期时间
     */
    protected abstract long getExpiredTime();

    protected HttpHeaders preHandleHttpHeader(HttpHeaders originHttpHeaders) {
        return originHttpHeaders;
    }


    /**
     * 参数的字段校验
     * @param timestamp 过期时间
     */
    private void validTime(String timestamp) {
        // 校验时间戳是否过期
        final long timestampMillis = Convert.toLong(timestamp);
        final long currentTimeMillis = TimeUtil.currentTimeMillis();
        if (Math.abs(currentTimeMillis - timestampMillis) > getExpiredTime()) {
            throw new BusinessException(ErrorCode.TIME_PASSED);
        }
    }

    /**
     * 参数的签名校验
     * @param innerApiInput
     */
    private void signCheckout(InnerApiInput innerApiInput) {
        //鉴权开关判断
        if (!authConfig.isEnabled()) {
            return;
        }
        if (ArrayUtil.isNotEmpty(authConfig.getExcludeUrls())) {
            String accessUrl = ContextUtil.getHttpRequest().getRequestURI();
            for (String excludeUrl : authConfig.getExcludeUrls()) {
                if (pathMatcher.match(excludeUrl.trim(), accessUrl)) {
                    return;
                }
            }
        }

        //签名校验
        String formatStr = String.format(ApiConstants.INNER_MD5_ENC_STR, innerApiInput.getTimestamp(),
                innerApiInput.getToken(), innerApiInput.getBody());
        String md5 = DigestUtil.md5Hex(formatStr);
        if (!StrUtil.equals(innerApiInput.getSign(), md5)) {
            log.info("Api sign error!! input:{} server:{}", innerApiInput.getSign(), md5);
            throw new BusinessException(ErrorCode.SIGN_ERROR);
        }
    }
}

通过继承上面的advice去实现真正的逻辑

@ControllerAdvice(
        assignableTypes = {
                ActivitiesClueController.class,
                InnerApiYmsClueController.class
        })
public class InnerApiControllerAdvice extends AbstractApiAdvice {

    /**
     * 过期时间定为10min
     */
    private static final long MAX_EXPIRED_DURATION_MILLIS = 10 * 60 * 1000L;

    private static final Map<Class<?>, String> INNER_API_TOKEN = new HashMap<Class<?>, String>(3) {{
        put(InnerApiSubjectClueController.class, ApiConstants.TOKEN_PUSH_SUBJECT_CLUE);
        put(InnerApiIspProjectClueReportController.class, ApiConstants.TOKEN_ISP_PROJECT_CLUE);
        put(InnerApiMonitorSubjectClueController.class, ApiConstants.TOKEN_MONITOR_SUBJECT_CLUE);
        put(InnerApiYmsClueController.class, ApiConstants.TOKEN_YMS_CLUE);
        put(ActivitiesClueController.class, ApiConstants.TOKEN_PUSH_ACTIVITIES_CLUE);
    }};

    @Override
    protected String getToken(HttpHeaders headers, MethodParameter parameter) {
        return INNER_API_TOKEN.get(parameter.getContainingClass());
    }

    @Override
    protected long getExpiredTime() {
        return MAX_EXPIRED_DURATION_MILLIS;
    }
}

其用法跟HandlerInterceptorAdaptor结合webMvcConfigurator差不多,都是起到拦截器的作用,

## 来源1