Spring源码设计模式:模板方法(Method Template)之上篇

目录

模板模式

RestTemplate

AsyncRestTemplate (ListenableFuture异步处理)

JdbcTemplate

HibernateTemplate

参考文章


模板模式

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

在spring源码中使用到模板模式的有以下几类(在Spring中大多数模板方式都是行为接口的定义:*Callback):

  • RestTemplate  Http Restful接口请求模板
  • AsyncRestTemplate 异步Http Restful接口请求模板
  • JdbcTemplate JDBC关系型数据库操作模板
  • HibernateTemplate Hibernate关系型数据库操作模板
  • JmsTemplate 消息队列模板
  • TransactionTemplate 编程式事务模板

特别说明:RedisTemplate Redis缓存数据操作模板(ReidsTemplate属于 spring-boot-starter-data-redis)

<!--Redis RedisTemplate-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

===介绍===

意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

主要解决:一些方法通用,却在每一个子类都重新写了这一方法。

何时使用:有一些通用的方法。

如何解决:将这些通用算法抽象出来。

关键代码:在抽象类实现,其他步骤在子类实现。

应用实例: 1、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2、西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。 3、spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。

优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。

缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。

注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。

===实现===

我们将创建一个定义操作的 Game 抽象类,其中,模板方法设置为 final,这样它就不会被重写。Cricket 和 Football 是扩展了 Game 的实体类,它们重写了抽象类的方法。

TemplatePatternDemo,我们的演示类使用 Game 来演示模板模式的用法。

模板模式的 UML 图

步骤 1

创建一个抽象类,它的模板方法被设置为 final。Game.java

public abstract class Game {
   abstract void initialize();
   abstract void startPlay();
   abstract void endPlay();
 
   //模板
   public final void play(){
 
      //初始化游戏
      initialize();
 
      //开始游戏
      startPlay();
 
      //结束游戏
      endPlay();
   }
}

步骤 2

创建扩展了上述类的实体类。Cricket.java

public class Cricket extends Game {
 
   @Override
   void endPlay() {
      System.out.println("Cricket Game Finished!");
   }
 
   @Override
   void initialize() {
      System.out.println("Cricket Game Initialized! Start playing.");
   }
 
   @Override
   void startPlay() {
      System.out.println("Cricket Game Started. Enjoy the game!");
   }
}

Football.java

public class Football extends Game {
 
   @Override
   void endPlay() {
      System.out.println("Football Game Finished!");
   }
 
   @Override
   void initialize() {
      System.out.println("Football Game Initialized! Start playing.");
   }
 
   @Override
   void startPlay() {
      System.out.println("Football Game Started. Enjoy the game!");
   }
}

步骤 3

使用 Game 的模板方法 play() 来演示游戏的定义方式。TemplatePatternDemo.java

public class TemplatePatternDemo {
   public static void main(String[] args) {
 
      Game game = new Cricket();
      game.play();
      System.out.println();
      game = new Football();
      game.play();      
   }
}

步骤 4

执行程序,输出结果:

Cricket Game Initialized! Start playing.
Cricket Game Started. Enjoy the game!
Cricket Game Finished!

Football Game Initialized! Start playing.
Football Game Started. Enjoy the game!
Football Game Finished!

以上为转载内容,下面进入正题。

RestTemplate

RestTemplate通过HTTP方法提供常见方案的模板,在除了通用的{@code exchange}和{@code execute}方法之外,支持频率较低的问题。

自5.0以来,此类处于维护模式,以后只接受较小的更改和错误请求。 请考虑使用{@code org.springframework.web.reactive.client.WebClient}
它具有更现代的API,并支持同步,异步和流传输方案。

该类所属包位于org.springframework.web.client ,继承自InterceptingHttpAccessor并实现 RestOperations接口:

package org.springframework.web.client;

import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.SpringProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.InterceptingHttpAccessor;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
import org.springframework.web.util.UriTemplateHandler;

/**
 * Synchronous client to perform HTTP requests, exposing a simple, template
 * method API over underlying HTTP client libraries such as the JDK
 * {@code HttpURLConnection}, Apache HttpComponents, and others.
 *
 * <p>The RestTemplate offers templates for common scenarios by HTTP method, in
 * addition to the generalized {@code exchange} and {@code execute} methods that
 * support of less frequent cases.
 *
 * <p><strong>NOTE:</strong> As of 5.0 this class is in maintenance mode, with
 * only minor requests for changes and bugs to be accepted going forward. Please,
 * consider using the {@code org.springframework.web.reactive.client.WebClient}
 * which has a more modern API and supports sync, async, and streaming scenarios.
 *
 * @author Arjen Poutsma
 * @author Brian Clozel
 * @author Roy Clarkson
 * @author Juergen Hoeller
 * @author Sam Brannen
 * @author Sebastien Deleuze
 * @since 3.0
 * @see HttpMessageConverter
 * @see RequestCallback
 * @see ResponseExtractor
 * @see ResponseErrorHandler
 */
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
   
   

InterceptingHttpAccessor类位于org.springframework.http.client.support,添加与拦截器有关的属性:

package org.springframework.http.client.support;

import java.util.ArrayList;
import java.util.List;

import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

/**
 * Base class for {@link org.springframework.web.client.RestTemplate}
 * and other HTTP accessing gateway helpers, adding interceptor-related
 * properties to {@link HttpAccessor}'s common properties.
 *
 * <p>Not intended to be used directly.
 * See {@link org.springframework.web.client.RestTemplate} for an entry point.
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 3.0
 * @see ClientHttpRequestInterceptor
 * @see InterceptingClientHttpRequestFactory
 * @see org.springframework.web.client.RestTemplate
 */
public abstract class InterceptingHttpAccessor extends HttpAccessor {
   
   

RestOperations类位于org.springframework.web.client,负责Http Restful风格相关请求的参数配置:

package org.springframework.web.client;

import java.net.URI;
import java.util.Map;
import java.util.Set;

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;

/**
 * Interface specifying a basic set of RESTful operations.
 * Implemented by {@link RestTemplate}. Not often used directly, but a useful
 * option to enhance testability, as it can easily be mocked or stubbed.
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 3.0
 * @see RestTemplate
 */
public interface RestOperations {
   
   

模板核心执行请求方法:

// General execution

	/**
	 * {@inheritDoc}
	 * <p>To provide a {@code RequestCallback} or {@code ResponseExtractor} only,
	 * but not both, consider using:
	 * <ul>
	 * <li>{@link #acceptHeaderRequestCallback(Class)}
	 * <li>{@link #httpEntityCallback(Object)}
	 * <li>{@link #httpEntityCallback(Object, Type)}
	 * <li>{@link #responseEntityExtractor(Type)}
	 * </ul>
	 */
	@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);
	}

 执行最终的HTTP接口请求:

	/**
	 * Execute the given method on the provided URI.
	 * <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};
	 * the response with the {@link ResponseExtractor}.
	 * @param url the fully-expanded URL to connect to
	 * @param method the HTTP method to execute (GET, POST, etc.)
	 * @param requestCallback object that prepares the request (can be {@code null})
	 * @param responseExtractor object that extracts the return value from the response (can be {@code null})
	 * @return an arbitrary object, as returned by the {@link ResponseExtractor}
	 */
	@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 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();
			}
		}
	}

RequestCallback功能性接口定义:

package org.springframework.web.client;

import java.io.IOException;
import java.lang.reflect.Type;

import org.springframework.http.client.ClientHttpRequest;

/**
 * Callback interface for code that operates on a {@link ClientHttpRequest}.
 * Allows manipulating the request headers, and write to the request body.
 *
 * <p>Used internally by the {@link RestTemplate}, but also useful for
 * application code. There several available factory methods:
 * <ul>
 * <li>{@link RestTemplate#acceptHeaderRequestCallback(Class)}
 * <li>{@link RestTemplate#httpEntityCallback(Object)}
 * <li>{@link RestTemplate#httpEntityCallback(Object, Type)}
 * </ul>
 *
 * @author Arjen Poutsma
 * @see RestTemplate#execute
 * @since 3.0
 */
@FunctionalInterface
public interface RequestCallback {

	/**
	 * Gets called by {@link RestTemplate#execute} with an opened {@code ClientHttpRequest}.
	 * Does not need to care about closing the request or about handling errors:
	 * this will all be handled by the {@code RestTemplate}.
	 * @param request the active HTTP request
	 * @throws IOException in case of I/O errors
	 */
	void doWithRequest(ClientHttpRequest request) throws IOException;

}

示例程序:

/*
 * Copyright 2002-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.client;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInitializer;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.DefaultUriBuilderFactory;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.HEAD;
import static org.springframework.http.HttpMethod.OPTIONS;
import static org.springframework.http.HttpMethod.PATCH;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
import static org.springframework.http.MediaType.parseMediaType;

/**
 * Unit tests for {@link RestTemplate}.
 *
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @author Brian Clozel
 * @author Sam Brannen
 */
@SuppressWarnings("unchecked")
class RestTemplateTests {

