微服务中如何使用RestTemplate优雅调用API(拦截器、异常处理、消息转换)

关注我,可以获取最新知识、经典面试题以及微服务技术分享

  在微服务中,rest服务互相调用是很普遍的,我们该如何优雅地调用,其实在Spring框架使用RestTemplate类可以优雅地进行rest服务互相调用,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接,操作使用简便,还可以自定义RestTemplate所需的模式。其中:

  • RestTemplate默认使用HttpMessageConverter实例将HTTP消息转换成POJO或者从POJO转换成HTTP消息。默认情况下会注册主mime类型的转换器,但也可以通过setMessageConverters注册自定义转换器。
  • RestTemplate使用了默认的DefaultResponseErrorHandler,对40X Bad Request或50X internal异常error等错误信息捕捉。
  • RestTemplate还可以使用拦截器interceptor,进行对请求链接跟踪,以及统一head的设置。

其中,RestTemplate还定义了很多的REST资源交互的方法,其中的大多数都对应于HTTP的方法,如下:

方法 解析
delete() 在特定的URL上对资源执行HTTP DELETE操作
exchange() 在URL上执行特定的HTTP方法,返回包含对象的ResponseEntity
execute() 在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象
getForEntity() 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象
getForObject() 发送一个HTTP GET请求,返回的请求体将映射为一个对象
postForEntity() POST 数据到一个URL,返回包含一个对象的ResponseEntity
postForObject() POST 数据到一个URL,返回根据响应体匹配形成的对象
headForHeaders() 发送HTTP HEAD请求,返回包含特定资源URL的HTTP头
optionsForAllow() 发送HTTP OPTIONS请求,返回对特定URL的Allow头信息
postForLocation() POST 数据到一个URL,返回新创建资源的URL
put() PUT 资源到特定的URL

1. RestTemplate源码

1.1 默认调用链路

restTemplate进行API调用时,默认调用链:

###########1.使用createRequest创建请求########
resttemplate->execute()->doExecute()
HttpAccessor->createRequest()
//获取拦截器Interceptor,InterceptingClientHttpRequestFactory,SimpleClientHttpRequestFactory
InterceptingHttpAccessor->getRequestFactory() 
//获取默认的SimpleBufferingClientHttpRequest
SimpleClientHttpRequestFactory->createRequest()

#######2.获取响应response进行处理###########
AbstractClientHttpRequest->execute()->executeInternal()
AbstractBufferingClientHttpRequest->executeInternal()

###########3.异常处理#####################
resttemplate->handleResponse()

##########4.响应消息体封装为java对象#######
HttpMessageConverterExtractor->extractData()

1.2 restTemplate->doExecute()

在默认调用链中,restTemplate 进行API调用都会调用 doExecute 方法,此方法主要可以进行如下步骤:

1)使用createRequest创建请求,获取响应
2)判断响应是否异常,处理异常
3)将响应消息体封装为java对象

@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

    Assert.notNull(url, "URI is required");
    Assert.notNull(method, "HttpMethod is required");
    ClientHttpResponse response = null;
    try {
        //使用createRequest创建请求
        ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null) {
            requestCallback.doWithRequest(request);
        }
        //获取响应response进行处理
        response = request.execute();
        //异常处理
        handleResponse(url, method, response);
        //响应消息体封装为java对象
        return (responseExtractor != null ? responseExtractor.extractData(response) : null);
    }catch (IOException ex) {
        String resource = url.toString();
        String query = url.getRawQuery();
        resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
        throw new ResourceAccessException("I/O error on " + method.name() +
                " request for \"" + resource + "\": " + ex.getMessage(), ex);
    }finally {
        if (response != null) {
            response.close();
        }
    }
}

1.3 InterceptingHttpAccessor->getRequestFactory()

在默认调用链中,InterceptingHttpAccessor的getRequestFactory()方法中,如果没有设置interceptor拦截器,就返回默认的SimpleClientHttpRequestFactory,反之,返回InterceptingClientHttpRequestFactoryrequestFactory,可以通过resttemplate.setInterceptors设置自定义拦截器interceptor

//Return the request factory that this accessor uses for obtaining client request handles.
public ClientHttpRequestFactory getRequestFactory() {
        //获取拦截器interceptor(自定义的)
        List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
        if (!CollectionUtils.isEmpty(interceptors)) {
            ClientHttpRequestFactory factory = this.interceptingRequestFactory;
            if (factory == null) {
                factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
                this.interceptingRequestFactory = factory;
            }
            return factory;
        }
        else {
            return super.getRequestFactory();
        }
    }

然后再调用SimpleClientHttpRequestFactory的createRequest创建连接:

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
    prepareConnection(connection, httpMethod.name());

    if (this.bufferRequestBody) {
        return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
    }
    else {
        return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
    }
}

1.4 resttemplate->handleResponse()

