SpringMvc接口中转设计(策略+模板方法)

一、前言

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

二、接口中转器整体设计

  

 

三、接口中转器核心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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/531204.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringSecurity整合JWT

一、前言 最近负责支付宝小程序后端项目设计&#xff0c;这里主要分享一下用户会话、接口鉴权的设计。参考过微信小程序后端的设计&#xff0c;会话需要依靠redis。相关的开发人员和我说依靠Redis并不是很靠谱&#xff0c;redis在业务高峰期不稳定&#xff0c;容易出现问题&…

Springboot定时任务原理及如何动态创建定时任务

一、前言 上周工作遇到了一个需求&#xff0c;同步多个省份销号数据&#xff0c;解绑微信粉丝。分省定时将销号数据放到SFTP服务器上&#xff0c;我需要开发定时任务去解析文件。因为是多省份&#xff0c;服务器、文件名规则、数据规则都不一定&#xff0c;所以要做成可配置是有…

转载:ThreadPoolExecutor 源码阅读

前言 之前研究了一下如何使用ScheduledThreadPoolExecutor动态创建定时任务(Springboot定时任务原理及如何动态创建定时任务)&#xff0c;简单了解了ScheduledThreadPoolExecutor相关源码。今天看了同学写的ThreadPoolExecutor 的源码解读&#xff0c;甚是NB&#xff0c;必须转…

Spring BPP中优雅的创建动态代理Bean

一、前言 本文章所讲并没有基于Aspectj&#xff0c;而是直接通过Cglib以及ProxyFactoryBean去创建代理Bean。通过下面的例子&#xff0c;可以看出Cglib方式创建的代理Bean和ProxyFactoryBean创建的代理Bean的区别。 二、基本测试代码 测试实体类&#xff0c;在BPP中创建BppTest…

使用pdfBox实现pdf转图片,解决中文方块乱码等问题

一、引入依赖 <dependency><groupId>org.apache.pdfbox</groupId><artifactId>fontbox</artifactId><version>2.0.13</version> </dependency> <dependency><groupId>org.apache.pdfbox</groupId><artif…

Spring异步调用原理及SpringAop拦截器链原理