	private final ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class);

	private final ClientHttpRequest request = mock(ClientHttpRequest.class);

	private final ClientHttpResponse response = mock(ClientHttpResponse.class);

	private final ResponseErrorHandler errorHandler = mock(ResponseErrorHandler.class);

	@SuppressWarnings("rawtypes")
	private final HttpMessageConverter converter = mock(HttpMessageConverter.class);

	private final RestTemplate template = new RestTemplate(Collections.singletonList(converter));


	@BeforeEach
	void setup() {
		template.setRequestFactory(requestFactory);
		template.setErrorHandler(errorHandler);
	}

	@Test
	void constructorPreconditions() {
		assertThatIllegalArgumentException()
				.isThrownBy(() -> new RestTemplate((List<HttpMessageConverter<?>>) null))
				.withMessage("At least one HttpMessageConverter is required");
		assertThatIllegalArgumentException()
				.isThrownBy(() -> new RestTemplate(Arrays.asList(null, this.converter)))
				.withMessage("The HttpMessageConverter list must not contain null elements");
	}

	@Test
	void setMessageConvertersPreconditions() {
		assertThatIllegalArgumentException()
				.isThrownBy(() -> template.setMessageConverters((List<HttpMessageConverter<?>>) null))
				.withMessage("At least one HttpMessageConverter is required");
		assertThatIllegalArgumentException()
				.isThrownBy(() -> template.setMessageConverters(Arrays.asList(null, this.converter)))
				.withMessage("The HttpMessageConverter list must not contain null elements");
	}

	@Test
	void varArgsTemplateVariables() throws Exception {
		mockSentRequest(GET, "https://example.com/hotels/42/bookings/21");
		mockResponseStatus(HttpStatus.OK);

		template.execute("https://example.com/hotels/{hotel}/bookings/{booking}", GET,
				null, null, "42", "21");

		verify(response).close();
	}

	@Test
	void varArgsNullTemplateVariable() throws Exception {
		mockSentRequest(GET, "https://example.com/-foo");
		mockResponseStatus(HttpStatus.OK);

		template.execute("https://example.com/{first}-{last}", GET, null, null, null, "foo");

		verify(response).close();
	}

	@Test
	void mapTemplateVariables() throws Exception {
		mockSentRequest(GET, "https://example.com/hotels/42/bookings/42");
		mockResponseStatus(HttpStatus.OK);

		Map<String, String> vars = Collections.singletonMap("hotel", "42");
		template.execute("https://example.com/hotels/{hotel}/bookings/{hotel}", GET, null, null, vars);

		verify(response).close();
	}

	@Test
	void mapNullTemplateVariable() throws Exception {
		mockSentRequest(GET, "https://example.com/-foo");
		mockResponseStatus(HttpStatus.OK);

		Map<String, String> vars = new HashMap<>(2);
		vars.put("first", null);
		vars.put("last", "foo");
		template.execute("https://example.com/{first}-{last}", GET, null, null, vars);

		verify(response).close();
	}

	@Test  // SPR-15201
	void uriTemplateWithTrailingSlash() throws Exception {
		String url = "https://example.com/spring/";
		mockSentRequest(GET, url);
		mockResponseStatus(HttpStatus.OK);

		template.execute(url, GET, null, null);

		verify(response).close();
	}

	@Test
	void errorHandling() throws Exception {
		String url = "https://example.com";
		mockSentRequest(GET, url);
		mockResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR);
		willThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR))
				.given(errorHandler).handleError(new URI(url), GET, response);

		assertThatExceptionOfType(HttpServerErrorException.class).isThrownBy(() ->
				template.execute(url, GET, null, null));

		verify(response).close();
	}

	@Test
	void getForObject() throws Exception {
		String expected = "Hello World";
		mockTextPlainHttpMessageConverter();
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(GET, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		mockTextResponseBody("Hello World");

		String result = template.getForObject("https://example.com", String.class);
		assertThat(result).as("Invalid GET result").isEqualTo(expected);
		assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);

		verify(response).close();
	}

	@Test
	void getUnsupportedMediaType() throws Exception {
		mockSentRequest(GET, "https://example.com/resource");
		mockResponseStatus(HttpStatus.OK);

		given(converter.canRead(String.class, null)).willReturn(true);
		MediaType supportedMediaType = new MediaType("foo", "bar");
		given(converter.getSupportedMediaTypes()).willReturn(Collections.singletonList(supportedMediaType));

		MediaType barBaz = new MediaType("bar", "baz");
		mockResponseBody("Foo", new MediaType("bar", "baz"));
		given(converter.canRead(String.class, barBaz)).willReturn(false);

		assertThatExceptionOfType(RestClientException.class).isThrownBy(() ->
				template.getForObject("https://example.com/{p}", String.class, "resource"));

		verify(response).close();
	}

	@Test
	void requestAvoidsDuplicateAcceptHeaderValues() throws Exception {
		HttpMessageConverter<?> firstConverter = mock(HttpMessageConverter.class);
		given(firstConverter.canRead(any(), any())).willReturn(true);
		given(firstConverter.getSupportedMediaTypes())
				.willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
		HttpMessageConverter<?> secondConverter = mock(HttpMessageConverter.class);
		given(secondConverter.canRead(any(), any())).willReturn(true);
		given(secondConverter.getSupportedMediaTypes())
				.willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));

		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(GET, "https://example.com/", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		mockTextResponseBody("Hello World");

		template.setMessageConverters(Arrays.asList(firstConverter, secondConverter));
		template.getForObject("https://example.com/", String.class);

		assertThat(requestHeaders.getAccept().size()).as("Sent duplicate Accept header values").isEqualTo(1);
	}

	@Test
	void getForEntity() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(GET, "https://example.com", requestHeaders);
		mockTextPlainHttpMessageConverter();
		mockResponseStatus(HttpStatus.OK);
		String expected = "Hello World";
		mockTextResponseBody(expected);

		ResponseEntity<String> result = template.getForEntity("https://example.com", String.class);
		assertThat(result.getBody()).as("Invalid GET result").isEqualTo(expected);
		assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
		assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN);
		assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);

		verify(response).close();
	}

	@Test
	void getForObjectWithCustomUriTemplateHandler() throws Exception {
		DefaultUriBuilderFactory uriTemplateHandler = new DefaultUriBuilderFactory();
		template.setUriTemplateHandler(uriTemplateHandler);
		mockSentRequest(GET, "https://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150");
		mockResponseStatus(HttpStatus.OK);
		given(response.getHeaders()).willReturn(new HttpHeaders());
		given(response.getBody()).willReturn(StreamUtils.emptyInput());

		Map<String, String> uriVariables = new HashMap<>(2);
		uriVariables.put("hotel", "1");
		uriVariables.put("publicpath", "pics/logo.png");
		uriVariables.put("scale", "150x150");

		String url = "https://example.com/hotels/{hotel}/pic/{publicpath}/size/{scale}";
		template.getForObject(url, String.class, uriVariables);

		verify(response).close();
	}

	@Test
	void headForHeaders() throws Exception {
		mockSentRequest(HEAD, "https://example.com");
		mockResponseStatus(HttpStatus.OK);
		HttpHeaders responseHeaders = new HttpHeaders();
		given(response.getHeaders()).willReturn(responseHeaders);

		HttpHeaders result = template.headForHeaders("https://example.com");

		assertThat(result).as("Invalid headers returned").isSameAs(responseHeaders);

		verify(response).close();
	}

	@Test
	void postForLocation() throws Exception {
		mockSentRequest(POST, "https://example.com");
		mockTextPlainHttpMessageConverter();
		mockResponseStatus(HttpStatus.OK);
		String helloWorld = "Hello World";
		HttpHeaders responseHeaders = new HttpHeaders();
		URI expected = new URI("https://example.com/hotels");
		responseHeaders.setLocation(expected);
		given(response.getHeaders()).willReturn(responseHeaders);

		URI result = template.postForLocation("https://example.com", helloWorld);
		assertThat(result).as("Invalid POST result").isEqualTo(expected);

		verify(response).close();
	}

	@Test
	void postForLocationEntityContentType() throws Exception {
		mockSentRequest(POST, "https://example.com");
		mockTextPlainHttpMessageConverter();
		mockResponseStatus(HttpStatus.OK);

		String helloWorld = "Hello World";
		HttpHeaders responseHeaders = new HttpHeaders();
		URI expected = new URI("https://example.com/hotels");
		responseHeaders.setLocation(expected);
		given(response.getHeaders()).willReturn(responseHeaders);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(MediaType.TEXT_PLAIN);
		HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);

		URI result = template.postForLocation("https://example.com", entity);
		assertThat(result).as("Invalid POST result").isEqualTo(expected);

		verify(response).close();
	}

	@Test
	void postForLocationEntityCustomHeader() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockTextPlainHttpMessageConverter();
		mockResponseStatus(HttpStatus.OK);
		HttpHeaders responseHeaders = new HttpHeaders();
		URI expected = new URI("https://example.com/hotels");
		responseHeaders.setLocation(expected);
		given(response.getHeaders()).willReturn(responseHeaders);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.set("MyHeader", "MyValue");
		HttpEntity<String> entity = new HttpEntity<>("Hello World", entityHeaders);

		URI result = template.postForLocation("https://example.com", entity);
		assertThat(result).as("Invalid POST result").isEqualTo(expected);
		assertThat(requestHeaders.getFirst("MyHeader")).as("No custom header set").isEqualTo("MyValue");

		verify(response).close();
	}

	@Test
	void postForLocationNoLocation() throws Exception {
		mockSentRequest(POST, "https://example.com");
		mockTextPlainHttpMessageConverter();
		mockResponseStatus(HttpStatus.OK);

		URI result = template.postForLocation("https://example.com", "Hello World");
		assertThat(result).as("Invalid POST result").isNull();

		verify(response).close();
	}

	@Test
	void postForLocationNull() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);

		template.postForLocation("https://example.com", null);
		assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0);

		verify(response).close();
	}

	@Test
	void postForObject() throws Exception {
		mockTextPlainHttpMessageConverter();
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		String expected = "42";
		mockResponseBody(expected, MediaType.TEXT_PLAIN);

		String result = template.postForObject("https://example.com", "Hello World", String.class);
		assertThat(result).as("Invalid POST result").isEqualTo(expected);
		assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);

		verify(response).close();
	}

	@Test
	void postForEntity() throws Exception {
		mockTextPlainHttpMessageConverter();
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		String expected = "42";
		mockResponseBody(expected, MediaType.TEXT_PLAIN);

		ResponseEntity<String> result = template.postForEntity("https://example.com", "Hello World", String.class);
		assertThat(result.getBody()).as("Invalid POST result").isEqualTo(expected);
		assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN);
		assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
		assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);

		verify(response).close();
	}

	@Test
	void postForObjectNull() throws Exception {
		mockTextPlainHttpMessageConverter();
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		HttpHeaders responseHeaders = new HttpHeaders();
		responseHeaders.setContentType(MediaType.TEXT_PLAIN);
		responseHeaders.setContentLength(10);
		given(response.getHeaders()).willReturn(responseHeaders);
		given(response.getBody()).willReturn(StreamUtils.emptyInput());
		given(converter.read(String.class, response)).willReturn(null);

		String result = template.postForObject("https://example.com", null, String.class);
		assertThat(result).as("Invalid POST result").isNull();
		assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0);

		verify(response).close();
	}

	@Test
	void postForEntityNull() throws Exception {
		mockTextPlainHttpMessageConverter();
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		HttpHeaders responseHeaders = new HttpHeaders();
		responseHeaders.setContentType(MediaType.TEXT_PLAIN);
		responseHeaders.setContentLength(10);
		given(response.getHeaders()).willReturn(responseHeaders);
		given(response.getBody()).willReturn(StreamUtils.emptyInput());
		given(converter.read(String.class, response)).willReturn(null);

		ResponseEntity<String> result = template.postForEntity("https://example.com", null, String.class);
		assertThat(result.hasBody()).as("Invalid POST result").isFalse();
		assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN);
		assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0);
		assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);

		verify(response).close();
	}

	@Test
	void put() throws Exception {
		mockTextPlainHttpMessageConverter();
		mockSentRequest(PUT, "https://example.com");
		mockResponseStatus(HttpStatus.OK);

		template.put("https://example.com", "Hello World");

		verify(response).close();
	}

	@Test
	void putNull() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(PUT, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);

		template.put("https://example.com", null);
		assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0);

		verify(response).close();
	}

	@Test // gh-23740
	void headerAcceptAllOnPut() throws Exception {
		try (MockWebServer server = new MockWebServer()) {
			server.enqueue(new MockResponse().setResponseCode(500).setBody("internal server error"));
			server.start();
			template.setRequestFactory(new SimpleClientHttpRequestFactory());
			template.put(server.url("/internal/server/error").uri(), null);
			assertThat(server.takeRequest().getHeader("Accept")).isEqualTo("*/*");
		}
	}

	@Test // gh-23740
	void keepGivenAcceptHeaderOnPut() throws Exception {
		try (MockWebServer server = new MockWebServer()) {
			server.enqueue(new MockResponse().setResponseCode(500).setBody("internal server error"));
			server.start();
			HttpHeaders headers = new HttpHeaders();
			headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
			HttpEntity<String> entity = new HttpEntity<>(null, headers);
			template.setRequestFactory(new SimpleClientHttpRequestFactory());
			template.exchange(server.url("/internal/server/error").uri(), PUT, entity, Void.class);

			RecordedRequest request = server.takeRequest();

			final List<List<String>> accepts = request.getHeaders().toMultimap().entrySet().stream()
					.filter(entry -> entry.getKey().equalsIgnoreCase("accept"))
					.map(Entry::getValue)
					.collect(Collectors.toList());

			assertThat(accepts).hasSize(1);
			assertThat(accepts.get(0)).hasSize(1);
			assertThat(accepts.get(0).get(0)).isEqualTo("application/json");
		}
	}

	@Test
	void patchForObject() throws Exception {
		mockTextPlainHttpMessageConverter();
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(PATCH, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		String expected = "42";
		mockResponseBody("42", MediaType.TEXT_PLAIN);

		String result = template.patchForObject("https://example.com", "Hello World", String.class);
		assertThat(result).as("Invalid POST result").isEqualTo(expected);
		assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);

		verify(response).close();
	}

	@Test
	void patchForObjectNull() throws Exception {
		mockTextPlainHttpMessageConverter();
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(PATCH, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		HttpHeaders responseHeaders = new HttpHeaders();
		responseHeaders.setContentType(MediaType.TEXT_PLAIN);
		responseHeaders.setContentLength(10);
		given(response.getHeaders()).willReturn(responseHeaders);
		given(response.getBody()).willReturn(StreamUtils.emptyInput());

		String result = template.patchForObject("https://example.com", null, String.class);
		assertThat(result).as("Invalid POST result").isNull();
		assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0);

		verify(response).close();
	}

	@Test
	void delete() throws Exception {
		mockSentRequest(DELETE, "https://example.com");
		mockResponseStatus(HttpStatus.OK);

		template.delete("https://example.com");

		verify(response).close();
	}

	@Test // gh-23740
	void headerAcceptAllOnDelete() throws Exception {
		try (MockWebServer server = new MockWebServer()) {
			server.enqueue(new MockResponse().setResponseCode(500).setBody("internal server error"));
			server.start();
			template.setRequestFactory(new SimpleClientHttpRequestFactory());
			template.delete(server.url("/internal/server/error").uri());
			assertThat(server.takeRequest().getHeader("Accept")).isEqualTo("*/*");
		}
	}

	@Test
	void optionsForAllow() throws Exception {
		mockSentRequest(OPTIONS, "https://example.com");
		mockResponseStatus(HttpStatus.OK);
		HttpHeaders responseHeaders = new HttpHeaders();
		EnumSet<HttpMethod> expected = EnumSet.of(GET, POST);
		responseHeaders.setAllow(expected);
		given(response.getHeaders()).willReturn(responseHeaders);

		Set<HttpMethod> result = template.optionsForAllow("https://example.com");
		assertThat(result).as("Invalid OPTIONS result").isEqualTo(expected);

		verify(response).close();
	}

	@Test  // SPR-9325, SPR-13860
	void ioException() throws Exception {
		String url = "https://example.com/resource?access_token=123";
		mockSentRequest(GET, url);
		mockHttpMessageConverter(new MediaType("foo", "bar"), String.class);
		given(request.execute()).willThrow(new IOException("Socket failure"));

		assertThatExceptionOfType(ResourceAccessException.class).isThrownBy(() ->
				template.getForObject(url, String.class))
			.withMessage("I/O error on GET request for \"https://example.com/resource\": " +
							"Socket failure; nested exception is java.io.IOException: Socket failure");
	}

	@Test  // SPR-15900
	void ioExceptionWithEmptyQueryString() throws Exception {
		// https://example.com/resource?
		URI uri = new URI("https", "example.com", "/resource", "", null);

		given(converter.canRead(String.class, null)).willReturn(true);
		given(converter.getSupportedMediaTypes()).willReturn(Collections.singletonList(parseMediaType("foo/bar")));
		given(requestFactory.createRequest(uri, GET)).willReturn(request);
		given(request.getHeaders()).willReturn(new HttpHeaders());
		given(request.execute()).willThrow(new IOException("Socket failure"));

		assertThatExceptionOfType(ResourceAccessException.class).isThrownBy(() ->
				template.getForObject(uri, String.class))
			.withMessage("I/O error on GET request for \"https://example.com/resource\": " +
					"Socket failure; nested exception is java.io.IOException: Socket failure");
	}

	@Test
	void exchange() throws Exception {
		mockTextPlainHttpMessageConverter();
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);
		String expected = "42";
		mockResponseBody(expected, MediaType.TEXT_PLAIN);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.set("MyHeader", "MyValue");
		HttpEntity<String> entity = new HttpEntity<>("Hello World", entityHeaders);
		ResponseEntity<String> result = template.exchange("https://example.com", POST, entity, String.class);
		assertThat(result.getBody()).as("Invalid POST result").isEqualTo(expected);
		assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN);
		assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
		assertThat(requestHeaders.getFirst("MyHeader")).as("Invalid custom header").isEqualTo("MyValue");
		assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);

		verify(response).close();
	}

	@Test
	@SuppressWarnings("rawtypes")
	void exchangeParameterizedType() throws Exception {
		GenericHttpMessageConverter converter = mock(GenericHttpMessageConverter.class);
		template.setMessageConverters(Collections.<HttpMessageConverter<?>>singletonList(converter));
		ParameterizedTypeReference<List<Integer>> intList = new ParameterizedTypeReference<List<Integer>>() {};
		given(converter.canRead(intList.getType(), null, null)).willReturn(true);
		given(converter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
		given(converter.canWrite(String.class, String.class, null)).willReturn(true);

		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		List<Integer> expected = Collections.singletonList(42);
		HttpHeaders responseHeaders = new HttpHeaders();
		responseHeaders.setContentType(MediaType.TEXT_PLAIN);
		responseHeaders.setContentLength(10);
		mockResponseStatus(HttpStatus.OK);
		given(response.getHeaders()).willReturn(responseHeaders);
		given(response.getBody()).willReturn(new ByteArrayInputStream(Integer.toString(42).getBytes()));
		given(converter.canRead(intList.getType(), null, MediaType.TEXT_PLAIN)).willReturn(true);
		given(converter.read(eq(intList.getType()), eq(null), any(HttpInputMessage.class))).willReturn(expected);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.set("MyHeader", "MyValue");
		HttpEntity<String> requestEntity = new HttpEntity<>("Hello World", entityHeaders);
		ResponseEntity<List<Integer>> result = template.exchange("https://example.com", POST, requestEntity, intList);
		assertThat(result.getBody()).as("Invalid POST result").isEqualTo(expected);
		assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN);
		assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
		assertThat(requestHeaders.getFirst("MyHeader")).as("Invalid custom header").isEqualTo("MyValue");
		assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);

		verify(response).close();
	}

	@Test  // SPR-15066
	void requestInterceptorCanAddExistingHeaderValueWithoutBody() throws Exception {
		ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {
			request.getHeaders().add("MyHeader", "MyInterceptorValue");
			return execution.execute(request, body);
		};
		template.setInterceptors(Collections.singletonList(interceptor));

		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.add("MyHeader", "MyEntityValue");
		HttpEntity<Void> entity = new HttpEntity<>(null, entityHeaders);
		template.exchange("https://example.com", POST, entity, Void.class);
		assertThat(requestHeaders.get("MyHeader")).contains("MyEntityValue", "MyInterceptorValue");

		verify(response).close();
	}

	@Test  // SPR-15066
	void requestInterceptorCanAddExistingHeaderValueWithBody() throws Exception {
		ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {
			request.getHeaders().add("MyHeader", "MyInterceptorValue");
			return execution.execute(request, body);
		};
		template.setInterceptors(Collections.singletonList(interceptor));

		MediaType contentType = MediaType.TEXT_PLAIN;
		given(converter.canWrite(String.class, contentType)).willReturn(true);
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(contentType);
		entityHeaders.add("MyHeader", "MyEntityValue");
		HttpEntity<String> entity = new HttpEntity<>("Hello World", entityHeaders);
		template.exchange("https://example.com", POST, entity, Void.class);
		assertThat(requestHeaders.get("MyHeader")).contains("MyEntityValue", "MyInterceptorValue");

		verify(response).close();
	}

	@Test
	void clientHttpRequestInitializerAndRequestInterceptorAreBothApplied() throws Exception {
		ClientHttpRequestInitializer initializer = request ->
			request.getHeaders().add("MyHeader", "MyInitializerValue");
		ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {
			request.getHeaders().add("MyHeader", "MyInterceptorValue");
			return execution.execute(request, body);
		};
		template.setClientHttpRequestInitializers(Collections.singletonList(initializer));
		template.setInterceptors(Collections.singletonList(interceptor));

		MediaType contentType = MediaType.TEXT_PLAIN;
		given(converter.canWrite(String.class, contentType)).willReturn(true);
		HttpHeaders requestHeaders = new HttpHeaders();
		mockSentRequest(POST, "https://example.com", requestHeaders);
		mockResponseStatus(HttpStatus.OK);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(contentType);
		HttpEntity<String> entity = new HttpEntity<>("Hello World", entityHeaders);
		template.exchange("https://example.com", POST, entity, Void.class);
		assertThat(requestHeaders.get("MyHeader")).contains("MyInterceptorValue", "MyInitializerValue");

		verify(response).close();
	}

	private void mockSentRequest(HttpMethod method, String uri) throws Exception {
		mockSentRequest(method, uri, new HttpHeaders());
	}

	private void mockSentRequest(HttpMethod method, String uri, HttpHeaders requestHeaders) throws Exception {
		given(requestFactory.createRequest(new URI(uri), method)).willReturn(request);
		given(request.getHeaders()).willReturn(requestHeaders);
	}

	private void mockResponseStatus(HttpStatus responseStatus) throws Exception {
		given(request.execute()).willReturn(response);
		given(errorHandler.hasError(response)).willReturn(responseStatus.isError());
		given(response.getStatusCode()).willReturn(responseStatus);
		given(response.getRawStatusCode()).willReturn(responseStatus.value());
		given(response.getStatusText()).willReturn(responseStatus.getReasonPhrase());
	}

	private void mockTextPlainHttpMessageConverter() {
		mockHttpMessageConverter(MediaType.TEXT_PLAIN, String.class);
	}

	private void mockHttpMessageConverter(MediaType mediaType, Class<?> type) {
		given(converter.canRead(type, null)).willReturn(true);
		given(converter.canRead(type, mediaType)).willReturn(true);
		given(converter.getSupportedMediaTypes())
				.willReturn(Collections.singletonList(mediaType));
		given(converter.canRead(type, mediaType)).willReturn(true);
		given(converter.canWrite(type, null)).willReturn(true);
		given(converter.canWrite(type, mediaType)).willReturn(true);
	}

	private void mockTextResponseBody(String expectedBody) throws Exception {
		mockResponseBody(expectedBody, MediaType.TEXT_PLAIN);
	}

	private void mockResponseBody(String expectedBody, MediaType mediaType) throws Exception {
		HttpHeaders responseHeaders = new HttpHeaders();
		responseHeaders.setContentType(mediaType);
		responseHeaders.setContentLength(expectedBody.length());
		given(response.getHeaders()).willReturn(responseHeaders);
		given(response.getBody()).willReturn(new ByteArrayInputStream(expectedBody.getBytes()));
		given(converter.read(eq(String.class), any(HttpInputMessage.class))).willReturn(expectedBody);
	}

}

集成测试示例:

package org.springframework.web.client;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonView;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.MediaType.MULTIPART_MIXED;

/**
 * Integration tests for {@link RestTemplate}.
 *
 * <h3>Logging configuration for {@code MockWebServer}</h3>
 *
 * <p>In order for our log4j2 configuration to be used in an IDE, you must
 * set the following system property before running any tests &mdash; for
 * example, in <em>Run Configurations</em> in Eclipse.
 *
 * <pre class="code">
 * -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
 * </pre>
 *
 * @author Arjen Poutsma
 * @author Brian Clozel
 * @author Sam Brannen
 */
