1. 程式人生 > 其它 >Spring-RetryTemplate-RestTemplate的使用 SpringBoot系列: RestTemplate 快速入門

Spring-RetryTemplate-RestTemplate的使用 SpringBoot系列: RestTemplate 快速入門

------------------------------------------------------------------------------------

1.基本概念

1.1應用場景

1.1.1 資料同步

有時候專案需要進行同步資料(定時任務),一定要同步成功,不然對於業務會有影響,偶發性的會出現呼叫介面失敗,失敗並不是特別多,一般的流程如下:
(1)迴圈的進行遠端呼叫,同步資料,記錄一下呼叫失敗的記錄
(2)休眠一段時間,繼續迴圈呼叫失敗的記錄
(3)如果再呼叫失敗、通過人工二次呼叫進行修復

1.1.2 丟擲xxx異常或者返回結果為x 需要重試

比如:遠端呼叫超時、網路突然中斷等可以重試

1.2 重試框架需要解決的問題

1.2.1 重試的策略(RetryPolicy)

無限重試?最多重試幾次、指定的時間範圍內可以重試、或者多種重試策略組合。

1.2.2 重試的要休眠多久(BackOffPolicy)

重試時間間隔,每次都休眠固定的時間、第一次1s 第二次2s 第三次4s 、隨機的休眠時間

1.2.3兜底方案(Recover)

如果所有的重試都失敗了、兜底方案是什麼?有點類似限流,最差返回你係統繁忙的介面。

2.spring retry

Spring Retry 是從 Spring batch 中獨立出來的一個功能,主要實現了重試和熔斷,對於那些重試後不會改變結果,毫無意義的操作,不建議使用重試。spring retry提供了註解和程式設計 兩種支援,提供了 RetryTemplate 支援,類似RestTemplate。整個流程如下:


  image.png

具體使用過程中涉及的核心物件有:
RetryTemplate: 封裝了Retry基本操作,是進入spring-retry框架的整體流程入口,通過RetryTemplate可以指定監聽、回退策略、重試策略等。
RetryCallback:該介面封裝了業務程式碼,且failback後,會再次呼叫RetryCallback介面
RetryPolicy:重試策略,描述將以什麼樣的方式呼叫RetryCallback介面
BackOffPolicy :回退策略,當出現錯誤時延遲多少時間繼續呼叫

2.1 新增依賴

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
            <version>1.1.5.RELEASE</version>
        </dependency>

2.2 使用步驟

(1)定義重試策略RetryPolicy
實際過程如果不定義,則預設SimpleRetryPolicy策略(重試3次)。重試策略有以下種:
NeverRetryPolicy:只允許呼叫RetryCallback一次,不允許重試;
AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當會導致死迴圈;
SimpleRetryPolicy:固定次數重試策略,預設重試最大次數為3次,RetryTemplate預設使用的策略;
TimeoutRetryPolicy:超時時間重試策略,預設超時時間為1秒,在指定的超時時間內允許重試;
CircuitBreakerRetryPolicy:有熔斷功能的重試策略,需設定3個引數openTimeout、resetTimeout和delegate;
CompositeRetryPolicy:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許重試即

// 重試策略,指定重試5次
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(5);
retryTemplate.setRetryPolicy(retryPolicy);

配置之後在RetryTemplate中指定
(2)定義退避策略(BackOffPolicy )
策略主要有以下幾種:
FixedBackOffPolicy 固定時間
ExponentialBackOffPolicy 指數退避策略
ExponentialRandomBackOffPolicy 指數隨機退避策略

        RetryTemplate retryTemplate = new RetryTemplate();
        ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
        backOffPolicy.setInitialInterval(3000);
        backOffPolicy.setMultiplier(2);
        backOffPolicy.setMaxInterval(15000);
        retryTemplate.setBackOffPolicy(backOffPolicy);

配置之後在RetryTemplate中指定
(3)RetryTemplate執行整體流程
RetryTemplate中指定回退策略為ExponentialBackOffPolicy,指定重試策略為SimpleRetryPolicy,執行操作使用(RetryCallback 執行業務邏輯 ,RecoveryCallback 兜底)。這裡面需要用到以下核心物件
RetryCallback :業務回撥入口,為retryTemplate.execute時執行的回撥
RecoveryCallback :兜底回撥入口
RetryContext 重試上下文

//execute接受兩個引數(回撥函式)業務回撥和兜底回撥
RefundApplicationFormrefundApplicationForm = retryTemplate.execute(
        //RetryCallback
    (RetryCallback<RefundApplicationForm, Throwable>) retryContext -> bankAutoRepay(mRefundApplicationForm,
            supplierBankId),
       //RecoveryCallback兜底
    retryContext -> {
        // 銀行多次重試後異常
        mRefundApplicationForm.setRepayStatCd("100");
        logger.error("銀行多次重試後異常、銀行自動退款異常");
        return mRefundApplicationForm;
    }
);

3.spring retry 註解方式

3.1 啟用Spring Retry支援

為了啟用Spring Retry的功能,需要向配置類新增@EnableRetry註釋。

@SpringBootApplication
@EnableRetry
public class Launcher {
    public static void main(String[] args) {
        SpringApplication.run(Launcher.class, args);
    }

3.2 啟用重試特性的方法上使用@Retryable註釋

通過此註解設定重試策略和回退策略。Retryable註解引數:
(1)value:指定發生的異常進行重試
(2)include:和value一樣,預設空,當exclude也為空時,所有異常都重試
(3)exclude:指定異常不重試,預設空,當include也為空時,所有異常都重試
(4)maxAttemps:重試次數,預設3
(5)backoff:重試補償機制,預設沒有

 /**
     * 指定異常CustomRetryException重試,重試最大次數為4(預設是3),重試補償機制間隔200毫秒
     * 還可以配置exclude,指定異常不充實,預設為空
     * @return result
     * @throws CustomRetryException 指定異常
     */
    @Retryable(value = {CustomRetryException.class},maxAttempts = 4,backoff = @Backoff(200))
    String retry() throws CustomRetryException;

@Backoff 註解 重試補償策略:
(1)不設定引數時,預設使用FixedBackOffPolicy(指定等待時間),重試等待1000ms
(2)設定delay,使用FixedBackOffPolicy(指定等待設定delay和maxDealy時,重試等待在這兩個值之間均態分佈)
(3)設定delay、maxDealy、multiplier,使用 ExponentialBackOffPolicy(指數級重試間隔的實現),multiplier即指定延遲倍數,比如delay=5000L,multiplier=2,則第一次重試為5秒,第二次為10秒,第三次為20秒

3.3 @Recover

重試多次失敗後,執行兜底方案

@Service
@Slf4j
public class RetryServiceImpl implements RetryService {
    private static int count = 1;
    @Override
    public String retry() throws CustomRetryException {
        log.info("retry{},throw CustomRetryException in method retry",count);
        count ++;
        throw new CustomRetryException("throw custom exception");
    }
    @Recover
    public String recover(Throwable throwable) {
        log.info("Default Retry service test");
        return "Error Class :: " + throwable.getClass().getName();
    }
}

通過Junit進行單元測試。

