javascript
SpringMvc接口中转设计(策略+模板方法)
一、前言
最近帶著兩個兄弟做支付寶小程序后端相關的開發,小程序首頁涉及到很多查詢的服務。小程序后端服務在我司屬于互聯網域,相關的查詢服務已經在核心域存在了,查詢這塊所要做的工作就是做接口中轉。參考了微信小程序的代碼,發現他們要么新寫一個接口調用,要么新寫一個接口包裹多個接口調用。這種方式不容易擴展。由于開發周期比較理想,所以決定設計一個接口中轉器。
二、接口中轉器整體設計
?
三、接口中轉器核心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() {@Overrideprotected 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來處理返回結果。
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四、請求轉發RestTempate配置
@Bean public RestTemplate directRestTemplate() throws Exception {try {RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {@Overridepublic 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。
- 設置最大連接數、路由并發數、重試次數、連接超時、數據超時、連接等待、連接空閑超時等參數。
五、接口中轉處理器設計
? 考慮到針對不同類型的接口直達請求會對應不同的接口中轉處理器,設計原則一定要明確(open-close)。平時也閱讀spingmvc源碼,很喜歡其中消息轉換器和參數解析器的設計模式(策略+模板方法)。仔細想想,接口中轉處理器的設計也可以借鑒一下。
接口中轉處理器接口類
public interface IDirectUrlProcessor {/*** 接口直達策略方法* 處理接口直達請求* */ResponseEntity<String> handle(HttpServletRequest request) throws Exception;/*** 處理器是否支持當前直達請求* */boolean support(HttpServletRequest request); }接口定義了子類需要根據不同的策略實現的兩個方法。
接口中轉處理器抽象類
public abstract class AbstractIDirectUrlProcessor implements IDirectUrlProcessor {private static Logger LOGGER = LoggerFactory.getLogger(AbstractIDirectUrlProcessor.class);@Autowiredprivate 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/formif (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);}@Overridepublic boolean support(HttpServletRequest request) {return request.getServletPath().contains(CONDITION_PATH);} }接口中轉處理器具體實現類需要根據請求的URL判斷是否支持處理當前請求,如果中轉請求中帶有敏感信息(如手機號)需要特殊處理(UriComponentsBuilder 是一個不錯的選擇呦)。
六、總結
接口中轉器擴展方便,只要按照如上方式根據不同類型的request實現具體的接口中轉處理器就可以了。另外就是接口文檔了,有了接口中轉處理器,只需要改一下真實服務的接口文檔就可以。比如真實服務的請求地址是http://172.17.20.92:28000/XXX/business/points/手機號信息,只需要改成http://172.17.20.92:28000/YYY/alipay-applet/direct/business/points。【手機號信息是敏感信息,需要后端從會話信息中獲取】。還有,不要問我為啥要花時間設計這個東西,第一領導同意了,第二開發周期理想,第三我喜歡!!!
轉載于:https://www.cnblogs.com/hujunzheng/p/10250403.html
總結
以上是生活随笔為你收集整理的SpringMvc接口中转设计(策略+模板方法)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 七桥问题c语言程序数据结构,数据结构与算
- 下一篇: 中铁投资集团什么级别