class RestTemplateIntegrationTests extends AbstractMockWebServerTests {

	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.METHOD)
	@ParameterizedTest(name = "[{index}] {0}")
	@MethodSource("clientHttpRequestFactories")
	@interface ParameterizedRestTemplateTest {
	}

	@SuppressWarnings("deprecation")
	static Stream<ClientHttpRequestFactory> clientHttpRequestFactories() {
		return Stream.of(
			new SimpleClientHttpRequestFactory(),
			new HttpComponentsClientHttpRequestFactory(),
			new org.springframework.http.client.Netty4ClientHttpRequestFactory(),
			new OkHttp3ClientHttpRequestFactory()
		);
	}


	private RestTemplate template;

	private ClientHttpRequestFactory clientHttpRequestFactory;


	/**
	 * Custom JUnit Jupiter extension that handles exceptions thrown by test methods.
	 *
	 * <p>If the test method throws an {@link HttpServerErrorException}, this
	 * extension will throw an {@link AssertionError} that wraps the
	 * {@code HttpServerErrorException} using the
	 * {@link HttpServerErrorException#getResponseBodyAsString() response body}
	 * as the failure message.
	 *
	 * <p>This mechanism provides an actually meaningful failure message if the
	 * test fails due to an {@code AssertionError} on the server.
	 */
	@RegisterExtension
	TestExecutionExceptionHandler serverErrorToAssertionErrorConverter = (context, throwable) -> {
		if (throwable instanceof HttpServerErrorException) {
			HttpServerErrorException ex = (HttpServerErrorException) throwable;
			String responseBody = ex.getResponseBodyAsString();
			String prefix = AssertionError.class.getName() + ": ";
			if (responseBody.startsWith(prefix)) {
				responseBody = responseBody.substring(prefix.length());
			}
			throw new AssertionError(responseBody, ex);
		}
		// Else throw as-is in order to comply with the contract of TestExecutionExceptionHandler.
		throw throwable;
	};


	private void setUpClient(ClientHttpRequestFactory clientHttpRequestFactory) {
		this.clientHttpRequestFactory = clientHttpRequestFactory;
		this.template = new RestTemplate(this.clientHttpRequestFactory);
	}

	@ParameterizedRestTemplateTest
	void getString(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		String s = template.getForObject(baseUrl + "/{method}", String.class, "get");
		assertThat(s).as("Invalid content").isEqualTo(helloWorld);
	}

	@ParameterizedRestTemplateTest
	void getEntity(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		ResponseEntity<String> entity = template.getForEntity(baseUrl + "/{method}", String.class, "get");
		assertThat(entity.getBody()).as("Invalid content").isEqualTo(helloWorld);
		assertThat(entity.getHeaders().isEmpty()).as("No headers").isFalse();
		assertThat(entity.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(textContentType);
		assertThat(entity.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
	}

	@ParameterizedRestTemplateTest
	void getNoResponse(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		String s = template.getForObject(baseUrl + "/get/nothing", String.class);
		assertThat(s).as("Invalid content").isNull();
	}

	@ParameterizedRestTemplateTest
	void getNoContentTypeHeader(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		byte[] bytes = template.getForObject(baseUrl + "/get/nocontenttype", byte[].class);
		assertThat(bytes).as("Invalid content").isEqualTo(helloWorld.getBytes("UTF-8"));
	}

	@ParameterizedRestTemplateTest
	void getNoContent(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		String s = template.getForObject(baseUrl + "/status/nocontent", String.class);
		assertThat(s).as("Invalid content").isNull();

		ResponseEntity<String> entity = template.getForEntity(baseUrl + "/status/nocontent", String.class);
		assertThat(entity.getStatusCode()).as("Invalid response code").isEqualTo(HttpStatus.NO_CONTENT);
		assertThat(entity.getBody()).as("Invalid content").isNull();
	}

	@ParameterizedRestTemplateTest
	void getNotModified(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		String s = template.getForObject(baseUrl + "/status/notmodified", String.class);
		assertThat(s).as("Invalid content").isNull();

		ResponseEntity<String> entity = template.getForEntity(baseUrl + "/status/notmodified", String.class);
		assertThat(entity.getStatusCode()).as("Invalid response code").isEqualTo(HttpStatus.NOT_MODIFIED);
		assertThat(entity.getBody()).as("Invalid content").isNull();
	}

	@ParameterizedRestTemplateTest
	void postForLocation(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		URI location = template.postForLocation(baseUrl + "/{method}", helloWorld, "post");
		assertThat(location).as("Invalid location").isEqualTo(new URI(baseUrl + "/post/1"));
	}

	@ParameterizedRestTemplateTest
	void postForLocationEntity(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(new MediaType("text", "plain", StandardCharsets.ISO_8859_1));
		HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
		URI location = template.postForLocation(baseUrl + "/{method}", entity, "post");
		assertThat(location).as("Invalid location").isEqualTo(new URI(baseUrl + "/post/1"));
	}

	@ParameterizedRestTemplateTest
	void postForObject(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		String s = template.postForObject(baseUrl + "/{method}", helloWorld, String.class, "post");
		assertThat(s).as("Invalid content").isEqualTo(helloWorld);
	}

	@ParameterizedRestTemplateTest
	void patchForObject(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		assumeFalse(clientHttpRequestFactory instanceof SimpleClientHttpRequestFactory,
				"JDK client does not support the PATCH method");

		setUpClient(clientHttpRequestFactory);

		String s = template.patchForObject(baseUrl + "/{method}", helloWorld, String.class, "patch");
		assertThat(s).as("Invalid content").isEqualTo(helloWorld);
	}

	@ParameterizedRestTemplateTest
	void notFound(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		assertThatExceptionOfType(HttpClientErrorException.class).isThrownBy(() ->
				template.execute(baseUrl + "/status/notfound", HttpMethod.GET, null, null))
			.satisfies(ex -> {
				assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
				assertThat(ex.getStatusText()).isNotNull();
				assertThat(ex.getResponseBodyAsString()).isNotNull();
			});
	}

	@ParameterizedRestTemplateTest
	void badRequest(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		assertThatExceptionOfType(HttpClientErrorException.class).isThrownBy(() ->
				template.execute(baseUrl + "/status/badrequest", HttpMethod.GET, null, null))
			.satisfies(ex -> {
				assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
				assertThat(ex.getMessage()).isEqualTo("400 Client Error: [no body]");
			});
	}

	@ParameterizedRestTemplateTest
	void serverError(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		assertThatExceptionOfType(HttpServerErrorException.class).isThrownBy(() ->
				template.execute(baseUrl + "/status/server", HttpMethod.GET, null, null))
			.satisfies(ex -> {
				assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
				assertThat(ex.getStatusText()).isNotNull();
				assertThat(ex.getResponseBodyAsString()).isNotNull();
			});
	}

	@ParameterizedRestTemplateTest
	void optionsForAllow(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		Set<HttpMethod> allowed = template.optionsForAllow(new URI(baseUrl + "/get"));
		assertThat(allowed).as("Invalid response").isEqualTo(EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE));
	}

	@ParameterizedRestTemplateTest
	void uri(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		String result = template.getForObject(baseUrl + "/uri/{query}", String.class, "Z\u00fcrich");
		assertThat(result).as("Invalid request URI").isEqualTo("/uri/Z%C3%BCrich");

		result = template.getForObject(baseUrl + "/uri/query={query}", String.class, "foo@bar");
		assertThat(result).as("Invalid request URI").isEqualTo("/uri/query=foo@bar");

		result = template.getForObject(baseUrl + "/uri/query={query}", String.class, "T\u014dky\u014d");
		assertThat(result).as("Invalid request URI").isEqualTo("/uri/query=T%C5%8Dky%C5%8D");
	}

	@ParameterizedRestTemplateTest
	void multipartFormData(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		template.postForLocation(baseUrl + "/multipartFormData", createMultipartParts());
	}

	@ParameterizedRestTemplateTest
	void multipartMixed(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.setContentType(MULTIPART_MIXED);
		template.postForLocation(baseUrl + "/multipartMixed", new HttpEntity<>(createMultipartParts(), requestHeaders));
	}

	@ParameterizedRestTemplateTest
	void multipartRelated(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		addSupportedMediaTypeToFormHttpMessageConverter(MULTIPART_RELATED);

		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.setContentType(MULTIPART_RELATED);
		template.postForLocation(baseUrl + "/multipartRelated", new HttpEntity<>(createMultipartParts(), requestHeaders));
	}

	private MultiValueMap<String, Object> createMultipartParts() {
		MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
		parts.add("name 1", "value 1");
		parts.add("name 2", "value 2+1");
		parts.add("name 2", "value 2+2");
		Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
		parts.add("logo", logo);
		return parts;
	}

	private void addSupportedMediaTypeToFormHttpMessageConverter(MediaType mediaType) {
		this.template.getMessageConverters().stream()
				.filter(FormHttpMessageConverter.class::isInstance)
				.map(FormHttpMessageConverter.class::cast)
				.findFirst()
				.orElseThrow(() -> new IllegalStateException("Failed to find FormHttpMessageConverter"))
				.addSupportedMediaTypes(mediaType);
	}

	@ParameterizedRestTemplateTest
	void form(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
		form.add("name 1", "value 1");
		form.add("name 2", "value 2+1");
		form.add("name 2", "value 2+2");

		template.postForLocation(baseUrl + "/form", form);
	}

	@ParameterizedRestTemplateTest
	void exchangeGet(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.set("MyHeader", "MyValue");
		HttpEntity<String> requestEntity = new HttpEntity<>(requestHeaders);
		ResponseEntity<String> response =
				template.exchange(baseUrl + "/{method}", HttpMethod.GET, requestEntity, String.class, "get");
		assertThat(response.getBody()).as("Invalid content").isEqualTo(helloWorld);
	}

	@ParameterizedRestTemplateTest
	void exchangePost(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.set("MyHeader", "MyValue");
		requestHeaders.setContentType(MediaType.TEXT_PLAIN);
		HttpEntity<String> entity = new HttpEntity<>(helloWorld, requestHeaders);
		HttpEntity<Void> result = template.exchange(baseUrl + "/{method}", POST, entity, Void.class, "post");
		assertThat(result.getHeaders().getLocation()).as("Invalid location").isEqualTo(new URI(baseUrl + "/post/1"));
		assertThat(result.hasBody()).isFalse();
	}

	@ParameterizedRestTemplateTest
	void jsonPostForObject(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
		MySampleBean bean = new MySampleBean();
		bean.setWith1("with");
		bean.setWith2("with");
		bean.setWithout("without");
		HttpEntity<MySampleBean> entity = new HttpEntity<>(bean, entityHeaders);
		String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class);
		assertThat(s.contains("\"with1\":\"with\"")).isTrue();
		assertThat(s.contains("\"with2\":\"with\"")).isTrue();
		assertThat(s.contains("\"without\":\"without\"")).isTrue();
	}

	@ParameterizedRestTemplateTest
	void jsonPostForObjectWithJacksonView(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
		MySampleBean bean = new MySampleBean("with", "with", "without");
		MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
		jacksonValue.setSerializationView(MyJacksonView1.class);
		HttpEntity<MappingJacksonValue> entity = new HttpEntity<>(jacksonValue, entityHeaders);
		String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class);
		assertThat(s.contains("\"with1\":\"with\"")).isTrue();
		assertThat(s.contains("\"with2\":\"with\"")).isFalse();
		assertThat(s.contains("\"without\":\"without\"")).isFalse();
	}

	@ParameterizedRestTemplateTest  // SPR-12123
	void serverPort(ClientHttpRequestFactory clientHttpRequestFactory) {
		setUpClient(clientHttpRequestFactory);

		String s = template.getForObject("http://localhost:{port}/get", String.class, port);
		assertThat(s).as("Invalid content").isEqualTo(helloWorld);
	}

	@ParameterizedRestTemplateTest  // SPR-13154
	void jsonPostForObjectWithJacksonTypeInfoList(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		List<ParentClass> list = new ArrayList<>();
		list.add(new Foo("foo"));
		list.add(new Bar("bar"));
		ParameterizedTypeReference<?> typeReference = new ParameterizedTypeReference<List<ParentClass>>() {};
		RequestEntity<List<ParentClass>> entity = RequestEntity
				.post(new URI(baseUrl + "/jsonpost"))
				.contentType(new MediaType("application", "json", StandardCharsets.UTF_8))
				.body(list, typeReference.getType());
		String content = template.exchange(entity, String.class).getBody();
		assertThat(content.contains("\"type\":\"foo\"")).isTrue();
		assertThat(content.contains("\"type\":\"bar\"")).isTrue();
	}

	@ParameterizedRestTemplateTest  // SPR-15015
	void postWithoutBody(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
		setUpClient(clientHttpRequestFactory);

		assertThat(template.postForObject(baseUrl + "/jsonpost", null, String.class)).isNull();
	}


	public interface MyJacksonView1 {}

	public interface MyJacksonView2 {}


	public static class MySampleBean {

		@JsonView(MyJacksonView1.class)
		private String with1;

		@JsonView(MyJacksonView2.class)
		private String with2;

		private String without;

		private MySampleBean() {
		}

		private MySampleBean(String with1, String with2, String without) {
			this.with1 = with1;
			this.with2 = with2;
			this.without = without;
		}

		public String getWith1() {
			return with1;
		}

		public void setWith1(String with1) {
			this.with1 = with1;
		}

		public String getWith2() {
			return with2;
		}

		public void setWith2(String with2) {
			this.with2 = with2;
		}

		public String getWithout() {
			return without;
		}

		public void setWithout(String without) {
			this.without = without;
		}
	}


	@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
	public static class ParentClass {

		private String parentProperty;

		public ParentClass() {
		}

		public ParentClass(String parentProperty) {
			this.parentProperty = parentProperty;
		}

		public String getParentProperty() {
			return parentProperty;
		}

		public void setParentProperty(String parentProperty) {
			this.parentProperty = parentProperty;
		}
	}


	@JsonTypeName("foo")
	public static class Foo extends ParentClass {

		public Foo() {
		}

		public Foo(String parentProperty) {
			super(parentProperty);
		}
	}


	@JsonTypeName("bar")
	public static class Bar extends ParentClass {

		public Bar() {
		}

		public Bar(String parentProperty) {
			super(parentProperty);
		}
	}

}

AsyncRestTemplate (ListenableFuture异步处理)

AsyncRestTemplate是对RestTemplate的异步请求的封装,AsyncRequestCallback是其核心回调函数,而AsyncRestOperations所对应的请求方法都是ListenableFuture的泛型。

package org.springframework.web.client;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureAdapter;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriTemplateHandler;

/**
 * <strong>Spring's central class for asynchronous client-side HTTP access.</strong>
 * Exposes similar methods as {@link RestTemplate}, but returns {@link ListenableFuture}
 * wrappers as opposed to concrete results.
 *
 * <p>The {@code AsyncRestTemplate} exposes a synchronous {@link RestTemplate} via the
 * {@link #getRestOperations()} method and shares its {@linkplain #setErrorHandler error handler}
 * and {@linkplain #setMessageConverters message converters} with that {@code RestTemplate}.
 *
 * <p><strong>Note:</strong> by default {@code AsyncRestTemplate} relies on
 * standard JDK facilities to establish HTTP connections. You can switch to use
 * a different HTTP library such as Apache HttpComponents, Netty, and OkHttp by
 * using a constructor accepting an {@link org.springframework.http.client.AsyncClientHttpRequestFactory}.
 *
 * <p>For more information, please refer to the {@link RestTemplate} API documentation.
 *
 * @author Arjen Poutsma
 * @since 4.0
 * @see RestTemplate
 * @deprecated as of Spring 5.0, in favor of {@link org.springframework.web.reactive.function.client.WebClient}
 */