    @Test
    void retry() {
        try {
            final String message = retryService.retry();
            log.info("message = "+message);
        } catch (CustomRetryException e) {
            log.error("Error while executing test {}",e.getMessage());
        }
------------------------------------------------------------------------------------

RestTemplate

spring框架提供的RestTemplate類可用於在應用中呼叫rest服務,它簡化了與http服務的通訊方式,統一了RESTful的標準,封裝了http連結, 我們只需要傳入url及返回值型別即可。相較於之前常用的HttpClient,RestTemplate是一種更優雅的呼叫RESTful服務的方式。

RestTemplate預設依賴JDK提供http連線的能力(HttpURLConnection),如果有需要的話也可以通過setRequestFactory方法替換為例如 Apache HttpComponents、Netty或OkHttp等其它HTTP library。

本篇介紹如何使用RestTemplate,以及在SpringBoot裡面的配置和注入。

實現邏輯
RestTemplate包含以下幾個部分:

HttpMessageConverter 物件轉換器
ClientHttpRequestFactory 預設是JDK的HttpURLConnection
ResponseErrorHandler 異常處理
ClientHttpRequestInterceptor 請求攔截器


直接進行簡單的使用

/**
* @author CodeWYX
* @date 2022/1/21 15:36
*/
public class RestTemplateTest {
public static void main(String[] args) {
RestTemplate restT = new RestTemplate();
//通過Jackson JSON processing library直接將返回值繫結到物件
List<User> user = restT.getForObject("http://localhost:8088/user", List.class);
System.out.println(Arrays.asList(user));
}
}

傳送Get請求
01.getForObject 不帶參

private void getForObject(){
RestTemplate restT = new RestTemplate();
//通過Jackson JSON processing library直接將返回值繫結到物件
List<User> user = restT.getForObject(url, List.class);
System.out.println(Arrays.asList(user));
}

02.getForObject 帶參

private void getForObject(){
RestTemplate restT = new RestTemplate();
//通過Jackson JSON processing library直接將返回值繫結到物件
User user = restT.getForObject(url+"/{id}", User.class,7);
System.out.println(Arrays.asList(user));
}

03.getForEntity 不帶參

private void getForEntity(){
RestTemplate restT = new RestTemplate();
//通過Jackson JSON processing library直接將返回值繫結到物件
ResponseEntity<List> forEntity = restT.getForEntity(url, List.class);
HttpStatus code = forEntity.getStatusCode();
HttpHeaders headers = forEntity.getHeaders();
List<User> body = forEntity.getBody();
System.out.println("code"+code);
System.out.println("headers"+headers);
System.out.println("body"+Arrays.asList(body));
}

04.getForEntity 帶參

private void getForEntity1(){
RestTemplate restT = new RestTemplate();
//通過Jackson JSON processing library直接將返回值繫結到物件
ResponseEntity<User> forEntity = restT.getForEntity(url+"/{id}", User.class,7);
HttpStatus code = forEntity.getStatusCode();
HttpHeaders headers = forEntity.getHeaders();
User body = forEntity.getBody();
System.out.println("code"+code);
System.out.println("headers"+headers);
System.out.println("body"+Arrays.asList(body));
}

傳送Post請求
01.postForObject

private void postForObject(){
RestTemplate restT = new RestTemplate();
User user = new User();
user.setName("hello");
user.setPassword("word");
User i = restT.postForObject(url, user, User.class);
System.out.println(i);
}

02.postForEntity

private void postForEntity(){
RestTemplate restT = new RestTemplate();
User user = new User();
user.setName("hello1");
user.setPassword("word2");
ResponseEntity<User> i = restT.postForEntity(url, user, User.class);
User body = i.getBody();
System.out.println(i);
System.out.println(body);
}

使用exchange()請求
private String postUser() {
RestTemplate restT = new RestTemplate();
String url = this.url;
//設定Http的Header
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
//設定訪問引數
HashMap<String, Object> params = new HashMap<>();
params.put("name", "aaaa");
params.put("password", "cascascasggreg");
//設定訪問的Entity
HttpEntity entity = new HttpEntity<>(params, headers);
ResponseEntity<String> result = null;
try {
//發起一個POST請求
result = restT.exchange(url, HttpMethod.POST, entity, String.class);
String body = result.getBody();
System.out.println(body);
} catch (Exception e) {
System.out.println("失敗: " + e.getMessage());
}
return null;
}


設定請求頭
// 1-Content-Type
RequestEntity<User> requestEntity = RequestEntity
.post(new URI(uri))
.contentType(MediaType.APPLICATION_JSON)
.body(user);

// 2-Accept
RequestEntity<User> requestEntity = RequestEntity
.post(new URI(uri))
.accept(MediaType.APPLICATION_JSON)
.body(user);

// 3-Other
RequestEntity<User> requestEntity = RequestEntity
.post(new URI(uri))
.header("Authorization", "Basic " + base64Credentials)
.body(user);


配置類
建立HttpClientConfig類,設定連線池大小、超時時間、重試機制等。配置如下:

/**
* @author CodeWYX
* @date 2022/1/21 17:27
*/
@Configuration
@EnableScheduling
public class HttpClientConfig {

private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientConfig.class);

@Resource
private HttpClientProperties p;

@Bean
public PoolingHttpClientConnectionManager poolingConnectionManager() {
SSLContextBuilder builder = new SSLContextBuilder();
try {
builder.loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] arg0, String arg1) {
return true;
}
});
} catch (NoSuchAlgorithmException | KeyStoreException e) {
LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e);
}

SSLConnectionSocketFactory sslsf = null;
try {
sslsf = new SSLConnectionSocketFactory(builder.build());
} catch (KeyManagementException | NoSuchAlgorithmException e) {
LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e);
}

Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
.<ConnectionSocketFactory>create()
.register("https", sslsf)
.register("http", new PlainConnectionSocketFactory())
.build();

PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
poolingConnectionManager.setMaxTotal(p.getMaxTotalConnections()); //最大連線數
poolingConnectionManager.setDefaultMaxPerRoute(p.getDefaultMaxPerRoute()); //同路由併發數
return poolingConnectionManager;
}

@Bean
public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
return new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext httpContext) {
HeaderElementIterator it = new BasicHeaderElementIterator
(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return p.getDefaultKeepAliveTimeMillis();
}
};
}

@Bean
public CloseableHttpClient httpClient() {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(p.getRequestTimeout())
.setConnectTimeout(p.getConnectTimeout())
.setSocketTimeout(p.getSocketTimeout()).build();

return HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(poolingConnectionManager())
.setKeepAliveStrategy(connectionKeepAliveStrategy())
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)) // 重試次數
.build();
}