在默认调用链中,resttemplate的handleResponse,响应处理,包括异常处理,而且异常处理可以通过调用setErrorHandler方法设置自定义的ErrorHandler,实现对请求响应异常的判别和处理。自定义的ErrorHandler需实现ResponseErrorHandler接口,同时Spring boot也提供了默认实现DefaultResponseErrorHandler,因此也可以通过继承该类来实现自己的ErrorHandler

DefaultResponseErrorHandler默认对40X Bad Request或50X internal异常error等错误信息捕捉。如果想捕捉服务本身抛出的异常信息,需要通过自行实现RestTemplateErrorHandler

ResponseErrorHandler errorHandler = getErrorHandler();
               //判断响应是否有异常
    boolean hasError = errorHandler.hasError(response);
    if (logger.isDebugEnabled()) {
        try {
            int code = response.getRawStatusCode();
            HttpStatus status = HttpStatus.resolve(code);
            logger.debug("Response " + (status != null ? status : code));
        }catch (IOException ex) {
            // ignore
        }
    }
    //有异常进行异常处理
    if (hasError) {
        errorHandler.handleError(url, method, response);
    }
}

1.5 HttpMessageConverterExtractor->extractData()

在默认调用链中, HttpMessageConverterExtractorextractData中进行响应消息体封装为java对象,就需要使用message转换器,可以通过追加的方式增加自定义的messageConverter:先获取现有的messageConverter,再将自定义的messageConverter添加进去。

根据restTemplatesetMessageConverters的源码可得,使用追加的方式可防止原有的messageConverter丢失,源码:

public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        //检验
        validateConverters(messageConverters);
        // Take getMessageConverters() List as-is when passed in here
        if (this.messageConverters != messageConverters) {
            //先清除原有的messageConverter
            this.messageConverters.clear();
            //后加载重新定义的messageConverter
            this.messageConverters.addAll(messageConverters);
        }
    }

HttpMessageConverterExtractor的extractData源码:

MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
    if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
        return null;
    }
    //获取到response的ContentType类型
    MediaType contentType = getContentType(responseWrapper);

    try {
        //依次循环messageConverter进行判断是否符合转换条件,进行转换java对象
        for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
        //会根据设置的返回类型responseType和contentType参数进行匹配,选择合适的MessageConverter
            if (messageConverter instanceof GenericHttpMessageConverter) {
                GenericHttpMessageConverter<?> genericMessageConverter =
                        (GenericHttpMessageConverter<?>) messageConverter;
                if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
                    if (logger.isDebugEnabled()) {
                        ResolvableType resolvableType = ResolvableType.forType(this.responseType);
                        logger.debug("Reading to [" + resolvableType + "]");
                    }
                    return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
                }
            }
            if (this.responseClass != null) {
                if (messageConverter.canRead(this.responseClass, contentType)) {
                    if (logger.isDebugEnabled()) {
                        String className = this.responseClass.getName();
                        logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
                    }
                    return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
                }
            }
        }
    }
    .....
}

1.6 contentType与messageConverter之间的关系

HttpMessageConverterExtractorextractData方法中看出,会根据contentTyperesponseClass选择messageConverter是否可读、消息转换。关系如下:

类名 支持的JavaType 支持的MediaType
ByteArrayHttpMessageConverter byte[] application/octet-stream, */*
StringHttpMessageConverter String text/plain, */*
ResourceHttpMessageConverter Resource */*
SourceHttpMessageConverter Source application/xml, text/xml, application/*+xml
AllEncompassingFormHttpMessageConverter Map<K, List<?>> application/x-www-form-urlencoded, multipart/form-data
MappingJackson2HttpMessageConverter Object application/json, application/*+json
Jaxb2RootElementHttpMessageConverter Object application/xml, text/xml, application/*+xml
JavaSerializationConverter Serializable x-java-serialization;charset=UTF-8
FastJsonHttpMessageConverter Object */*

2. springboot集成RestTemplate

  根据上述源码的分析学习,可以轻松,简单地在项目进行对RestTemplate进行优雅地使用,比如增加自定义的异常处理、MessageConverter以及拦截器interceptor。本文使用示例demo,详情请查看接下来的内容。

2.1. 导入依赖:(RestTemplate集成在Web Start中)

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.10</version>
  <scope>provided</scope>
</dependency>

2.2. RestTemplat配置:

  • 使用ClientHttpRequestFactory属性配置RestTemplat参数,比如ConnectTimeoutReadTimeout;
  • 增加自定义的interceptor拦截器和异常处理;
  • 追加message转换器;
  • 配置自定义的异常处理.


 @Configuration
public class RestTemplateConfig {

    @Value("${resttemplate.connection.timeout}")
    private int restTemplateConnectionTimeout;
    @Value("${resttemplate.read.timeout}")
    private int restTemplateReadTimeout;

