RestTemplate (一) : ClientHttpRequestFactory、ResponseErrorHandler、ResponseExtractor、UriComponents


一、RestTemplate简介

  • RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它提供了常见的REST请求方案的模版,例如 GET 请求、POST 请求、PUT 请求、DELETE 请求以及一些通用的请求执行方法 exchange 以及 execute

  • RestTemplate 继承 InterceptingHttpAccessor 并且实现了 RestOperations 接口,其中 RestOperations 接口定义了基本的 RESTful 操作,这些操作在 RestTemplate 中都得到了实现。

  • RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端库。它提供了一套接口,然后分别用三种 Java 最常用 Http 连接的库来分别实现这套接口:

    • JDK 自带的 HttpURLConnection
    • Apache 的 HttpClient
    • OKHttp3

二、ClientHttpRequestFactory

客户端Http请求工厂, 它是个函数式接口,用于根据URIHttpMethod创建出一个ClientHttpRequest来发送请求

在这里插入图片描述
在这里插入图片描述

// @since 3.0  RestTemplate这个体系都是3.0后才有的
@FunctionalInterface
public interface ClientHttpRequestFactory {	
	// 返回一个ClientHttpRequest,这样调用其execute()方法就可以发送rest请求了
	ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException;
}

继承结构如下图 :

在这里插入图片描述
可以直观的看到,我们可以使用Apache的HttpClientOkHttp3Netty4这些三方包,但这些都需要额外导包,默认情况下Spring使用的是java.net.HttpURLConnection

2.1 ClientHttpRequest & ClientHttpResponse

  • ClientHttpRequest代表请求的客户端,该接口继承自HttpRequestHttpOutputMessage,只有一个ClientHttpResponse execute() throws IOException 方法。

  • 其中HttpURLConnectionHttpComponents(HttpClient)OkHttp3Netty4对它都有实现
    在这里插入图片描述

ClientHttpResponse继承树

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
使用工厂创建ClientHttpRequest,具体采用哪种http客户端, 使用对应的工厂来创建即可, 发送请求就不需要关心具体的细节了

2.2 SimpleClientHttpRequestFactory

它是Spring内置默认的实现,使用的是JDK内置的java.net.URLConnection作为client客户端

在这里插入图片描述
在这里插入图片描述

public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory {

	private static final int DEFAULT_CHUNK_SIZE = 4096;
	@Nullable
	private Proxy proxy; //java.net.Proxy
	private boolean bufferRequestBody = true; // 默认会缓冲body
	
	// 若值设置为0,表示永不超时 @see URLConnection#setConnectTimeout(int)
	private int connectTimeout = -1;
	
	// URLConnection#setReadTimeout(int) 
	// 超时规则同上
	private int readTimeout = -1;
	
	//Set if the underlying URLConnection can be set to 'output streaming' mode.
	private boolean outputStreaming = true;

	// 异步的时候需要
	@Nullable
	private AsyncListenableTaskExecutor taskExecutor;
	... // 省略所有的set方法
	
	@Override
	public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
		
		// 打开一个HttpURLConnection
		HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
		// 设置超时时间、请求方法等一些参数到connection
		prepareConnection(connection, httpMethod.name());

		//SimpleBufferingClientHttpRequest的excute方法最终使用的是connection.connect();
		// 然后从connection中得到响应码、响应体
		if (this.bufferRequestBody) {
			return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
		} else {
			return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
		}
	}

	// createAsyncRequest()方法略,无非就是在线程池里异步完成请求
	...
}

Demo Test

public class HttpClientRequestTest {

    public static void main(String[] args) throws IOException {
        SimpleClientHttpRequestFactory clientFactory = new SimpleClientHttpRequestFactory();

        // ConnectTimeout只有在网络正常的情况下才有效,因此两个一般都设置
        clientFactory.setConnectTimeout(5000); //建立连接的超时时间  5秒
        clientFactory.setReadTimeout(5000); // 传递数据的超时时间(在网络抖动的情况下,这个参数很有用)

        ClientHttpRequest clientHttpRequest = clientFactory.createRequest(URI.create("https://www.baidu.com"), HttpMethod.GET);
        // 发送请求
        ClientHttpResponse response = clientHttpRequest.execute();
        System.out.println(response.getStatusCode()); // 200 OK
        System.out.println(response.getStatusText()); // OK
        System.out.println(response.getHeaders()); // [Content-Length:"2443", Content-Type:"text/html", Server:"bfe", Date:"Tue, 23 Aug 2022 03:43:19 GMT"]

        // 返回内容 是个InputStream
        byte[] bytes = FileCopyUtils.copyToByteArray(response.getBody());
        System.out.println(new String(bytes, StandardCharsets.UTF_8)); // 百度首页内容的html
    }
}

2.2.1 HttpURLConnection注意点

关于HttpURLConnection的API使用,需注意如下几点:

  1. HttpURLConnection对象不能直接构造,需要通过URL类中的openConnection()方法来获得
  2. HttpURLConnectionconnect()方法,实际上只是建立了一个与服务器的TCP连接,并没有实际发送HTTP请求。HTTP请求实际上直到我们获取服务器响应数据(如调用getInputStream()、getResponseCode()等方法)时才正式发送出去

    配置信息都需要在connect()方法执行之前完成

  3. HttpURLConnection是基于HTTP协议的,其底层通过socket通信实现。如果不设置超时(timeout),在网络异常的情况下,可能会导致程序僵死而不继续往下执行。所以一定要设置超时时间
  4. HTTP正文的内容是通过OutputStream流写入的, 向流中写入的数据不会立即发送到网络,而是存在于内存缓冲区中,待流关闭时,根据写入的内容生成HTTP正文
  5. 调用getInputStream()方法时,返回一个输入流,用于从中读取服务器对于HTTP请求的返回信息
  6. HttpURLConnection.connect()不是必须的。当我们需要返回值时,比如我们使用HttpURLConnection.getInputStream()方法的时候它就会自动发送请求了,所以完全没有必要调用connect()方法了