@Bean
public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) {
return new Runnable() {
@Override
@Scheduled(fixedDelay = 10000)
public void run() {
try {
if (connectionManager != null) {
LOGGER.trace("run IdleConnectionMonitor - Closing expired and idle connections...");
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(p.getCloseIdleConnectionWaitTimeSecs(), TimeUnit.SECONDS);
} else {
LOGGER.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised");
}
} catch (Exception e) {
LOGGER.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e);
}
}
};
}

@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("poolScheduler");
scheduler.setPoolSize(50);
return scheduler;
}
}


然後再配置RestTemplateConfig類,使用之前配置好的CloseableHttpClient類注入,同時配置一些預設的訊息轉換器:

/**
* RestTemplate客戶端連線池配置
* @author CodeWYX
* @date 2022/1/21 17:39
*/
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class RestTemplateConfig {

@Resource
private CloseableHttpClient httpClient;

@Bean
public RestTemplate restTemplate(MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());

List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("utf-8"));
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(jackson2HttpMessageConverter);
restTemplate.setMessageConverters(messageConverters);

return restTemplate;
}

@Bean
public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory();
rf.setHttpClient(httpClient);
return rf;
}

}


建立HttpClientProperties類

/**
* @author CodeWYX
* @date 2022/1/21 17:27
*/
@Component
@ConfigurationProperties(prefix = "httpclient")
public class HttpClientProperties {
/**
* 建立連線的超時時間
*/
private int connectTimeout = 20000;
/**
* 連線不夠用的等待時間
*/
private int requestTimeout = 20000;
/**
* 每次請求等待返回的超時時間
*/
private int socketTimeout = 30000;
/**
* 每個主機最大連線數
*/
private int defaultMaxPerRoute = 100;
/**
* 最大連線數
*/
private int maxTotalConnections = 300;
/**
* 預設連線保持活躍的時間
*/
private int defaultKeepAliveTimeMillis = 20000;
/**
* 空閒連線生的存時間
*/
private int closeIdleConnectionWaitTimeSecs = 30;

public int getConnectTimeout() {
return connectTimeout;
}

public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}

public int getRequestTimeout() {
return requestTimeout;
}

public void setRequestTimeout(int requestTimeout) {
this.requestTimeout = requestTimeout;
}

public int getSocketTimeout() {
return socketTimeout;
}

public void setSocketTimeout(int socketTimeout) {
this.socketTimeout = socketTimeout;
}

public int getDefaultMaxPerRoute() {
return defaultMaxPerRoute;
}

public void setDefaultMaxPerRoute(int defaultMaxPerRoute) {
this.defaultMaxPerRoute = defaultMaxPerRoute;
}

public int getMaxTotalConnections() {
return maxTotalConnections;
}

public void setMaxTotalConnections(int maxTotalConnections) {
this.maxTotalConnections = maxTotalConnections;
}

public int getDefaultKeepAliveTimeMillis() {
return defaultKeepAliveTimeMillis;
}

public void setDefaultKeepAliveTimeMillis(int defaultKeepAliveTimeMillis) {
this.defaultKeepAliveTimeMillis = defaultKeepAliveTimeMillis;
}

public int getCloseIdleConnectionWaitTimeSecs() {
return closeIdleConnectionWaitTimeSecs;
}

public void setCloseIdleConnectionWaitTimeSecs(int closeIdleConnectionWaitTimeSecs) {
this.closeIdleConnectionWaitTimeSecs = closeIdleConnectionWaitTimeSecs;
}
}


注意,如果沒有apache的HttpClient類,需要在pom檔案中新增:

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

傳送檔案
MultiValueMap<String, Object> multiPartBody = new LinkedMultiValueMap<>();
multiPartBody.add("file", new ClassPathResource("/tmp/user.txt"));
RequestEntity<MultiValueMap<String, Object>> requestEntity = RequestEntity
.post(uri)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(multiPartBody);

下載檔案
// 小檔案
RequestEntity requestEntity = RequestEntity.get(uri).build();
ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);
byte[] downloadContent = responseEntity.getBody();

// 大檔案
ResponseExtractor<ResponseEntity<File>> responseExtractor = new ResponseExtractor<ResponseEntity<File>>() {
@Override
public ResponseEntity<File> extractData(ClientHttpResponse response) throws IOException {
File rcvFile = File.createTempFile("rcvFile", "zip");
FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));
return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(rcvFile);
}
};
File getFile = this.restTemplate.execute(targetUri, HttpMethod.GET, null, responseExtractor);

Service注入
@Service
public class DeviceService {
private static final Logger logger = LoggerFactory.getLogger(DeviceService.class);

@Resource
private RestTemplate restTemplate;
}

實際使用例子
// 開始推送訊息
logger.info("解綁成功後推送訊息給對應的POS機");
LoginParam loginParam = new LoginParam();
loginParam.setUsername(managerInfo.getUsername());
loginParam.setPassword(managerInfo.getPassword());
HttpBaseResponse r = restTemplate.postForObject(
p.getPosapiUrlPrefix() + "/notifyLogin", loginParam, HttpBaseResponse.class);
if (r.isSuccess()) {
logger.info("推送訊息登入認證成功");
String token = (String) r.getData();
UnbindParam unbindParam = new UnbindParam();
unbindParam.setImei(pos.getImei());
unbindParam.setLocation(location);
// 設定HTTP Header資訊
URI uri;
try {
uri = new URI(p.getPosapiUrlPrefix() + "/notify/unbind");
} catch (URISyntaxException e) {
logger.error("URI構建失敗", e);
return 1;
}
RequestEntity<UnbindParam> requestEntity = RequestEntity
.post(uri)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.body(unbindParam);
ResponseEntity<HttpBaseResponse> responseEntity = restTemplate.exchange(requestEntity, HttpBaseResponse.class);
HttpBaseResponse r2 = responseEntity.getBody();
if (r2.isSuccess()) {
logger.info("推送訊息解綁網點成功");
} else {
logger.error("推送訊息解綁網點失敗,errmsg = " + r2.getMsg());
}
} else {
logger.error("推送訊息登入認證失敗");
}


建立一個請求的工具類
/**
* @author CodeWYX
* @date 2022/1/21 17:27
*/
@Component
public class HttpUtil {

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

@Resource
private RestTemplate restTemplate;

private static HttpUtil httpUtil;

@PostConstruct
public void init(){
httpUtil = this;
httpUtil.restTemplate = this.restTemplate;
}

public static <T> String httpRequest(String url, HttpMethod method, HttpEntity<T> entity){
try {
ResponseEntity<String> result = httpUtil.restTemplate.exchange(url, method, entity, String.class);
return result.getBody();
} catch (Exception e) {
logger.error("請求失敗: " + e.getMessage());
}
return null;
}

}

----------------------------------------------

