SpringMvc接口中轉設計(策略+模板方法)

1、前言

  最近帶着兩個兄弟作支付寶小程序後端相關的開發,小程序首頁涉及到不少查詢的服務。小程序後端服務在我司屬於互聯網域,相關的查詢服務已經在覈心域存在了,查詢這塊所要作的工做就是作接口中轉。參考了微信小程序的代碼,發現他們要麼新寫一個接口調用,要麼新寫一個接口包裹多個接口調用。這種方式不容易擴展。因爲開發週期比較理想,因此決定設計一個接口中轉器。web

2、接口中轉器總體設計

  

 

3、接口中轉器核心Bean

@Bean
public SimpleUrlHandlerMapping directUrlHandlerMapping(@Autowired RequestMappingHandlerAdapter handlerAdapter
        , ObjectProvider<List<IDirectUrlProcessor>> directUrlProcessorsProvider) {
    List<IDirectUrlProcessor> directUrlProcessors = directUrlProcessorsProvider.getIfAvailable();
    Assert.notEmpty(directUrlProcessors, "接口直達解析器(IDirectUrlProcessor)列表不能爲空!!!");
    SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
    Map<String, Controller> urlMappings = Maps.newHashMap();
    urlMappings.put("/alipay-applet/direct/**", new AbstractController() {
        @Override
        protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
            for (IDirectUrlProcessor directUrlProcessor : directUrlProcessors) {
                if (directUrlProcessor.support(request)) {
                    String accept = request.getHeader("Accept");
                    request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Sets.newHashSet(MediaType.APPLICATION_JSON_UTF8));
                    if (StringUtils.isNotBlank(accept) && !accept.contains(MediaType.ALL_VALUE)) {
                        request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Sets.newHashSet(
                                Arrays.stream(accept.split(","))
                                        .map(value -> MediaType.parseMediaType(value.trim()))
                                        .toArray(size -> new MediaType[size])
                        ));
                    }
                    HandlerMethod handlerMethod = new HandlerMethod(directUrlProcessor, ReflectionUtils.findMethod(IDirectUrlProcessor.class, "handle", HttpServletRequest.class));
                    return handlerAdapter.handle(request, response, handlerMethod);
                }
            }
            throw new RuntimeException("未找到具體的接口直達處理器...");
        }
    });
    mapping.setUrlMap(urlMappings);
    mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
    return mapping;
}

  關於核心Bean的示意以下。

  • 使用SimpleUrlHandlerMapping 來過濾請求路徑中包含"/alipay-applet/direct/**"的請求,認爲這樣的請求須要作接口中轉。
  • 針對中轉的請求使用一個Controller進行處理,即AbstractController的一個實例,並重寫其handleRequestInternal。
  • 對於不一樣的中轉請求找到對應的中轉處理器,而後建立相應的HandlerMethod ,再借助SpringMvc的RequestMappingHandlerAdapter調用具體中轉處理器接口以及返回值的處理。

  爲何要使用RequestMappingHandlerAdapter?由於中轉處理器的返回值類型統一爲ReponseEntity<String>,想借助RequestMappingHandlerAdapter中的HandlerMethodReturnValueHandler來處理返回結果。spring

request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Sets.newHashSet(MediaType.APPLICATION_JSON_UTF8));

  爲何會有這段代碼?這是HandlerMethodReturnValueHandler調用的MessageConverter須要的,代碼以下。小程序

  

  我手動設置的緣由是由於RequestMappingHandlerAdapter是和RequestMappingHandlerMapping配合使用的,RequestMappingHandlerMapping會在request的attribute中設置RequestMappingInfo.producesCondition.getProducibleMediaTypes()這個值。具體參考代碼以下。後端

org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping#handleMatch
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo

4、請求轉發RestTempate配置

