zoukankan      html  css  js  c++  java
  • 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() {
            @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。【手机号信息是敏感信息,需要后端从会话信息中获取】。还有,不要问我为啥要花时间设计这个东西,第一领导同意了,第二开发周期理想,第三我喜欢!!!

  • 相关阅读:
    vuex状态管理
    vue3.0创建一个项目
    Django + Vue
    Django部署
    django简单使用
    Django模型
    Centos7编译openjdk8源码
    深入了解final
    深入了解java值传递
    java自带的Logger日志系统
  • 原文地址:https://www.cnblogs.com/hujunzheng/p/10250403.html
Copyright © 2011-2022 走看看