一、使用restTemplate的post請求附帶檔案
HttpHeaders headers = new HttpHeaders();
//post介面請求頭設定
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, ByteArrayResource> form = new LinkedMultiValueMap<>(1);
//呼叫第三方介面需要將檔案轉化為byte[]
ByteArrayResource is = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return voice.getOriginalFilename();
}
};
form.add("file", is);
//遠端呼叫的引數
HttpEntity<MultiValueMap<String, ByteArrayResource>> httpEntity = new HttpEntity<>(form, headers);
//遠端呼叫後的返回值:ResponseMessage是我自定義的一個類,和第三方介面返回的資料介面一致
ResponseEntity<ResponseMessage> responseEntity = restTemplate.postForEntity(url, httpEntity, ResponseMessage.class);

二、接收restTemplate返回的檔案
HttpHeaders headers = new HttpHeaders();
//post請求傳遞json合適引數
headers.setContentType(MediaType.APPLICATION_JSON);
ObjectNode form = objectMapper.createObjectNode();
form.put("param","引數");
HttpEntity<ObjectNode> httpEntity = new HttpEntity(form, headers);
ResponseEntity<byte[]> textToVoice = restTemplate.postForEntity(url, httpEntity, byte[].class);

------------------------------------------------------------------------------------

最近使用Spring 的 RestTemplate

工具類請求介面的時候發現引數傳遞的一個坑,也就是當我們把引數封裝在Map裡面的時候,Map 的型別選擇。 使用RestTemplate post請求的時候主要可以通過三種方式實現

    1、呼叫postForObject方法  2、使用postForEntity方法 3、呼叫exchange方法     postForObject和postForEntity方法的區別主要在於可以在postForEntity方法中設定header的屬性,當需要指定header的屬性值的時候,使用postForEntity方法。exchange方法和postForEntity類似,但是更靈活,exchange還可以呼叫get、put、delete請求。使用這三種方法呼叫post請求傳遞引數,Map不能定義為以下兩種型別(url使用佔位符進行引數傳遞時除外)
1 2 3 Map<String, Object> paramMap = new HashMap<String, Object>();   Map<String, Object> paramMap = new LinkedHashMap<String, Object>();

   經過測試,我發現這兩種map裡面的引數都不能被後臺接收到,這個問題困擾我兩天,終於,當我把Map型別換成LinkedMultiValueMap後,引數成功傳遞到後臺。

