javascript
SpringMvc接口中转设计(策略+模板方法)
一、前言
最近帶著兩個(gè)兄弟做支付寶小程序后端相關(guān)的開(kāi)發(fā),小程序首頁(yè)涉及到很多查詢(xún)的服務(wù)。小程序后端服務(wù)在我司屬于互聯(lián)網(wǎng)域,相關(guān)的查詢(xún)服務(wù)已經(jīng)在核心域存在了,查詢(xún)這塊所要做的工作就是做接口中轉(zhuǎn)。參考了微信小程序的代碼,發(fā)現(xiàn)他們要么新寫(xiě)一個(gè)接口調(diào)用,要么新寫(xiě)一個(gè)接口包裹多個(gè)接口調(diào)用。這種方式不容易擴(kuò)展。由于開(kāi)發(fā)周期比較理想,所以決定設(shè)計(jì)一個(gè)接口中轉(zhuǎn)器。
二、接口中轉(zhuǎn)器整體設(shè)計(jì)
?
三、接口中轉(zhuǎn)器核心Bean
@Bean public SimpleUrlHandlerMapping directUrlHandlerMapping(@Autowired RequestMappingHandlerAdapter handlerAdapter, ObjectProvider<List<IDirectUrlProcessor>> directUrlProcessorsProvider) {List<IDirectUrlProcessor> directUrlProcessors = directUrlProcessorsProvider.getIfAvailable();Assert.notEmpty(directUrlProcessors, "接口直達(dá)解析器(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("未找到具體的接口直達(dá)處理器...");}});mapping.setUrlMap(urlMappings);mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);return mapping; }關(guān)于核心Bean的示意如下。
- 使用SimpleUrlHandlerMapping 來(lái)過(guò)濾請(qǐng)求路徑中包含"/alipay-applet/direct/**"的請(qǐng)求,認(rèn)為這樣的請(qǐng)求需要做接口中轉(zhuǎn)。
- 針對(duì)中轉(zhuǎn)的請(qǐng)求使用一個(gè)Controller進(jìn)行處理,即AbstractController的一個(gè)實(shí)例,并重寫(xiě)其handleRequestInternal。
- 對(duì)于不同的中轉(zhuǎn)請(qǐng)求找到對(duì)應(yīng)的中轉(zhuǎn)處理器,然后創(chuàng)建相應(yīng)的HandlerMethod ,再借助SpringMvc的RequestMappingHandlerAdapter調(diào)用具體中轉(zhuǎn)處理器接口以及返回值的處理。
為什么要使用RequestMappingHandlerAdapter?因?yàn)橹修D(zhuǎn)處理器的返回值類(lèi)型統(tǒng)一為ReponseEntity<String>,想借助RequestMappingHandlerAdapter中的HandlerMethodReturnValueHandler來(lái)處理返回結(jié)果。
request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Sets.newHashSet(MediaType.APPLICATION_JSON_UTF8));為什么會(huì)有這段代碼?這是HandlerMethodReturnValueHandler調(diào)用的MessageConverter需要的,代碼如下。
我手動(dòng)設(shè)置的原因是因?yàn)镽equestMappingHandlerAdapter是和RequestMappingHandlerMapping配合使用的,RequestMappingHandlerMapping會(huì)在request的attribute中設(shè)置RequestMappingInfo.producesCondition.getProducibleMediaTypes()這個(gè)值。具體參考代碼如下。
org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping#handleMatch org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo四、請(qǐng)求轉(zhuǎn)發(fā)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內(nèi)容轉(zhuǎn)換器restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));return restTemplate;} catch (Exception e) {throw new Exception("網(wǎng)絡(luò)異常或請(qǐng)求錯(cuò)誤.", e);} }/*** 接受未信任的請(qǐng)求** @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; }關(guān)于RestTemplte配置的示意如下。
- 設(shè)置RestTemplte統(tǒng)一異常處理器,統(tǒng)一返回RestClientResponseException。
- 設(shè)置RestTemplte?HttpRequestFactory連接池工廠(HttpClientBuilder的build方法會(huì)創(chuàng)建PoolingHttpClientConnectionManager)。
- 設(shè)置RestTemplte?StringHttpMessageConverter的編碼格式為UTF-8。
- 設(shè)置最大連接數(shù)、路由并發(fā)數(shù)、重試次數(shù)、連接超時(shí)、數(shù)據(jù)超時(shí)、連接等待、連接空閑超時(shí)等參數(shù)。
五、接口中轉(zhuǎn)處理器設(shè)計(jì)
? 考慮到針對(duì)不同類(lèi)型的接口直達(dá)請(qǐng)求會(huì)對(duì)應(yīng)不同的接口中轉(zhuǎn)處理器,設(shè)計(jì)原則一定要明確(open-close)。平時(shí)也閱讀spingmvc源碼,很喜歡其中消息轉(zhuǎn)換器和參數(shù)解析器的設(shè)計(jì)模式(策略+模板方法)。仔細(xì)想想,接口中轉(zhuǎn)處理器的設(shè)計(jì)也可以借鑒一下。
接口中轉(zhuǎn)處理器接口類(lèi)
public interface IDirectUrlProcessor {/*** 接口直達(dá)策略方法* 處理接口直達(dá)請(qǐng)求* */ResponseEntity<String> handle(HttpServletRequest request) throws Exception;/*** 處理器是否支持當(dāng)前直達(dá)請(qǐng)求* */boolean support(HttpServletRequest request); }接口定義了子類(lèi)需要根據(jù)不同的策略實(shí)現(xiàn)的兩個(gè)方法。
接口中轉(zhuǎn)處理器抽象類(lèi)
public abstract class AbstractIDirectUrlProcessor implements IDirectUrlProcessor {private static Logger LOGGER = LoggerFactory.getLogger(AbstractIDirectUrlProcessor.class);@Autowiredprivate RestTemplate directRestTemplate;/*** 接口直達(dá)模板方法* */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("接口直達(dá)UserId = %s, RequestEntity = %s", userId, directRequest));ResponseEntity<String> directResponse = directRestTemplate.exchange(directRequest, String.class);LOGGER.info(String.format("接口直達(dá)UserId = %s, URL = %s, ResponseEntity = %s", userId, directRequest.getUrl(), directResponse));return ResponseEntity.ok(directResponse.getBody());} catch (RestClientResponseException e) {LOGGER.error("restapi 內(nèi)部異常", e);return ResponseEntity.status(e.getRawStatusCode()).body(e.getResponseBodyAsString());} catch (Exception e) {LOGGER.error("restapi 內(nèi)部異常,未知錯(cuò)誤...", e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("restapi 內(nèi)部異常,未知錯(cuò)誤...");}} }抽象類(lèi)中帶有接口直達(dá)模板方法,子類(lèi)可以直接調(diào)用,完成請(qǐng)求的轉(zhuǎn)發(fā)。
接口中轉(zhuǎn)處理器具體實(shí)現(xiàn)類(lèi)
/*** 自助服務(wù)直達(dá)查詢(xún)*/ @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);} }接口中轉(zhuǎn)處理器具體實(shí)現(xiàn)類(lèi)需要根據(jù)請(qǐng)求的URL判斷是否支持處理當(dāng)前請(qǐng)求,如果中轉(zhuǎn)請(qǐng)求中帶有敏感信息(如手機(jī)號(hào))需要特殊處理(UriComponentsBuilder 是一個(gè)不錯(cuò)的選擇呦)。
六、總結(jié)
接口中轉(zhuǎn)器擴(kuò)展方便,只要按照如上方式根據(jù)不同類(lèi)型的request實(shí)現(xiàn)具體的接口中轉(zhuǎn)處理器就可以了。另外就是接口文檔了,有了接口中轉(zhuǎn)處理器,只需要改一下真實(shí)服務(wù)的接口文檔就可以。比如真實(shí)服務(wù)的請(qǐng)求地址是http://172.17.20.92:28000/XXX/business/points/手機(jī)號(hào)信息,只需要改成http://172.17.20.92:28000/YYY/alipay-applet/direct/business/points。【手機(jī)號(hào)信息是敏感信息,需要后端從會(huì)話信息中獲取】。還有,不要問(wèn)我為啥要花時(shí)間設(shè)計(jì)這個(gè)東西,第一領(lǐng)導(dǎo)同意了,第二開(kāi)發(fā)周期理想,第三我喜歡!!!
轉(zhuǎn)載于:https://www.cnblogs.com/hujunzheng/p/10250403.html
總結(jié)
以上是生活随笔為你收集整理的SpringMvc接口中转设计(策略+模板方法)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 七桥问题c语言程序数据结构,数据结构与算
- 下一篇: 中铁投资集团什么级别