HttpURLConnection使用工具类

public class HttpRequest {

    private static final Logger logger = LoggerFactory.getLogger(HttpRequest.class);

    public static String http(String method, String httpUrl, Integer timeout, String... headers) {
        String message = "";
        HttpURLConnection connection = null;
        InputStream inputStream = null;
        try {
            URL url = new URL(httpUrl);
            connection = (HttpURLConnection) url.openConnection();
            if (headers != null && headers.length > 0) {
                for (int i = 0; i < headers.length; i++) {
                    connection.setRequestProperty(headers[i], headers[i++]);
                }
            }
            connection.setRequestMethod(method);
            connection.setConnectTimeout(timeout);
            connection.setReadTimeout(timeout);
            connection.connect();

            inputStream = connection.getInputStream();
            message = streamToString(inputStream);

        } catch (Exception e) {
            logger.error("url:{}", httpUrl, e);
        } finally {
            try {
                inputStream.close();
                connection.disconnect();
            } catch (Throwable ignored) {
            }
        }
        return message;
    }

    /**
     * @param inputStream inputStream
     * @return 字符串转换之后的
     */
    public static String streamToString(InputStream inputStream) {
        try(BufferedReader br =new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
            StringBuilder builder = new StringBuilder();
            String output;
            while((output = br.readLine())!=null){
                builder.append(output);
            }
            return builder.toString();
        }  catch (IOException e) {
            throw new RuntimeException("Http 服务调用失败",e);
        }
    }
}

2.3 HttpComponentsClientHttpRequestFactory

2.3.1 使用HttpClient发送请求

HttpURLConnection在功能上是有些不足(简单的提交参数可以满足)。绝大部分情况下Web站点的网页可能没这么简单,这些页面并不是通过一个简单的URL就可访问的,可能需要用户登录而且具有相应的权限才可访问该页面。在这种情况下,就需要涉及Session、Cookie的处理了,如果打算使用HttpURLConnection来处理这些细节,当然也是可能实现的,只是处理起来难度就大了。

我们可以Apache开源组织提供了一个HttpClient项目,可以用于发送HTTP请求,接收HTTP响应(包含HttpGet、HttpPost…等各种发送请求的对象)

引入依赖

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

Demo Test

把上面案例的SimpleClientHttpRequestFactory换成HttpComponentsClientHttpRequestFactory它的实例,其余都不用变化即可成功看到效果。

// @since 3.1 3.1后出现的。
public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean {

	private HttpClient httpClient;
	@Nullable
	private RequestConfig requestConfig; // 这个配置就是可以配置超时等等的类
	private boolean bufferRequestBody = true;

	//=========下面是两个构造函数=========
	public HttpComponentsClientHttpRequestFactory() {
		// HttpClientBuilder.create().useSystemProperties().build();
		// 所有若是这里,配置超时时间可以这么来设置也可:
		// System.setProperty(”sun.net.client.defaultConnectTimeout”, “5000″);
		this.httpClient = HttpClients.createSystem();
	}
	// 当然可以把你配置好了的Client扔进来
	public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) {
		this.httpClient = httpClient;
	}
	... // 省略设置超时时间。。。等等属性的一些get/set
	// 超时信息啥的都是保存在`RequestConfig`里的


	@Override
	public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
		HttpClient client = getHttpClient(); // 拿到你指定的client)=(或者系统缺省的)
		// switch语句逻辑:HttpMethod == GET --> HttpGet HEAD --> HttpHead ...
		HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
		postProcessHttpRequest(httpRequest);
		...
	}
}

另外OkHttp3ClientHttpRequestFactory使用的是okhttp3.OkHttpClient发送请求;Netty4ClientHttpRequestFactory使用的是io.netty.channel.EventLoopGroup

2.3.2 HttpURLConnection、HttpClient、OkHttpClient比较

HttpURLConnection

  • 优点:JDK内置支持,java的标准类
  • 缺点:API不够友好,什么都没封装,对用高级功能使用不方便

HttpClient

  • 优点:功能强大,API友好,使用率够高,几乎成为了实际意义上的标准(相当于对HttpURLConnection的封装)
  • 缺点:性能稍低(比HttpURLConnection低,但4.3后使用连接池进行了改善),API较臃肿

OkHttpClient : 新一代的Http访问客户端

  • 优点:一个专注于性能和易用性的HTTP客户端(节约宽带,Android推荐使用),它设计的首要目标就是高效。提供了最新的 HTTP 协议版本 HTTP/2 和 SPDY 的支持。如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率

    默认情况下,OKHttp会自动处理常见的网络问题,像二次连接SSL的握手问题。支持文件上传、下载、cookie、session、https证书等几乎所有功能; 支持取消某个请求

连接池:可能是http请求,也可能是https请求
加入池化技术,就不用每次发起请求都新建一个连接(每次连接握手三次,效率太低)

2.4 AbstractClientHttpRequestFactoryWrapper

ClientHttpRequestFactory的一个包装抽象类

在这里插入图片描述