1 MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();

  注:HashMap是以請求體傳遞,MultiValueMap是表單傳遞。

  經過測試,正確的POST傳參方式如下

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main(String[] args) {         RestTemplate template = new RestTemplate();         String url = "http://192.168.2.40:8081/channel/channelHourData/getHourNewUserData";         // 封裝引數,千萬不要替換為Map與HashMap,否則引數無法傳遞         MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();         paramMap.add("dt""20180416");           // 1、使用postForObject請求介面         String result = template.postForObject(url, paramMap, String.class);         System.out.println("result1==================" + result);           // 2、使用postForEntity請求介面         HttpHeaders headers = new HttpHeaders();         HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<MultiValueMap<String, Object>>(paramMap,headers);         ResponseEntity<String> response2 = template.postForEntity(url, httpEntity, String.class);         System.out.println("result2====================" + response2.getBody());           // 3、使用exchange請求介面         ResponseEntity<String> response3 = template.exchange(url, HttpMethod.POST, httpEntity, String.class);         System.out.println("result3====================" + response3.getBody()); }

  補充:POST傳參物件

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Autowired private RestTemplate restTemplate; private String url="http://localhost:8080/users";   public Integer save(User user){     Map<String,String> map = restTemplate.postForObject(url, user, Map.class);     if(map.get("result").equals("success")){         //新增成功         return 1;     }     return -1; }    //這是訪問的controller方法   @RequestMapping(value = "users",method = RequestMethod.POST) public Map<String,String> save(@RequestBody User user){     Integer save = userService.save(user);     Map<String,String> map=new HashMap<>();     if(save>0){         map.put("result","success");         return map;     }     map.put("result","error");     return map; }

  ps:post請求也可以通過佔位符的方式進行傳參(類似get),但是看起來不優雅,推薦使用文中的方式。

GET方式傳參說明

如果是get請求,又想要把引數封裝到map裡面進行傳遞的話,Map需要使用HashMap,且url需要使用佔位符,如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main(String[] args) {         RestTemplate restTemplate2 = new RestTemplate();         String url = "http://127.0.0.1:8081/interact/getData?dt={dt}&ht={ht}";             // 封裝引數,這裡是HashMap     Map<String, Object> paramMap = new HashMap<String, Object>();     paramMap.put("dt""20181116");     paramMap.put("ht""10");       //1、使用getForObject請求介面     String result1 = template.getForObject(url, String.class, paramMap);     System.out.println("result1====================" + result1);       //2、使用exchange請求介面     HttpHeaders headers = new HttpHeaders();     headers.set("id""lidy");     HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<MultiValueMap<String, Object>>(null,headers);     ResponseEntity<String> response2 = template.exchange(url, HttpMethod.GET, httpEntity, String.class,paramMap);     System.out.println("result2====================" + response2.getBody()); }

  

    RestTemplate提供的delete()和put()方法都沒有返回值,但是我要呼叫的介面是有返回值的,網上的資料很多都是寫的呼叫exchange()方法來實現,但是基本上都沒有給出完整例項,導致我在參考他們的程式碼的時候會出現引數無法傳遞的問題,目前我已經解決該問題,現將我的解決方法分享出來        我同樣是使用exchange()方法來實現,但是url有講究,需要像使用exchange方法呼叫get請求一樣,使用佔位符        delete請求例項,請求方式使用 HttpMethod.DELETE(resultful風格使用佔位符)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /**  * 刪除使用者  * @param id  * @return  */ public String delete(Long id) {     StringBuffer url = new StringBuffer(baseUrl)             .append("/user/delete/{id}");       Map<String, Object> paramMap = new HashMap<>();     paramMap.put("id", id);       ResponseEntity<String > response = restTemplate.exchange(url.toString(), HttpMethod.DELETE, null, String .class, paramMap);     String result = response.getBody();       return result; }

  補充:resultful風格直接拼接url

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //負責呼叫provider的方法,獲取資料 @Autowired private RestTemplate restTemplate; //在provider端資源的路徑 private String url="http://localhost:8080/details";      public String deleteDetail(Integer id){     ResponseEntity<String> response = restTemplate.exchange(url + "/" + id, HttpMethod.DELETE, null, String.class);     String result = response.getBody();     return result; }   //被呼叫的controller方法 @ResponseBody @RequestMapping(value = "details/{id}",method = RequestMethod.DELETE) public String deleteDetail(@PathVariable Integer id){     Integer integer = detailService.deleteDetail(id);     if(integer>0){         return "success";     }     return "error"; }

  不是resultful風格可以使用佔位符

1 2 3 4 5 6 7 8 9 10 private String url="http://localhost:8080/details?id={id}";   public String deleteDetail(Integer id){                   Map<String, Object> paramMap = new HashMap<>();         paramMap.put("id", id);         ResponseEntity<String > response = restTemplate.exchange(url.toString(), HttpMethod.DELETE, null, String .class, paramMap);         String result = response.getBody();         return result;     }

  

put請求例項,請求方式使用 HttpMethod.PUT

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /**  * 更新使用者基礎資訊  * @param userInfoDTO  * @return  */ public String edit(UserInfoDTO userInfoDTO) {     StringBuffer url = new StringBuffer(baseUrl)             .append("/user/edit?tmp=1")             .append("&id={id}")             .append("&userName={userName}")             .append("&nickName={nickName}")             .append("&realName={realName}")             .append("&sex={sex}")             .append("&birthday={birthday}");       Map<String, Object> paramMap = new HashMap<>();     paramMap.put("userId", userInfoDTO.getId());     paramMap.put("userName", userInfoDTO.getUserName());     paramMap.put("nickName", userInfoDTO.getNickName());     paramMap.put("realName", userInfoDTO.getRealName());     paramMap.put("sex", userInfoDTO.getSex());     paramMap.put("birthday", userInfoDTO.getBirthday());       ResponseEntity<String > response = restTemplate.exchange(url.toString(), HttpMethod.PUT, null, String .class, paramMap);     String result = response.getBody();     return result;   }
    再次補充exchange()傳參物件:     參考:https://www.cnblogs.com/jnba/p/10522608.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 //測試post的controller @RequestMapping(value = "detailsPost",method = RequestMethod.POST) public String test02(@RequestBody Detail detail){     System.out.println("POST-"+detail);     return "error"; } //測試put的controller @RequestMapping(value = "detailsPut",method = RequestMethod.PUT) public String test03(@RequestBody Detail detail){     System.out.println("PUT-"+detail);     return "error"; } //測試delete的controller @RequestMapping(value = "detailsDelete",method = RequestMethod.DELETE) public String test04(@RequestBody Detail detail){     System.out.println("DELETE-"+detail);     return "error"; }     //測試方法 public String test(){     //put傳遞物件     //String json = "{\"author\":\"zsw\",\"createdate\":1582010438846,\"id\":1,\"summary\":\"牡丹\",\"title\":\"菏澤\"}";     //HttpHeaders headers = new HttpHeaders();     //headers.setContentType(MediaType.APPLICATION_JSON);     //HttpEntity<String> entity = new HttpEntity<>(json,headers);     //ResponseEntity<String> resp = restTemplate.exchange("http://localhost:8080/detailsPut", HttpMethod.PUT, entity, String.class);       //delete傳遞物件     Detail detail=new Detail();     detail.setId(1L);     detail.setSummary("牡丹");     detail.setTitle("菏澤");     detail.setAuthor("zsw");     detail.setCreatedate(new Timestamp(new Date().getTime()));     HttpHeaders headers = new HttpHeaders();     headers.setContentType(MediaType.APPLICATION_JSON);     HttpEntity<Detail> entity = new HttpEntity<>(detail,headers);     ResponseEntity<String> resp = restTemplate.exchange("http://localhost:8080/detailsDelete", HttpMethod.DELETE, entity, String.class);       String result = resp.getBody();     System.out.println(result);     return result; }

  delete請求和上面一樣,但是get不行,直接報錯400。好像是get不支援這種傳參。https://blog.belonk.com/c/http_resttemplate_get_with_body.htm 和這大哥的情況一樣,但是他的解決方案我沒搞明白,so 如有大佬還望指點一下老弟,不勝感激。

  exchange()傳遞單個引數可以使用佔位符:

1 2 3 4 5 6 7         //post傳遞單參 //        ResponseEntity<String> resp = restTemplate.exchange("http://localhost:8080/detailsPostD?id={id}&name={name}", HttpMethod.POST, null, String.class,1,"zsw");         //put傳遞單參         Map<String,Object> map=new HashMap<>();         map.put("id",1);         map.put("name","zsw");         ResponseEntity<String> resp = restTemplate.exchange("http://localhost:8080/detailsPutD?id={id}&name={name}", HttpMethod.PUT, null, String.class,map);

  get、post、put、delete請求通用。

------------------------------------------------------------------------------------

SpringBoot系列: RestTemplate 快速入門

微服務程序之間的通訊有 http 和 rpc 兩種協議, 在 Spring Cloud 專案中一般都以 http 通訊, 常用的訪問框架有:
1. JdkHttpConnection 元件
2. Apache HttpClient 元件
3. RestTemplate (Spring Framework 提供的 webclient, 預設是基於 JdkHttpConnection 實現的, 也可以基於 Apache HttpClient 、 OkHttp 實現)
4. Feign (spring-cloud-starter-feign 專案提供的 webclient)
5. OkHttp (Square 開源的 http 客戶端)
6. AsyncHttpClient(基於 Netty 的 http 客戶端)
7. Retrofit (Square 開源的 http 客戶端, 對於 OkHttp 做了封裝)

JdkHttpConnection/Apache HttpClient 等 web 客戶端是底層客戶端, 如果直接在微服務專案中使用, 需要處理很多工作. 其他幾個客戶端都針對 Rest 服務做了很多封裝, 這包括:
1. 連線池
2. 超時設定
3. 請求和響應的編碼/解碼 (json <-> pojo)
4. 支援非同步


因為我們開發的專案是基於 Spring Boot 的, 考慮到整合性和 Spring 官方的支援程度, 自然選擇 RestTemplate 或 Feign 了.
有關 http 通訊經常會看到 Robin 相關資料, 該技術是 Spring Cloud Netflix 的一個專案, 是一個基於 Http 和 Tcp 的客戶端負載均衡器, 支援兩種策略 Round robin 或 weigh based. Robin 可以和 RestTemplate/Feign 搭配使用, 為 web 請求提供負載均衡特性.


==========================
pom.xml
==========================
RestTemplate 預設使用 jackson 完成 json 序列化和反序列化.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

==========================
RestTemplate 例項化
==========================
RestTemplate 例項最好是由 Spring 容器管理, 而不是在用到時候 new RestTemplate() 一個例項.
可以在 @Controller/@Service/@Configuration 類中, 宣告一個 restTemplate bean, 其他地方直接注入即可使用.

@RestController
class HelloController {
    //宣告 bean
    @Bean
    @LoadBalanced   //增加 load balance 特性. 
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    //注入
    @Autowired
    private RestTemplate restTemplate;    
    
    private void someMethod(){
       //使用 restTemplate
    }
}


或者, 先注入 RestTemplateBuilder, 然後通過該 builder 來構建 RestTemplate 例項. 使用 builder 可以為 RestTemplate 定製化東西:
builder.additionalInterceptors() 方法: 可以通過增加攔截器為每次 request 記錄 log,
builder.additionalMessageConverters() 方法: 比如增加 MessageConverter 實現特定的 json <-> pojo 的轉換,

@RestController
class Hello2Controller {    
    //注入 RestTemplateBuilder
    @Autowired
    private void initRestTemplate(RestTemplateBuilder builder){
        this.restTemplate=builder.build();
    }
       
    private RestTemplate restTemplate;    
    
    private void someMethod(){
       //使用 restTemplate
    }
}

==========================
RestTemplate 使用
==========================
RestTemplate 主要方法

Http 方法 | RestTemplate 方法
DELETE | delete
GET | getForObject(), getForEntity()
HEAD | headForHeaders()
OPTIONS | OptionsForAllow()
PUT | put
any | exchange(), execute()

1. delete() 方法, 在 url 資源執行 http DELETE 操作.
2. exchange() 方法, 通用的 web 請求方法, 返回一個 ResponseEntity 物件, 這個物件是從響應體對映而來. 該方法支援多種 web method, 是其他 RestTemplate 方法的基礎.
3. execute() 方法, 是 exchange() 方法的基礎.
4. getForEntity() 方法, 傳送一個 GET 請求, 返回一個通用的 ResponseEntity 物件, 使用該物件可以得到 Response 字串.
5. getForObject() 方法, 傳送一個 GET 請求, 返回一個 pojo 物件.
6. headForHeaders() 方法, 傳送一個 HEAD 請求, 返回包含特定資源 url 的 http 頭.
8. optionsForAllow() 方法, 傳送一個 HTTP OPTIONS 請求, 返回對於特定 url 的 Allow 頭資訊.
9. PostForEntity() 方法, 傳送一個 Post 請求, 返回一個 ResponseEntity 物件, 這個物件是從響應體對映而來.
10. PostForObject() 方法, 傳送一個 POST 請求, 返回一個特定的物件, 該物件是從響應體對映而來.
11. PostForLocation() 方法, 傳送一個 POST 請求, 返回新建立資源的 URL.
12. put() 方法, 傳送 PUT 請求.


--------------------------
獲取 plain json
--------------------------

ResponseEntity<String> response=restTemplate.getForEntity(url, String.class)
// 獲取包含 plain text Body 的 response
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
// 獲取 status code 
System.out.println("status code:" + response.getStatusCode());
// 使用 jackson 解析 json 字串
// class: com.fasterxml.jackson.databind.ObjectMapper
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response.getBody());
JsonNode value = root.path("type");

--------------------------
獲取 Pojo 物件
--------------------------
如果 Rest 服務返回下面的 json 格式:
{ "firstName":"John" , "lastName":"Doe" }

RestTemplate 很容易可以將 json 轉成物件:
Employee foo = restTemplate.getForObject(url, Employee.class);

如果 Rest 服務返回下面的 json 格式, json 中有一個根節點 employees, 其包含了多個 Employee 資訊.

{
    "employees": [
        { "firstName":"John" , "lastName":"Doe" },
        { "firstName":"Anna" , "lastName":"Smith" },
        { "firstName":"Peter" , "lastName":"Jones" }
    ]
}

對於這種格式的 json, 我們仍然可以使用 getForObject() 方法, 只要基於 Employee 類 做個 list wrapper 類即可. 

public class EmployeeList {
    private List<Employee> employees; 
    public EmployeeList() {
        employees = new ArrayList<>();
    }
     // standard constructor and getter/setter
}
EmployeeList response = restTemplate.getForObject(
  "http://localhost:8080/employees",
  EmployeeList.class);
List<Employee> employees = response.getEmployees();

--------------------------
獲取 json 陣列物件
--------------------------
雖然 restTemplate.getForObject() 能很方便地將 json 轉成 pojo, 但僅僅適合於處理單個物件的情形. 下面的 json 直接返回了一個數組, 這時使用 getForObject() 就不管用了.

[
    { "firstName":"John" , "lastName":"Doe" },
    { "firstName":"Anna" , "lastName":"Smith" },
    { "firstName":"Peter" , "lastName":"Jones" }
]


我們可以使用 exchange() 方法, 最關鍵一點是將 List<Employee> 型別傳進去, 這樣 RestTemplate 就知道如何將 json 陣列轉成 object list 了.

ResponseEntity<List<Employee>> response = restTemplate.exchange(
  "http://localhost:8080/employees/",
  HttpMethod.GET,
  null,
  new ParameterizedTypeReference<List<Employee>>(){});
List<Employee> employees = response.getBody();

--------------------------
向 url 傳參
--------------------------
在 POST 和 GET 等方法, 最後一個形參往往是 url 引數變數, 比如:
getForEntity(String url,Class responseType,Object...urlVariables)
getForEntity(String url,Class responseType,Map urlVariables)

處理方式 1:
如果要使用陣列或可變引數方式傳入 url param, url 的引數必須使用數字下標來佔位.

String url = http://USER-SERVICE/user.do?name={1}&age={2};
String[] urlVariables=["jason",26];

處理方式 2:
如果要 Map 傳入 url param, url 的引數必須使用 named 方式佔位

String url = http://USER-SERVICE/user.do?name={name}&age={age};
Map<String, Object> urlVariables = new HashMap<String, Object>();
urlVariables.put("name",jason);
urlVariables.put("age",26);

--------------------------
設定 header, Post 一個 json 串
--------------------------

String url="url";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
String body="some json body";
HttpEntity<String> requestEntity = new HttpEntity<String>(body, headers);
ResponseEntity<String> response= restTemplate.postForEntity(url, requestEntity, String.class);

HttpEntity 經常被用到, 它可以將 Headers 和要提交的資料合併成一個物件, 作為 request 物件傳參給 POST/PUT/PATCH 等很多方法. 

--------------------------
Post 一個物件 list
--------------------------
Post 操作可以直接使用 restTemplate.postForObject() 方法, 該方法即可 Post 單個物件, 也可以 Post 物件的 List.

List<Employee> newEmployees = new ArrayList<>();
newEmployees.add(new Employee(3, "Intern"));
newEmployees.add(new Employee(4, "CEO"));
 
restTemplate.postForObject(
  "http://localhost:8080/employees/",
  newEmployees,
  ResponseEntity.class);


--------------------------
使用 HEAD 獲取 headers
--------------------------

HttpHeaders httpHeaders = restTemplate.headForHeaders(fooResourceUrl);
assertTrue(httpHeaders.getContentType().includes(MediaType.APPLICATION_JSON));


--------------------------
檔案上傳下載
--------------------------
參考 https://www.jianshu.com/p/bbd9848c0cfc

@Test
    public void upload() throws Exception {
        Resource resource = new FileSystemResource("/home/lake/github/wopi/build.gradle");
        MultiValueMap multiValueMap = new LinkedMultiValueMap();
        multiValueMap.add("username","lake");
        multiValueMap.add("files",resource);
        ActResult result = testRestTemplate.postForObject("/test/upload",multiValueMap,ActResult.class);
        Assert.assertEquals(result.getCode(),0);
    }

@Test
    public void download() throws Exception {
        HttpHeaders headers = new HttpHeaders();
        headers.set("token","xxxxxx");
        HttpEntity formEntity = new HttpEntity(headers);
        String[] urlVariables = new String[]{"admin"};
        ResponseEntity<byte[]> response = testRestTemplate.exchange("/test/download?username={1}",HttpMethod.GET,formEntity,byte[].class,urlVariables);
        if (response.getStatusCode() == HttpStatus.OK) {
            Files.write(response.getBody(),new File("/home/lake/github/file/test.gradle"));
        }
    }
 


--------------------------
定製化 RestTemplate
--------------------------
增加一個自定義 ErrorHandler:
restTemplate.setErrorHandler(errorHandler);

設定 httpClient 的工廠類:
restTemplate.setRequestFactory(requestFactory);
可以為 RestTemplate 設定 httpClient 的工廠類, 主要有兩個工廠類:
1. SimpleClientHttpRequestFactory 工廠類, 這是預設的工廠類, 底層用的是 jdk 的 HttpConnection, 預設超時為-1.
2. HttpComponentsClientHttpRequestFactory 底層用的是 Apache HttpComponents HttpClient, 比 JDK 的 HttpConnection 強大, 可以配置連線池和證書等, 支援 https.
3. OkHttp3ClientHttpRequestFactory 底層使用的是 square 公司開源的 OkHttp, 該客戶端支援 https 等高階特性,  pom.xml 需要增加依賴.
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.8.1</version>
        </dependency>

====================================
參考
====================================
https://www.jianshu.com/p/bbd9848c0cfc
http://www.cnblogs.com/okong/p/springcloud-four.html
https://my.oschina.net/lifany/blog/688889
http://www.cnblogs.com/okong/p/springcloud-four.html
https://blog.csdn.net/QiaoRui_/article/details/80453799

https://spring.io/guides/gs/consuming-rest/
https://www.tutorialspoint.com/spring_boot/spring_boot_rest_template.htm
https://www.baeldung.com/rest-template
https://www.baeldung.com/spring-rest-template-list

------------------------------------------------------------------------------------

精講RestTemplate第6篇-檔案上傳下載與大檔案流式下載

RestTemplate是HTTP客戶端庫,所以為了使用RestTemplate進行檔案上傳和下載,需要我們先編寫服務端的支援檔案上傳和下載的程式。請參考我之前寫的一篇文章:SpringBoot實現本地儲存檔案上傳及提供HTTP訪問服務 。按照此文完成學習之後,可以獲得

一個以訪問服務URI為"/upload”的檔案上傳服務端點
服務端點上傳檔案成功後會返回一個HTTP連線,可以用來下載檔案。
下面我們就開始學習使用RestTemplate是HTTP客戶端庫,進行檔案的上傳與下載。

一、檔案上傳
寫一個單元測試類,來完成RestTemplate檔案上傳功能,具體實現細節參考程式碼註釋

@SpringBootTest
class UpDownLoadTests {

@Resource
private RestTemplate restTemplate;

@Test
void testUpload() {
// 檔案上傳服務上傳介面
String url = "http://localhost:8888/upload";
// 待上傳的檔案(存在客戶端本地磁碟)
String filePath = "D:\\data\\local\\splash.png";

// 封裝請求引數
FileSystemResource resource = new FileSystemResource(new File(filePath));
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("uploadFile", resource); //服務端MultipartFile uploadFile
//param.add("param1", "test"); //服務端如果接受額外引數,可以傳遞


// 傳送請求並輸出結果
System.out.println("--- 開始上傳檔案 ---");
String result = restTemplate.postForObject(url, param, String.class);
System.out.println("--- 訪問地址:" + result);
}

}


輸出結果如下:

--- 開始上傳檔案 ---
--- 訪問地址:http://localhost:8888/2020/08/12/028b38f1-3f9b-4088-9bea-1af8c18cd619.png
1
2
檔案上傳之後,可以通過上面的訪問地址,在瀏覽器訪問。或者通過RestTemplate客戶端進行下載。

二、檔案下載
執行下列程式碼之後,被下載檔案url,會被正確的儲存到本地磁碟目錄targetPath。

@Test
void testDownLoad() throws IOException {
// 待下載的檔案地址
String url = "http://localhost:8888/2020/08/12/028b38f1-3f9b-4088-9bea-1af8c18cd619.png";
ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class);
System.out.println("檔案下載請求結果狀態碼:" + rsp.getStatusCode());

// 將下載下來的檔案內容儲存到本地
String targetPath = "D:\\data\\local\\splash-down.png";
Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(),
"未獲取到下載檔案"));
}