@Deprecated
public class AsyncRestTemplate extends org.springframework.http.client.support.InterceptingAsyncHttpAccessor
		implements AsyncRestOperations {

	private final RestTemplate syncTemplate;

 AsyncRequestCallback功能性接口定义:

package org.springframework.web.client;

import java.io.IOException;

/**
 * Callback interface for code that operates on an
 * {@link org.springframework.http.client.AsyncClientHttpRequest}. Allows to
 * manipulate the request headers, and write to the request body.
 *
 * <p>Used internally by the {@link AsyncRestTemplate}, but also useful for
 * application code.
 *
 * @author Arjen Poutsma
 * @see org.springframework.web.client.AsyncRestTemplate#execute
 * @since 4.0
 * @deprecated as of Spring 5.0, in favor of
 * {@link org.springframework.web.reactive.function.client.ExchangeFilterFunction}
 */
@FunctionalInterface
@Deprecated
public interface AsyncRequestCallback {

	/**
	 * Gets called by {@link AsyncRestTemplate#execute} with an opened {@code ClientHttpRequest}.
	 * Does not need to care about closing the request or about handling errors:
	 * this will all be handled by the {@code RestTemplate}.
	 * @param request the active HTTP request
	 * @throws java.io.IOException in case of I/O errors
	 */
	void doWithRequest(org.springframework.http.client.AsyncClientHttpRequest request) throws IOException;

}

 AysncRestOperations异步调用定义:

package org.springframework.web.client;

import java.net.URI;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.util.concurrent.ListenableFuture;

/**
 * Interface specifying a basic set of asynchronous RESTful operations.
 * Implemented by {@link AsyncRestTemplate}. Not often used directly, but a useful
 * option to enhance testability, as it can easily be mocked or stubbed.
 *
 * @author Arjen Poutsma
 * @since 4.0
 * @see AsyncRestTemplate
 * @see RestOperations
 * @deprecated as of Spring 5.0, in favor of {@link org.springframework.web.reactive.function.client.WebClient}
 */
@Deprecated
public interface AsyncRestOperations {

	/**
	 * Expose the synchronous Spring RestTemplate to allow synchronous invocation.
	 */
	RestOperations getRestOperations();


	// GET

	/**
	 * Asynchronously retrieve an entity by doing a GET on the specified URL.
	 * The response is converted and stored in an {@link ResponseEntity}.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * @param url the URL
	 * @param responseType the type of the return value
	 * @param uriVariables the variables to expand the template
	 * @return the entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> getForEntity(String url, Class<T> responseType,
			Object... uriVariables) throws RestClientException;

	/**
	 * Asynchronously retrieve a representation by doing a GET on the URI template.
	 * The response is converted and stored in an {@link ResponseEntity}.
	 * <p>URI Template variables are expanded using the given map.
	 * @param url the URL
	 * @param responseType the type of the return value
	 * @param uriVariables the map containing variables for the URI template
	 * @return the entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> getForEntity(String url, Class<T> responseType,
			Map<String, ?> uriVariables) throws RestClientException;

	/**
	 * Asynchronously retrieve a representation by doing a GET on the URL.
	 * The response is converted and stored in an {@link ResponseEntity}.
	 * @param url the URL
	 * @param responseType the type of the return value
	 * @return the entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> getForEntity(URI url, Class<T> responseType)
			throws RestClientException;


	// HEAD

	/**
	 * Asynchronously retrieve all headers of the resource specified by the URI template.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * @param url the URL
	 * @param uriVariables the variables to expand the template
	 * @return all HTTP headers of that resource wrapped in a {@link Future}
	 */
	ListenableFuture<HttpHeaders> headForHeaders(String url, Object... uriVariables)
			throws RestClientException;

	/**
	 * Asynchronously retrieve all headers of the resource specified by the URI template.
	 * <p>URI Template variables are expanded using the given map.
	 * @param url the URL
	 * @param uriVariables the map containing variables for the URI template
	 * @return all HTTP headers of that resource wrapped in a {@link Future}
	 */
	ListenableFuture<HttpHeaders> headForHeaders(String url, Map<String, ?> uriVariables)
			throws RestClientException;

	/**
	 * Asynchronously retrieve all headers of the resource specified by the URL.
	 * @param url the URL
	 * @return all HTTP headers of that resource wrapped in a {@link Future}
	 */
	ListenableFuture<HttpHeaders> headForHeaders(URI url) throws RestClientException;


	// POST

	/**
	 * Create a new resource by POSTing the given object to the URI template, and
	 * asynchronously returns the value of the {@code Location} header. This header
	 * typically indicates where the new resource is stored.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * @param url the URL
	 * @param request the Object to be POSTed (may be {@code null})
	 * @param uriVariables the variables to expand the template
	 * @return the value for the {@code Location} header wrapped in a {@link Future}
	 * @see org.springframework.http.HttpEntity
	 */
	ListenableFuture<URI> postForLocation(String url, @Nullable HttpEntity<?> request, Object... uriVariables)
			throws RestClientException;

	/**
	 * Create a new resource by POSTing the given object to the URI template, and
	 * asynchronously returns the value of the {@code Location} header. This header
	 * typically indicates where the new resource is stored.
	 * <p>URI Template variables are expanded using the given map.
	 * @param url the URL
	 * @param request the Object to be POSTed (may be {@code null})
	 * @param uriVariables the variables to expand the template
	 * @return the value for the {@code Location} header wrapped in a {@link Future}
	 * @see org.springframework.http.HttpEntity
	 */
	ListenableFuture<URI> postForLocation(String url, @Nullable HttpEntity<?> request, Map<String, ?> uriVariables)
			throws RestClientException;

	/**
	 * Create a new resource by POSTing the given object to the URL, and asynchronously
	 * returns the value of the {@code Location} header. This header typically indicates
	 * where the new resource is stored.
	 * @param url the URL
	 * @param request the Object to be POSTed (may be {@code null})
	 * @return the value for the {@code Location} header wrapped in a {@link Future}
	 * @see org.springframework.http.HttpEntity
	 */
	ListenableFuture<URI> postForLocation(URI url, @Nullable HttpEntity<?> request) throws RestClientException;

	/**
	 * Create a new resource by POSTing the given object to the URI template,
	 * and asynchronously returns the response as {@link ResponseEntity}.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * @param url the URL
	 * @param request the Object to be POSTed (may be {@code null})
	 * @param uriVariables the variables to expand the template
	 * @return the entity wrapped in a {@link Future}
	 * @see org.springframework.http.HttpEntity
	 */
	<T> ListenableFuture<ResponseEntity<T>> postForEntity(String url, @Nullable HttpEntity<?> request,
			Class<T> responseType, Object... uriVariables) throws RestClientException;

	/**
	 * Create a new resource by POSTing the given object to the URI template,
	 * and asynchronously returns the response as {@link ResponseEntity}.
	 * <p>URI Template variables are expanded using the given map.
	 * @param url the URL
	 * @param request the Object to be POSTed (may be {@code null})
	 * @param uriVariables the variables to expand the template
	 * @return the entity wrapped in a {@link Future}
	 * @see org.springframework.http.HttpEntity
	 */
	<T> ListenableFuture<ResponseEntity<T>> postForEntity(String url, @Nullable HttpEntity<?> request,
			Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;

	/**
	 * Create a new resource by POSTing the given object to the URL,
	 * and asynchronously returns the response as {@link ResponseEntity}.
	 * @param url the URL
	 * @param request the Object to be POSTed (may be {@code null})
	 * @return the entity wrapped in a {@link Future}
	 * @see org.springframework.http.HttpEntity
	 */
	<T> ListenableFuture<ResponseEntity<T>> postForEntity(URI url, @Nullable HttpEntity<?> request,
			Class<T> responseType) throws RestClientException;


	// PUT

	/**
	 * Create or update a resource by PUTting the given object to the URI.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * <p>The Future will return a {@code null} result upon completion.
	 * @param url the URL
	 * @param request the Object to be PUT (may be {@code null})
	 * @param uriVariables the variables to expand the template
	 * @see HttpEntity
	 */
	ListenableFuture<?> put(String url, @Nullable HttpEntity<?> request, Object... uriVariables)
			throws RestClientException;

	/**
	 * Creates a new resource by PUTting the given object to URI template.
	 * <p>URI Template variables are expanded using the given map.
	 * <p>The Future will return a {@code null} result upon completion.
	 * @param url the URL
	 * @param request the Object to be PUT (may be {@code null})
	 * @param uriVariables the variables to expand the template
	 * @see HttpEntity
	 */
	ListenableFuture<?> put(String url, @Nullable HttpEntity<?> request, Map<String, ?> uriVariables)
			throws RestClientException;

	/**
	 * Creates a new resource by PUTting the given object to URL.
	 * <p>The Future will return a {@code null} result upon completion.
	 * @param url the URL
	 * @param request the Object to be PUT (may be {@code null})
	 * @see HttpEntity
	 */
	ListenableFuture<?> put(URI url, @Nullable HttpEntity<?> request) throws RestClientException;


	// DELETE

	/**
	 * Asynchronously delete the resources at the specified URI.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * <p>The Future will return a {@code null} result upon completion.
	 * @param url the URL
	 * @param uriVariables the variables to expand in the template
	 */
	ListenableFuture<?> delete(String url, Object... uriVariables) throws RestClientException;

	/**
	 * Asynchronously delete the resources at the specified URI.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * <p>The Future will return a {@code null} result upon completion.
	 * @param url the URL
	 * @param uriVariables the variables to expand in the template
	 */
	ListenableFuture<?> delete(String url, Map<String, ?> uriVariables) throws RestClientException;

	/**
	 * Asynchronously delete the resources at the specified URI.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * <p>The Future will return a {@code null} result upon completion.
	 * @param url the URL
	 */
	ListenableFuture<?> delete(URI url) throws RestClientException;


	// OPTIONS

	/**
	 * Asynchronously return the value of the Allow header for the given URI.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * @param url the URL
	 * @param uriVariables the variables to expand in the template
	 * @return the value of the allow header wrapped in a {@link Future}
	 */
	ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Object... uriVariables)
			throws RestClientException;

	/**
	 * Asynchronously return the value of the Allow header for the given URI.
	 * <p>URI Template variables are expanded using the given map.
	 * @param url the URL
	 * @param uriVariables the variables to expand in the template
	 * @return the value of the allow header wrapped in a {@link Future}
	 */
	ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Map<String, ?> uriVariables)
			throws RestClientException;

	/**
	 * Asynchronously return the value of the Allow header for the given URL.
	 * @param url the URL
	 * @return the value of the allow header wrapped in a {@link Future}
	 */
	ListenableFuture<Set<HttpMethod>> optionsForAllow(URI url) throws RestClientException;


	// exchange

	/**
	 * Asynchronously execute the HTTP method to the given URI template, writing the
	 * given request entity to the request, and returns the response as
	 * {@link ResponseEntity}.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestEntity the entity (headers and/or body) to write to the request
	 * (may be {@code null})
	 * @param responseType the type of the return value
	 * @param uriVariables the variables to expand in the template
	 * @return the response as entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> exchange(String url, HttpMethod method,
			@Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
			throws RestClientException;

	/**
	 * Asynchronously execute the HTTP method to the given URI template, writing the
	 * given request entity to the request, and returns the response as
	 * {@link ResponseEntity}.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestEntity the entity (headers and/or body) to write to the request
	 * (may be {@code null})
	 * @param responseType the type of the return value
	 * @param uriVariables the variables to expand in the template
	 * @return the response as entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> exchange(String url, HttpMethod method,
			@Nullable HttpEntity<?> requestEntity, Class<T> responseType,
			Map<String, ?> uriVariables) throws RestClientException;

	/**
	 * Asynchronously execute the HTTP method to the given URI template, writing the
	 * given request entity to the request, and returns the response as
	 * {@link ResponseEntity}.
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestEntity the entity (headers and/or body) to write to the request
	 * (may be {@code null})
	 * @param responseType the type of the return value
	 * @return the response as entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> exchange(URI url, HttpMethod method,
			@Nullable HttpEntity<?> requestEntity, Class<T> responseType)
			throws RestClientException;

	/**
	 * Asynchronously execute the HTTP method to the given URI template, writing the given
	 * request entity to the request, and returns the response as {@link ResponseEntity}.
	 * The given {@link ParameterizedTypeReference} is used to pass generic type
	 * information:
	 * <pre class="code">
	 * ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt; myBean =
	 *     new ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt;() {};
	 *
	 * ResponseEntity&lt;List&lt;MyBean&gt;&gt; response =
	 *     template.exchange(&quot;https://example.com&quot;,HttpMethod.GET, null, myBean);
	 * </pre>
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestEntity the entity (headers and/or body) to write to the
	 * request (may be {@code null})
	 * @param responseType the type of the return value
	 * @param uriVariables the variables to expand in the template
	 * @return the response as entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> exchange(String url, HttpMethod method,
			@Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType,
			Object... uriVariables) throws RestClientException;

	/**
	 * Asynchronously execute the HTTP method to the given URI template, writing the given
	 * request entity to the request, and returns the response as {@link ResponseEntity}.
	 * The given {@link ParameterizedTypeReference} is used to pass generic type
	 * information:
	 * <pre class="code">
	 * ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt; myBean =
	 *     new ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt;() {};
	 *
	 * ResponseEntity&lt;List&lt;MyBean&gt;&gt; response =
	 *     template.exchange(&quot;https://example.com&quot;,HttpMethod.GET, null, myBean);
	 * </pre>
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestEntity the entity (headers and/or body) to write to the request
	 * (may be {@code null})
	 * @param responseType the type of the return value
	 * @param uriVariables the variables to expand in the template
	 * @return the response as entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> exchange(String url, HttpMethod method,
			@Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType,
			Map<String, ?> uriVariables) throws RestClientException;

	/**
	 * Asynchronously execute the HTTP method to the given URI template, writing the given
	 * request entity to the request, and returns the response as {@link ResponseEntity}.
	 * The given {@link ParameterizedTypeReference} is used to pass generic type
	 * information:
	 * <pre class="code">
	 * ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt; myBean =
	 *     new ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt;() {};
	 *
	 * ResponseEntity&lt;List&lt;MyBean&gt;&gt; response =
	 *     template.exchange(&quot;https://example.com&quot;,HttpMethod.GET, null, myBean);
	 * </pre>
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestEntity the entity (headers and/or body) to write to the request
	 * (may be {@code null})
	 * @param responseType the type of the return value
	 * @return the response as entity wrapped in a {@link Future}
	 */
	<T> ListenableFuture<ResponseEntity<T>> exchange(URI url, HttpMethod method,
			@Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType)
			throws RestClientException;


	// general execution

	/**
	 * Asynchronously execute the HTTP method to the given URI template, preparing the
	 * request with the {@link AsyncRequestCallback}, and reading the response with a
	 * {@link ResponseExtractor}.
	 * <p>URI Template variables are expanded using the given URI variables, if any.
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestCallback object that prepares the request
	 * @param responseExtractor object that extracts the return value from the response
	 * @param uriVariables the variables to expand in the template
	 * @return an arbitrary object, as returned by the {@link ResponseExtractor}
	 */
	<T> ListenableFuture<T> execute(String url, HttpMethod method,
			@Nullable AsyncRequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor,
			Object... uriVariables) throws RestClientException;

	/**
	 * Asynchronously execute the HTTP method to the given URI template, preparing the
	 * request with the {@link AsyncRequestCallback}, and reading the response with a
	 * {@link ResponseExtractor}.
	 * <p>URI Template variables are expanded using the given URI variables map.
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestCallback object that prepares the request
	 * @param responseExtractor object that extracts the return value from the response
	 * @param uriVariables the variables to expand in the template
	 * @return an arbitrary object, as returned by the {@link ResponseExtractor}
	 */
	<T> ListenableFuture<T> execute(String url, HttpMethod method,
			@Nullable AsyncRequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor,
			Map<String, ?> uriVariables) throws RestClientException;

	/**
	 * Asynchronously execute the HTTP method to the given URL, preparing the request
	 * with the {@link AsyncRequestCallback}, and reading the response with a
	 * {@link ResponseExtractor}.
	 * @param url the URL
	 * @param method the HTTP method (GET, POST, etc)
	 * @param requestCallback object that prepares the request
	 * @param responseExtractor object that extracts the return value from the response
	 * @return an arbitrary object, as returned by the {@link ResponseExtractor}
	 */
	<T> ListenableFuture<T> execute(URI url, HttpMethod method,
			@Nullable AsyncRequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor)
			throws RestClientException;

}

集成测试示例:

package org.springframework.web.client;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;

/**
 * Integration tests for {@link AsyncRestTemplate}.
 *
 * <h3>Logging configuration for {@code MockWebServer}</h3>
 *
 * <p>In order for our log4j2 configuration to be used in an IDE, you must
 * set the following system property before running any tests &mdash; for
 * example, in <em>Run Configurations</em> in Eclipse.
 *
 * <pre class="code">
 * -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
 * </pre>
 *
 * @author Arjen Poutsma
 * @author Sebastien Deleuze
 */
@SuppressWarnings("deprecation")
public class AsyncRestTemplateIntegrationTests extends AbstractMockWebServerTests {

	private final AsyncRestTemplate template = new AsyncRestTemplate(
			new org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory());