它有如下两个子类实现: InterceptingClientHttpRequestFactory, BufferingClientHttpRequestFactory

2.4.1 InterceptingClientHttpRequestFactory

Interceptor拦截的概念,还是蛮重要的。它持有的ClientHttpRequestInterceptor对于我们若想要拦截发出去的请求非常之重要

// @since 3.1
public class InterceptingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {
	// 持有所有的请求拦截器
	private final List<ClientHttpRequestInterceptor> interceptors;

	public InterceptingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory, @Nullable List<ClientHttpRequestInterceptor> interceptors) {
		super(requestFactory);
		// 拦截器只允许通过构造函数设置进来,并且并没有提供get方法方法~
		this.interceptors = (interceptors != null ? interceptors : Collections.emptyList());
	}

	// 此处返回的是一个InterceptingClientHttpRequest,显然它肯定是个ClientHttpRequest
	@Override
	protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
		return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
	}
}

InterceptingClientHttpRequestexecute()方法的特点是:若存在拦截器,交给给拦截器去执行发送请求return nextInterceptor.intercept(request, body, this),否则就使用自己的execute()方法。

// InterceptingClientHttpRequest中的内部类
	private class InterceptingRequestExecution implements ClientHttpRequestExecution {

		private final Iterator<ClientHttpRequestInterceptor> iterator;

		public InterceptingRequestExecution() {
			this.iterator = interceptors.iterator();
		}

		@Override
		public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
			if (this.iterator.hasNext()) {
				ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
				return nextInterceptor.intercept(request, body, this);
			}
			else {
				HttpMethod method = request.getMethod();
				Assert.state(method != null, "No standard HTTP method");
				ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
				request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
				if (body.length > 0) {
					if (delegate instanceof StreamingHttpOutputMessage) {
						StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
						streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
					}
					else {
						StreamUtils.copy(body, delegate.getBody());
					}
				}
				return delegate.execute();
			}
		}
	}
ClientHttpRequestInterceptor请求拦截器

在这里插入图片描述
在这里插入图片描述

BasicAuthorizationInterceptor(已过时)
// @since 4.3.1  但在Spring5.1.1后推荐使用BasicAuthenticationInterceptor
@Deprecated
public class BasicAuthorizationInterceptor implements ClientHttpRequestInterceptor {
	
	private final String username;
	private final String password;
	
	// 注意:username不允许包含:这个字符,但是密码是允许的
	public BasicAuthorizationInterceptor(@Nullable String username, @Nullable String password) {
		Assert.doesNotContain(username, ":", "Username must not contain a colon");
		this.username = (username != null ? username : "");
		this.password = (password != null ? password : "");
	}

	@Override
	public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
		// 用户名密码连接起来后,用Base64对字节码进行编码~
		String token = Base64Utils.encodeToString((this.username + ":" + this.password).getBytes(StandardCharsets.UTF_8));
	
		// 放进请求头:key为`Authorization`  然后执行请求的发送
		request.getHeaders().add("Authorization", "Basic " + token);
		return execution.execute(request, body);
	}
}

这个拦截器没有对body有任何改动,只是把用户名、密码帮你放进了请求头上

BasicAuthenticationInterceptor

它是用来代替BasicAuthorizationInterceptor。它使用标准的授权头来处理,参考HttpHeaders#setBasicAuth、HttpHeaders#AUTHORIZATION

public class BasicAuthenticationInterceptor implements ClientHttpRequestInterceptor {
	private final String username;
	private final String password;
	// 编码,一般不用指定
	@Nullable
	private final Charset charset;
	... // 构造函数略

	@Override
	public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

		HttpHeaders headers = request.getHeaders();
		// 只有当请求里不包含`Authorization`这个key的时候,此处才会设置授权头哦
		if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
			
			// 这个方法是@since 5.1之后才提供的~~~~~
			// 若不包含此key,就设置标准的授权头(根据用户名、密码) 它内部也有这如下三步:
			
			// String credentialsString = username + ":" + password;
			// byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(charset));
			// String encodedCredentials = new String(encodedBytes, charset);
			
			// 注意:它内部最终还是调用set(AUTHORIZATION, "Basic " + encodedCredentials);这个方法的
			headers.setBasicAuth(this.username, this.password, this.charset);
		}
		return execution.execute(request, body);
	}
}

说明:这两个请求拦截器默认都是没有被"装配"的,如果使用需要手动装配

2.4.2 BufferingClientHttpRequestFactory

包装ClientHttpRequestFactory,使得具有缓存的能力。若开启缓存功能(有开关可控),会使用BufferingClientHttpRequestWrapper包装原来的ClientHttpRequest。这样发送请求后得到的是BufferingClientHttpResponseWrapper响应。

三、ResponseErrorHandler (对响应错误进行处理)

用于确定特定响应是否有错误的策略接口。

在这里插入图片描述

// @since 3.0
public interface ResponseErrorHandler {

	// response里是否有错
	boolean hasError(ClientHttpResponse response) throws IOException;
	// 只有hasError = true时才会调用此方法
	void handleError(ClientHttpResponse response) throws IOException;
	 // @since 5.0
	default void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
		handleError(response);
	}
}

继承树
在这里插入图片描述

3.1 DefaultResponseErrorHandler

Spring对此策略接口的默认实现,RestTemplate默认使用的错误处理器就是它

// @since 3.0
public class DefaultResponseErrorHandler implements ResponseErrorHandler {

