欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】

程序员文章站 2022-05-15 10:41:33
一个可以沉迷于技术的程序猿,wx加入加入技术群:fsx641385712 ......

每篇一句

人圆月圆心圆,人和家和国和---中秋节快乐

前言

在阅读本篇之前,建议先阅读效果更佳。resttemplate是spring提供的用于访问rest服务的客户端工具,它提供了多种便捷访问远程http服务的方法,能够大大提高客户端的编写效率
弱弱呼吁一句:对于那些在spring环境下还在使用httpclient(或其它client)的同学,今儿看完本文后,建议切换到resttemplate (有特殊需求的当然除外喽~)。

resttemplate简化了与http服务的通信,程序代码可以给它提供url,并提取结果。它默认使用的jdk 的httpurlconnection进行通信,然而我们是可以通过resttemplate.setrequestfactory切换到不同的http源:如apache httpcomponentsnettyokhttp等等。

restoperations

指定一组基本restful操作的接口,定义了基本的rest操作集合,它的唯一实现是resttemplate;不直接使用,但这是增强可测试性的一个有用选项,因为它很容易被模拟或存根(后面这句话请好好理解)。

可以对比参照redisoperations,它的实现类也只有redistemplate一个。他俩都采用了设计模式中的模板模式

方法们:

由于此接口里的方法实在太多了(40+个),因此我按照http标准进行分类如下表格:

// @since 3.0
public enum httpmethod {
    get, head, post, put, patch, delete, options, trace;
    ...
}
httpmethod 方法
get RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
head RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
post RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
put RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
patch RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
delete RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
options RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
trace
any(执行任何http方法) RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】

观察发现,虽然方法众多但有很强的规律可循。每个方法都有三种重载实现:2种的url参数为字符串,一种uri参数,所以掌握规律后再使用,就不用害怕它的多而不知咋使用了。

xxxforobject:返回响应体(也就直接是body体力的内容) (t)
xxxforentity:返回的相应行、响应头、响应码、响应体等等 (responseentity
xxxforlocation:提交成功之后,返回新资源的uri。这个只需要服务提供者返回一个 uri 即可,该 uri 表示新资源的位置,可谓非常轻量。 (uri)

注意:使用字符串类型的url默认会对url进行转义,如http://example.com/hotel list在执行时会转义为http://example.com/hotel%20list,隐式的转义这样是没有问题的。但如果你自己已经转义过了,那就不ok了。
若不想要这种隐式的转义,建议使用uri(uri uri = uricomponents.touri())来构造。

==resttemplate中post请求的三种方式==

post请求代表新建/创建一个资源,所以它是有返回值的。因为它的使用最为复杂,因此本文以它为例进行讲解。

你如果熟练使用过浏览器的开发者工具调试过,你肯定知道post请求它传参是有两种方式的:

  1. form data方式:我们用from表单提交的方式就是它;使用ajax(注意:这里指的是jquery的ajax,而不是源生js的)默认的提交方式也是它~
    RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
  2. request payload方式:多部分方式/json方式
    RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
    RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】

这两种方式是通过content-type来区别的:若是application/x-www-form-urlencoded那就是formdata方式;若是application/json或者multipart/form-data等方式那就是request payload方式

jquery在执行post请求时,默认会给你设置content-typeapplication/x-www-form-urlencoded,所以服务器能够正确解析。
若使用js原生的ajax,如果不显示的设置content-type,那么默认是text/plain,这时服务器就不知道怎么解析数据了,所以才只能通过获取原始数据流的方式来进行解析请求数据。(相信没人这么干吧~)

exchange和execute方法:

exchange方法:更通用的请求方法。它入参必须接受一个requestentity,从而可以设置请求的路径、头等等信息,最终全都是返回一个responseentity(可以发送get、post、put等所有请求)。
execute方法:最最最底层、通用的请求方法。

requestcallback:用于操作请求头和body,在请求发出执行;responseextractor:解析/提取http响应的数据,而且不需要担心异常和资源的关闭
requestcallback.dowithrequest(clienthttprequest)说白了就是拿到clienthttprequest后对他进行继续处理~
resttemplateacceptheaderrequestcallback、httpentitycallback这些方法可以设置它~
---

httpaccessor、interceptinghttpaccessor

这两个抽象类不容忽视,hystrixcommand和ribbon的逻辑都和它有关系(拦截器)。
httpaccessor是个抽象基类,它定义要操作clienthttprequestfactory的公共属性,它一般不直接使用。

// @since 3.0
public abstract class httpaccessor {
    
    // resttemplate默认使用的客户端工厂:基于源生jdk
    private clienthttprequestfactory requestfactory = new simpleclienthttprequestfactory();

    // 若要切换成三方库的底层组件,设置此方法便可
    public void setrequestfactory(clienthttprequestfactory requestfactory) {
        this.requestfactory = requestfactory;
    }
    ... // get方法
    
    // 供给子类非常方便的拿到一个clienthttprequest
    protected clienthttprequest createrequest(uri url, httpmethod method) throws ioexception {
        clienthttprequest request = getrequestfactory().createrequest(url, method);
        return request;
    }   
}

它的子类是:interceptinghttpaccessor,也还是个抽象实现,主要是管理起了请求的拦截器们:clienthttprequestinterceptor

interceptinghttpaccessor

// @since 3.0
// @see interceptingclienthttprequestfactory
public abstract class interceptinghttpaccessor extends httpaccessor {

    // 装载需要作用在resttemplate上的拦截器们~~~
    private final list<clienthttprequestinterceptor> interceptors = new arraylist<>();
    @nullable
    private volatile clienthttprequestfactory interceptingrequestfactory;

    // 这里语意是set,所以是完全的替换掉(支持ordered排序哦~~~)
    public void setinterceptors(list<clienthttprequestinterceptor> interceptors) {
        if (this.interceptors != interceptors) {
            this.interceptors.clear();
            this.interceptors.addall(interceptors);
            annotationawareordercomparator.sort(this.interceptors);
        }
    }

    // 复写了父类的这个方法很有意思
    // 意思为:若你调用者手动set进来了,那就以调用者设置的工厂为准 否则使用的是interceptingclienthttprequestfactory
    @override
    public void setrequestfactory(clienthttprequestfactory requestfactory) {
        super.setrequestfactory(requestfactory);
        this.interceptingrequestfactory = null;
    }

    // 若配置了拦截器,那么默认就使用interceptingclienthttprequestfactory,而不再是simpleclienthttprequestfactory了~~~
    @override
    public clienthttprequestfactory getrequestfactory() {
        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();
        }
    }
}