	@Test
	public void getEntity() throws Exception {
		Future<ResponseEntity<String>> future = template.getForEntity(baseUrl + "/{method}", String.class, "get");
		ResponseEntity<String> entity = future.get();
		assertThat(entity.getBody()).as("Invalid content").isEqualTo(helloWorld);
		assertThat(entity.getHeaders().isEmpty()).as("No headers").isFalse();
		assertThat(entity.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(textContentType);
		assertThat(entity.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
	}

	@Test
	public void getEntityFromCompletable() throws Exception {
		ListenableFuture<ResponseEntity<String>> future = template.getForEntity(baseUrl + "/{method}", String.class, "get");
		ResponseEntity<String> entity = future.completable().get();
		assertThat(entity.getBody()).as("Invalid content").isEqualTo(helloWorld);
		assertThat(entity.getHeaders().isEmpty()).as("No headers").isFalse();
		assertThat(entity.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(textContentType);
		assertThat(entity.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
	}

	@Test
	public void multipleFutureGets() throws Exception {
		Future<ResponseEntity<String>> future = template.getForEntity(baseUrl + "/{method}", String.class, "get");
		future.get();
		future.get();
	}

	@Test
	public void getEntityCallback() throws Exception {
		ListenableFuture<ResponseEntity<String>> futureEntity =
				template.getForEntity(baseUrl + "/{method}", String.class, "get");
		futureEntity.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
			@Override
			public void onSuccess(ResponseEntity<String> entity) {
				assertThat(entity.getBody()).as("Invalid content").isEqualTo(helloWorld);
				assertThat(entity.getHeaders().isEmpty()).as("No headers").isFalse();
				assertThat(entity.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(textContentType);
				assertThat(entity.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(futureEntity);
	}

	@Test
	public void getEntityCallbackWithLambdas() throws Exception {
		ListenableFuture<ResponseEntity<String>> futureEntity =
				template.getForEntity(baseUrl + "/{method}", String.class, "get");
		futureEntity.addCallback(entity -> {
			assertThat(entity.getBody()).as("Invalid content").isEqualTo(helloWorld);
			assertThat(entity.getHeaders().isEmpty()).as("No headers").isFalse();
			assertThat(entity.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(textContentType);
			assertThat(entity.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
		}, ex -> fail(ex.getMessage()));
		waitTillDone(futureEntity);
	}

	@Test
	public void getNoResponse() throws Exception {
		Future<ResponseEntity<String>> futureEntity = template.getForEntity(baseUrl + "/get/nothing", String.class);
		ResponseEntity<String> entity = futureEntity.get();
		assertThat(entity.getBody()).as("Invalid content").isNull();
	}

	@Test
	public void getNoContentTypeHeader() throws Exception {
		Future<ResponseEntity<byte[]>> futureEntity = template.getForEntity(baseUrl + "/get/nocontenttype", byte[].class);
		ResponseEntity<byte[]> responseEntity = futureEntity.get();
		assertThat(responseEntity.getBody()).as("Invalid content").isEqualTo(helloWorld.getBytes("UTF-8"));
	}

	@Test
	public void getNoContent() throws Exception {
		Future<ResponseEntity<String>> responseFuture = template.getForEntity(baseUrl + "/status/nocontent", String.class);
		ResponseEntity<String> entity = responseFuture.get();
		assertThat(entity.getStatusCode()).as("Invalid response code").isEqualTo(HttpStatus.NO_CONTENT);
		assertThat(entity.getBody()).as("Invalid content").isNull();
	}

	@Test
	public void getNotModified() throws Exception {
		Future<ResponseEntity<String>> responseFuture = template.getForEntity(baseUrl + "/status/notmodified", String.class);
		ResponseEntity<String> entity = responseFuture.get();
		assertThat(entity.getStatusCode()).as("Invalid response code").isEqualTo(HttpStatus.NOT_MODIFIED);
		assertThat(entity.getBody()).as("Invalid content").isNull();
	}

	@Test
	public void headForHeaders() throws Exception {
		Future<HttpHeaders> headersFuture = template.headForHeaders(baseUrl + "/get");
		HttpHeaders headers = headersFuture.get();
		assertThat(headers.containsKey("Content-Type")).as("No Content-Type header").isTrue();
	}

	@Test
	public void headForHeadersCallback() throws Exception {
		ListenableFuture<HttpHeaders> headersFuture = template.headForHeaders(baseUrl + "/get");
		headersFuture.addCallback(new ListenableFutureCallback<HttpHeaders>() {
			@Override
			public void onSuccess(HttpHeaders result) {
				assertThat(result.containsKey("Content-Type")).as("No Content-Type header").isTrue();
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(headersFuture);
	}

	@Test
	public void headForHeadersCallbackWithLambdas() throws Exception {
		ListenableFuture<HttpHeaders> headersFuture = template.headForHeaders(baseUrl + "/get");
		headersFuture.addCallback(result -> assertThat(result.containsKey("Content-Type")).as("No Content-Type header").isTrue(), ex -> fail(ex.getMessage()));
		waitTillDone(headersFuture);
	}

	@Test
	public void postForLocation() throws Exception  {
		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(new MediaType("text", "plain", StandardCharsets.ISO_8859_1));
		HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
		Future<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post");
		URI location = locationFuture.get();
		assertThat(location).as("Invalid location").isEqualTo(new URI(baseUrl + "/post/1"));
	}

	@Test
	public void postForLocationCallback() throws Exception  {
		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(new MediaType("text", "plain", StandardCharsets.ISO_8859_1));
		HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
		final URI expected = new URI(baseUrl + "/post/1");
		ListenableFuture<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post");
		locationFuture.addCallback(new ListenableFutureCallback<URI>() {
			@Override
			public void onSuccess(URI result) {
				assertThat(result).as("Invalid location").isEqualTo(expected);
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(locationFuture);
	}

	@Test
	public void postForLocationCallbackWithLambdas() throws Exception  {
		HttpHeaders entityHeaders = new HttpHeaders();
		entityHeaders.setContentType(new MediaType("text", "plain", StandardCharsets.ISO_8859_1));
		HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
		final URI expected = new URI(baseUrl + "/post/1");
		ListenableFuture<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post");
		locationFuture.addCallback(result -> assertThat(result).as("Invalid location").isEqualTo(expected),
				ex -> fail(ex.getMessage()));
		waitTillDone(locationFuture);
	}

	@Test
	public void postForEntity() throws Exception  {
		HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld);
		Future<ResponseEntity<String>> responseEntityFuture =
				template.postForEntity(baseUrl + "/{method}", requestEntity, String.class, "post");
		ResponseEntity<String> responseEntity = responseEntityFuture.get();
		assertThat(responseEntity.getBody()).as("Invalid content").isEqualTo(helloWorld);
	}

	@Test
	public void postForEntityCallback() throws Exception  {
		HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld);
		ListenableFuture<ResponseEntity<String>> responseEntityFuture =
				template.postForEntity(baseUrl + "/{method}", requestEntity, String.class, "post");
		responseEntityFuture.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
			@Override
			public void onSuccess(ResponseEntity<String> result) {
				assertThat(result.getBody()).as("Invalid content").isEqualTo(helloWorld);
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(responseEntityFuture);
	}

	@Test
	public void postForEntityCallbackWithLambdas() throws Exception  {
		HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld);
		ListenableFuture<ResponseEntity<String>> responseEntityFuture =
				template.postForEntity(baseUrl + "/{method}", requestEntity, String.class, "post");
		responseEntityFuture.addCallback(
				result -> assertThat(result.getBody()).as("Invalid content").isEqualTo(helloWorld),
				ex -> fail(ex.getMessage()));
		waitTillDone(responseEntityFuture);
	}

	@Test
	public void put() throws Exception  {
		HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld);
		Future<?> responseEntityFuture = template.put(baseUrl + "/{method}", requestEntity, "put");
		responseEntityFuture.get();
	}

	@Test
	public void putCallback() throws Exception  {
		HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld);
		ListenableFuture<?> responseEntityFuture = template.put(baseUrl + "/{method}", requestEntity, "put");
		responseEntityFuture.addCallback(new ListenableFutureCallback<Object>() {
			@Override
			public void onSuccess(Object result) {
				assertThat(result).isNull();
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(responseEntityFuture);
	}

	@Test
	public void delete() throws Exception  {
		Future<?> deletedFuture = template.delete(new URI(baseUrl + "/delete"));
		deletedFuture.get();
	}

	@Test
	public void deleteCallback() throws Exception  {
		ListenableFuture<?> deletedFuture = template.delete(new URI(baseUrl + "/delete"));
		deletedFuture.addCallback(new ListenableFutureCallback<Object>() {
			@Override
			public void onSuccess(Object result) {
				assertThat(result).isNull();
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(deletedFuture);
	}

	@Test
	public void deleteCallbackWithLambdas() throws Exception  {
		ListenableFuture<?> deletedFuture = template.delete(new URI(baseUrl + "/delete"));
		deletedFuture.addCallback(result -> assertThat(result).isNull(), ex -> fail(ex.getMessage()));
		waitTillDone(deletedFuture);
	}

	@Test
	public void identicalExceptionThroughGetAndCallback() throws Exception {
		final HttpClientErrorException[] callbackException = new HttpClientErrorException[1];

		final CountDownLatch latch = new CountDownLatch(1);
		ListenableFuture<?> future = template.execute(baseUrl + "/status/notfound", HttpMethod.GET, null, null);
		future.addCallback(new ListenableFutureCallback<Object>() {
			@Override
			public void onSuccess(Object result) {
				fail("onSuccess not expected");
			}
			@Override
			public void onFailure(Throwable ex) {
				boolean condition = ex instanceof HttpClientErrorException;
				assertThat(condition).isTrue();
				callbackException[0] = (HttpClientErrorException) ex;
				latch.countDown();
			}
		});

		try {
			future.get();
			fail("Exception expected");
		}
		catch (ExecutionException ex) {
			Throwable cause = ex.getCause();
			boolean condition = cause instanceof HttpClientErrorException;
			assertThat(condition).isTrue();
			latch.await(5, TimeUnit.SECONDS);
			assertThat(cause).isSameAs(callbackException[0]);
		}
	}

	@Test
	public void notFoundGet() throws Exception {
		assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> {
				Future<?> future = template.execute(baseUrl + "/status/notfound", HttpMethod.GET, null, null);
				future.get();
				})
			.withCauseInstanceOf(HttpClientErrorException.class)
			.satisfies(ex -> {
				HttpClientErrorException cause = (HttpClientErrorException) ex.getCause();
				assertThat(cause.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
				assertThat(cause.getStatusText()).isNotNull();
				assertThat(cause.getResponseBodyAsString()).isNotNull();
			});
	}

	@Test
	public void notFoundCallback() throws Exception {
		ListenableFuture<?> future = template.execute(baseUrl + "/status/notfound", HttpMethod.GET, null, null);
		future.addCallback(new ListenableFutureCallback<Object>() {
			@Override
			public void onSuccess(Object result) {
				fail("onSuccess not expected");
			}
			@Override
			public void onFailure(Throwable t) {
				boolean condition = t instanceof HttpClientErrorException;
				assertThat(condition).isTrue();
				HttpClientErrorException ex = (HttpClientErrorException) t;
				assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
				assertThat(ex.getStatusText()).isNotNull();
				assertThat(ex.getResponseBodyAsString()).isNotNull();
			}
		});
		waitTillDone(future);
	}

	@Test
	public void notFoundCallbackWithLambdas() throws Exception {
		ListenableFuture<?> future = template.execute(baseUrl + "/status/notfound", HttpMethod.GET, null, null);
		future.addCallback(result -> fail("onSuccess not expected"), ex -> {
			boolean condition = ex instanceof HttpClientErrorException;
			assertThat(condition).isTrue();
			HttpClientErrorException hcex = (HttpClientErrorException) ex;
			assertThat(hcex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
			assertThat(hcex.getStatusText()).isNotNull();
			assertThat(hcex.getResponseBodyAsString()).isNotNull();
		});
		waitTillDone(future);
	}

	@Test
	public void serverError() throws Exception {
		try {
			Future<Void> future = template.execute(baseUrl + "/status/server", HttpMethod.GET, null, null);
			future.get();
			fail("HttpServerErrorException expected");
		}
		catch (ExecutionException ex) {
			boolean condition = ex.getCause() instanceof HttpServerErrorException;
			assertThat(condition).isTrue();
			HttpServerErrorException cause = (HttpServerErrorException)ex.getCause();

			assertThat(cause.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
			assertThat(cause.getStatusText()).isNotNull();
			assertThat(cause.getResponseBodyAsString()).isNotNull();
		}
	}

	@Test
	public void serverErrorCallback() throws Exception {
		ListenableFuture<Void> future = template.execute(baseUrl + "/status/server", HttpMethod.GET, null, null);
		future.addCallback(new ListenableFutureCallback<Void>() {
			@Override
			public void onSuccess(Void result) {
				fail("onSuccess not expected");
			}
			@Override
			public void onFailure(Throwable ex) {
				boolean condition = ex instanceof HttpServerErrorException;
				assertThat(condition).isTrue();
				HttpServerErrorException hsex = (HttpServerErrorException) ex;
				assertThat(hsex.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
				assertThat(hsex.getStatusText()).isNotNull();
				assertThat(hsex.getResponseBodyAsString()).isNotNull();
			}
		});
		waitTillDone(future);
	}

	@Test
	public void serverErrorCallbackWithLambdas() throws Exception {
		ListenableFuture<Void> future = template.execute(baseUrl + "/status/server", HttpMethod.GET, null, null);
		future.addCallback(result -> fail("onSuccess not expected"), ex -> {
			boolean condition = ex instanceof HttpServerErrorException;
			assertThat(condition).isTrue();
			HttpServerErrorException hsex = (HttpServerErrorException) ex;
			assertThat(hsex.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
			assertThat(hsex.getStatusText()).isNotNull();
			assertThat(hsex.getResponseBodyAsString()).isNotNull();
		});
		waitTillDone(future);
	}

	@Test
	public void optionsForAllow() throws Exception {
		Future<Set<HttpMethod>> allowedFuture = template.optionsForAllow(new URI(baseUrl + "/get"));
		Set<HttpMethod> allowed = allowedFuture.get();
		assertThat(allowed).as("Invalid response").isEqualTo(EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE));
	}

	@Test
	public void optionsForAllowCallback() throws Exception {
		ListenableFuture<Set<HttpMethod>> allowedFuture = template.optionsForAllow(new URI(baseUrl + "/get"));
		allowedFuture.addCallback(new ListenableFutureCallback<Set<HttpMethod>>() {
			@Override
			public void onSuccess(Set<HttpMethod> result) {
				assertThat(result).as("Invalid response").isEqualTo(EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS,
								HttpMethod.HEAD, HttpMethod.TRACE));
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(allowedFuture);
	}

	@Test
	public void optionsForAllowCallbackWithLambdas() throws Exception{
		ListenableFuture<Set<HttpMethod>> allowedFuture = template.optionsForAllow(new URI(baseUrl + "/get"));
		allowedFuture.addCallback(result -> assertThat(result).as("Invalid response").isEqualTo(EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD,HttpMethod.TRACE)),
				ex -> fail(ex.getMessage()));
		waitTillDone(allowedFuture);
	}

	@Test
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void exchangeGet() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.set("MyHeader", "MyValue");
		HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
		Future<ResponseEntity<String>> responseFuture =
				template.exchange(baseUrl + "/{method}", HttpMethod.GET, requestEntity, String.class, "get");
		ResponseEntity<String> response = responseFuture.get();
		assertThat(response.getBody()).as("Invalid content").isEqualTo(helloWorld);
	}

	@Test
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void exchangeGetCallback() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.set("MyHeader", "MyValue");
		HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
		ListenableFuture<ResponseEntity<String>> responseFuture =
				template.exchange(baseUrl + "/{method}", HttpMethod.GET, requestEntity, String.class, "get");
		responseFuture.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
			@Override
			public void onSuccess(ResponseEntity<String> result) {
				assertThat(result.getBody()).as("Invalid content").isEqualTo(helloWorld);
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(responseFuture);
	}

	@Test
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void exchangeGetCallbackWithLambdas() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.set("MyHeader", "MyValue");
		HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
		ListenableFuture<ResponseEntity<String>> responseFuture =
				template.exchange(baseUrl + "/{method}", HttpMethod.GET, requestEntity, String.class, "get");
		responseFuture.addCallback(result -> assertThat(result.getBody()).as("Invalid content").isEqualTo(helloWorld), ex -> fail(ex.getMessage()));
		waitTillDone(responseFuture);
	}

	@Test
	public void exchangePost() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.set("MyHeader", "MyValue");
		requestHeaders.setContentType(MediaType.TEXT_PLAIN);
		HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders);
		Future<ResponseEntity<Void>> resultFuture =
				template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post");
		ResponseEntity<Void> result = resultFuture.get();
		assertThat(result.getHeaders().getLocation()).as("Invalid location").isEqualTo(new URI(baseUrl + "/post/1"));
		assertThat(result.hasBody()).isFalse();
	}

	@Test
	public void exchangePostCallback() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.set("MyHeader", "MyValue");
		requestHeaders.setContentType(MediaType.TEXT_PLAIN);
		HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders);
		ListenableFuture<ResponseEntity<Void>> resultFuture =
				template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post");
		final URI expected =new URI(baseUrl + "/post/1");
		resultFuture.addCallback(new ListenableFutureCallback<ResponseEntity<Void>>() {
			@Override
			public void onSuccess(ResponseEntity<Void> result) {
				assertThat(result.getHeaders().getLocation()).as("Invalid location").isEqualTo(expected);
				assertThat(result.hasBody()).isFalse();
			}
			@Override
			public void onFailure(Throwable ex) {
				fail(ex.getMessage());
			}
		});
		waitTillDone(resultFuture);
	}

	@Test
	public void exchangePostCallbackWithLambdas() throws Exception {
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.set("MyHeader", "MyValue");
		requestHeaders.setContentType(MediaType.TEXT_PLAIN);
		HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders);
		ListenableFuture<ResponseEntity<Void>> resultFuture =
				template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post");
		final URI expected =new URI(baseUrl + "/post/1");
		resultFuture.addCallback(result -> {
			assertThat(result.getHeaders().getLocation()).as("Invalid location").isEqualTo(expected);
			assertThat(result.hasBody()).isFalse();
		}, ex -> fail(ex.getMessage()));
		waitTillDone(resultFuture);
	}

	@Test
	public void multipartFormData() throws Exception {
		MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
		parts.add("name 1", "value 1");
		parts.add("name 2", "value 2+1");
		parts.add("name 2", "value 2+2");
		Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
		parts.add("logo", logo);

		HttpEntity<MultiValueMap<String, Object>> requestBody = new HttpEntity<>(parts);
		Future<URI> future = template.postForLocation(baseUrl + "/multipartFormData", requestBody);
		future.get();
	}

	@Test
	public void getAndInterceptResponse() throws Exception {
		RequestInterceptor interceptor = new RequestInterceptor();
		template.setInterceptors(Collections.singletonList(interceptor));
		ListenableFuture<ResponseEntity<String>> future = template.getForEntity(baseUrl + "/get", String.class);

		interceptor.latch.await(5, TimeUnit.SECONDS);
		assertThat(interceptor.response).isNotNull();
		assertThat(interceptor.response.getStatusCode()).isEqualTo(HttpStatus.OK);
		assertThat(interceptor.exception).isNull();
		assertThat(future.get().getBody()).isEqualTo(helloWorld);
	}

	@Test
	public void getAndInterceptError() throws Exception {
		RequestInterceptor interceptor = new RequestInterceptor();
		template.setInterceptors(Collections.singletonList(interceptor));
		template.getForEntity(baseUrl + "/status/notfound", String.class);

		interceptor.latch.await(5, TimeUnit.SECONDS);
		assertThat(interceptor.response).isNotNull();
		assertThat(interceptor.response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
		assertThat(interceptor.exception).isNull();
	}

	private void waitTillDone(ListenableFuture<?> future) {
		while (!future.isDone()) {
			try {
				Thread.sleep(5);
			}
			catch (InterruptedException ex) {
				Thread.currentThread().interrupt();
			}
		}
	}


	private static class RequestInterceptor implements org.springframework.http.client.AsyncClientHttpRequestInterceptor {

		private final CountDownLatch latch = new CountDownLatch(1);

		private volatile ClientHttpResponse response;

		private volatile Throwable exception;

		@Override
		public ListenableFuture<ClientHttpResponse> intercept(HttpRequest request, byte[] body,
				org.springframework.http.client.AsyncClientHttpRequestExecution execution) throws IOException {

			ListenableFuture<ClientHttpResponse> future = execution.executeAsync(request, body);
			future.addCallback(
					resp -> {
						response = resp;
						this.latch.countDown();
					},
					ex -> {
						exception = ex;
						this.latch.countDown();
					});
			return future;
		}
	}

}

AsyncRestTemplate 是针对异步请求场景而设计的。ListenableFuture是对原有Future的增强,可以用于监听Future任务的执行状况,是执行成功还是执行失败,并提供响应的接口用于对不同结果的处理。示例如下:

//线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new CustomizableThreadFactory("demo"), new ThreadPoolExecutor.DiscardPolicy());
ListeningExecutorService listeningExecutor = MoreExecutors.listeningDecorator(poolExecutor);
 
//获得一个随着jvm关闭而关闭的线程池,通过Runtime.getRuntime().addShutdownHook(hook)实现,修改ThreadFactory为创建守护线程,默认jvm关闭时最多等待120秒关闭线程池,重载方法可以设置时间
ExecutorService newPoolExecutor = MoreExecutors.getExitingExecutorService(poolExecutor);
 
//只增加关闭线程池的钩子,不改变ThreadFactory
MoreExecutors.addDelayedShutdownHook(poolExecutor, 120, TimeUnit.SECONDS);
 
//提交任务
ListenableFuture<String> listenableFuture = listeningExecutor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        return "";
    }
});
//注册回调
Futures.addCallback(listenableFuture, new FutureCallback<String>() {
    @Override
    public void onSuccess(String result) {
    }
    @Override
    public void onFailure(Throwable t) {
    }
});
/**
* 链式语法
**/
ListenableFutureTask<String> task1 = ListenableFutureTask.create(new Callable<String>() {
    @Override
    public String call() throws Exception {
        return "";
    }
});
new Thread(task1).start();
task1.addListener(new Runnable() {
    @Override
    public void run() {
        ListenableFutureTask<String> task2 = ListenableFutureTask.create(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "";
            }
        });
        task2.addListener(new Runnable() {
            @Override
            public void run() {
                ...
            }
        }, MoreExecutors.directExecutor());
        new Thread(task2).start();
    }
}, MoreExecutors.directExecutor());
ListenableFuture<String> task2 = Futures.transform(task1, new Function<String, String>() {
    @Override
    public String apply(String input) {
        return "";
    }
});
ListenableFuture<String> task3 = Futures.transform(task2, new Function<String, String>() {
    @Override
    public String apply(String input) {
        return "";
    }
});
//处理最终的异步任务
Futures.addCallback(task3, new FutureCallback<String>() {
    @Override
    public void onSuccess(String result) {        
    }
    @Override
    public void onFailure(Throwable t) {
    }
});

Future和ListenableFuture适用场景:

  • 如果一个主任务开始执行,然后需要执行各个小任务,并且需要等待返回结果,统一返回给前端,此时Future和ListenableFuture作用几乎差不多,都是通过get()方法阻塞等待每个任务执行完毕返回。
  • 如果一个主任务开始执行,然后执行各个小任务,主任务不需要等待每个小任务执行完,不需要每个小任务的结果,此时用ListenableFuture非常合适,它提供的FutureCallback接口可以对每个任务的成功或失败单独做出响应。
  • 如果需要知道多个任务执行完成的先后顺序可以使用CompletableFuture,CompletableFuture也是对Futrue的扩展和提升。

JdbcTemplate

JdbcTemplate是JDBC核心软件包中的中心类。它简化了JDBC的使用,并有助于避免常见错误。它执行核心JDBC工作流程,留下应用程序代码以提供SQL并提取结果。此类执行SQL查询或更新,以启动在ResultSet上进行迭代并捕获JDBC异常并进行翻译,它们在  {@code org.springframework.dao}包。

  •    使用此类的代码仅需要实现回调接口,从而他们签订了明确的合同。 {@link PreparedStatementCreator}回调接口为给定的Connection创建准备好的语句,并提供SQL和任何必要的参数。 {@link ResultSetExtractor}接口提取ResultSet中的值。另请参见{@link PreparedStatementSetter}和 {@link RowMapper}用于两个流行的备用回调接口。
  •    可以通过直接实例化在服务实现中使用数据源引用,或在应用程序上下文中进行准备并提供给服务作为bean参考。注意:数据源应在第一种情况下,总是在应用程序上下文中被配置为bean直接提供给服务,第二种情况是提供给准备好的模板。
  •    因为此类可以通过回调接口和  {@link org.springframework.jdbc.support.SQLExceptionTranslator}   接口,则无需对其进行子类化。
  •    该类执行的所有SQL操作均在调试级别记录, 使用“ org.springframework.jdbc.core.JdbcTemplate”作为日志类别。

spring通过JdbcTemplate提供原生的JDBC SQL语句执行操作。JdbcTemplate和JdbcOperations均位于org.springframework.jdbc.core下,JdbcOperations提供最基本的SQL方法执行接口,JdbcTemplate负责模板方法调用。

package org.springframework.jdbc.core;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.BatchUpdateException;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import javax.sql.DataSource;

import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.InvalidResultSetAccessException;
import org.springframework.jdbc.SQLWarningException;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.jdbc.datasource.ConnectionProxy;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcAccessor;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;

/**
 * <b>This is the central class in the JDBC core package.</b>
 * It simplifies the use of JDBC and helps to avoid common errors.
 * It executes core JDBC workflow, leaving application code to provide SQL
 * and extract results. This class executes SQL queries or updates, initiating
 * iteration over ResultSets and catching JDBC exceptions and translating
 * them to the generic, more informative exception hierarchy defined in the
 * {@code org.springframework.dao} package.
 *
 * <p>Code using this class need only implement callback interfaces, giving
 * them a clearly defined contract. The {@link PreparedStatementCreator} callback
 * interface creates a prepared statement given a Connection, providing SQL and
 * any necessary parameters. The {@link ResultSetExtractor} interface extracts
 * values from a ResultSet. See also {@link PreparedStatementSetter} and
 * {@link RowMapper} for two popular alternative callback interfaces.
 *
 * <p>Can be used within a service implementation via direct instantiation
 * with a DataSource reference, or get prepared in an application context
 * and given to services as bean reference. Note: The DataSource should
 * always be configured as a bean in the application context, in the first case
 * given to the service directly, in the second case to the prepared template.
 *
 * <p>Because this class is parameterizable by the callback interfaces and
 * the {@link org.springframework.jdbc.support.SQLExceptionTranslator}
 * interface, there should be no need to subclass it.
 *
 * <p>All SQL operations performed by this class are logged at debug level,
 * using "org.springframework.jdbc.core.JdbcTemplate" as log category.
 *
 * <p><b>NOTE: An instance of this class is thread-safe once configured.</b>
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @author Thomas Risberg
 * @since May 3, 2001
 * @see PreparedStatementCreator
 * @see PreparedStatementSetter
 * @see CallableStatementCreator
 * @see PreparedStatementCallback
 * @see CallableStatementCallback
 * @see ResultSetExtractor
 * @see RowCallbackHandler
 * @see RowMapper
 * @see org.springframework.jdbc.support.SQLExceptionTranslator
 */
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
   
   

核心执行模板方法如下 :

//-------------------------------------------------------------------------
	// Methods dealing with a plain java.sql.Connection
	//-------------------------------------------------------------------------

	@Override
	@Nullable
	public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
		Assert.notNull(action, "Callback object must not be null");

		Connection con = DataSourceUtils.getConnection(obtainDataSource());
		try {
			// Create close-suppressing Connection proxy, also preparing returned Statements.
			Connection conToUse = createConnectionProxy(con);
			return action.doInConnection(conToUse);
		}
		catch (SQLException ex) {
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			String sql = getSql(action);
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw translateException("ConnectionCallback", sql, ex);
		}
		finally {
			DataSourceUtils.releaseConnection(con, getDataSource());
		}
	}

	/**
	 * Create a close-suppressing proxy for the given JDBC Connection.
	 * Called by the {@code execute} method.
	 * <p>The proxy also prepares returned JDBC Statements, applying
	 * statement settings such as fetch size, max rows, and query timeout.
	 * @param con the JDBC Connection to create a proxy for
	 * @return the Connection proxy
	 * @see java.sql.Connection#close()
	 * @see #execute(ConnectionCallback)
	 * @see #applyStatementSettings
	 */
	protected Connection createConnectionProxy(Connection con) {
		return (Connection) Proxy.newProxyInstance(
				ConnectionProxy.class.getClassLoader(),
				new Class<?>[] {ConnectionProxy.class},
				new CloseSuppressingInvocationHandler(con));
	}


	//-------------------------------------------------------------------------
	// Methods dealing with static SQL (java.sql.Statement)
	//-------------------------------------------------------------------------

	@Nullable
	private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
		Assert.notNull(action, "Callback object must not be null");

		Connection con = DataSourceUtils.getConnection(obtainDataSource());
		Statement stmt = null;
		try {
			stmt = con.createStatement();
			applyStatementSettings(stmt);
			T result = action.doInStatement(stmt);
			handleWarnings(stmt);
			return result;
		}
		catch (SQLException ex) {
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			String sql = getSql(action);
			JdbcUtils.closeStatement(stmt);
			stmt = null;
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw translateException("StatementCallback", sql, ex);
		}
		finally {
			if (closeResources) {
				JdbcUtils.closeStatement(stmt);
				DataSourceUtils.releaseConnection(con, getDataSource());
			}
		}
	}

 ConnectionCallback和StatementCallback功能性接口定义:

package org.springframework.jdbc.core;

import java.sql.Connection;
import java.sql.SQLException;

import org.springframework.dao.DataAccessException;
import org.springframework.lang.Nullable;

/**
 * Generic callback interface for code that operates on a JDBC Connection.
 * Allows to execute any number of operations on a single Connection,
 * using any type and number of Statements.
 *
 * <p>This is particularly useful for delegating to existing data access code
 * that expects a Connection to work on and throws SQLException. For newly
 * written code, it is strongly recommended to use JdbcTemplate's more specific
 * operations, for example a {@code query} or {@code update} variant.
 *
 * @author Juergen Hoeller
 * @since 1.1.3
 * @param <T> the result type
 * @see JdbcTemplate#execute(ConnectionCallback)
 * @see JdbcTemplate#query
 * @see JdbcTemplate#update
 */
@FunctionalInterface
public interface ConnectionCallback<T> {

	/**
	 * Gets called by {@code JdbcTemplate.execute} with an active JDBC
	 * Connection. Does not need to care about activating or closing the
	 * Connection, or handling transactions.
	 * <p>If called without a thread-bound JDBC transaction (initiated by
	 * DataSourceTransactionManager), the code will simply get executed on the
	 * JDBC connection with its transactional semantics. If JdbcTemplate is
	 * configured to use a JTA-aware DataSource, the JDBC Connection and thus
	 * the callback code will be transactional if a JTA transaction is active.
	 * <p>Allows for returning a result object created within the callback, i.e.
	 * a domain object or a collection of domain objects. Note that there's special
	 * support for single step actions: see {@code JdbcTemplate.queryForObject}
	 * etc. A thrown RuntimeException is treated as application exception:
	 * it gets propagated to the caller of the template.
	 * @param con active JDBC Connection
	 * @return a result object, or {@code null} if none
	 * @throws SQLException if thrown by a JDBC method, to be auto-converted
	 * to a DataAccessException by an SQLExceptionTranslator
	 * @throws DataAccessException in case of custom exceptions
	 * @see JdbcTemplate#queryForObject(String, Class)
	 * @see JdbcTemplate#queryForRowSet(String)
	 */
	@Nullable
	T doInConnection(Connection con) throws SQLException, DataAccessException;

}

package org.springframework.jdbc.core;

import java.sql.SQLException;
import java.sql.Statement;

import org.springframework.dao.DataAccessException;
import org.springframework.lang.Nullable;

/**
 * Generic callback interface for code that operates on a JDBC Statement.
 * Allows to execute any number of operations on a single Statement,
 * for example a single {@code executeUpdate} call or repeated
 * {@code executeUpdate} calls with varying SQL.
 *
 * <p>Used internally by JdbcTemplate, but also useful for application code.
 *
 * @author Juergen Hoeller
 * @since 16.03.2004
 * @param <T> the result type
 * @see JdbcTemplate#execute(StatementCallback)
 */
@FunctionalInterface
public interface StatementCallback<T> {

	/**
	 * Gets called by {@code JdbcTemplate.execute} with an active JDBC
	 * Statement. Does not need to care about closing the Statement or the
	 * Connection, or about handling transactions: this will all be handled
	 * by Spring's JdbcTemplate.
	 * <p><b>NOTE:</b> Any ResultSets opened should be closed in finally blocks
	 * within the callback implementation. Spring will close the Statement
	 * object after the callback returned, but this does not necessarily imply
	 * that the ResultSet resources will be closed: the Statement objects might
	 * get pooled by the connection pool, with {@code close} calls only
	 * returning the object to the pool but not physically closing the resources.
	 * <p>If called without a thread-bound JDBC transaction (initiated by
	 * DataSourceTransactionManager), the code will simply get executed on the
	 * JDBC connection with its transactional semantics. If JdbcTemplate is
	 * configured to use a JTA-aware DataSource, the JDBC connection and thus
	 * the callback code will be transactional if a JTA transaction is active.
	 * <p>Allows for returning a result object created within the callback, i.e.
	 * a domain object or a collection of domain objects. Note that there's
	 * special support for single step actions: see JdbcTemplate.queryForObject etc.
	 * A thrown RuntimeException is treated as application exception, it gets
	 * propagated to the caller of the template.
	 * @param stmt active JDBC Statement
	 * @return a result object, or {@code null} if none
	 * @throws SQLException if thrown by a JDBC method, to be auto-converted
	 * to a DataAccessException by an SQLExceptionTranslator
	 * @throws DataAccessException in case of custom exceptions
	 * @see JdbcTemplate#queryForObject(String, Class)
	 * @see JdbcTemplate#queryForRowSet(String)
	 */
	@Nullable
	T doInStatement(Statement stmt) throws SQLException, DataAccessException;

}

JDBC定义操作的行为,让具体调用实现来处理对应的业务数据库处理。 以下是JdbcTemplate测试示例:

package org.springframework.jdbc.core;

import java.sql.BatchUpdateException;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.SQLWarningException;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter;
import org.springframework.jdbc.datasource.ConnectionProxy;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

/**
 * Mock object based tests for JdbcTemplate.
 *
 * @author Rod Johnson
 * @author Thomas Risberg
 * @author Juergen Hoeller
 * @author Phillip Webb
 */
public class JdbcTemplateTests {

	private Connection connection;

	private DataSource dataSource;

	private PreparedStatement preparedStatement;

	private Statement statement;

	private ResultSet resultSet;

	private JdbcTemplate template;

	private CallableStatement callableStatement;


	@BeforeEach
	public void setup() throws Exception {
		this.connection = mock(Connection.class);
		this.dataSource = mock(DataSource.class);
		this.preparedStatement = mock(PreparedStatement.class);
		this.statement = mock(Statement.class);
		this.resultSet = mock(ResultSet.class);
		this.template = new JdbcTemplate(this.dataSource);
		this.callableStatement = mock(CallableStatement.class);
		given(this.dataSource.getConnection()).willReturn(this.connection);
		given(this.connection.prepareStatement(anyString())).willReturn(this.preparedStatement);
		given(this.preparedStatement.executeQuery()).willReturn(this.resultSet);
		given(this.preparedStatement.executeQuery(anyString())).willReturn(this.resultSet);
		given(this.preparedStatement.getConnection()).willReturn(this.connection);
		given(this.statement.getConnection()).willReturn(this.connection);
		given(this.statement.executeQuery(anyString())).willReturn(this.resultSet);
		given(this.connection.prepareCall(anyString())).willReturn(this.callableStatement);
		given(this.callableStatement.getResultSet()).willReturn(this.resultSet);
	}


	@Test
	public void testBeanProperties() throws Exception {
		assertThat(this.template.getDataSource() == this.dataSource).as("datasource ok").isTrue();
		assertThat(this.template.isIgnoreWarnings()).as("ignores warnings by default").isTrue();
		this.template.setIgnoreWarnings(false);
		boolean condition = !this.template.isIgnoreWarnings();
		assertThat(condition).as("can set NOT to ignore warnings").isTrue();
	}

	@Test
	public void testUpdateCount() throws Exception {
		final String sql = "UPDATE INVOICE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		int idParam = 11111;
		given(this.preparedStatement.executeUpdate()).willReturn(1);
		Dispatcher d = new Dispatcher(idParam, sql);
		int rowsAffected = this.template.update(d);
		assertThat(rowsAffected == 1).as("1 update affected 1 row").isTrue();
		verify(this.preparedStatement).setInt(1, idParam);
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testBogusUpdate() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final int idParam = 6666;

		// It's because Integers aren't canonical
		SQLException sqlException = new SQLException("bad update");
		given(this.preparedStatement.executeUpdate()).willThrow(sqlException);

		Dispatcher d = new Dispatcher(idParam, sql);
		assertThatExceptionOfType(UncategorizedSQLException.class).isThrownBy(() ->
				this.template.update(d))
			.withCause(sqlException);
		verify(this.preparedStatement).setInt(1, idParam);
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testStringsWithStaticSql() throws Exception {
		doTestStrings(null, null, null, null, (template, sql, rch) -> template.query(sql, rch));
	}

	@Test
	public void testStringsWithStaticSqlAndFetchSizeAndMaxRows() throws Exception {
		doTestStrings(10, 20, 30, null, (template, sql, rch) -> template.query(sql, rch));
	}

	@Test
	public void testStringsWithEmptyPreparedStatementSetter() throws Exception {
		doTestStrings(null, null, null, null, (template, sql, rch) ->
				template.query(sql, (PreparedStatementSetter) null, rch));
	}

	@Test
	public void testStringsWithPreparedStatementSetter() throws Exception {
		final Integer argument = 99;
		doTestStrings(null, null, null, argument, (template, sql, rch) ->
			template.query(sql, ps -> ps.setObject(1, argument), rch));
	}

	@Test
	@SuppressWarnings("deprecation")
	public void testStringsWithEmptyPreparedStatementArgs() throws Exception {
		doTestStrings(null, null, null, null,
				(template, sql, rch) -> template.query(sql, (Object[]) null, rch));
	}

	@Test
	@SuppressWarnings("deprecation")
	public void testStringsWithPreparedStatementArgs() throws Exception {
		final Integer argument = 99;
		doTestStrings(null, null, null, argument,
				(template, sql, rch) -> template.query(sql, new Object[] {argument}, rch));
	}

	private void doTestStrings(Integer fetchSize, Integer maxRows, Integer queryTimeout,
			Object argument, JdbcTemplateCallback jdbcTemplateCallback) throws Exception {

		String sql = "SELECT FORENAME FROM CUSTMR";
		String[] results = {"rod", "gary", " portia"};

		class StringHandler implements RowCallbackHandler {
			private List<String> list = new ArrayList<>();
			@Override
			public void processRow(ResultSet rs) throws SQLException {
				this.list.add(rs.getString(1));
			}
			public String[] getStrings() {
				return StringUtils.toStringArray(this.list);
			}
		}

		given(this.resultSet.next()).willReturn(true, true, true, false);
		given(this.resultSet.getString(1)).willReturn(results[0], results[1], results[2]);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);

		StringHandler sh = new StringHandler();
		JdbcTemplate template = new JdbcTemplate();
		template.setDataSource(this.dataSource);
		if (fetchSize != null) {
			template.setFetchSize(fetchSize.intValue());
		}
		if (maxRows != null) {
			template.setMaxRows(maxRows.intValue());
		}
		if (queryTimeout != null) {
			template.setQueryTimeout(queryTimeout.intValue());
		}
		jdbcTemplateCallback.doInJdbcTemplate(template, sql, sh);

		// Match
		String[] forenames = sh.getStrings();
		assertThat(forenames.length == results.length).as("same length").isTrue();
		for (int i = 0; i < forenames.length; i++) {
			assertThat(forenames[i].equals(results[i])).as("Row " + i + " matches").isTrue();
		}

		if (fetchSize != null) {
			verify(this.preparedStatement).setFetchSize(fetchSize.intValue());
		}
		if (maxRows != null) {
			verify(this.preparedStatement).setMaxRows(maxRows.intValue());
		}
		if (queryTimeout != null) {
			verify(this.preparedStatement).setQueryTimeout(queryTimeout.intValue());
		}
		if (argument != null) {
			verify(this.preparedStatement).setObject(1, argument);
		}
		verify(this.resultSet).close();
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testLeaveConnectionOpenOnRequest() throws Exception {
		String sql = "SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3";

		given(this.resultSet.next()).willReturn(false);
		given(this.connection.isClosed()).willReturn(false);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);
		// if close is called entire test will fail
		willThrow(new RuntimeException()).given(this.connection).close();

		SingleConnectionDataSource scf = new SingleConnectionDataSource(this.dataSource.getConnection(), false);
		this.template = new JdbcTemplate(scf, false);
		RowCountCallbackHandler rcch = new RowCountCallbackHandler();
		this.template.query(sql, rcch);

		verify(this.resultSet).close();
		verify(this.preparedStatement).close();
	}

	@Test
	public void testConnectionCallback() throws Exception {
		String result = this.template.execute(new ConnectionCallback<String>() {
			@Override
			public String doInConnection(Connection con) {
				assertThat(con instanceof ConnectionProxy).isTrue();
				assertThat(((ConnectionProxy) con).getTargetConnection()).isSameAs(JdbcTemplateTests.this.connection);
				return "test";
			}
		});
		assertThat(result).isEqualTo("test");
	}

	@Test
	public void testConnectionCallbackWithStatementSettings() throws Exception {
		String result = this.template.execute(new ConnectionCallback<String>() {
			@Override
			public String doInConnection(Connection con) throws SQLException {
				PreparedStatement ps = con.prepareStatement("some SQL");
				ps.setFetchSize(10);
				ps.setMaxRows(20);
				ps.close();
				return "test";
			}
		});

		assertThat(result).isEqualTo("test");
		verify(this.preparedStatement).setFetchSize(10);
		verify(this.preparedStatement).setMaxRows(20);
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testCloseConnectionOnRequest() throws Exception {
		String sql = "SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3";

		given(this.resultSet.next()).willReturn(false);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);

		RowCountCallbackHandler rcch = new RowCountCallbackHandler();
		this.template.query(sql, rcch);

		verify(this.resultSet).close();
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	/**
	 * Test that we see a runtime exception come back.
	 */
	@Test
	public void testExceptionComesBack() throws Exception {
		final String sql = "SELECT ID FROM CUSTMR";
		final RuntimeException runtimeException = new RuntimeException("Expected");

		given(this.resultSet.next()).willReturn(true);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);

		try {
			assertThatExceptionOfType(RuntimeException.class).isThrownBy(() ->
					this.template.query(sql, (RowCallbackHandler) rs -> {
						throw runtimeException;
					}))
				.withMessage(runtimeException.getMessage());
		}
		finally {
			verify(this.resultSet).close();
			verify(this.preparedStatement).close();
			verify(this.connection).close();
		}
	}

	/**
	 * Test update with static SQL.
	 */
	@Test
	public void testSqlUpdate() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 4";
		int rowsAffected = 33;

		given(this.statement.executeUpdate(sql)).willReturn(rowsAffected);
		given(this.connection.createStatement()).willReturn(this.statement);

		int actualRowsAffected = this.template.update(sql);
		assertThat(actualRowsAffected == rowsAffected).as("Actual rows affected is correct").isTrue();
		verify(this.statement).close();
		verify(this.connection).close();
	}

	/**
	 * Test update with dynamic SQL.
	 */
	@Test
	public void testSqlUpdateWithArguments() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ? and PR = ?";
		int rowsAffected = 33;
		given(this.preparedStatement.executeUpdate()).willReturn(rowsAffected);

		int actualRowsAffected = this.template.update(sql,
				4, new SqlParameterValue(Types.NUMERIC, 2, Float.valueOf(1.4142f)));
		assertThat(actualRowsAffected == rowsAffected).as("Actual rows affected is correct").isTrue();
		verify(this.preparedStatement).setObject(1, 4);
		verify(this.preparedStatement).setObject(2, Float.valueOf(1.4142f), Types.NUMERIC, 2);
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testSqlUpdateEncountersSqlException() throws Exception {
		SQLException sqlException = new SQLException("bad update");
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 4";

		given(this.statement.executeUpdate(sql)).willThrow(sqlException);
		given(this.connection.createStatement()).willReturn(this.statement);

		assertThatExceptionOfType(DataAccessException.class).isThrownBy(() ->
				this.template.update(sql))
			.withCause(sqlException);
		verify(this.statement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testSqlUpdateWithThreadConnection() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 4";
		int rowsAffected = 33;

		given(this.statement.executeUpdate(sql)).willReturn(rowsAffected);
		given(this.connection.createStatement()).willReturn(this.statement);

		int actualRowsAffected = this.template.update(sql);
		assertThat(actualRowsAffected == rowsAffected).as("Actual rows affected is correct").isTrue();

		verify(this.statement).close();
		verify(this.connection).close();
	}

	@Test
	public void testBatchUpdate() throws Exception {
		final String[] sql = {"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 1",
				"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 2"};

		given(this.statement.executeBatch()).willReturn(new int[] {1, 1});
		mockDatabaseMetaData(true);
		given(this.connection.createStatement()).willReturn(this.statement);

		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = template.batchUpdate(sql);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();

		verify(this.statement).addBatch(sql[0]);
		verify(this.statement).addBatch(sql[1]);
		verify(this.statement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testBatchUpdateWithBatchFailure() throws Exception {
		final String[] sql = {"A", "B", "C", "D"};
		given(this.statement.executeBatch()).willThrow(
				new BatchUpdateException(new int[] {1, Statement.EXECUTE_FAILED, 1, Statement.EXECUTE_FAILED}));
		mockDatabaseMetaData(true);
		given(this.connection.createStatement()).willReturn(this.statement);

		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);
		try {
			template.batchUpdate(sql);
		}
		catch (UncategorizedSQLException ex) {
			assertThat(ex.getSql()).isEqualTo("B; D");
		}
	}

	@Test
	public void testBatchUpdateWithNoBatchSupport() throws Exception {
		final String[] sql = {"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 1",
				"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 2"};

		given(this.statement.execute(sql[0])).willReturn(false);
		given(this.statement.getUpdateCount()).willReturn(1, 1);
		given(this.statement.execute(sql[1])).willReturn(false);

		mockDatabaseMetaData(false);
		given(this.connection.createStatement()).willReturn(this.statement);

		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = template.batchUpdate(sql);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();

		verify(this.statement, never()).addBatch(anyString());
		verify(this.statement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testBatchUpdateWithNoBatchSupportAndSelect() throws Exception {
		final String[] sql = {"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 1",
				"SELECT * FROM NOSUCHTABLE"};

		given(this.statement.execute(sql[0])).willReturn(false);
		given(this.statement.getUpdateCount()).willReturn(1);
		given(this.statement.execute(sql[1])).willReturn(true);
		mockDatabaseMetaData(false);
		given(this.connection.createStatement()).willReturn(this.statement);

		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);
		assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
				template.batchUpdate(sql));
		verify(this.statement, never()).addBatch(anyString());
		verify(this.statement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testBatchUpdateWithPreparedStatement() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final int[] ids = new int[] {100, 200};
		final int[] rowsAffected = new int[] {1, 2};

		given(this.preparedStatement.executeBatch()).willReturn(rowsAffected);
		mockDatabaseMetaData(true);

		BatchPreparedStatementSetter setter = new BatchPreparedStatementSetter() {
			@Override
			public void setValues(PreparedStatement ps, int i) throws SQLException {
				ps.setInt(1, ids[i]);
			}
			@Override
			public int getBatchSize() {
				return ids.length;
			}
		};

		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = template.batchUpdate(sql, setter);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
		assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
		assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);

		verify(this.preparedStatement, times(2)).addBatch();
		verify(this.preparedStatement).setInt(1, ids[0]);
		verify(this.preparedStatement).setInt(1, ids[1]);
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testInterruptibleBatchUpdate() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final int[] ids = new int[] {100, 200};
		final int[] rowsAffected = new int[] {1, 2};

		given(this.preparedStatement.executeBatch()).willReturn(rowsAffected);
		mockDatabaseMetaData(true);

		BatchPreparedStatementSetter setter =
				new InterruptibleBatchPreparedStatementSetter() {
					@Override
					public void setValues(PreparedStatement ps, int i) throws SQLException {
						if (i < ids.length) {
							ps.setInt(1, ids[i]);
						}
					}
					@Override
					public int getBatchSize() {
						return 1000;
					}
					@Override
					public boolean isBatchExhausted(int i) {
						return (i >= ids.length);
					}
				};

		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = template.batchUpdate(sql, setter);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
		assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
		assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);

		verify(this.preparedStatement, times(2)).addBatch();
		verify(this.preparedStatement).setInt(1, ids[0]);
		verify(this.preparedStatement).setInt(1, ids[1]);
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testInterruptibleBatchUpdateWithBaseClass() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final int[] ids = new int[] {100, 200};
		final int[] rowsAffected = new int[] {1, 2};

		given(this.preparedStatement.executeBatch()).willReturn(rowsAffected);
		mockDatabaseMetaData(true);

		BatchPreparedStatementSetter setter =
				new AbstractInterruptibleBatchPreparedStatementSetter() {
					@Override
					protected boolean setValuesIfAvailable(PreparedStatement ps, int i) throws SQLException {
						if (i < ids.length) {
							ps.setInt(1, ids[i]);
							return true;
						}
						else {
							return false;
						}
					}
				};

		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = template.batchUpdate(sql, setter);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
		assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
		assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);

		verify(this.preparedStatement, times(2)).addBatch();
		verify(this.preparedStatement).setInt(1, ids[0]);
		verify(this.preparedStatement).setInt(1, ids[1]);
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testInterruptibleBatchUpdateWithBaseClassAndNoBatchSupport() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final int[] ids = new int[] {100, 200};
		final int[] rowsAffected = new int[] {1, 2};

		given(this.preparedStatement.executeUpdate()).willReturn(rowsAffected[0], rowsAffected[1]);
		mockDatabaseMetaData(false);

		BatchPreparedStatementSetter setter =
				new AbstractInterruptibleBatchPreparedStatementSetter() {
					@Override
					protected boolean setValuesIfAvailable(PreparedStatement ps, int i) throws SQLException {
						if (i < ids.length) {
							ps.setInt(1, ids[i]);
							return true;
						}
						else {
							return false;
						}
					}
				};

		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = template.batchUpdate(sql, setter);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
		assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
		assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);

		verify(this.preparedStatement, never()).addBatch();
		verify(this.preparedStatement).setInt(1, ids[0]);
		verify(this.preparedStatement).setInt(1, ids[1]);
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testBatchUpdateWithPreparedStatementAndNoBatchSupport() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final int[] ids = new int[] {100, 200};
		final int[] rowsAffected = new int[] {1, 2};

		given(this.preparedStatement.executeUpdate()).willReturn(rowsAffected[0], rowsAffected[1]);

		BatchPreparedStatementSetter setter = new BatchPreparedStatementSetter() {
			@Override
			public void setValues(PreparedStatement ps, int i) throws SQLException {
				ps.setInt(1, ids[i]);
			}
			@Override
			public int getBatchSize() {
				return ids.length;
			}
		};

		int[] actualRowsAffected = this.template.batchUpdate(sql, setter);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
		assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
		assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);

		verify(this.preparedStatement, never()).addBatch();
		verify(this.preparedStatement).setInt(1, ids[0]);
		verify(this.preparedStatement).setInt(1, ids[1]);
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testBatchUpdateFails() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final int[] ids = new int[] {100, 200};
		SQLException sqlException = new SQLException();

		given(this.preparedStatement.executeBatch()).willThrow(sqlException);
		mockDatabaseMetaData(true);

		BatchPreparedStatementSetter setter = new BatchPreparedStatementSetter() {
			@Override
			public void setValues(PreparedStatement ps, int i) throws SQLException {
				ps.setInt(1, ids[i]);
			}
			@Override
			public int getBatchSize() {
				return ids.length;
			}
		};

		try {
			assertThatExceptionOfType(DataAccessException.class).isThrownBy(() ->
					this.template.batchUpdate(sql, setter))
				.withCause(sqlException);
		}
		finally {
			verify(this.preparedStatement, times(2)).addBatch();
			verify(this.preparedStatement).setInt(1, ids[0]);
			verify(this.preparedStatement).setInt(1, ids[1]);
			verify(this.preparedStatement).close();
			verify(this.connection, atLeastOnce()).close();
		}
	}

	@Test
	public void testBatchUpdateWithEmptyList() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = template.batchUpdate(sql, Collections.emptyList());
		assertThat(actualRowsAffected.length == 0).as("executed 0 updates").isTrue();
	}

	@Test
	public void testBatchUpdateWithListOfObjectArrays() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final List<Object[]> ids = new ArrayList<>(2);
		ids.add(new Object[] {100});
		ids.add(new Object[] {200});
		final int[] rowsAffected = new int[] {1, 2};

		given(this.preparedStatement.executeBatch()).willReturn(rowsAffected);
		mockDatabaseMetaData(true);
		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = template.batchUpdate(sql, ids);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
		assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
		assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);

		verify(this.preparedStatement, times(2)).addBatch();
		verify(this.preparedStatement).setObject(1, 100);
		verify(this.preparedStatement).setObject(1, 200);
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testBatchUpdateWithListOfObjectArraysPlusTypeInfo() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final List<Object[]> ids = new ArrayList<>(2);
		ids.add(new Object[] {100});
		ids.add(new Object[] {200});
		final int[] sqlTypes = new int[] {Types.NUMERIC};
		final int[] rowsAffected = new int[] {1, 2};

		given(this.preparedStatement.executeBatch()).willReturn(rowsAffected);
		mockDatabaseMetaData(true);
		this.template = new JdbcTemplate(this.dataSource, false);

		int[] actualRowsAffected = this.template.batchUpdate(sql, ids, sqlTypes);
		assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
		assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
		assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);
		verify(this.preparedStatement, times(2)).addBatch();
		verify(this.preparedStatement).setObject(1, 100, sqlTypes[0]);
		verify(this.preparedStatement).setObject(1, 200, sqlTypes[0]);
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testBatchUpdateWithCollectionOfObjects() throws Exception {
		final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?";
		final List<Integer> ids = Arrays.asList(100, 200, 300);
		final int[] rowsAffected1 = new int[] {1, 2};
		final int[] rowsAffected2 = new int[] {3};

		given(this.preparedStatement.executeBatch()).willReturn(rowsAffected1, rowsAffected2);
		mockDatabaseMetaData(true);

		ParameterizedPreparedStatementSetter<Integer> setter = (ps, argument) -> ps.setInt(1, argument.intValue());
		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);

		int[][] actualRowsAffected = template.batchUpdate(sql, ids, 2, setter);
		assertThat(actualRowsAffected[0].length).as("executed 2 updates").isEqualTo(2);
		assertThat(actualRowsAffected[0][0]).isEqualTo(rowsAffected1[0]);
		assertThat(actualRowsAffected[0][1]).isEqualTo(rowsAffected1[1]);
		assertThat(actualRowsAffected[1][0]).isEqualTo(rowsAffected2[0]);

		verify(this.preparedStatement, times(3)).addBatch();
		verify(this.preparedStatement).setInt(1, ids.get(0));
		verify(this.preparedStatement).setInt(1, ids.get(1));
		verify(this.preparedStatement).setInt(1, ids.get(2));
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testCouldNotGetConnectionForOperationOrExceptionTranslator() throws SQLException {
		SQLException sqlException = new SQLException("foo", "07xxx");
		this.dataSource = mock(DataSource.class);
		given(this.dataSource.getConnection()).willThrow(sqlException);
		JdbcTemplate template = new JdbcTemplate(this.dataSource, false);
		RowCountCallbackHandler rcch = new RowCountCallbackHandler();

		assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() ->
				template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch))
			.withCause(sqlException);
	}