	// 是否有错误是根据响应码来的,所以请严格遵守响应码的规范啊
	// 简单的说4xx和5xx都会被认为有错,否则是无错的  参考:HttpStatus.Series
	@Override
	public boolean hasError(ClientHttpResponse response) throws IOException {
		int rawStatusCode = response.getRawStatusCode();
		HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
		return (statusCode != null ? hasError(statusCode) : hasError(rawStatusCode));
	}
	...
	// 处理错误
	@Override
	public void handleError(ClientHttpResponse response) throws IOException {
		HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
		if (statusCode == null) {
			throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response));
		}
		handleError(response, statusCode);
	}
	
	// protected方法,子类对它有复写
	protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
		String statusText = response.getStatusText();
		HttpHeaders headers = response.getHeaders();
		byte[] body = getResponseBody(response); // 拿到body,把InputStream转换为字节数组
		Charset charset = getCharset(response); // 注意这里的编码,是从返回的contentType里拿的~~~
		
		// 分别针对于客户端错误、服务端错误 包装为HttpClientErrorException和HttpServerErrorException进行抛出
		// 异常内包含有状态码、状态text、头、body、编码等等信息~~~~
		switch (statusCode.series()) {
			case CLIENT_ERROR:
				throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
			case SERVER_ERROR:
				throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
			default:
				throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
		}
	}
	...
}

我们经常能看到客户端错误,就是因为这两个异常: HttpClientErrorException, HttpServerErrorException

3.1.1 HttpClientErrorException

它针对不同的状态码HttpStatus,创建了不同的类型进行返回

  • BadRequest、Unauthorized、Forbidden等等都是HttpClientErrorException的子类
public class HttpClientErrorException extends HttpStatusCodeException {
	...
	public static HttpClientErrorException create(
			HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) {

		switch (statusCode) {
			case BAD_REQUEST:
				return new HttpClientErrorException.BadRequest(statusText, headers, body, charset);
			case UNAUTHORIZED:
				return new HttpClientErrorException.Unauthorized(statusText, headers, body, charset);
			case FORBIDDEN:
				return new HttpClientErrorException.Forbidden(statusText, headers, body, charset);
			case NOT_FOUND:
				return new HttpClientErrorException.NotFound(statusText, headers, body, charset);
			case METHOD_NOT_ALLOWED:
				return new HttpClientErrorException.MethodNotAllowed(statusText, headers, body, charset);
			case NOT_ACCEPTABLE:
				return new HttpClientErrorException.NotAcceptable(statusText, headers, body, charset);
			case CONFLICT:
				return new HttpClientErrorException.Conflict(statusText, headers, body, charset);
			case GONE:
				return new HttpClientErrorException.Gone(statusText, headers, body, charset);
			case UNSUPPORTED_MEDIA_TYPE:
				return new HttpClientErrorException.UnsupportedMediaType(statusText, headers, body, charset);
			case TOO_MANY_REQUESTS:
				return new HttpClientErrorException.TooManyRequests(statusText, headers, body, charset);
			case UNPROCESSABLE_ENTITY:
				return new HttpClientErrorException.UnprocessableEntity(statusText, headers, body, charset);
			default:
				return new HttpClientErrorException(statusCode, statusText, headers, body, charset);
		}
	}
	...
}

3.1.2 HttpServerErrorException (同上代码类似)

3.2 ExtractingResponseErrorHandler

继承自DefaultResponseErrorHandler。它将http错误响应利用HttpMessageConverter转换为对应的RestClientException

// @since 5.0 它出现得还是很晚的。继承自DefaultResponseErrorHandler 
// 若你的RestTemplate想使用它,请调用RestTemplate#setErrorHandler(ResponseErrorHandler)设置即可
public class ExtractingResponseErrorHandler extends DefaultResponseErrorHandler {
	private List<HttpMessageConverter<?>> messageConverters = Collections.emptyList();
	
	// 对响应码做缓存
	private final Map<HttpStatus, Class<? extends RestClientException>> statusMapping = new LinkedHashMap<>();
	private final Map<HttpStatus.Series, Class<? extends RestClientException>> seriesMapping = new LinkedHashMap<>();

	// 构造函数、set方法给上面两个Map赋值。因为我们可以自己控制哪些状态码应该报错,哪些不应该了~
	// 以及可以自定义:那个状态码抛我们自定义的异常,哪一系列状态码抛我们自定义的异常,这个十分的便于我们做监控
	... // 省略构造函数和set方法。。。


	// 增加缓存功能~~~  否则在交给父类
	@Override
	protected boolean hasError(HttpStatus statusCode) {
		if (this.statusMapping.containsKey(statusCode)) {
			return this.statusMapping.get(statusCode) != null;
		} else if (this.seriesMapping.containsKey(statusCode.series())) {
			return this.seriesMapping.get(statusCode.series()) != null;
		} else {
			return super.hasError(statusCode);
		}
	}

	// 这个它做的事:extract:提取
	@Override
	public void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
		if (this.statusMapping.containsKey(statusCode)) {
			extract(this.statusMapping.get(statusCode), response);
		} else if (this.seriesMapping.containsKey(statusCode.series())) {
			extract(this.seriesMapping.get(statusCode.series()), response);
		} else {
			super.handleError(response, statusCode);
		}
	}


	private void extract(@Nullable Class<? extends RestClientException> exceptionClass, ClientHttpResponse response) throws IOException {
		if (exceptionClass == null) {
			return;
		}

		// 这里使用到了ResponseExtractor返回值提取器,从返回值里提取内容(本文是提取异常)
		HttpMessageConverterExtractor<? extends RestClientException> extractor =
				new HttpMessageConverterExtractor<>(exceptionClass, this.messageConverters);
		RestClientException exception = extractor.extractData(response);
		if (exception != null) { // 若提取到了异常信息,抛出即可
			throw exception;
		}
	}
}

