一、前言

  最近带着两个兄弟做支付宝小程序后端相关的开发,小程序首页涉及到很多查询的服务。小程序后端服务在我司属于互联网域,相关的查询服务已经在核心域存在了,查询这块所要做的工作就是做接口中转。参考了微信小程序的代码,发现他们要么新写一个接口调用,要么新写一个接口包裹多个接口调用。这种方式不容易扩展。由于开发周期比较理想,所以决定设计一个接口中转器。

二、接口中转器整体设计

  

三、接口中转器核心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接口中转设计(策略+模板方法)相关推荐

  1. 【2021软件创新实验室暑假集训】SpringMVC框架(设计原理、简单使用、源码探究)

    系列文章目录 20级 Java篇 [2021软件创新实验室暑假集训]计算机的起源与大致原理 [2021软件创新实验室暑假集训]Java基础(一) [2021软件创新实验室暑假集训]Java基础(二) ...

  2. 软件测试之接口测试用例设计,全网独一份

    1.接口测试用例设计简介 我们对系统的需求分析完成之后,即可设计对应的接口测试用例,然后用接口测试用例进行接口测试.接口测试用例的设计也需要用到黑盒测试方法,其与功能测试用例设计的方法类似,接口测试用 ...

  3. 【理论了解】接口测试简介以及接口测试用例设计思路

    接口测试简介 1.什么是接口 接口就是内部模块对模块,外部系统对其他服务提供的一种可调用或者连接的能力的标准,就好比usb接口,他是系统向外接提供的一种用于物理数据传输的一个接口,当然仅仅是一个接口是 ...

  4. APP架构设计经验谈:接口的设计

    APP架构设计经验谈:接口的设计 原创文章,转载请注明:转载自Keegan小钢并标明原文链接:http://keeganlee.me/post/architecture/20160107微信订阅号:k ...

  5. 从bitmap到布隆过滤器,再到高并发缓存设计策略

    点击关注公众号,Java干货及时送达 作者:that_is_cool blog.csdn.net/that_is_cool/article/details/91346356 前言:怎么能把风马牛不相及 ...

  6. 谈谈Java接口Result设计

    这篇文章酝酿了很久,一直想写,却一直觉得似乎要讲的东西有点杂,又不是很容易讲清楚,又怕争议的地方很多,就一拖再拖.但是,每次看到不少遇到跟这个设计相关导致的问题,又忍不住跟人讨论,但又很难一次说清楚, ...

  7. 面向对象之内置方法(简单)、组合。以及接口归一化设计与抽象类

    一.内置方法 一 isinstance(obj,cls)和issubclass(sub,super) isinstance(obj,cls)检查是否obj是否是类 cls 的对象 class Foo( ...

  8. 组件接口(API)设计指南-文件夹

    组件接口(API)设计指南-文件夹 组件接口(API)设计指南[1]-要考虑的问题 组件接口(API)设计指南[2]-类接口(class interface) 组件接口(API)设计指南[3]-托付( ...

  9. 【案例分析】分布式系统的接口幂等性设计!

    概念 幂等性, Idempotence, 这个词来源自数学领域, 百科 上一元运算的幂等性解释如下:设 f 为一由 {x} 映射至 {x} 的一元运算, 则 f 为幂等的, 当对于所有在 {x} 内的 ...

最新文章

  1. GPT-3 不够 Open,BigScience 构建开放语言模型,规模小 16 倍
  2. Nature:深大李猛组揭示阿斯加德古菌新门(悟空古菌)及其与真核生物的关系
  3. jpa SessionFactory事物失效
  4. 使用 Spring Boot CLI 运行第一个Spring boot程序
  5. SAP Kyma的Lambda Function describe命令输出
  6. python sort 多级排序_Python使用sort和class实现的多级排序功能示例
  7. 关于CDN的部署思路和技术架构
  8. Mock 框架 Moq 的使用
  9. VS2005-此计算机下已安装了试用版,必须先卸载以前安装的试用版后才能安装另一个试用版
  10. MSN Messenger
  11. 【转】Skyline软件介绍
  12. java种子_MC速通各类种子(java版,更新至6.26)
  13. 一本通1325:【例7.4】 循环比赛日程表
  14. android 通知 广告,解决三星/小米等Android手机通知栏推送广告的问题
  15. criterial查询
  16. ASCII码_字符与数字转换等问题
  17. Monte Carlo tree search 学习
  18. ROS中工作空间和功能包的创建以及发布者Publisher的实现
  19. 观点丨企业需要一个什么样的独立云管平台?
  20. 使用R做方差分析实现多重比较可视化结果

热门文章

  1. java实验册_Java实验报告册Java实验报告册.doc
  2. java csv 追加_如何在Java中添加一个包含CSV数据的列
  3. Mongo 安装、配置、启动 Windows
  4. 发送http和https请求工具类 Json封装数据
  5. 企业实战_17_MyCat水平扩展_跨分片查询_ER分片
  6. Mycat_MySql更新数据库失败 --read-only
  7. 系统机构设计师 - 软件质量属性
  8. java超时结束程序_java本机进程超时
  9. C++多重继承师生类复盘
  10. mysql pt_MySQL慢查询之pt-query-digest分析慢查询日志