	@Test
	public void testCouldNotGetConnectionForOperationWithLazyExceptionTranslator() throws SQLException {
		SQLException sqlException = new SQLException("foo", "07xxx");
		this.dataSource = mock(DataSource.class);
		given(this.dataSource.getConnection()).willThrow(sqlException);
		this.template = new JdbcTemplate();
		this.template.setDataSource(this.dataSource);
		this.template.afterPropertiesSet();
		RowCountCallbackHandler rcch = new RowCountCallbackHandler();

		assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() ->
				this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch))
			.withCause(sqlException);
	}

	@Test
	public void testCouldNotGetConnectionInOperationWithExceptionTranslatorInitializedViaBeanProperty()
			throws SQLException {

		doTestCouldNotGetConnectionInOperationWithExceptionTranslatorInitialized(true);
	}

	@Test
	public void testCouldNotGetConnectionInOperationWithExceptionTranslatorInitializedInAfterPropertiesSet()
			throws SQLException {

		doTestCouldNotGetConnectionInOperationWithExceptionTranslatorInitialized(false);
	}

	/**
	 * If beanProperty is true, initialize via exception translator bean property;
	 * if false, use afterPropertiesSet().
	 */
	private void doTestCouldNotGetConnectionInOperationWithExceptionTranslatorInitialized(boolean beanProperty)
			throws SQLException {

		SQLException sqlException = new SQLException("foo", "07xxx");
		this.dataSource = mock(DataSource.class);
		given(this.dataSource.getConnection()).willThrow(sqlException);
		this.template = new JdbcTemplate();
		this.template.setDataSource(this.dataSource);
		this.template.setLazyInit(false);
		if (beanProperty) {
			// This will get a connection.
			this.template.setExceptionTranslator(new SQLErrorCodeSQLExceptionTranslator(this.dataSource));
		}
		else {
			// This will cause creation of default SQL translator.
			this.template.afterPropertiesSet();
		}
		RowCountCallbackHandler rcch = new RowCountCallbackHandler();
		assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() ->
				this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch))
			.withCause(sqlException);
	}

	@Test
	public void testPreparedStatementSetterSucceeds() throws Exception {
		final String sql = "UPDATE FOO SET NAME=? WHERE ID = 1";
		final String name = "Gary";
		int expectedRowsUpdated = 1;

		given(this.preparedStatement.executeUpdate()).willReturn(expectedRowsUpdated);

		PreparedStatementSetter pss = ps -> ps.setString(1, name);
		int actualRowsUpdated = new JdbcTemplate(this.dataSource).update(sql, pss);
		assertThat(expectedRowsUpdated).as("updated correct # of rows").isEqualTo(actualRowsUpdated);
		verify(this.preparedStatement).setString(1, name);
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testPreparedStatementSetterFails() throws Exception {
		final String sql = "UPDATE FOO SET NAME=? WHERE ID = 1";
		final String name = "Gary";
		SQLException sqlException = new SQLException();
		given(this.preparedStatement.executeUpdate()).willThrow(sqlException);

		PreparedStatementSetter pss = ps -> ps.setString(1, name);
		assertThatExceptionOfType(DataAccessException.class).isThrownBy(() ->
				new JdbcTemplate(this.dataSource).update(sql, pss))
			.withCause(sqlException);
		verify(this.preparedStatement).setString(1, name);
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testCouldNotClose() throws Exception {
		SQLException sqlException = new SQLException("bar");
		given(this.connection.createStatement()).willReturn(this.statement);
		given(this.resultSet.next()).willReturn(false);
		willThrow(sqlException).given(this.resultSet).close();
		willThrow(sqlException).given(this.statement).close();
		willThrow(sqlException).given(this.connection).close();

		RowCountCallbackHandler rcch = new RowCountCallbackHandler();
		this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch);
		verify(this.connection).close();
	}

	/**
	 * Mock objects allow us to produce warnings at will
	 */
	@Test
	public void testFatalWarning() throws Exception {
		String sql = "SELECT forename from custmr";
		SQLWarning warnings = new SQLWarning("My warning");

		given(this.resultSet.next()).willReturn(false);
		given(this.preparedStatement.getWarnings()).willReturn(warnings);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);

		JdbcTemplate t = new JdbcTemplate(this.dataSource);
		t.setIgnoreWarnings(false);

		ResultSetExtractor<Byte> extractor = rs -> rs.getByte(1);
		assertThatExceptionOfType(SQLWarningException.class).isThrownBy(() ->
				t.query(sql, extractor))
			.withCause(warnings);
		verify(this.resultSet).close();
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testIgnoredWarning() throws Exception {
		String sql = "SELECT forename from custmr";
		SQLWarning warnings = new SQLWarning("My warning");

		given(this.resultSet.next()).willReturn(false);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);
		given(this.preparedStatement.getWarnings()).willReturn(warnings);

		// Too long: truncation

		this.template.setIgnoreWarnings(true);
		RowCallbackHandler rch = rs -> rs.getByte(1);
		this.template.query(sql, rch);

		verify(this.resultSet).close();
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testSQLErrorCodeTranslation() throws Exception {
		final SQLException sqlException = new SQLException("I have a known problem", "99999", 1054);
		final String sql = "SELECT ID FROM CUSTOMER";

		given(this.resultSet.next()).willReturn(true);
		mockDatabaseMetaData(false);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);

		assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() ->
				this.template.query(sql, (RowCallbackHandler) rs -> {
					throw sqlException;
				}))
			.withCause(sqlException);
		verify(this.resultSet).close();
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testSQLErrorCodeTranslationWithSpecifiedDbName() throws Exception {
		final SQLException sqlException = new SQLException("I have a known problem", "99999", 1054);
		final String sql = "SELECT ID FROM CUSTOMER";

		given(this.resultSet.next()).willReturn(true);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);

		JdbcTemplate template = new JdbcTemplate();
		template.setDataSource(this.dataSource);
		template.setDatabaseProductName("MySQL");
		template.afterPropertiesSet();

		assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() ->
				template.query(sql, (RowCallbackHandler) rs -> {
					throw sqlException;
				}))
			.withCause(sqlException);
		verify(this.resultSet).close();
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	/**
	 * Test that we see an SQLException translated using Error Code.
	 * If we provide the SQLExceptionTranslator, we shouldn't use a connection
	 * to get the metadata
	 */
	@Test
	public void testUseCustomSQLErrorCodeTranslator() throws Exception {
		// Bad SQL state
		final SQLException sqlException = new SQLException("I have a known problem", "07000", 1054);
		final String sql = "SELECT ID FROM CUSTOMER";

		given(this.resultSet.next()).willReturn(true);
		given(this.connection.createStatement()).willReturn(this.preparedStatement);

		JdbcTemplate template = new JdbcTemplate();
		template.setDataSource(this.dataSource);
		// Set custom exception translator
		template.setExceptionTranslator(new SQLStateSQLExceptionTranslator());
		template.afterPropertiesSet();

		assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() ->
				template.query(sql, (RowCallbackHandler) rs -> {
					throw sqlException;
				}))
			.withCause(sqlException);
		verify(this.resultSet).close();
		verify(this.preparedStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testStaticResultSetClosed() throws Exception {
		ResultSet resultSet2 = mock(ResultSet.class);
		reset(this.preparedStatement);
		given(this.preparedStatement.executeQuery()).willReturn(resultSet2);
		given(this.connection.createStatement()).willReturn(this.statement);

		assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
				this.template.query("my query", (ResultSetExtractor<Object>) rs -> {
					throw new InvalidDataAccessApiUsageException("");
				}));
		assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
				this.template.query(con -> con.prepareStatement("my query"), (ResultSetExtractor<Object>) rs2 -> {
					throw new InvalidDataAccessApiUsageException("");
				}));

		verify(this.resultSet).close();
		verify(resultSet2).close();
		verify(this.preparedStatement).close();
		verify(this.connection, atLeastOnce()).close();
	}

	@Test
	public void testExecuteClosed() throws Exception {
		given(this.resultSet.next()).willReturn(true);
		given(this.callableStatement.execute()).willReturn(true);
		given(this.callableStatement.getUpdateCount()).willReturn(-1);

		SqlParameter param = new SqlReturnResultSet("", (RowCallbackHandler) rs -> {
			throw new InvalidDataAccessApiUsageException("");
		});

		assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
				this.template.call(conn -> conn.prepareCall("my query"), Collections.singletonList(param)));
		verify(this.resultSet).close();
		verify(this.callableStatement).close();
		verify(this.connection).close();
	}

	@Test
	public void testCaseInsensitiveResultsMap() throws Exception {
		given(this.callableStatement.execute()).willReturn(false);
		given(this.callableStatement.getUpdateCount()).willReturn(-1);
		given(this.callableStatement.getObject(1)).willReturn("X");

		boolean condition = !this.template.isResultsMapCaseInsensitive();
		assertThat(condition).as("default should have been NOT case insensitive").isTrue();

		this.template.setResultsMapCaseInsensitive(true);
		assertThat(this.template.isResultsMapCaseInsensitive()).as("now it should have been set to case insensitive").isTrue();

		Map<String, Object> out = this.template.call(
				conn -> conn.prepareCall("my query"), Collections.singletonList(new SqlOutParameter("a", 12)));

		assertThat(out).isInstanceOf(LinkedCaseInsensitiveMap.class);
		assertThat(out.get("A")).as("we should have gotten the result with upper case").isNotNull();
		assertThat(out.get("a")).as("we should have gotten the result with lower case").isNotNull();
		verify(this.callableStatement).close();
		verify(this.connection).close();
	}

	@Test  // SPR-16578
	public void testEquallyNamedColumn() throws SQLException {
		given(this.connection.createStatement()).willReturn(this.statement);

		ResultSetMetaData metaData = mock(ResultSetMetaData.class);
		given(metaData.getColumnCount()).willReturn(2);
		given(metaData.getColumnLabel(1)).willReturn("x");
		given(metaData.getColumnLabel(2)).willReturn("X");
		given(this.resultSet.getMetaData()).willReturn(metaData);

		given(this.resultSet.next()).willReturn(true, false);
		given(this.resultSet.getObject(1)).willReturn("first value");
		given(this.resultSet.getObject(2)).willReturn("second value");

		Map<String, Object> map = this.template.queryForMap("my query");
		assertThat(map.size()).isEqualTo(1);
		assertThat(map.get("x")).isEqualTo("first value");
	}


	private void mockDatabaseMetaData(boolean supportsBatchUpdates) throws SQLException {
		DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
		given(databaseMetaData.getDatabaseProductName()).willReturn("MySQL");
		given(databaseMetaData.supportsBatchUpdates()).willReturn(supportsBatchUpdates);
		given(this.connection.getMetaData()).willReturn(databaseMetaData);
	}


	private interface JdbcTemplateCallback {

		void doInJdbcTemplate(JdbcTemplate template, String sql, RowCallbackHandler rch);
	}


	private static class Dispatcher implements PreparedStatementCreator, SqlProvider {

		private int id;

		private String sql;

		public Dispatcher(int id, String sql) {
			this.id = id;
			this.sql = sql;
		}

		@Override
		public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
			PreparedStatement ps = connection.prepareStatement(this.sql);
			ps.setInt(1, this.id);
			return ps;
		}

		@Override
		public String getSql() {
			return this.sql;
		}
	}

}