四、ResponseExtractor (从响应中提取数据)

响应提取器:从Response中提取数据。 RestTemplate请求完成后,都是通过它来从ClientHttpResponse提取出指定内容(比如请求头、请求Body体等)

在这里插入图片描述

它的直接实现似乎只有HttpMessageConverterExtractor,当然它也是最为重要的一个实现,和HttpMessageConverter相关。

4.1 MessageBodyClientHttpResponseWrapper

它的作用就是包装后,提供两个方法hasMessageBody、hasEmptyMessageBody方便了对body体内容进行判断

MessageBodyClientHttpResponseWrapper,它的特点:它不仅可以通过实际读取输入流来检查响应是否有消息体,还可以检查其长度是否为0(即空)

// @since 4.1.5  它是一个访问权限是default的类,是对其它ClientHttpResponse的一个包装
class MessageBodyClientHttpResponseWrapper implements ClientHttpResponse {
	private final ClientHttpResponse response;
	// java.io.PushbackInputStream
	@Nullable
	private PushbackInputStream pushbackInputStream;
	
	// 判断相应里是否有body体
	// 若响应码是1xx 或者是204;或者getHeaders().getContentLength() == 0 那就返回false  否则返回true
	public boolean hasMessageBody() throws IOException {
		HttpStatus status = HttpStatus.resolve(getRawStatusCode());
		if (status != null && (status.is1xxInformational() || status == HttpStatus.NO_CONTENT || status == HttpStatus.NOT_MODIFIED)) {
			return false;
		}
		if (getHeaders().getContentLength() == 0) {
			return false;
		}
		return true;
	}

	// 上面是完全格局状态码(ContentLength)来判断是否有body体的~~~这里会根据流来判断
	// 如果response.getBody() == null,返回true
	// 若流里有内容,最终就用new PushbackInputStream(body)包装起来~~~
	public boolean hasEmptyMessageBody() throws IOException {
		...
	}
	
	...  // 其余接口方法都委托~
	@Override
	public InputStream getBody() throws IOException {
		return (this.pushbackInputStream != null ? this.pushbackInputStream : this.response.getBody());
	}
}

4.2 HttpMessageConverterExtractor

它的处理逻辑理解起来非常简单:利用contentType找到一个消息转换器,最终HttpMessageConverter.read()把消息读出来转换成Java对象。

// @since 3.0 泛型T:the data type
public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
	// java.lang.reflect.Type
	private final Type responseType;
	// 这个泛型也是T,表示数据的Class嘛~
	// 该calss有可能就是上面的responseType
	@Nullable
	private final Class<T> responseClass;
	// 重要:用于消息解析的转换器
	private final List<HttpMessageConverter<?>> messageConverters;
	... // 省略构造函数


	// 从ClientHttpResponse 里提取值
	@Override
	@SuppressWarnings({"unchecked", "rawtypes", "resource"})
	public T extractData(ClientHttpResponse response) throws IOException {
		MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
		// 若没有消息体(状态码不对 或者 消息体为空都被认为是木有)
		if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
			return null;
		}
	
		// content-type若响应头header里没有指定,那默认是它MediaType.APPLICATION_OCTET_STREAM
		MediaType contentType = getContentType(responseWrapper);
		
		// 遍历所有的messageConverters,根据contentType 来选则一个消息转换器
		for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
			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);
				}
			}
		}
		// 最终return messageConverter.read((Class) this.responseClass, responseWrapper)
		...
	}
}

它还有两个内部类的实现如下(都是RestTemplate的私有内部类):

RestTemplate// 提取为`ResponseEntity`  最终委托给HttpMessageConverterExtractor完成的
	private class ResponseEntityResponseExtractor<T> implements ResponseExtractor<ResponseEntity<T>> {

		@Nullable
		private final HttpMessageConverterExtractor<T> delegate;

		public ResponseEntityResponseExtractor(@Nullable Type responseType) {
			// 显然:只有请求的返回值不为null 才有意义~
			if (responseType != null && Void.class != responseType) {
				this.delegate = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
			} else {
				this.delegate = null;
			}
		}

		// 数据提取。都是交给`delegate.extractData(response)`做了,然后new一个ResponseEntity出来包装进去
		// 若木有返回值(delegate=null),那就是一个`ResponseEntity`实例,body为null
		@Override
		public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {
			if (this.delegate != null) {
				T body = this.delegate.extractData(response);
				return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);
			}
			else {
				return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();
			}
		}
	}

	// 提取请求头
	private static class HeadersExtractor implements ResponseExtractor<HttpHeaders> {
		@Override
		public HttpHeaders extractData(ClientHttpResponse response) {
			return response.getHeaders();
		}
	}

五、UriTemplateHandler

这个组件它用于定义用变量扩展uri模板的方法

// @since 4.2 出现较晚  
// @see RestTemplate#setUriTemplateHandler(UriTemplateHandler)
public interface UriTemplateHandler {
	URI expand(String uriTemplate, Map<String, ?> uriVariables);
	URI expand(String uriTemplate, Object... uriVariables);
}

关于URI的处理,最终都是委托给UriComponentsBuilder来完成。

5.1 URI Builder

用来处理URIURL等和HTTP协议相关的元素,它提供了非常好用、功能强大的URI Builder模式来完成

  • Spring MVC从3.1开始提供了一种机制,可以通过UriComponentsBuilderUriComponents面向对象的构造和编码URI

