Spring源码学习之RestTemplate

35 篇文章 1 订阅
31 篇文章 1 订阅

一、背景

今天有个同事使用RestTemplate想设置超时时间,不知道怎么设置,帮忙翻了下源码,萌生了写个源码学习的文章

二、简述RestTemplate

RestTemplate是Spring框架中的一个核心类,用于在客户端(例如Web应用程序)中调用RESTful服务。它是一个HTTP客户端,可以用于向RESTful服务发送HTTP请求,并接收响应。
RestTemplate可以发送HTTP GET、POST、PUT、DELETE等请求,支持JSON、XML等数据格式的请求和响应。
RestTemplate默认使用Java的URLConnection来发送HTTP请求,也可以配置成使用Apache HttpClient、OkHttp等第三方HTTP客户端。可以通过配置RestTemplate的ClientHttpRequestFactory来指定使用的HTTP客户端。

三、用HttpURLConnection写个http连接请求

我们这先用jdk自带包里的HttpURLConnection写个http请求,代码如下:

@Test
public void testHttpGet() {
    //链接
    HttpURLConnection connection = null;
    InputStream is = null;
    BufferedReader br = null;
    StringBuffer result = new StringBuffer();
    try {
        //创建连接
        URL url = new URL("https://www.baidu.com");
        connection = (HttpURLConnection) url.openConnection();
        //设置请求方式
        connection.setRequestMethod("GET");
        //设置连接超时时间
        connection.setReadTimeout(15000);
        //开始连接
        connection.connect();
        //获取响应数据
        if (connection.getResponseCode() == 200) {
            //获取返回的数据
            is = connection.getInputStream();
            if (null != is) {
                br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                String temp;
                while (null != (temp = br.readLine())) {
                    result.append(temp);
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (null != br) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (null != is) {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //关闭远程连接
        connection.disconnect();
    }
    System.out.println(result.toString());
}

有几个比较关键的代码,我们可以先记住,之后的源码跟读中,可以找找在源码中对应的位置

  • url.openConnection()
  • connection.connect();
  • connection.getResponseCode()
  • connection.getInputStream()

四、RestTemplate源码

1、RestTemplate的类图

首先看下RestTemplate的类图
图片.png
RestOperations是一个接口,抽象出了Restful风格的操作方法
HttpAccessor是一个抽象类,内部定义创建连接的工厂类,默认是SimpleClientHttpRequestFactory,也可以自己设置,另外就是创建Request对象的方法createRequest,具体交给工厂类去创建
InterceptingHttpAccessor是HttpAccessor的抽象子类,在HttpAccessor的基础上,增加了拦截器的逻辑

2、写个例子

@Test
public void testRestTemplateGet() {
    RestTemplate INSTANCE = new RestTemplate();
    String result = INSTANCE.getForObject("https://www.baidu.com", String.class);
    System.out.println(result);
}

3、构造方法

public RestTemplate() {
    this.messageConverters.add(new ByteArrayHttpMessageConverter());
    this.messageConverters.add(new StringHttpMessageConverter());
    this.messageConverters.add(new ResourceHttpMessageConverter(false));
    try {
        this.messageConverters.add(new SourceHttpMessageConverter<>());
    }
    catch (Error err) {
        // Ignore when no TransformerFactory implementation is available
    }
    this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

    if (romePresent) {
        this.messageConverters.add(new AtomFeedHttpMessageConverter());
        this.messageConverters.add(new RssChannelHttpMessageConverter());
    }

    if (jackson2XmlPresent) {
        this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
    }
    else if (jaxb2Present) {
        this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
    }

    if (jackson2Present) {
        this.messageConverters.add(new MappingJackson2HttpMessageConverter());
    }
    else if (gsonPresent) {
        this.messageConverters.add(new GsonHttpMessageConverter());
    }
    else if (jsonbPresent) {
        this.messageConverters.add(new JsonbHttpMessageConverter());
    }

    if (jackson2SmilePresent) {
        this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
    }
    if (jackson2CborPresent) {
        this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
    }

    this.uriTemplateHandler = initUriTemplateHandler();
}

构造方法中主要干了两件事:

  • 添加了很多HttpMessageConverter的实例对象,针对不同的消息形式,有不同的HttpMessageConverter实现类来处理
  • initUriTemplateHandler初始化URI模板处理器,用来拼接url的

4、执行请求

4.1、getForObject

//RestTemplate
@Override
@Nullable
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
    //获取请求回调
    RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
    //创建http消息转换抽取器
    HttpMessageConverterExtractor<T> responseExtractor =
            new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
    return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
}

这个方法包含:

  • acceptHeaderRequestCallback获取请求回调:该方法内部创建了AcceptHeaderRequestCallback类的实例,AcceptHeaderRequestCallback是RestTemplate的内部类,里面只有一个实现方法,doWithRequest,这个方法就是遍历所有的消息转换器,找到适合的,然后再从这些转换器中找到所有支持的媒体类型,最后将所有支持的媒体类型设置到请求头部Accept中。
  • HttpMessageConverterExtractor创建http消息转换抽取器:该方法就是将消息转换器,log,返回类型封装成HttpMessageConverterExtractor对象,供后面使用
  • execute执行请求方法:后面细说

4.2、execute

//RestTemplate
@Override
@Nullable
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {

    //拼接url
    URI expanded = getUriTemplateHandler().expand(url, uriVariables);
    return doExecute(expanded, method, requestCallback, responseExtractor);
}

这步没什么好说的,拼接url之后,调用doExecute,我们看doExecute(一般do开头的是真正的核心代码)

4.3、doExecute

//RestTemplate
@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 {
        //创建ClientHttpRequest
        ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null) {
            //处理回调
            requestCallback.doWithRequest(request);
        }
        //执行请求
        response = request.execute();
        //处理响应
        handleResponse(url, method, response);
        //用响应抽取器抽取数据,并转换成我们想要的返回类型
        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();
        }
    }
}