這種下載方法實際上是將下載檔案一次性載入到客戶端本地記憶體,然後從記憶體將檔案寫入磁碟。這種方式對於小檔案的下載還比較適合,如果檔案比較大或者檔案下載併發量比較大,容易造成記憶體的大量佔用,從而降低應用的執行效率。

三、大檔案下載
這種下載方式的區別在於

設定了請求頭APPLICATION_OCTET_STREAM,表示以流的形式進行資料載入
RequestCallback 結合File.copy保證了接收到一部分檔案內容,就向磁碟寫入一部分內容。而不是全部載入到記憶體,最後再寫入磁碟檔案。
@Test
void testDownLoadBigFile() throws IOException {
// 待下載的檔案地址
String url = "http://localhost:8888/2020/08/12/028b38f1-3f9b-4088-9bea-1af8c18cd619.png";
// 檔案儲存的本地路徑
String targetPath = "D:\\data\\local\\splash-down-big.png";
//定義請求頭的接收型別
RequestCallback requestCallback = request -> request.getHeaders()
.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
//對響應進行流式處理而不是將其全部載入到記憶體中
restTemplate.execute(url, HttpMethod.GET, requestCallback, clientHttpResponse -> {
Files.copy(clientHttpResponse.getBody(), Paths.get(targetPath));
return null;
});
}