5.2 UriComponents

它表示一个不可变的URI组件集合,将组件类型映射到字符串值, 具有更强大的编码选项和对URI模板变量的支持

  • URI:统一资源标识符; URI可以唯一的标识某一资源, 比如学号可以唯一标识学生, 身份证号可以唯一标识一个人等等
  • URL:统一资源定位符; URL是URI的子集, 不仅可以唯一标识一个资源,还能告诉你他在哪。 比如某学生在5号公寓楼328寝5床, 这就是一个URL

在这里插入图片描述

  • 一般构建UriComponents我们使用UriComponentsBuilder构建器
// @since 3.1 是个抽象类。
public abstract class UriComponents implements Serializable {

	// 捕获URI模板变量名
	private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
	
	@Nullable
	private final String scheme;
	@Nullable
	private final String fragment;

	// 唯一构造,是protected 的
	protected UriComponents(@Nullable String scheme, @Nullable String fragment) {
		this.scheme = scheme;
		this.fragment = fragment;
	}

	... // 省略它俩的get方法(无set方法)
	@Nullable
	public abstract String getSchemeSpecificPart();
	@Nullable
	public abstract String getUserInfo();
	@Nullable
	public abstract String getHost();
	// 如果没有设置port,就返回-1
	public abstract int getPort();
	@Nullable
	public abstract String getPath();
	public abstract List<String> getPathSegments();
	@Nullable
	public abstract String getQuery();
	public abstract MultiValueMap<String, String> getQueryParams();

	// 此方法是public且是final的哦~
	// 注意它的返回值还是UriComponents
	public final UriComponents encode() {
		return encode(StandardCharsets.UTF_8);
	}
	public abstract UriComponents encode(Charset charset);

	// 这是它最为强大的功能:对模版变量的支持
	// 用给定Map映射中的值替换**所有**URI模板变量
	public final UriComponents expand(Map<String, ?> uriVariables) {
		return expandInternal(new MapTemplateVariables(uriVariables));
	}
	// 给定的是变量数组,那就按照顺序替换
	public final UriComponents expand(Object... uriVariableValues) {...}
	public final UriComponents expand(UriTemplateVariables uriVariables) { ... }

	// 真正的expand方法,其实还是子类来实现的
	abstract UriComponents expandInternal(UriTemplateVariables uriVariables);
	// 规范化路径移除**序列**,如“path/…”。
	// 请注意,规范化应用于完整路径,而不是单个路径段。
	public abstract UriComponents normalize();
	// 连接所有URI组件以返回完全格式的URI字符串。
	public abstract String toUriString();
	public abstract URI toUri();

	@Override
	public final String toString() {
		return toUriString();
	}

	// 拷贝
	protected abstract void copyToUriComponentsBuilder(UriComponentsBuilder builder);
	... // 提供静态工具方法expandUriComponent和sanitizeSource
}

它包含有和Http相关的各个部分:如schema、port、path、query等; 此抽象类有两个实现类:OpaqueUriComponentsHierarchicalUriComponents

由于在实际使用中会使用构建器UriComponentsBuilder来创建实例,所以都是面向抽象类编程,并不需要关心具体实现

5.3 UriComponentsBuilder

使用了Builder模式,用于构建UriComponents。实际开发工作中所有的UriComponents都应是通过此构建器构建

在这里插入图片描述

在这里插入图片描述

public class UriComponentsBuilder implements UriBuilder, Cloneable {
	... // 省略所有正则(包括提取查询参数、scheme、port等等等等)
	... // 它所有的构造函数都是protected的
	
	// ******************实例化静态方法(7种)******************

	// 创建一个空的bulder,里面schema,port等等啥都木有
	public static UriComponentsBuilder newInstance() {
		return new UriComponentsBuilder();
	}
	// 直接从path路径里面,分析出一个builder。常用
	public static UriComponentsBuilder fromPath(String path) {...}
	
	// 直接从URI路径里面,分析出一个builder(要注意编码问题)
	public static UriComponentsBuilder fromUri(URI uri) {...}
	
	/*
		String uriString = "/hotels/42?filter={value}";
		UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold");

		/hotels/42?filter=hot&cold
	*/
	public static UriComponentsBuilder fromUriString(String uri) {}
	
	// 直接从httpUrl路径里面,分析出一个builder
	/*
		String urlString = "https://example.com/hotels/42?filter={value}";
		UriComponentsBuilder.fromHttpUrl(urlString).buildAndExpand("hot&cold");
		
		https://example.com/hotels/42?filter=hot&cold
	*/
	public static UriComponentsBuilder fromHttpUrl(String httpUrl) {}
	
	// HttpRequest是HttpMessage的子接口。它的原理是:fromUri(request.getURI())
	// 然后再调用本类的adaptFromForwardedHeaders(request.getHeaders())
	// 解释:从头Forwarded、X-Forwarded-Proto等拿到https、port等设置值~~
	public static UriComponentsBuilder fromHttpRequest(HttpRequest request) {}

	// 和fromUriString()方法差不多
	public static UriComponentsBuilder fromOriginHeader(String origin) {}

	// *******************下面都是实例方法*******************
	
	public final UriComponentsBuilder encode() {
		return encode(StandardCharsets.UTF_8);
	}
	
	public UriComponentsBuilder encode(Charset charset) {}

	// 调用此方法生成一个UriComponents
	public UriComponents build() {
		return build(false);
	}
	