这一步有几个重要的方法:

  • createRequest(url, method);
  • requestCallback.doWithRequest(request);
  • response = request.execute();
  • handleResponse(url, method, response);
  • responseExtractor.extractData(response)

下面分别看看这几个方法

4.4、createRequest(url, method);

//HttpAccessor
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
    ClientHttpRequest request = getRequestFactory().createRequest(url, method);
    if (logger.isDebugEnabled()) {
        logger.debug("HTTP " + method.name() + " " + url);
    }
    return request;
}

这里使用的是工厂方法,先获取工厂类,然后用工厂类创建ClientHttpRequest对象,先看下getRequestFactory方法,因为这个方法被子类实现了,所以调用的是子类InterceptingHttpAccessor里的getRequestFactory方法

//InterceptingHttpAccessor
@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();
    }
}

这里是获取拦截器,如果有拦截器就封装成InterceptingClientHttpRequestFactory这个包含拦截器的工厂类,最终会创建包含拦截器的InterceptingClientHttpRequest,如果没有拦截器,就调用父类的getRequestFactory方法,返回HttpAccessor里默认的工厂类,这里我们没有加拦截器,我们看HttpAccessor里默认的工厂类

private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

HttpAccessor默认的工厂类是SimpleClientHttpRequestFactory,我们再回到createRequest方法

//HttpAccessor
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
    ClientHttpRequest request = getRequestFactory().createRequest(url, method);
    if (logger.isDebugEnabled()) {
        logger.debug("HTTP " + method.name() + " " + url);
    }
    return request;
}

我们再来看工厂的createRequest方法,上面我们知道返回的工厂类是SimpleClientHttpRequestFactory,所以下一步的它的createRequest方法

//SimpleClientHttpRequestFactory
@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);
    }
}

这一步主要有两个方法,最后返回一个SimpleBufferingClientHttpRequest对象的Request

  • openConnection
  • prepareConnection

openConnection,看名字就知道是打开连接,里面可以找到打开连接的方法url.openConnection()