    @Bean
    //@LoadBalanced
    public RestTemplate restTemplate( ClientHttpRequestFactory simleClientHttpRequestFactory) {
        RestTemplate restTemplate = new RestTemplate();
        //配置自定义的message转换器
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        messageConverters.add(new CustomMappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        //配置自定义的interceptor拦截器
        List<ClientHttpRequestInterceptor> interceptors=new ArrayList<ClientHttpRequestInterceptor>();
        interceptors.add(new HeadClientHttpRequestInterceptor());
        interceptors.add(new TrackLogClientHttpRequestInterceptor());
        restTemplate.setInterceptors(interceptors);
        //配置自定义的异常处理
        restTemplate.setErrorHandler(new CustomResponseErrorHandler());
        restTemplate.setRequestFactory(simleClientHttpRequestFactory);

        return restTemplate;
    }

    @Bean
    public ClientHttpRequestFactory simleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory reqFactory= new SimpleClientHttpRequestFactory();
        reqFactory.setConnectTimeout(restTemplateConnectionTimeout);
        reqFactory.setReadTimeout(restTemplateReadTimeout);
        return reqFactory;
    }
}

2.3. 组件(自定义异常处理、interceptor拦截器、message转化器)

自定义interceptor拦截器,实现ClientHttpRequestInterceptor接口

  • 自定义TrackLogClientHttpRequestInterceptor,记录resttemplaterequestresponse信息,可进行追踪分析;
  • 自定义HeadClientHttpRequestInterceptor,设置请求头的参数。API发送各种请求,很多请求都需要用到相似或者相同的Http Header。如果在每次请求之前都把Header填入HttpEntity/RequestEntity,这样的代码会显得十分冗余,可以在拦截器统一设置。

TrackLogClientHttpRequestInterceptor:

/**
 * @Auther: ccww
 * @Date: 2019/10/25 22:48,记录resttemplate访问信息
 * @Description:   记录resttemplate访问信息
 */
@Slf4j
public class TrackLogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        trackRequest(request,body);
        ClientHttpResponse httpResponse = execution.execute(request, body);
        trackResponse(httpResponse);
        return httpResponse;
    }

    private void trackResponse(ClientHttpResponse httpResponse)throws IOException {
        log.info("============================response begin==========================================");
        log.info("Status code  : {}", httpResponse.getStatusCode());
        log.info("Status text  : {}", httpResponse.getStatusText());
        log.info("Headers      : {}", httpResponse.getHeaders());
        log.info("=======================response end=================================================");
    }

    private void trackRequest(HttpRequest request, byte[] body)throws UnsupportedEncodingException {
        log.info("======= request begin ========");
        log.info("uri : {}", request.getURI());
        log.info("method : {}", request.getMethod());
        log.info("headers : {}", request.getHeaders());
        log.info("request body : {}", new String(body, "UTF-8"));
        log.info("======= request end ========");
    }
}

HeadClientHttpRequestInterceptor:

@Slf4j
public class HeadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
       log.info("#####head handle########");
        HttpHeaders headers = httpRequest.getHeaders();
        headers.add("Accept", "application/json");
        headers.add("Accept-Encoding", "gzip");
        headers.add("Content-Encoding", "UTF-8");
        headers.add("Content-Type", "application/json; charset=UTF-8");
        ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes);
        HttpHeaders headersResponse = response.getHeaders();
        headersResponse.add("Accept", "application/json");
        return  response;
    }
}


自定义异常处理,可继承DefaultResponseErrorHandler或者实现ResponseErrorHandler接口:

  • 实现自定义ErrorHandler的思路是根据响应消息体进行相应的异常处理策略,对于其他异常情况由父类DefaultResponseErrorHandler来进行处理。
  • 自定义CustomResponseErrorHandler进行30x异常处理

CustomResponseErrorHandler:

/**
 * @Auther: Ccww
 * @Date: 2019/10/28 17:00
 * @Description:  30X的异常处理
 */
@Slf4j
public class CustomResponseErrorHandler extends DefaultResponseErrorHandler {
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        HttpStatus statusCode = response.getStatusCode();
        if(statusCode.is3xxRedirection()){
            return true;
        }
        return super.hasError(response);
    }

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        HttpStatus statusCode = response.getStatusCode();
        if(statusCode.is3xxRedirection()){
            log.info("########30X错误,需要重定向!##########");
            return;
        }
        super.handleError(response);
    }

}


自定义message转化器

/**
 * @Auther: Ccww
 * @Date: 2019/10/29 21:15
 * @Description: 将Content-Type:"text/html"转换为Map类型格式
 */
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
    public CustomMappingJackson2HttpMessageConverter() {
        List<MediaType> mediaTypes = new ArrayList<MediaType>();
        mediaTypes.add(MediaType.TEXT_PLAIN);
        mediaTypes.add(MediaType.TEXT_HTML);  //加入text/html类型的支持
        setSupportedMediaTypes(mediaTypes);// tag6
    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容