Spring-RetryTemplate-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()传参对象:
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);
        }
    }
}
复制代码

 

------------------------------------------------------------------------------------
------------------------------------------------------------------------------------
------------------------------------------------------------------------------------
posted @ 2022-12-02 15:06  hanease  阅读(1248)  评论(0编辑  收藏  举报