xChar

相关文章:

{% post_link 拦截和分发 %}

策略注入

针对所有的 /core/ycApi 的接口路径进行拦截,拦截器为 YcForwardFilter。

存在 YcForwardFilter 实例时注入该策略

@Bean
@ConditionalOnBean(YcForwardFilter.class)
public FilterRegistrationBean myFilterRegistration() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    // 注入过滤器
    registration.setFilter(ycForwardFilter);
    // 设置拦截规则
    registration.addUrlPatterns("/core/ycApi/*");
    // 设置拦截名称
    registration.setName("ycFilter");
    // 设置拦截顺序
    registration.setOrder(1);
    return registration;
}

拦截器

拦截器需要实现接口 Filter 中的 doFilter 方法

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
	// 拦截的逻辑
}

转发

拦截到请求后,可使用 filterChain 留在系统中处理,也可以转发出去

private void forwardFilter(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 获取转发路径
    String forwardUrl = getUrlByRule();
    HttpRequestBase httpRequestBase = null;
    // 1. 请求地址
    String uri = request.getRequestURI();
    // 此处为 application/x-www-form-urlencoded
    String query = request.getQueryString();
    String urlParam = Arrays.asList(
        query,
        Optional.ofNullable(appKey).filter(s -> !ObjectUtils.isEmpty(s)).map(s -> "appkey=" + s).orElse(""),
        Optional.ofNullable(random).filter(s -> !ObjectUtils.isEmpty(s)).map(s -> "random=" + s).orElse(""),
        Optional.ofNullable(digest).filter(s -> !ObjectUtils.isEmpty(s)).map(s -> "digest=" + s).orElse("")
    ).stream()
        .filter(s -> !ObjectUtils.isEmpty(s))
        .collect(Collectors.joining("&&"));
    String newUrl = forwardUrl + uri + Optional.of(urlParam).filter(s -> s.length() > 0).map(s -> "?" + s).orElse("");
    log.info("接口转发:newUrl={}", newUrl);
    // 2. 设置请求方式
    String method = request.getMethod();
    log.info("请求方式:method={}", method);
    if (HttpMethod.GET.name().equalsIgnoreCase(method)) {
        httpRequestBase = new HttpGet(newUrl);
    } else if (HttpMethod.POST.name().equalsIgnoreCase(method)) {
        httpRequestBase = new HttpPost(newUrl);
    }
    // 3. 设置请求头
    copyRequestHeader(request, httpRequestBase);
    // 重置请求头中的认证信息
    //        resetAuthInfo(httpRequestBase);
    // 4. 设置POST参数
    String contentType = request.getContentType();
    log.info("contentType={}", contentType);
    if (httpRequestBase instanceof HttpPost && !ObjectUtils.isEmpty(contentType)) {
        if (contentType.startsWith(ContentType.APPLICATION_JSON.getMimeType())) {
            // application/json
            BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
            String body = reader.lines().collect(Collectors.joining());
            // 请求中的空格不能去,为了发现空格
            log.info("request body={}", StrUtil.brief(body, 300));
            ((HttpPost) httpRequestBase).setEntity(new StringEntity(body));
        } else if (contentType.startsWith(ContentType.MULTIPART_FORM_DATA.getMimeType())) {
            // multipart/form-data
            copyFormData(request, httpRequestBase);
        } else {
            log.warn("不支持 application/json、multipart/form-data 之外的POST类型");
        }
    }
    // 5. 连接设置
    RequestConfig config = RequestConfig.custom()
        .setConnectionRequestTimeout(connectionRequestTimeout)
        .setSocketTimeout(socketTimeout)
        .setConnectTimeout(connectionTimeout)
        .build();
    httpRequestBase.setConfig(config);
    // 6. 提交请求
    CloseableHttpClient httpClient = HttpClients.createDefault();
    CloseableHttpResponse execute = httpClient.execute(httpRequestBase);
    // 打印结果需要读取流,尽量不要对源数据进行处理,只打印响应码
    int statusCode = execute.getStatusLine().getStatusCode();
    log.info("响应结果码:{}", statusCode);
    // 7. 返回前的处理
    HttpEntity responseEntity = execute.getEntity();
    // 8. 设置响应头
    Header[] responseHeaders = execute.getAllHeaders();
    for (Header header : responseHeaders) {
        // Content-Length 和 Content-Encoding 不能同时存在,故两个属性都不复制
        if (HTTP.CONTENT_LEN.equalsIgnoreCase(header.getName())
            || HTTP.TRANSFER_ENCODING.equalsIgnoreCase(header.getName())) {
            continue;
        }
        log.debug("response header name={}, value={}", header.getName(), header.getValue());
        // 复制响应头
        response.setHeader(header.getName(), header.getValue());
    }
    // 9. 转发结果写入响应体
    if (statusCode == HttpStatus.SC_NOT_FOUND){
        // 针对404单独处理
        response404(response, request.getServletPath());
    } else {
        responseEntity.writeTo(response.getOutputStream());
    }
}
/**
  * 针对404单独处理
  * @param response 响应体
  * @param path 请求URI
  */