HibernateTemplate

HibernateTemplate (javadoc-api)简化Hibernate数据访问代码的Helper类。按照org.springframework.dao异常层次结构,自动将HibernateExceptions转换为DataAccessExceptions。
核心方法是execute,它支持实现HibernateCallback接口的Hibernate访问代码。它提供了Hibernate Session处理,因此HibernateCallback实现和调用代码都无需显式关心检索/关闭Hibernate Session或处理Session生命周期异常。对于典型的单步操作,有多种便捷方法(find,load,saveOrUpdate,delete)。

可以在服务实现中通过使用SessionFactory引用进行直接实例化来使用,也可以在应用程序上下文中进行准备并作为bean引用提供给服务。注意:在应用程序上下文中,应始终将SessionFactory配置为bean,在第一种情况下,直接将其提供给服务,在第二种情况下,将其配置为准备好的模板。

注意:Hibernate访问代码也可以针对本机Hibernate会话进行编码。因此,对于新启动的项目,请考虑对SessionFactory.getCurrentSession()采用标准的Hibernate编码风格。或者,对带有回调函数的Session使用Java 8 lambda代码块的execute(HibernateCallback),这也会产生优美的代码,并与Hibernate Session生命周期脱钩。同时,此HibernateTemplate上的其余操作已被弃用,并且主要作为现有应用程序中较旧的Hibernate 3.x / 4.x数据访问代码的迁移帮助器存在。