	public UriComponents build(boolean encoded) {
		// encoded=true,取值就是FULLY_ENCODED 全部编码
		// 否则只编码模版或者不编码
		return buildInternal(encoded ? EncodingHint.FULLY_ENCODED :
				(this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE)
				);
	}
	
	// buildInternal内部就会自己new子类:OpaqueUriComponents或者HierarchicalUriComponents
	// 以及执行UriComponents.expand方法了(若指定了参数的话),使用者不用关心了
	
	// 显然这就是个多功能方法了:设置好参数。build后立马Expand
	public UriComponents buildAndExpand(Map<String, ?> uriVariables) {
		return build().expand(uriVariables);
	}
	
	public UriComponents buildAndExpand(Object... uriVariableValues) {}

	//build成为一个URI。注意这里编码方式是:EncodingHint.ENCODE_TEMPLATE
	@Override
	public URI build(Object... uriVariables) {
		return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri();
	}
	
	@Override
	public URI build(Map<String, ?> uriVariables) {
		return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri();
	}

	// @since 4.1
	public String toUriString() { ... }

	// ====重构/重新设置Builder====
	public UriComponentsBuilder uri(URI uri) {}
	
	public UriComponentsBuilder uriComponents(UriComponents uriComponents) {}
	
	@Override
	public UriComponentsBuilder scheme(@Nullable String scheme) {
		this.scheme = scheme;
		return this;
	}
	
	@Override
	public UriComponentsBuilder userInfo(@Nullable String userInfo) {
		this.userInfo = userInfo;
		resetSchemeSpecificPart();
		return this;
	}
	
	public UriComponentsBuilder host(@Nullable String host){ ... }
	... // 省略其它部分

	// 给URL后面拼接查询参数(键值对)
	@Override
	public UriComponentsBuilder query(@Nullable String query) {}
	
	// 遇上相同的key就替代,而不是直接在后面添加了(上面query是添加)
	@Override
	public UriComponentsBuilder replaceQuery(@Nullable String query) {}
	
	@Override
	public UriComponentsBuilder queryParam(String name, Object... values) {}
	... replaceQueryParam

	// 可以先单独设置参数,但不expend哦~
	public UriComponentsBuilder uriVariables(Map<String, Object> uriVariables) {}

	@Override
	public Object clone() {
		return cloneBuilder();
	}
	// @since 4.2.7
	public UriComponentsBuilder cloneBuilder() {
		return new UriComponentsBuilder(this);
	}
	...
}

Demo Test

public static void main(String[] args) {
    String url;
    UriComponents uriComponents = UriComponentsBuilder.newInstance()
            //.encode(StandardCharsets.UTF_8)
            .scheme("https").host("www.baidu.com").path("/test").path("/{template}") //此处{}就成 不要写成${}
            //.uriVariables(传一个Map).build();
            .build().expand("myhome"); // 此效果同上一句,但推荐这么使用,方便一些
    url = uriComponents.toUriString();
    System.out.println(url); // https://www.baidu.com/test/myhome

    // 从URL字符串中构造(注意:toUriString方法内部是调用了build和expend方法的)
    System.out.println(UriComponentsBuilder.fromHttpUrl(url).toUriString()); // https://www.baidu.com/test/myhome
    System.out.println(UriComponentsBuilder.fromUriString(url).toUriString()); // https://www.baidu.com/test/myhome

    // 给URL中放添加参数 query和replaceQuery
    uriComponents = UriComponentsBuilder.fromHttpUrl(url).query("name=中国&age=18").query("&name=二次拼接").build();
    url = uriComponents.toUriString();
    // 效果描述:&test前面这个&不写也是木有问题的。并且两个name都出现了
    System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=中国&name=二次拼接&age=18

    uriComponents = UriComponentsBuilder.fromHttpUrl(url).query("name=中国&age=18").replaceQuery("name=二次拼接").build();
    url = uriComponents.toUriString();
    // 这种够狠:后面的直接覆盖前面“所有的”查询串
    System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=二次拼接

    //queryParam/queryParams/replaceQueryParam/replaceQueryParams
    // queryParam:一次性指定一个key,queryParams一次性可以搞多个key
    url = "https://www.baidu.com/test/myhome"; // 重置一下
    uriComponents = UriComponentsBuilder.fromHttpUrl(url).queryParam("name","中国","美国").queryParam("age",18)
            .queryParam("name","英国").build();
    url = uriComponents.toUriString();
    // 发现是不会有repalace的效果的
    System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=中国&name=美国&name=英国&age=18
    
    // 关于repalceParam相关方法,交给各位自己去试验吧~~~

    // 不需要domain,构建局部路径,它也是把好手
    uriComponents = UriComponentsBuilder.fromPath("").path("/test").build();
    // .fromPath("/").path("/test") --> /test
    // .fromPath("").path("/test") --> /test
    // .fromPath("").path("//test") --> /test
    // .fromPath("").path("test") --> /test
    System.out.println(uriComponents.toUriString()); // /test?name=fsx
}

使用这种方式来构建URL还是非常方便的,它的容错性非常高,写法灵活且不容易出错。

  • URI构建的任意部分(包括查询参数、scheme等等)都是可以用{}这种形式的模版参数的
  • 被替换的模版中还支持这么来写:/myurl/{name:[a-z]}/show,这样用expand也能正常赋值

使用案例

public abstract class AbstractRemoteHelper {
    @Autowired
    private RestTemplate restTemplate;

    protected abstract URI buildURI(String apiPath, Map<String, Object> params);