@Bean
public RestTemplate directRestTemplate() throws Exception {
    try {
        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                throw new RestClientResponseException(response.getStatusCode().value() + " " + response.getStatusText(),
                        response.getStatusCode().value()
                        , response.getStatusText()
                        , response.getHeaders()
                        , getResponseBody(response)
                        , getCharset(response));
            }

            protected byte[] getResponseBody(ClientHttpResponse response) {
                try {
                    InputStream responseBody = response.getBody();
                    if (responseBody != null) {
                        return FileCopyUtils.copyToByteArray(responseBody);
                    }
                } catch (IOException ex) {
                    // ignore
                }
                return new byte[0];
            }

            protected Charset getCharset(ClientHttpResponse response) {
                HttpHeaders headers = response.getHeaders();
                MediaType contentType = headers.getContentType();
                return contentType != null ? contentType.getCharset() : null;
            }
        });
        // 修改StringHttpMessageConverter內容轉換器
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    } catch (Exception e) {
        throw new Exception("網絡異常或請求錯誤.", e);
    }
}

/**
 * 接受未信任的請求
 *
 * @return
 * @throws KeyStoreException
 * @throws NoSuchAlgorithmException
 * @throws KeyManagementException
 */
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory()
        throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
    HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
    SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build();

    httpClientBuilder.setSSLContext(sslContext)
            .setMaxConnTotal(MAX_CONNECTION_TOTAL)
            .setMaxConnPerRoute(ROUTE_MAX_COUNT)
            .evictIdleConnections(CONNECTION_IDLE_TIME_OUT, TimeUnit.MILLISECONDS);

    httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(RETRY_COUNT, true));
    httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
    CloseableHttpClient client = httpClientBuilder.build();

    HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(client);
    clientHttpRequestFactory.setConnectTimeout(CONNECTION_TIME_OUT);
    clientHttpRequestFactory.setReadTimeout(READ_TIME_OUT);
    clientHttpRequestFactory.setConnectionRequestTimeout(CONNECTION_REQUEST_TIME_OUT);
    clientHttpRequestFactory.setBufferRequestBody(false);
    return clientHttpRequestFactory;
}

  關於RestTemplte配置的示意以下。微信小程序

  • 設置RestTemplte統一異常處理器,統一返回RestClientResponseException。
  • 設置RestTemplte HttpRequestFactory鏈接池工廠(HttpClientBuilder的build方法會建立PoolingHttpClientConnectionManager)。
  • 設置RestTemplte StringHttpMessageConverter的編碼格式爲UTF-8。
  • 設置最大鏈接數、路由併發數、重試次數、鏈接超時、數據超時、鏈接等待、鏈接空閒超時等參數。

5、接口中轉處理器設計

   考慮到針對不一樣類型的接口直達請求會對應不一樣的接口中轉處理器,設計原則必定要明確(open-close)。平時也閱讀spingmvc源碼,很喜歡其中消息轉換器和參數解析器的設計模式(策略+模板方法)。仔細想一想,接口中轉處理器的設計也能夠借鑑一下。設計模式

  接口中轉處理器接口類

public interface IDirectUrlProcessor {
    /**
     * 接口直達策略方法
     * 處理接口直達請求
     * */
    ResponseEntity<String> handle(HttpServletRequest request) throws Exception;

    /**
     * 處理器是否支持當前直達請求
     * */
    boolean support(HttpServletRequest request);
}

  接口定義了子類須要根據不一樣的策略實現的兩個方法。api

  接口中轉處理器抽象類

public abstract class AbstractIDirectUrlProcessor implements IDirectUrlProcessor {
    private static Logger LOGGER = LoggerFactory.getLogger(AbstractIDirectUrlProcessor.class);

    @Autowired
    private RestTemplate directRestTemplate;