------------------------------------------------------------------------------------ 【Java框架】-- SpringBoot大檔案RestTemplate下載解決方案

近期基於專案上使用到的RestTemplate下載檔案流,遇到1G以上的大檔案,下載需要3-4分鐘,因為呼叫API介面沒有做分片與多執行緒, 檔案流全部採用同步方式載入,效能很慢。最近結合網上案例及自己總結,寫了一個分片下載tuling/fileServer專案: 1.包含同步下載檔案流在瀏覽器載入輸出相關程式碼; 2.包含分片多執行緒下載分片檔案及合併檔案相關程式碼;

另外在DownloadThread專案中使用程式碼完成了一個遠端RestUrl請求去獲取一個遠端資源大檔案進行多執行緒分片下載 到本地的一個案例,可以下載一些諸如.mp4/.avi等視訊類大檔案。相關程式碼也一併打包上傳。

同步下載,支援分片下載Range主要程式碼:

@Controller
public class DownLoadController {
    private static final String UTF8 = "UTF-8";
    @RequestMapping("/download")
    public void downLoadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
        File file = new File("D:\\DevTools\\ideaIU-2021.1.3.exe");
        response.setCharacterEncoding(UTF8);
        InputStream is = null;
        OutputStream os = null;
        try {
            // 分片下載 Range表示方式 bytes=100-1000  100-
            long fSize = file.length();
            response.setContentType("application/x-download");
            String fileName = URLEncoder.encode(file.getName(), UTF8);
            response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
            // 支援分片下載
            response.setHeader("Accept-Range", "bytes");
            response.setHeader("fSize", String.valueOf(fSize));
            response.setHeader("fName", fileName);

            long pos = 0, last = fSize - 1, sum = 0;
            if (null != request.getHeader("Range")) {
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                String numberRange = request.getHeader("Range").replaceAll("bytes=", "");
                String[] strRange = numberRange.split("-");
                if (strRange.length == 2) {
                    pos = Long.parseLong(strRange[0].trim());
                    last = Long.parseLong(strRange[1].trim());
                    if (last > fSize-1) {
                        last = fSize - 1;
                    }
                } else {
                    pos = Long.parseLong(numberRange.replaceAll("-", "").trim());
                }
            }
            long rangeLength = last - pos + 1;
            String contentRange = new StringBuffer("bytes").append(pos).append("-").append(last).append("/").append(fSize).toString();
            response.setHeader("Content-Range", contentRange);
            response.setHeader("Content-Length", String.valueOf(rangeLength));

            os = new BufferedOutputStream(response.getOutputStream());
            is = new BufferedInputStream(new FileInputStream(file));
            is.skip(pos);
            byte[] buffer = new byte[1024];
            int length = 0;
            while (sum < rangeLength) {
                int readLength = (int) (rangeLength - sum);
                length = is.read(buffer, 0, (rangeLength - sum) <= buffer.length ? readLength : buffer.length);
                sum += length;
                os.write(buffer,0, length);
            }
            System.out.println("下載完成");
        }finally {
            if (is != null){
                is.close();
            }
            if (os != null){
                os.close();
            }
        }
    }
}