    protected <T> T executeForBean(String apiPath, Map<String, Object> params) {
        return this.restTemplate.exchange(
                buildURI(apiPath, params),
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<T>() {
                }
        ).getBody();
    }

    protected String executeForRaw(String apiPath, Map<String, Object> params) {
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return this.restTemplate.exchange(
                buildURI(apiPath, params),
                HttpMethod.GET,
                null,
                String.class
        ).getBody();
    }

    protected String executeForRawPost(String apiPath, JSONObject object) {
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        HttpEntity<Object> requestEntity = new HttpEntity<>(object, null);
        return this.restTemplate.exchange(
                buildURI(apiPath, null),
                HttpMethod.POST,
                requestEntity,
                String.class
        ).getBody();
    }

    protected String executeForRawAndHeaderPost(String apiPath,  @Nullable Map<String, Object> params,
                                       @Nullable Map<String, String> headers) {
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        HttpHeaders httpHeaders = null;
        if (headers != null) {
            httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON);
            Iterator<Map.Entry<String, String>> it = headers.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, String> next = it.next();
                httpHeaders.add(next.getKey(), next.getValue());
            }
        }
        HttpEntity<Object> requestEntity = new HttpEntity<>(params, httpHeaders);
        return this.restTemplate.exchange(
                buildURI(apiPath, null),
                HttpMethod.POST,
                requestEntity,
                String.class
        ).getBody();
    }
}
@Component
public class MatrixHelper extends AbstractRemoteHelper {

    @Value("${matrix.url}")
    private String matrixUrl;

    private static final String STATISTIC_REPORT = "/usoppu/getUsoppuParamChart";

    private static final String LOG_BY_CONDITION_REPORT = "/usoppu/getUsoppuLogByCondition";


    public List<SoaStatisticEntity> getUsoppuParamChartData(Map<String, Object> params) {
        params.put("appId", "AppTest");
        params.put("contrast", 0);
        params.put("alertType", 0);
        String result = this.executeForRaw(MatrixHelper.STATISTIC_REPORT, params);
        JSONObject data = JSON.parseObject(result);


        List<SoaStatisticEntity> allList = new ArrayList<>();
        if (data != null) {
            JSONArray records = data.getJSONArray("data");
            List<SoaStatisticEntity> latencyList = records.stream().map(list -> {
                SoaStatisticEntity entity = new SoaStatisticEntity();
                JSONObject t = (JSONObject) list;
                entity.setAppId(t.getString("name"));
                entity.setNumber(t.getBigDecimal("value").stripTrailingZeros());
                return entity;
            }).collect(Collectors.toList());
            if(CollectionUtils.isNotEmpty(latencyList)){
                allList.addAll(latencyList);
            }
        }
        return allList;
    }


    public JSONArray getUsoppuLogByCondition(Map<String, Object> params,Integer pageIndex) {
        params.put("appId", "AppTest");
        params.put("size", 20);
        params.put("current", pageIndex);
        params.put("isError", 1);
        params.put("sortOrder", 0);
        String result = this.executeForRaw(MatrixHelper.LOG_BY_CONDITION_REPORT, params);
        JSONObject data = JSON.parseObject(result);

        if (data != null) {
            JSONArray records = data.getJSONObject("data").getJSONArray("records");
         return records;
        }
        return new JSONArray();
    }

    @Override
    protected URI buildURI(String apiPath, Map<String, Object> params) {
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(matrixUrl)
                .path(apiPath);
        if (MapUtils.isNotEmpty(params)) {
            params.forEach((key, value) -> builder.queryParam(key, value));
        }
        URI uri = builder.build(true).encode().toUri();
        return uri;
    }
}
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
使用RestTemplate发送GET请求的步骤如下: 1. 创建 RestTemplate 实例。 ```java RestTemplate restTemplate = new RestTemplate(); ``` 2. 使用 `getForObject()` 或 `getForEntity()` 方法发送请求并获取响应。 ```java String url = "http://example.com/api/data"; String response = restTemplate.getForObject(url, String.class); // 或者 ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class); String response = responseEntity.getBody(); ``` 其中,`getForObject()` 方法直接返回响应体,而 `getForEntity()` 方法返回一个 `ResponseEntity` 对象,包含了响应头、响应状态码等信息。 3. 可以在请求中传递参数,例如: ```java String url = "http://example.com/api/data?param1=value1&param2=value2"; String response = restTemplate.getForObject(url, String.class); ``` 也可以使用 `UriComponentsBuilder` 来构建带参数的 URL,例如: ```java UriComponents uriComponents = UriComponentsBuilder .fromUriString("http://example.com/api/data") .queryParam("param1", "value1") .queryParam("param2", "value2") .build(); String url = uriComponents.toUriString(); String response = restTemplate.getForObject(url, String.class); ``` 4. 如果需要设置请求头,可以使用 `HttpHeaders` 对象,例如: ```java HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); headers.set("User-Agent", "Mozilla/5.0"); HttpEntity<?> entity = new HttpEntity<>(headers); String url = "http://example.com/api/data"; String response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class).getBody(); ``` 这里使用 `exchange()` 方法发送请求,并将请求头和请求体封装到 `HttpEntity` 对象中。 5. 最后别忘了处理异常情况,例如: ```java try { String url = "http://example.com/api/data"; String response = restTemplate.getForObject(url, String.class); } catch (RestClientException e) { // 处理异常 } ``` RestClientException 是 RestTemplate 发送请求时可能抛出的异常,例如网络异常、HTTP 错误等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

white camel

感谢支持~

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

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

打赏作者

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

抵扣说明:

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

余额充值