一、Spring异步调用底层原理 开启异步调用只需一个注解EnableAsync Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Import(AsyncConfigurationSelector.class) public interface EnableAsync {/*** Indicate the async annotation type to be detec…

线程池优化之充分利用线程池资源

一、前言 最近做了电子发票的需求&#xff0c;分省开票接口和发票下载接口都有一定的延迟。为了完成开票后自动将发票插入用户微信卡包&#xff0c;目前的解决方案是利用线程池&#xff0c;将开票后插入卡包的任务&#xff08;轮询分省发票接口&#xff0c;直到获取到发票相关信…

Spring MVC源码——Root WebApplicationContext

Spring MVC源码——Root WebApplicationContext 打算开始读一些框架的源码,先拿 Spring MVC 练练手,欢迎点击这里访问我的源码注释, SpringMVC官方文档一开始就给出了这样的两段示例: WebApplicationInitializer示例: public class MyWebApplicationInitializer implements Web…

Spring MVC源码——Servlet WebApplicationContext

上一篇笔记(Spring MVC源码——Root WebApplicationContext)中记录了下 Root WebApplicationContext 的初始化代码.这一篇来看 Servlet WebApplicationContext 的初始化代码 DispatcherServlet 是另一个需要在 web.xml 中配置的类, Servlet WebApplicationContext 就由它来创建…

Springboot源码——应用程序上下文分析

前两篇(Spring MVC源码——Root WebApplicationContext 和 Spring MVC源码——Servlet WebApplicationContext)讲述了springmvc项目创建上下文的过程&#xff0c;这一篇带大家了解一下springboot项目创建上下文的过程。 SpringApplication引导类 SpringApplication类用于启动或…

基于zookeeper实现分布式配置中心(一)

最近在学习zookeeper&#xff0c;发现zk真的是一个优秀的中间件。在分布式环境下&#xff0c;可以高效解决数据管理问题。在学习的过程中&#xff0c;要深入zk的工作原理&#xff0c;并根据其特性做一些简单的分布式环境下数据管理工具。本文首先对zk的工作原理和相关概念做一下…

基于zookeeper实现分布式配置中心(二)

上一篇&#xff08;基于zookeeper实现分布式配置中心&#xff08;一&#xff09;&#xff09;讲述了zookeeper相关概念和工作原理。接下来根据zookeeper的特性&#xff0c;简单实现一个分布式配置中心。 配置中心的优势 1、各环境配置集中管理。 2、配置更改&#xff0c;实时推…

Redis分布式锁实战

背景 目前开发过程中&#xff0c;按照公司规范&#xff0c;需要依赖框架中的缓存组件。不得不说&#xff0c;做组件的大牛对CRUD操作的封装&#xff0c;连接池、缓存路由、缓存安全性的管控都处理的无可挑剔。但是有一个小问题&#xff0c;该组件没有对分布式锁做实现&#xff…

基于RobotFramework实现自动化测试

Java robotframework seleniumlibrary 使用Robot Framework Maven Plugin&#xff08;http://robotframework.org/MavenPlugin/&#xff09;执行自动化测试chromedriver下载&#xff1a; http://chromedriver.storage.googleapis.com/index.htmlchromedriver和chrome版本对应…

Springboot国际化信息(i18n)解析

国际化信息理解 国际化信息也称为本地化信息 。 Java 通过 java.util.Locale 类来表示本地化对象&#xff0c;它通过 “语言类型” 和 “国家/地区” 来创建一个确定的本地化对象 。举个例子吧&#xff0c;比如在发送一个具体的请求的时候&#xff0c;在header中设置一个键值对…

看了就知道为什么别人C语言学习效率那么高了

谈及C语言&#xff0c;我想C语言功能强大都应该知道、应用广泛&#xff0c;一旦掌握了后&#xff0c;你就可以理直气壮地对他人说“我是电脑高手&#xff01;”&#xff0c;而且以后若是再自学其他语言就显得轻而易举了。忧虑的是&#xff0c;C语言般博大精深&#xff0c;太难学…

C语言一看就能上手的干货!你确定你不来看吗?

本地环境设置 如果您想要设置 C 语言环境&#xff0c;您需要确保电脑上有以下两款可用的软件&#xff0c;文本编辑器和 C 编译器。 文本编辑器 这将用于输入您的程序。文本编辑器包括 Windows Notepad、OS Edit command、Brief、Epsilon、EMACS 和 vim/vi。文本编辑器的名称…

C语言爆炸干货,小白你还不来看看嘛!

①&#xff1a;数据类型 int(整型)&#xff0c;short int(短整型)&#xff0c;long int(长整型)&#xff0c; char(字符型)&#xff0c;float&#xff08;单精度浮点型&#xff09; double&#xff08;双精度浮点型&#xff09; C语言编程交流群815393895 ②&#xff1a;逻…

10万码农五年的C语言笔记!你现在知道别人为什么这么优秀了吗?

c语言对许多同学来说确实是一门比较难学的课程&#xff0c;不仅抽象&#xff0c;而且繁琐&#xff0c;但这又是一门不得不学的课程。前两节可能还有兴致听一听&#xff0c;然而&#xff0c;再过几节课就是一脸蒙比。凭空要想出一道题的算法和程序&#xff0c;根本无从下手。 所…

C语言从来都没有过时,你大爷终究是你大爷

直到今天&#xff0c;有人在喊C语言过时的语言&#xff0c;还有什么值得学习的&#xff0c;现在看Python&#xff0c;PHP等语言现在都很容易用&#xff0c;谁还在学习老C语言&#xff0c;其实这是真的吗&#xff1f;作者下载了两种语言的源代码作为下载器。由于空间的限制&…