多執行緒分片下載分片檔案,下載完成之後合併分片主要程式碼:

@RestController
public class DownloadClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(DownloadClient.class);
    private final static long PER_PAGE = 1024L * 1024L * 50L;
    private final static String DOWN_PATH = "F:\\fileItem";
    ExecutorService taskExecutor = Executors.newFixedThreadPool(10);

    @RequestMapping("/downloadFile")
    public String downloadFile() {
        // 探測下載
        FileInfo fileInfo = download(0, 10, -1, null);
        if (fileInfo != null) {
            long pages =  fileInfo.fSize / PER_PAGE;
            for (long i = 0; i <= pages; i++) {
                Future<FileInfo> future = taskExecutor.submit(new DownloadThread(i * PER_PAGE, (i + 1) * PER_PAGE - 1, i, fileInfo.fName));
                if (!future.isCancelled()) {
                    try {
                        fileInfo = future.get();
                    } catch (InterruptedException | ExecutionException e) {
                        e.printStackTrace();
                    }
                }
            }
            return System.getProperty("user.home") + "\\Downloads\\" + fileInfo.fName;
        }
        return null;
    }

    class FileInfo {
        long fSize;
        String fName;

        public FileInfo(long fSize, String fName) {
            this.fSize = fSize;
            this.fName = fName;
        }
    }

    /**
     * 根據開始位置/結束位置
     * 分片下載檔案,臨時儲存檔案分片
     * 檔案大小=結束位置-開始位置
     *
     * @return
     */
    private FileInfo download(long start, long end, long page, String fName) {
        File dir = new File(DOWN_PATH);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        // 斷點下載
        File file = new File(DOWN_PATH, page + "-" + fName);
        if (file.exists() && page != -1 && file.length() == PER_PAGE) {
            return null;
        }
        try {
            HttpClient client = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/download");
            httpGet.setHeader("Range", "bytes=" + start + "-" + end);
            HttpResponse response = client.execute(httpGet);
            String fSize = response.getFirstHeader("fSize").getValue();
            fName = URLDecoder.decode(response.getFirstHeader("fName").getValue(), "UTF-8");
            HttpEntity entity = response.getEntity();
            InputStream is = entity.getContent();
            FileOutputStream fos = new FileOutputStream(file);
            byte[] buffer = new byte[1024];
            int ch;
            while ((ch = is.read(buffer)) != -1) {
                fos.write(buffer, 0, ch);
            }
            is.close();
            fos.flush();
            fos.close();
            // 最後一個分片
            if (end - Long.parseLong(fSize) > 0) {
                // 開始合併檔案
                mergeFile(fName, page);
            }

            return new FileInfo(Long.parseLong(fSize), fName);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private void mergeFile(String fName, long page) {
        File file = new File(DOWN_PATH, fName);
        try {
            BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file));
            for (long i = 0; i <= page; i++) {
                File tempFile = new File(DOWN_PATH, i + "-" + fName);
                while (!file.exists() || (i != page && tempFile.length() < PER_PAGE)) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                byte[] bytes = FileUtils.readFileToByteArray(tempFile);
                os.write(bytes);
                os.flush();
                tempFile.delete();
            }
            File testFile = new File(DOWN_PATH, -1 + "-null");
            testFile.delete();
            os.flush();
            os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 獲取遠端檔案尺寸
     */
    private long getRemoteFileSize(String remoteFileUrl) throws IOException {
        long fileSize = 0;
        HttpURLConnection httpConnection = (HttpURLConnection) new URL(remoteFileUrl).openConnection();
        //使用HEAD方法
        httpConnection.setRequestMethod("HEAD");
        int responseCode = httpConnection.getResponseCode();
        if (responseCode >= 400) {
            LOGGER.debug("Web伺服器響應錯誤!");
            return 0;
        }
        String sHeader;
        for (int i = 1;; i++) {
            sHeader = httpConnection.getHeaderFieldKey(i);
            if (sHeader != null && sHeader.equals("Content-Length")) {
                LOGGER.debug("檔案大小ContentLength:" + httpConnection.getContentLength());
                fileSize = Long.parseLong(httpConnection.getHeaderField(sHeader));
                break;
            }
        }
        return fileSize;
    }

    class DownloadThread implements Callable<FileInfo> {
        long start;
        long end;
        long page;
        String fName;

        public DownloadThread(long start, long end, long page, String fName) {
            this.start = start;
            this.end = end;
            this.page = page;
            this.fName = fName;
        }

        @Override
        public FileInfo call() {
            return download(start, end, page, fName);
        }
    }
}
------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------