private void response404(HttpServletResponse response, String path){
    try (ServletOutputStream outputStream = response.getOutputStream()) {
        YcRspBase rsp404 = YcRspBase.failed("404", "Not Found : " + path);
        response.setStatus(HttpStatus.SC_NOT_FOUND);
        ObjectMapper objectMapper = new ObjectMapper();
        String str404 = objectMapper.writeValueAsString(rsp404);
        outputStream.write(str404.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
    } catch (Exception e){
        log.error("写入数据到response失败", e);
    }
}

/**
  * 复制 POST 请求 form-data 请求方式中的请求数据
  *
  * @param request         待复制请求
  * @param httpRequestBase 转发请求
  * @throws IOException
  * @throws ServletException
  */
private void copyFormData(HttpServletRequest request, HttpRequestBase httpRequestBase) throws IOException, ServletException {
    // 附带文件的请求,contentType后有数据描述,小数据为base64的值,大数据疑似为文件大小
    // e.g. multipart/form-data; boundary=--------------------------394758598706425280136232
    MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
        // 防止中文文件名导致的乱码
        .setMode(HttpMultipartMode.RFC6532);
    // multipart/form-data 时才能获取,否则报错
    Collection<Part> parts = request.getParts();
    if (!ObjectUtils.isEmpty(parts)) {
        // 数据为文件时,Content-Type 中含有长度信息 [boundary],需要去除长度
        httpRequestBase.removeHeaders(HTTP.CONTENT_TYPE);
        for (Part part : parts){
            try {
                InputStream is = part.getInputStream();
                entityBuilder.addBinaryBody(
                    part.getName(),
                    is,
                    ContentType.APPLICATION_OCTET_STREAM,
                    part.getSubmittedFileName());
            } catch (Exception e) {
                log.error("copy part failed", e);
            }
        }
    }
    // 流读取放在 getParameterMap 之前,防止读取一次后数据丢失
    Map<String, String[]> parameterMap = request.getParameterMap();
    // 多个重复的 k 会在 v[] 数组中处理,一般不建议传数组 or 后续再处理
    //        parameterMap.forEach((k, v) -> entityBuilder.addTextBody(k, v[0]));
    parameterMap.forEach((k, v) -> Stream.of(v).forEach(i -> entityBuilder.addTextBody(k, i)));
    ((HttpPost) httpRequestBase).setEntity(entityBuilder.build());
}

/**
  * 复制请求中的请求头
  * <p>
  * 1. 不能设置 Content-Length 的值,让 CloseableHttpClient 自动设置
  * 2. multipart/form-data 不能复制请求头,错误的请求头,会导致接收端文件读取不到,委托 CloseableHttpClient 自动设置
  *
  * @param request         原请求
  * @param httpRequestBase 转发请求
  */
private void copyRequestHeader(HttpServletRequest request, HttpRequestBase httpRequestBase) {
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
        String key = headerNames.nextElement();
        if (HTTP.CONTENT_LEN.equalsIgnoreCase(key)) {
            // 不设置header长度
            continue;
        }
        String value = request.getHeader(key);
        log.debug("request header key={}, value={}", key, value);
        httpRequestBase.setHeader(key, value);
    }
}