interceptinghttpaccessor最主要的处理逻辑为:若发现调用者设置了请求拦截器,那么它创建的工厂是具有拦截功能的interceptingclienthttprequestfactory,否则就是默认的simpleclienthttprequestfactory

interceptingclienthttprequestfactory工厂它产生的clienthttprequestinterceptingclienthttprequest,然而它就会执行拦截器的拦截方法喽:nextinterceptor.intercept(request, body, this)

提问:如有配置有多个请求拦截器,都会执行吗?
解答:这个千万不要犯迷糊和轻易下结论:以为没有迭代它(for循环)而只是iterator.next()就以为若有多个就只会执行一个,那就大错特错了。这里实际是形成了一个执行链条,只要拦截器的intercept方法内最终还调用执行器的intercept()方法,那么拦截器链就会一直执行下去。其根本缘由是第三个参数传入的是this,至始至终都是同一个执行器(this=interceptingrequestexecution

---


==resttemplate==

resttemplate采用同步方式执行 http 请求的类,底层默认使用jdk原生 httpurlconnection api。它实现了接口restoperations,提供了非常多的模版方法(重载方法)让开发者能更简单地发送 http 请求。

需要注意的是,resttemplatespring 3.0就有了,但在spring5.0后,spring官方是推荐使用org.springframework.web.reactive.function.client.webclient替代它,特别是对于异步的场景。

resttemplate因为使用极其广泛,so即使到了spring 5.0,官方只是建议替代,但并没有标注@deprecated,因此至少目前你还可以想咋用就咋用吧。
但是asyncresttemplate是明确标注了@deprecated,强烈建议使用org.springframework.web.reactive.function.client.webclient去代替,所以在5.0后不建议再使用它了~。

当然还需要说明一点:若你的项目中没有使用到webflux的技术栈来处理请求,那么也没必要说为了使用而使用,所以没必要专门为了它而导包(个人建议)~

// @since 3.0
public class resttemplate extends interceptinghttpaccessor implements restoperations {
    // 去classpath探测  是否有这些消息转换器相关的jar~
    // 一般情况下我们都会导jackson2present~~~
    private static boolean romepresent;
    private static final boolean jaxb2present;
    private static final boolean jackson2present;
    private static final boolean jackson2xmlpresent;
    private static final boolean jackson2smilepresent;
    private static final boolean jackson2cborpresent;
    private static final boolean gsonpresent;
    private static final boolean jsonbpresent;
    ...
    
    // 下面四个变量很重要:

    // 消息转换器们(显然对json格式默认是支持得最好的)
    private final list<httpmessageconverter<?>> messageconverters = new arraylist<>();
    // 默认的请求异常处理器,spring5.0后其实可以使用它extractingresponseerrorhandler
    // 它能够利用消息换换气提取你的错误内容。并且还支持自定义错误码、错误序列等等~
    private responseerrorhandler errorhandler = new defaultresponseerrorhandler();
    // 用于url的构建
    private uritemplatehandler uritemplatehandler;
    // 默认的返回值提取器~~~~
    private final responseextractor<httpheaders> headersextractor = new headersextractor();

    // 空构造,应该是平时使用得最多的了:一切都使用默认的组件配置resource等等
    public resttemplate() {
        // 这个几个消息转换器是支持的。字节数组、字符串、
        this.messageconverters.add(new bytearrayhttpmessageconverter());
        this.messageconverters.add(new stringhttpmessageconverter());
        this.messageconverters.add(new resourcehttpmessageconverter(false));
        this.messageconverters.add(new sourcehttpmessageconverter<>());
        // 对form表单提交方式的支持
        this.messageconverters.add(new allencompassingformhttpmessageconverter());

        // 接下里便是一些列的判断,若类路径上有才会加进来
        if (jackson2present) {
            this.messageconverters.add(new mappingjackson2httpmessageconverter());
        }
        ...
        // new defaulturibuilderfactory()
        this.uritemplatehandler = inituritemplatehandler();
    }

    // 你懂的,若想用okhttp,也可以在构造时就指定
    public resttemplate(clienthttprequestfactory requestfactory) {
        this();
        setrequestfactory(requestfactory);
    }

    // 若不想用默认的消息转换器,也可以自己指定(其实一般都不这么去干,而是后面自己再add进来)
    public resttemplate(list<httpmessageconverter<?>> messageconverters) {
        assert.notempty(messageconverters, "at least one httpmessageconverter required");
        this.messageconverters.addall(messageconverters);
        this.uritemplatehandler = inituritemplatehandler();
    }
    ... // 省略上面属性的get/set犯法们
}

这部分源码我列出来,都是在对构建一个resttemplate实例的准备工作相关方法,包括对各个相关组件的设置。

接下来更重要的便是它实现的接口方法了,我抽出一些关键点进行描述说明:

resttemplate:

    @override
    @nullable
    public <t> t getforobject(string url, class<t> responsetype, object... urivariables) throws restclientexception {
        //1、new acceptheaderrequestcallback(responsetype)  它能在发送请求的之前这样一件事:
        // request.getheaders().setaccept(allsupportedmediatypes)
        requestcallback requestcallback = acceptheaderrequestcallback(responsetype);
        httpmessageconverterextractor<t> responseextractor = new httpmessageconverterextractor<>(responsetype, getmessageconverters(), logger);

        // 最终调用的是execute方法,此时url是个字符串
        // responseextractor返回值提取器使用的是消息转换器去读取body哒~
        // 返回值就是返回的body本身(不含有返回的响应头等等信息~)
        return execute(url, httpmethod.get, requestcallback, responseextractor, urivariables);
    }

    // 它返回的是responseentity,不会返回null的  最终调用的依旧是execute方法
    // 此时候用的就不是消息转换器的提取器了,而是内部类`responseentityresponseextractor`(底层还是依赖消息转换器)
    // 但是这个提取器,提取出来的可都是responseentity<t>实例~
    @override
    public <t> responseentity<t> getforentity(string url, class<t> responsetype, object... urivariables) throws restclientexception {
        requestcallback requestcallback = acceptheaderrequestcallback(responsetype);
        responseextractor<responseentity<t>> responseextractor = responseentityextractor(responsetype);
        return nonnull(execute(url, httpmethod.get, requestcallback, responseextractor, urivariables));
    }

    // head请求:很简单,使用的提取器就是headersextractor,从返回值里把响应header拿出来即可
    @override
    public httpheaders headforheaders(string url, object... urivariables) throws restclientexception {
        return nonnull(execute(url, httpmethod.head, null, headersextractor(), urivariables));
    }


    // post请求
    @override
    @nullable
    public uri postforlocation(string url, @nullable object request, object... urivariables) throws restclientexception {
        // 1、httpentityrequestcallback  适配:把request适配成一个httpentity
        // 然后执行前,通过消息转换器把头信息、body信息等等都write进去
        requestcallback requestcallback = httpentitycallback(request);
        // 因为需要拿到uri,所以此处使用headersextractor提取器先拿到响应的header即可~~~
        httpheaders headers = execute(url, httpmethod.post, requestcallback, headersextractor(), urivariables);
        return (headers != null ? headers.getlocation() : null);
    }

    // 除了httpentitycallback()不一样,其余和get请求一样
    @override
    @nullable
    public <t> t postforobject(string url, @nullable object request, class<t> responsetype, object... urivariables) throws restclientexception {
        requestcallback requestcallback = httpentitycallback(request, responsetype);
        httpmessageconverterextractor<t> responseextractor =
                new httpmessageconverterextractor<>(responsetype, getmessageconverters(), logger);
        return execute(url, httpmethod.post, requestcallback, responseextractor, urivariables);
    }

    // put请求:因为没有返回值,所以不需要返回值提取器。所以,非常的简单~~~
    @override
    public void put(string url, @nullable object request, object... urivariables) throws restclientexception {
        requestcallback requestcallback = httpentitycallback(request);
        execute(url, httpmethod.put, requestcallback, null, urivariables);
    }

    // delete请求:也是木有返回值的。
    // 并且请注意:delete请求这里可都是不能接收body的,不能给请求设置请求体的
    // (虽然可能底层httpclient支持,但这里不支持,请遵守规范)
    @override
    public void delete(string url, object... urivariables) throws restclientexception {
        execute(url, httpmethod.delete, null, null, urivariables);
    }

    // options请求:和head请求的处理逻辑几乎一样
    @override
    public set<httpmethod> optionsforallow(string url, object... urivariables) throws restclientexception {
        responseextractor<httpheaders> headersextractor = headersextractor();
        httpheaders headers = execute(url, httpmethod.options, null, headersextractor, urivariables);
        return (headers != null ? headers.getallow() : collections.emptyset());
    }

所有方法大体执行逻辑一致,都是和requestcallbackresponseextractor等有关,且最终都是委托给了最为底层的execute()方法去执行。

你是否疑问:它提供的put方法返回值都是void,若我put请求就有返回值肿么办呢?那么接下来就介绍更为通用的一个方法:exchange()

resttemplate:

    @override
    public <t> responseentity<t> exchange(string url, httpmethod method, @nullable httpentity<?> requestentity, class<t> responsetype, map<string, ?> urivariables) throws restclientexception {
        // 把请求体适配为httpentity
        requestcallback requestcallback = httpentitycallback(requestentity, responsetype);
        // 消息提取器使用responseentityresponseextractor
        responseextractor<responseentity<t>> responseextractor = responseentityextractor(responsetype);

        // 从上两个部分就能看到:exchange方法的入参、出参都是非常通用的~~~
        return nonnull(execute(url, method, requestcallback, responseextractor, urivariables));
    }

    // parameterizedtypereference参数化类型,用于处理泛型
    // 上面的responsetype就是个class。这里是个参数化类型~~~~~
    @override
    public <t> responseentity<t> exchange(string url, httpmethod method, @nullable httpentity<?> requestentity, parameterizedtypereference<t> responsetype, object... urivariables) throws restclientexception {

        type type = responsetype.gettype();
        requestcallback requestcallback = httpentitycallback(requestentity, type);
        responseextractor<responseentity<t>> responseextractor = responseentityextractor(type);
        return nonnull(execute(url, method, requestcallback, responseextractor, urivariables));
    }

    // 这个方法就非常精简了,让调用者自己去构造requestentity,里面是包含了请求的url和方法等信息的
    @override
    public <t> responseentity<t> exchange(requestentity<?> requestentity, class<t> responsetype) throws restclientexception {
        requestcallback requestcallback = httpentitycallback(requestentity, responsetype);
        responseextractor<responseentity<t>> responseextractor = responseentityextractor(responsetype);
        return nonnull(doexecute(requestentity.geturl(), requestentity.getmethod(), requestcallback, responseextractor));
    }

exchange所有方法使用的都是httpentityresponseentity代表请求实体和响应实体,足以见到它设计的通用性。

在spring3.2后提供了parameterizedtypereference来处理参数化类型---> 主要是为了处理list等的泛型

可以发现即使是exchange()方法,最终还是委托给execute/doexecute去执行的:

resttemplate:

    // 3个execute方法。最终调用的都是doexecute方法
    // 它做的一件事:使用uritemplatehandler把url的参数填进去~~~
    // 底层使用的是我上文介绍的`uricomponentsbuilder`,还是比较简单的
    @override
    @nullable
    public <t> t execute(string url, httpmethod method, @nullable requestcallback requestcallback, @nullable responseextractor<t> responseextractor, object... urivariables) throws restclientexception {
        uri expanded = geturitemplatehandler().expand(url, urivariables);
        return doexecute(expanded, method, requestcallback, responseextractor);
    }

doexecute方法:
    @nullable
    protected <t> t doexecute(uri url, @nullable httpmethod method, @nullable requestcallback requestcallback, @nullable responseextractor<t> responseextractor) throws restclientexception {
        clienthttpresponse response = null;
        clienthttprequest request = createrequest(url, method);
        // 如果有回调,那就先回调处理一下子请求
        if (requestcallback != null) {
            requestcallback.dowithrequest(request);
        }
        // 真正意义上的发送请求。
        // 请注意:如果这里的request是`interceptingclienthttprequest`,那就回执行拦截器的intercept方法哦~~~
        // 至于什么时候是interceptingclienthttprequest呢?这个上面有讲的
        response = request.execute();
        // 处理结果(若有错误,那就抛出异常~~~)
        handleresponse(url, method, response);
        
        // 请求正常。那就使用返回值提取器responseextractor提取出内容即可了~~~
        return (responseextractor != null ? responseextractor.extractdata(response) : null);
        ...
        // 关闭响应(clienthttpresponse继承了closeable接口)
        finally {
            if (response != null) {
                response.close();
            }
        }
    }

看完doexecute()的模板式的实现步骤,就清楚了resttemplate从发出一个请求到收到一个响应的完整过程。spring设计了多个相关组件,提供钩子程序让我们可以干预到流程里面去,最常见的当然就是请求拦截器了,它在ribbon负载均衡和hystrix熔断器里面有很好的应用~

asyncresttemplate

它是@since 4.0新增的用于解决一些异步http请求的场景,但它寿命比较短,在spring5.0就标记为@deprecated,而被推荐使用webclient去代替它。

它的实现基础原理是:resttemplate + simpleasynctaskexecutor任务池的方式去实现的异步请求,返回值均为listenablefuture。掌握了resttemplate后,它使用起来是没有什么障碍的

极简使用demo show

看过了原理的描述,我有理由相信你已经烂熟于胸并对resttemplate能够运用自如了。因此关于使用方面,本文只给如下非常简单的一个demo show我认为是够了的:

public static void main(string[] args) throws ioexception {
    resttemplate resttemplate = new resttemplate();
    string pagehtml = resttemplate.getforobject("http://www.baidu.com", string.class);
    system.out.println(pagehtml); // 百度首页的html...
}

解释一点:这里请求得到的是一个html网页,所以httpmessageconverterextractor去提取响应时,使用的是stringhttpmessageconverter去处理的,提取代码如下:

stringhttpmessageconverter:
    @override
    protected string readinternal(class<? extends string> clazz, httpinputmessage inputmessage) throws ioexception {
        // 从响应头的contenttype里提取(若是application/json,那默认也是urf-8)
        // 若没有指定编码,就取值getdefaultcharset。比如本处访问百度,就取值默认值`iso-8859-1`对body体进行编码的~
        charset charset = getcontenttypecharset(inputmessage.getheaders().getcontenttype());
        return streamutils.copytostring(inputmessage.getbody(), charset);
    }

小伙伴把此请求案例可以和上面我使用clienthttprequestfactory发送请求的案例对比(或者和你自己使用httpclient步骤对比),感受感受使用resttemplate是多么的优雅~

推荐阅读

resttemplate组件:clienthttprequestfactory、clienthttprequestinterceptor、responseextractor【享学spring mvc】
为何一个@loadbalanced注解就能让resttemplate拥有负载均衡的能力?【享学spring cloud】

总结

微服务作为主流的今天,resttemplate可谓是一把利器,每个程序员都应该掌握它。深入理解它对实际应用、调优都有很现实的意义,所以我相信本文能够帮助到你,做到烂熟于胸。
预告一下:下篇文章会原理分析告诉大家为何一个简单的@loadbalanced注解就能让resttemplate拥有负载均衡的能力?

== 若对spring、springboot、mybatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==
== 若对spring、springboot、mybatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==