一、前言

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

二、接口中转器整体设计

  

三、接口中转器核心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。【手机号信息是敏感信息,需要后端从会话信息中获取】。还有,不要问我为啥要花时间设计这个东西,第一领导同意了,第二开发周期理想,第三我喜欢!!!

SpringMvc接口中转设计(策略+模板方法)的更多相关文章

  1. 【转】App架构设计经验谈:接口的设计

    App架构设计经验谈:接口的设计 App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉. 安全机制的设计 现在,大部分App的接口都采用REST ...

  2. 防盗链&CSRF&API接口幂等性设计

    防盗链技术 CSRF(模拟请求) 分析防止伪造Token请求攻击 互联网API接口幂等性设计 忘记密码漏洞分析 1.Http请求防盗链 什么是防盗链 比如A网站有一张图片,被B网站直接通过img标签属 ...

  3. 聊聊高并发(三十九)解析java.util.concurrent各个组件(十五) 理解ExecutorService接口的设计

    上一篇讲了Executor接口的设计,目的是将任务的运行和任务的提交解耦.能够隐藏任务的运行策略.这篇说说ExecutorService接口.它扩展了Executor接口,对Executor的生命周期 ...

  4. App版本更新接口的设计

    前段时间公司业务调整,新开了新的移动端的项目,所以和朋友聊到了“版本号”和“版本更新所需的数据表设计”. 一般来讲大部分的软件版本号分3段,比如 A.B.C A 表示大版本号,一般当软件整体重写,或出 ...

  5. Dubbo入门到精通学习笔记(十):dubbo服务集群 、Dubbo分布式服务子系统的划分、Dubbo服务接口的设计原则

    文章目录 dubbo服务集群 Dubbo服务集群部署 Dubbo服务集群容错配置--集群容错模式 1.Failover Cluster 失败自动切换,当出现失败,重试其它服务器.`(缺省) 通常用于读 ...

  6. 用Map+函数式接口来实现策略模式

    用Map+函数式接口来实现策略模式 目前在魔都,贝壳找房是我的雇主,平时关注一些 java 领域相关的技术,希望你们能在这篇文章中找到些有用的东西.个人水平有限,如果文章有错误还请指出,在留言区一起交 ...

  7. Java设计原则:面向接口的设计

    前言:在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的.在这种情况下,各个对象内部是如何实现对系统设计人员来说就不那么重要:而各个对象之间的协作关系则成为系统设计的关键.小到不同 ...

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

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

  9. 推荐一款接口 API 设计神器!

    今天栈长给大家推荐一款接口 API 设计神器,传说中的,牛逼哄洪的 Swagger,它到底是什么?今天为大家揭开谜底! Swagger是什么? 官网:https://swagger.io/ Swagg ...

随机推荐

  1. Confluence 6 恢复一个空间

    你可以导出一个空间 – 包括页面,评论和附件到一个压缩的 XML 文件中,可选的你可以在 XML 文件中包括所有空间使用的附件.希望导入空间到其他的 Confluence 站点中,请按照下面的方法进行 ...

  2. vue 树状图数据的循环 递归循环

    在main.js中注册一个子组件 在父组件中引用 树状图的数据格式 绑定一个数据传入子组件,子组件props接收数据 子组件中循环调用组件,就实现了递归循环

  3. spark中RDD的转化操作和行动操作

    本文主要是讲解spark里RDD的基础操作.RDD是spark特有的数据模型,谈到RDD就会提到什么弹性分布式数据集,什么有向无环图,本文暂时不去展开这些高深概念,在阅读本文时候,大家可以就把RDD当 ...

  4. bzoj2200拓扑排序+最短路+联通块

    自己写的不知道哪里wa了,明明和网上的代码差不多.,. /* 给定一张图,有的边是无向边,有的是有向边,有向边不会出现在环中,且有可能是负权值 现在给定起点s,求出s到其余所有点的最短路长度 任何存在 ...

  5. python+selenium十四:xpath和contains模糊匹配

    xpath可以以标签定位,也可以@任意属性: 如:以input标签定位:driver.find_element_by_xpath("//input[@id='kw']") 如:@t ...

  6. Nginx详解十:Nginx场景实践篇之Nginx静态资源场景配置

    一.静态资源WEB服务 1.静态资源类型:非服务器动态运行生成的文件 2.静态资源服务场景-CDN 假设静态资源存储中心在云南,用户在北京去请求一个文件,那么就会造成一个传输的延时,而如果Nginx同 ...

  7. Linux文本分析命令awk的妙用

    基本用法 awk是一个强大的文本分析工具,简单来说awk就是把文件逐行读入,(空格,制表符)为默认分隔符将每行切片,切开的部分再进行各种分析处理 awk命令格式如下 awk [-F field-sep ...

  8. Python中的xxx+=xxx和xxx=xxx+xxx一些区别及执行过程

    预知小知识: Python中的变量与其他语言稍有差异,如a = 10并不是直接在内存中创建一个变量a其值为10,而是在内存中创建一个a这个a指向这个10,在Python中所有牵扯到等号的均不是值赋值, ...

  9. PHP编译安装时常见错误解决办法

    转载自:http://www.bkjia.com/PHPjc/1008013.html This article is post on https://coderwall.com/p/ggmpfa c ...

  10. Jmeter BeanShell PostProcessor提取json数据

    需求:提取sample返回json数据中所有name字段值,返回的json格式如下: {“body”:{“apps”:[{“name”:”111”},{“name”:”222”}]}} jmeter中 ...