//SimpleClientHttpRequestFactory
protected HttpURLConnection openConnection(URL url, @Nullable Proxy proxy) throws IOException {
    URLConnection urlConnection = (proxy != null ? url.openConnection(proxy) : url.openConnection());
    if (!HttpURLConnection.class.isInstance(urlConnection)) {
        throw new IllegalStateException("HttpURLConnection required for [" + url + "] but got: " + urlConnection);
    }
    return (HttpURLConnection) urlConnection;
}

继续看prepareConnection,这步是设置connection的一些属性

//SimpleClientHttpRequestFactory
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
    if (this.connectTimeout >= 0) {
        //设置连接超时时间
        connection.setConnectTimeout(this.connectTimeout);
    }
    if (this.readTimeout >= 0) {
        //设置读取超时时间
        connection.setReadTimeout(this.readTimeout);
    }

    connection.setDoInput(true);

    if ("GET".equals(httpMethod)) {
        connection.setInstanceFollowRedirects(true);
    }
    else {
        connection.setInstanceFollowRedirects(false);
    }

    if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) ||
            "PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) {
        connection.setDoOutput(true);
    }
    else {
        connection.setDoOutput(false);
    }

    //设置请求方法
    connection.setRequestMethod(httpMethod);
}

4.5、requestCallback.doWithRequest(request);

回到doExecute,我们再来看requestCallback.doWithRequest(request);前面我们看到RequestCallback的实例是AcceptHeaderRequestCallback,我们看下它的doWithRequest方法

//AcceptHeaderRequestCallback
@Override
public void doWithRequest(ClientHttpRequest request) throws IOException {
    if (this.responseType != null) {
        List<MediaType> allSupportedMediaTypes = getMessageConverters().stream()
                .filter(converter -> canReadResponse(this.responseType, converter))
                .flatMap(this::getSupportedMediaTypes)
                .distinct()
                .sorted(MediaType.SPECIFICITY_COMPARATOR)
                .collect(Collectors.toList());
        if (logger.isDebugEnabled()) {
            logger.debug("Accept=" + allSupportedMediaTypes);
        }
        request.getHeaders().setAccept(allSupportedMediaTypes);
    }
}

这一步就是把支持的MediaType设置进请求头中

4.6、response = request.execute();

回到doExecute,继续看request.execute(),前面我们知道这个request是SimpleBufferingClientHttpRequest类型的,但是点击request.execute方法却找不到这个类型
图片.png应该是在其父类中,所以我们先看类图
图片.png
所以在其父类AbstractClientHttpRequest中

//AbstractClientHttpRequest
@Override
public final ClientHttpResponse execute() throws IOException {
	//检查是否执行过,如果执行过会报错
    assertNotExecuted();
	//执行请求
    ClientHttpResponse result = executeInternal(this.headers);
	//执行后标记为已执行
    this.executed = true;
    return result;
}

重点看executeInternal,在子类AbstractBufferingClientHttpRequest中

//AbstractBufferingClientHttpRequest
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
    byte[] bytes = this.bufferedOutput.toByteArray();
    if (headers.getContentLength() < 0) {
        headers.setContentLength(bytes.length);
    }
    ClientHttpResponse result = executeInternal(headers, bytes);
    this.bufferedOutput = new ByteArrayOutputStream(0);
    return result;
}

再看executeInternal(headers, bytes);在子类SimpleBufferingClientHttpRequest中

//SimpleBufferingClientHttpRequest
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
    addHeaders(this.connection, headers);
    // JDK <1.8 doesn't support getOutputStream with HTTP DELETE
    if (getMethod() == HttpMethod.DELETE && bufferedOutput.length == 0) {
        this.connection.setDoOutput(false);
    }
    if (this.connection.getDoOutput() && this.outputStreaming) {
        this.connection.setFixedLengthStreamingMode(bufferedOutput.length);
    }
    this.connection.connect();
    if (this.connection.getDoOutput()) {
        FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());
    }
    else {
        // Immediately trigger the request in a no-output scenario as well
        this.connection.getResponseCode();
    }
    return new SimpleClientHttpResponse(this.connection);
}