异常捕获

spring处理异常

需要借助 HandlerExceptionResolver 异常处理器来协助我们处理局部异常

@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver exceptionResolver;

HandlerExceptionResolver 是个多实例注入对象,Spring MVC 使用的实例名为 handlerExceptionResolver
但是注入的也不是单个处理器,而是 HandlerExceptionResolverComposite 对象,持有多个处理器

public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {
    @Nullable
    private List<HandlerExceptionResolver> resolvers;
    private int order = Integer.MAX_VALUE;
    
    ...

}

通过处理器中的 resolveException 方法,将拦截器中的异常捕获并统一处理

exceptionResolver.resolveException(request, response, null, e);

以上方法会将异常交给spring处理

自定义异常

有时因对接不同的系统,要求不同的返回数据结构,我们会要求指定异常处理类

获取该controller的引用(也许不需要)

@Autowired
private YcApiController ycController;

在 resolveException 指定方法handler对象

HandlerMethod handlerMethod = new HandlerMethod(ycController, ycController.getClass().getDeclaredMethods()[0]);
exceptionResolver.resolveException(request, response, handlerMethod, e);

正常情况下,请求在servlet分发时,会匹配到 HandlerMethod

如果没有特别的需求,找到controller对应的Advice即可

备注,exceptionResolver在处理 basePackageClasses 指定的类时,匹配的是同包及其子包

// org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) {
    Class<?> handlerType = null;
    // 方法handler在这里转换成类型
    if (handlerMethod != null) {
        handlerType = handlerMethod.getBeanType();
        ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver)this.exceptionHandlerCache.get(handlerType);
        if (resolver == null) {
            resolver = new ExceptionHandlerMethodResolver(handlerType);
            this.exceptionHandlerCache.put(handlerType, resolver);
        }
        ...
    }
        
    ...
        
    while(var9.hasNext()) {
         Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry = (Map.Entry)var9.next();
         ControllerAdviceBean advice = (ControllerAdviceBean)entry.getKey();
         // 这里会检查basePackages和类型,handlerType为空将交由下一个Advice处理异常
         if (advice.isApplicableToBeanType(handlerType)) {
             ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver)entry.getValue();
             Method method = resolver.resolveMethod(exception);
             if (method != null) {
                 return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
              }
          }
    }
    return null;
}

在Advice类头指定basePackages时的生效地

// org.springframework.web.method.HandlerTypePredicate
private boolean hasSelectors() {
    // basePackages 是以'com.xx.xxx.'方式存储,表示的是注解的当前包及其子包
    return !this.basePackages.isEmpty() || !this.assignableTypes.isEmpty() || !this.annotations.isEmpty();
}
示例
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    try {
        // 转发
        forwardFilter(request, response);
    } catch (Exception e){
        log.error("转发服务异常", e);
        // 交给Spring的异常解析器去处理
        //            exceptionResolver.resolveException(request, response, null, e);
        // 必须得传 HandlerMethod 否则会走GlobalExceptionHandler,而不是指定的YcApiAdvice
        // 没有通过servlet进行分发,因此没有获取到方法对象
        // 如有必要再添加对方法的匹配,这里默认去找Controller的第一个方法
        HandlerMethod handlerMethod = new HandlerMethod(ycController, ycController.getClass().getDeclaredMethods()[0]);
        exceptionResolver.resolveException(request, response, handlerMethod, e);
    }
}

自定义异常捕获

/**
 * 会捕获指定类[basePackageClasses]同包及其子包下的异常
 * 拥有类包及其子包下异常捕获的最高优先级
 *
 * @author **
 * @date 2023/04/18 09:42:50
 */
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice(basePackageClasses = YcApiController.class)
public class YcApiAdvice {
    /**
     * 参数校验未通过
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public YcRspBase methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        log.error("MethodArgumentNotValidException {}", e.getMessage());
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return YcRspBase.failed("80111016", objectError.getDefaultMessage());
    }
}
Loading comments...