package org.springframework.orm.hibernate5;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.persistence.PersistenceException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.Filter;
import org.hibernate.FlushMode;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.ReplicationMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Example;
import org.hibernate.query.Query;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.ResourceHolderSupport;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;

/**
 * Helper class that simplifies Hibernate data access code. Automatically
 * converts HibernateExceptions into DataAccessExceptions, following the
 * {@code org.springframework.dao} exception hierarchy.
 *
 * <p>The central method is {@code execute}, supporting Hibernate access code
 * implementing the {@link HibernateCallback} interface. It provides Hibernate Session
 * handling such that neither the HibernateCallback implementation nor the calling
 * code needs to explicitly care about retrieving/closing Hibernate Sessions,
 * or handling Session lifecycle exceptions. For typical single step actions,
 * there are various convenience methods (find, load, saveOrUpdate, delete).
 *
 * <p>Can be used within a service implementation via direct instantiation
 * with a SessionFactory reference, or get prepared in an application context
 * and given to services as bean reference. Note: The SessionFactory should
 * always be configured as bean in the application context, in the first case
 * given to the service directly, in the second case to the prepared template.
 *
 * <p><b>NOTE: Hibernate access code can also be coded against the native Hibernate
 * {@link Session}. Hence, for newly started projects, consider adopting the standard
 * Hibernate style of coding against {@link SessionFactory#getCurrentSession()}.
 * Alternatively, use {@link #execute(HibernateCallback)} with Java 8 lambda code blocks
 * against the callback-provided {@code Session} which results in elegant code as well,
 * decoupled from the Hibernate Session lifecycle. The remaining operations on this
 * HibernateTemplate are deprecated in the meantime and primarily exist as a migration
 * helper for older Hibernate 3.x/4.x data access code in existing applications.</b>
 *
 * @author Juergen Hoeller
 * @since 4.2
 * @see #setSessionFactory
 * @see HibernateCallback
 * @see Session
 * @see LocalSessionFactoryBean
 * @see HibernateTransactionManager
 * @see org.springframework.orm.hibernate5.support.OpenSessionInViewFilter
 * @see org.springframework.orm.hibernate5.support.OpenSessionInViewInterceptor
 */
public class HibernateTemplate implements HibernateOperations, InitializingBean {

核心执行方法:


	@Override
	@Nullable
	public <T> T execute(HibernateCallback<T> action) throws DataAccessException {
		return doExecute(action, false);
	}

/**
	 * Execute the action specified by the given action object within a Session.
	 * @param action callback object that specifies the Hibernate action
	 * @param enforceNativeSession whether to enforce exposure of the native
	 * Hibernate Session to callback code
	 * @return a result object returned by the action, or {@code null}
	 * @throws DataAccessException in case of Hibernate errors
	 */
	@Nullable
	protected <T> T doExecute(HibernateCallback<T> action, boolean enforceNativeSession) throws DataAccessException {
		Assert.notNull(action, "Callback object must not be null");

		Session session = null;
		boolean isNew = false;
		try {
			session = obtainSessionFactory().getCurrentSession();
		}
		catch (HibernateException ex) {
			logger.debug("Could not retrieve pre-bound Hibernate session", ex);
		}
		if (session == null) {
			session = obtainSessionFactory().openSession();
			session.setHibernateFlushMode(FlushMode.MANUAL);
			isNew = true;
		}

		try {
			enableFilters(session);
			Session sessionToExpose =
					(enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session));
			return action.doInHibernate(sessionToExpose);
		}
		catch (HibernateException ex) {
			throw SessionFactoryUtils.convertHibernateAccessException(ex);
		}
		catch (PersistenceException ex) {
			if (ex.getCause() instanceof HibernateException) {
				throw SessionFactoryUtils.convertHibernateAccessException((HibernateException) ex.getCause());
			}
			throw ex;
		}
		catch (RuntimeException ex) {
			// Callback code threw application exception...
			throw ex;
		}
		finally {
			if (isNew) {
				SessionFactoryUtils.closeSession(session);
			}
			else {
				disableFilters(session);
			}
		}
	}

 HibernateCallback功能性接口定义:

package org.springframework.orm.hibernate5;

import org.hibernate.HibernateException;
import org.hibernate.Session;

import org.springframework.lang.Nullable;

/**
 * Callback interface for Hibernate code. To be used with {@link HibernateTemplate}'s
 * execution methods, often as anonymous classes within a method implementation.
 * A typical implementation will call {@code Session.load/find/update} to perform
 * some operations on persistent objects.
 *
 * @author Juergen Hoeller
 * @since 4.2
 * @param <T> the result type
 * @see HibernateTemplate
 * @see HibernateTransactionManager
 */
@FunctionalInterface
public interface HibernateCallback<T> {

	/**
	 * Gets called by {@code HibernateTemplate.execute} with an active
	 * Hibernate {@code Session}. Does not need to care about activating
	 * or closing the {@code Session}, or handling transactions.
	 * <p>Allows for returning a result object created within the callback,
	 * i.e. a domain object or a collection of domain objects.
	 * A thrown custom RuntimeException is treated as an application exception:
	 * It gets propagated to the caller of the template.
	 * @param session active Hibernate session
	 * @return a result object, or {@code null} if none
	 * @throws HibernateException if thrown by the Hibernate API
	 * @see HibernateTemplate#execute
	 */
	@Nullable
	T doInHibernate(Session session) throws HibernateException;

}

Hibernate Dao 支持实现:

package org.springframework.orm.hibernate5.support;

import org.hibernate.Session;
import org.hibernate.SessionFactory;

import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.support.DaoSupport;
import org.springframework.lang.Nullable;
import org.springframework.orm.hibernate5.HibernateTemplate;
import org.springframework.util.Assert;

/**
 * Convenient super class for Hibernate-based data access objects.
 *
 * <p>Requires a {@link SessionFactory} to be set, providing a
 * {@link org.springframework.orm.hibernate5.HibernateTemplate} based on it to
 * subclasses through the {@link #getHibernateTemplate()} method.
 * Can alternatively be initialized directly with a HibernateTemplate,
 * in order to reuse the latter's settings such as the SessionFactory,
 * exception translator, flush mode, etc.
 *
 * <p>This class will create its own HibernateTemplate instance if a SessionFactory
 * is passed in. The "allowCreate" flag on that HibernateTemplate will be "true"
 * by default. A custom HibernateTemplate instance can be used through overriding
 * {@link #createHibernateTemplate}.
 *
 * <p><b>NOTE: Hibernate access code can also be coded in plain Hibernate style.
 * Hence, for newly started projects, consider adopting the standard Hibernate
 * style of coding data access objects instead, based on
 * {@link SessionFactory#getCurrentSession()}.
 * This HibernateTemplate primarily exists as a migration helper for Hibernate 3
 * based data access code, to benefit from bug fixes in Hibernate 5.x.</b>
 *
 * @author Juergen Hoeller
 * @since 4.2
 * @see #setSessionFactory
 * @see #getHibernateTemplate
 * @see org.springframework.orm.hibernate5.HibernateTemplate
 */
public abstract class HibernateDaoSupport extends DaoSupport {

	@Nullable
	private HibernateTemplate hibernateTemplate;


	/**
	 * Set the Hibernate SessionFactory to be used by this DAO.
	 * Will automatically create a HibernateTemplate for the given SessionFactory.
	 * @see #createHibernateTemplate
	 * @see #setHibernateTemplate
	 */
	public final void setSessionFactory(SessionFactory sessionFactory) {
		if (this.hibernateTemplate == null || sessionFactory != this.hibernateTemplate.getSessionFactory()) {
			this.hibernateTemplate = createHibernateTemplate(sessionFactory);
		}
	}

	/**
	 * Create a HibernateTemplate for the given SessionFactory.
	 * Only invoked if populating the DAO with a SessionFactory reference!
	 * <p>Can be overridden in subclasses to provide a HibernateTemplate instance
	 * with different configuration, or a custom HibernateTemplate subclass.
	 * @param sessionFactory the Hibernate SessionFactory to create a HibernateTemplate for
	 * @return the new HibernateTemplate instance
	 * @see #setSessionFactory
	 */
	protected HibernateTemplate createHibernateTemplate(SessionFactory sessionFactory) {
		return new HibernateTemplate(sessionFactory);
	}

	/**
	 * Return the Hibernate SessionFactory used by this DAO.
	 */
	@Nullable
	public final SessionFactory getSessionFactory() {
		return (this.hibernateTemplate != null ? this.hibernateTemplate.getSessionFactory() : null);
	}

	/**
	 * Set the HibernateTemplate for this DAO explicitly,
	 * as an alternative to specifying a SessionFactory.
	 * @see #setSessionFactory
	 */
	public final void setHibernateTemplate(@Nullable HibernateTemplate hibernateTemplate) {
		this.hibernateTemplate = hibernateTemplate;
	}

	/**
	 * Return the HibernateTemplate for this DAO,
	 * pre-initialized with the SessionFactory or set explicitly.
	 * <p><b>Note: The returned HibernateTemplate is a shared instance.</b>
	 * You may introspect its configuration, but not modify the configuration
	 * (other than from within an {@link #initDao} implementation).
	 * Consider creating a custom HibernateTemplate instance via
	 * {@code new HibernateTemplate(getSessionFactory())}, in which case
	 * you're allowed to customize the settings on the resulting instance.
	 */
	@Nullable
	public final HibernateTemplate getHibernateTemplate() {
		return this.hibernateTemplate;
	}

	@Override
	protected final void checkDaoConfig() {
		if (this.hibernateTemplate == null) {
			throw new IllegalArgumentException("'sessionFactory' or 'hibernateTemplate' is required");
		}
	}


	/**
	 * Conveniently obtain the current Hibernate Session.
	 * @return the Hibernate Session
	 * @throws DataAccessResourceFailureException if the Session couldn't be created
	 * @see SessionFactory#getCurrentSession()
	 */
	protected final Session currentSession() throws DataAccessResourceFailureException {
		SessionFactory sessionFactory = getSessionFactory();
		Assert.state(sessionFactory != null, "No SessionFactory set");
		return sessionFactory.getCurrentSession();
	}

}

参考文章

模板模式

深入理解 Spring 中的 ThreadPoolTaskExecutor 与 ListenableFuture 对象

ListenableFuture异步多线程代码实现

ListenableFuture、CompletableFuture、RxJava(Observable)区别

猜你喜欢

转载自blog.csdn.net/boonya/article/details/115208189