connection.connect(); 找到了,最后将connection封装到SimpleClientHttpResponse类型的Response里

4.7、handleResponse(url, method, response);

再回到doExecute,继续看handleResponse(url, method, response);

//RestTemplate
protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
	//获取错误处理器
    ResponseErrorHandler errorHandler = getErrorHandler();
    //判断返回有没有错误
    boolean hasError = errorHandler.hasError(response);
    if (logger.isDebugEnabled()) {
        try {
            //获取状态码
            int code = response.getRawStatusCode();
            //将状态码转换成HttpStatus枚举类
            HttpStatus status = HttpStatus.resolve(code);
            logger.debug("Response " + (status != null ? status : code));
        }
        catch (IOException ex) {
            // ignore
        }
    }
    if (hasError) {
        //有错误,用错误处理器处理
        errorHandler.handleError(url, method, response);
    }
}

这一步主要就是判断返回有没有错误,有错误就处理错误,处理错误最终就是抛异常,所以最后那步的抽取数据不会执行,所以重点就是errorHandler.hasError(response);跟进去

//DefaultResponseErrorHandler
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
    int rawStatusCode = response.getRawStatusCode();
    HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
    return (statusCode != null ? hasError(statusCode) : hasError(rawStatusCode));
}

response.getRawStatusCode();跟进去,到类SimpleClientHttpResponse中

//SimpleClientHttpResponse
@Override
public int getRawStatusCode() throws IOException {
    return this.connection.getResponseCode();
}

找到connection.getResponseCode();
再回头看下hasError(statusCode),一直跟进去

//HttpStatus
public boolean isError() {
    return (is4xxClientError() || is5xxServerError());
}

其实就是看如果是4或者5开头的,就是错误

4.8、responseExtractor.extractData(response)

最后来看responseExtractor.extractData(response),我们前面知道responseExtractor是HttpMessageConverterExtractor类型的

//HttpMessageConverterExtractor
@Override
@SuppressWarnings({"unchecked", "rawtypes", "resource"})
public T extractData(ClientHttpResponse response) throws IOException {
    MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
    if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
        return null;
    }
    MediaType contentType = getContentType(responseWrapper);

    try {
    	//遍历所有的HttpMessageConverter
        for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
            //如果是GenericHttpMessageConverter类型的,走这个分支
            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);
                }
            }
            //非GenericHttpMessageConverter类型的,走这个分支
            if (this.responseClass != null) {
                //根据返回的类型判断这个messageConverter是否可以读取
                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);
                }
            }
        }
    }
    catch (IOException | HttpMessageNotReadableException ex) {
        throw new RestClientException("Error while extracting response for type [" +
                this.responseType + "] and content type [" + contentType + "]", ex);
    }

    throw new RestClientException("Could not extract response: no suitable HttpMessageConverter found " +
            "for response type [" + this.responseType + "] and content type [" + contentType + "]");
}

这一步简单来说就是,遍历所有的HttpMessageConverter,如果发现哪个可以读取响应数据,就用这个HttpMessageConverter转换后返回,由于我们设置的返回类型是String,所以这里是用的StringHttpMessageConverter,先进入其父类AbstractHttpMessageConverter的read方法

//AbstractHttpMessageConverter
@Override
public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException {

    return readInternal(clazz, inputMessage);
}

再进入子类StringHttpMessageConverter的readInternal方法

//StringHttpMessageConverter
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
    Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
    return StreamUtils.copyToString(inputMessage.getBody(), charset);
}

先获取字符集,这个没什么好说的,我们重点看下inputMessage.getBody(),想找到最后的那个connection.getInputStream(),我们前面知道inputMessage是MessageBodyClientHttpResponseWrapper类型的

//MessageBodyClientHttpResponseWrapper
@Override
public InputStream getBody() throws IOException {
    return (this.pushbackInputStream != null ? this.pushbackInputStream : this.response.getBody());
}

