一, 前言
最近带着两个兄弟做支付宝小程序后端相关的开发, 小程序首页涉及到很多查询的服务. 小程序后端服务在我司属于互联网域, 相关的查询服务已经在核心域存在了, 查询这块所要做的工作就是做接口中转. 参考了微信小程序的代码, 发现他们要么新写一个接口调用, 要么新写一个接口包裹多个接口调用. 这种方式不容易扩展. 由于开发周期比较理想, 所以决定设计一个接口中转器.
二, 接口中转器整体设计
三, 接口中转器核心 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.[手机号信息是敏感信息, 需要后端从会话信息中获取] . 还有, 不要问我为啥要花时间设计这个东西, 第一领导同意了, 第二开发周期理想, 第三我喜欢!!!
来源: https://www.cnblogs.com/hujunzheng/p/10250403.html