一、前言
最近带着两个兄弟做支付宝小程序后端相关的开发,小程序首页涉及到很多查询的服务。小程序后端服务在我司属于互联网域,相关的查询服务已经在核心域存在了,查询这块所要做的工作就是做接口中转。参考了微信小程序的代码,发现他们要么新写一个接口调用,要么新写一个接口包裹多个接口调用。这种方式不容易扩展。由于开发周期比较理想,所以决定设计一个接口中转器。
二、接口中转器整体设计
三、接口中转器核心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来处理返回结果。
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() { @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。
- 设置最大连接数、路由并发数、重试次数、连接超时、数据超时、连接等待、连接空闲超时等参数。
五、接口中转处理器设计
考虑到针对不同类型的接口直达请求会对应不同的接口中转处理器,设计原则一定要明确(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); @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 是一个不错的选择呦)。
六、总结
接口中转器扩展方便,只要按照如上方式根据不同类型的request实现具体的接口中转处理器就可以了。另外就是接口文档了,有了接口中转处理器,只需要改一下真实服务的接口文档就可以。比如真实服务的请求地址是http://172.17.20.92:28000/XXX/business/points/手机号信息,只需要改成http://172.17.20.92:28000/YYY/alipay-applet/direct/business/points。【手机号信息是敏感信息,需要后端从会话信息中获取】。还有,不要问我为啥要花时间设计这个东西,第一领导同意了,第二开发周期理想,第三我喜欢!!!