可以看到来源是pushbackInputStream,pushbackInputStream是在调用hasEmptyMessageBody方法时设置的,其实就是response.getBody()的返回做了一层包装,我们看下response.getBody(),在SimpleClientHttpResponse类中

//SimpleClientHttpResponse
@Override
public InputStream getBody() throws IOException {
    InputStream errorStream = this.connection.getErrorStream();
    this.responseStream = (errorStream != null ? errorStream : this.connection.getInputStream());
    return this.responseStream;
}

终于找到了connection.getInputStream()
再回头看StreamUtils.copyToString方法

public static String copyToString(@Nullable InputStream in, Charset charset) throws IOException {
    if (in == null) {
        return "";
    }

    StringBuilder out = new StringBuilder();
    InputStreamReader reader = new InputStreamReader(in, charset);
    char[] buffer = new char[BUFFER_SIZE];
    int bytesRead = -1;
    while ((bytesRead = reader.read(buffer)) != -1) {
        out.append(buffer, 0, bytesRead);
    }
    return out.toString();
}

是不是和之前写的例子很像,读取流,拼接成结果返回;
到此RestTemplate源码分析结束;

五、什么是Restful风格

看完源码,我们知道,其实RestTemplate封装了原生的HttpURLConnection,采用Restful的理念,更优雅地来完成对HTTP服务的调用,那么什么是Restful风格,网上看到大神的回答

用 URL 定位资源,用 HTTP 动词(GET,POST,DELETE,PUT)描述操作。

用URL来指定具体的资源,如图片,文字,或者一个服务;用HTTP动词来描述操作;
再回头看RestTemplate,它用HttpMethod来定义Http的动词,包含下面这些:

  • GET:请求服务器端已经存在的资源,通常将Query String查询条件以键值对的编码形式追加到URL中。
  • HEAD:HEAD与GET几乎相同,但没有响应主体,只返回响应头部信息。HEAD请求对于在实际发出GET请求之前(例如在下载大文件或响应正文之前)检查GET请求将返回的内容很有用。
  • POST:POST用于用户创建,获取,或更新服务器上的资源,通过POST发送到服务器的数据存储在HTTP请求的请求主体中。
  • PUT:PUT用于将数据发送到服务器来创建/更新资源。POST和PUT之间的区别在于PUT请求是幂等的。也就是说,多次调用相同的PUT请求将始终产生相同的结果。相反,重复调用POST请求具有多次创建相同资源的副作用。
  • PATCH:用来更新服务器端已经存在资源的局部属性。
  • DELETE:用于删除服务器端指定的资源,指定资源的设定追加在URL中。
  • OPTIONS:描述目标资源所支持的http方法选项。
  • TRACE:诊断请求,允许客户端查看最终发送到服务器的请求消息(由于请求在中间节点可能会被修改)。提供了一种实用的debug机制。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
对于学习Spring源码的网站,可以参考以下链接: 1. Spring官方网站:https://spring.io/ 在官方网站上,你可以找到关于Spring的所有文档、教程和源码下载链接。它是学习Spring的首要资源。 2. GitHub上的Spring源码仓库:https://github.com/spring-projects/spring-framework 这是Spring源码的官方GitHub仓库,你可以在这里找到最新的Spring源码,并参与到开发讨论中。 3. CSDN博客:https://blog.csdn.net/navyfrost/article/details/102919323 这是一篇关于Spring源码学习的博客文章,作者分享了学习Spring源码的心得和方法,并提供了一些学习资源和案例。 希望以上资源可以帮助你开始学习Spring源码。祝你学习顺利!<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span> #### 引用[.reference_title] - *1* [Spring源码学习加注释,方便学习.zip](https://download.csdn.net/download/weixin_47367099/85350853)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* *4* [Spring源码学习系列——源码下载和环境](https://blog.csdn.net/shangguoli/article/details/124710529)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天进步亿点点的小码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值