    /**
     * 接口直達模板方法
     * */
    protected ResponseEntity<String> handleRestfulCore(HttpServletRequest request, URI uri, String userId) throws Exception {
        HttpMethod method = HttpMethod.resolve(request.getMethod());
        Object body;
        if (method == HttpMethod.GET) {
            body = null;
        } else {
            body = new BufferedReader(new InputStreamReader(request.getInputStream()))
                    .lines()
                    .collect(Collectors.joining());
            // post/form
            if (StringUtils.isBlank((String) body)) {
                MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
                if (!CollectionUtils.isEmpty(request.getParameterMap())) {
                    request.getParameterMap()
                            .forEach(
                                    (paramName, paramValues) -> Arrays.stream(paramValues)
                                            .forEach(paramValue -> params.add(paramName, paramValue))
                            );
                    body = params;
                }
            }
        }

        HttpHeaders headers = new HttpHeaders();
        CollectionUtils.toIterator(request.getHeaderNames())
                .forEachRemaining(headerName -> CollectionUtils.toIterator(request.getHeaders(headerName))
                        .forEachRemaining(headerValue -> headers.add(headerName, headerValue)));

        RequestEntity directRequest = new RequestEntity(body, headers, method, uri);
        try {
            LOGGER.info(String.format("接口直達UserId = %s, RequestEntity = %s", userId, directRequest));
            ResponseEntity<String> directResponse = directRestTemplate.exchange(directRequest, String.class);
            LOGGER.info(String.format("接口直達UserId = %s, URL = %s, ResponseEntity = %s", userId, directRequest.getUrl(), directResponse));
            return ResponseEntity.ok(directResponse.getBody());
        } catch (RestClientResponseException e) {
            LOGGER.error("restapi 內部異常", e);
            return ResponseEntity.status(e.getRawStatusCode()).body(e.getResponseBodyAsString());
        } catch (Exception e) {
            LOGGER.error("restapi 內部異常,未知錯誤...", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("restapi 內部異常,未知錯誤...");
        }
    }
}

  抽象類中帶有接口直達模板方法,子類能夠直接調用,完成請求的轉發。微信

  接口中轉處理器具體實現類

/**
 * 自助服務直達查詢
 */
@Component
public class SelfServiceIDirectUrlProcessor extends AbstractIDirectUrlProcessor {

    private static final String CONDITION_PATH = "/alipay-applet/direct";

    @Reference(group = "wmhcomplexmsgcenter")
    private IAlipayAppletUserInfoSV alipayAppletUserInfoSV;

    private void buildQueryAndPath(UriComponentsBuilder uriComponentsBuilder, AlipayAppletUser userInfo) {
        uriComponentsBuilder.path("/" + userInfo.getTelephone())
                .queryParam("channel", "10008")
                .queryParam("uid", userInfo.getUserId())
                .queryParam("provinceid", userInfo.getProvinceCode());
    }

    public ResponseEntity<String> handle(HttpServletRequest request) throws Exception {
        String userId = JwtUtils.resolveUserId();
        AlipayAppletUser userInfo = alipayAppletUserInfoSV.queryUserInfo(userId);

        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(AppletConstants.ISERVICE_BASEURL
                + request.getServletPath().replace(CONDITION_PATH, StringUtils.EMPTY));

        if (StringUtils.isNotBlank(request.getQueryString())) {
            uriComponentsBuilder.query(request.getQueryString());
        }

        this.buildQueryAndPath(uriComponentsBuilder, userInfo);

        String url = uriComponentsBuilder.build().toUriString();
        URI uri = URI.create(url);
        return handleRestfulCore(request, uri, userId);
    }

    @Override
    public boolean support(HttpServletRequest request) {
        return request.getServletPath().contains(CONDITION_PATH);
    }
}

  接口中轉處理器具體實現類須要根據請求的URL判斷是否支持處理當前請求,若是中轉請求中帶有敏感信息(如手機號)須要特殊處理(UriComponentsBuilder 是一個不錯的選擇呦)。網絡

6、總結

  接口中轉器擴展方便,只要按照如上方式根據不一樣類型的request實現具體的接口中轉處理器就能夠了。另外就是接口文檔了,有了接口中轉處理器,只須要改一下真實服務的接口文檔就能夠。好比真實服務的請求地址是http://172.17.20.92:28000/XXX/business/points/手機號信息,只須要改爲http://172.17.20.92:28000/YYY/alipay-applet/direct/business/points。【手機號信息是敏感信息,須要後端從會話信息中獲取】。還有,不要問我爲啥要花時間設計這個東西,第一領導贊成了,第二開發週期理想,第三我喜歡!!!併發

相關文章
相關標籤/搜索