SpringBoot - 网络请求模版类RestTemplate使用详解

SpringBoot - 网络请求模版类RestTemplate使用详解

一、基本介绍

1,什么是 RestTemplate?

(1)RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种可以便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。

RestTemplate 是 Spring 3 中引入的同步阻塞式 HTTP 客户端。根据 Spring 官方文档介绍,在将来的版本中它可能会被弃用,因为他们已在 Spring 5 中引入了 WebClient 作为非阻塞式 Reactive HTTP 客户端。

(2)RestTemplate 定义了 36 个与 REST 资源交互的方法,其中的大多数都对应于 HTTP 的方法。

注意:
严格来说只有 11 个独立的方法,其中有 10 个有三种重载形式,而第 11 个则重载了 6 次,这样一共形成了 36 个方法。
实际上,由于 Post 操作的非幂等性,它几乎可以代替其他的 CRUD 操作。
  • delete():这个方法是在特定的 URL 上对资源执行 HTTP DELETE 操作
  • exchange():在 URL 上执行特定的 HTTP 方法,返回包含对象的 ResponseEntity,这个对象是从响应体中映射得到的
  • execute():在 URL 上执行特定的 HTTP 方法,返回一个从响应体映射得到的对象
  • getForEntity():发送一个 HTTP GET 请求,返回的 ResponseEntity 包含了响应体所映射成的对象
  • getForObject():发送一个 HTTP GET 请求,返回的请求体将映射为一个对象
  • postForEntity():POST 数据到一个 URL,返回包含一个对象的 ResponseEntity,这个对象是从响应体中映射得到的
  • postForObject():POST 数据到一个 URL,返回根据响应体匹配形成的对象
  • headForHeaders():发送 HTTP HEAD 请求,返回包含特定资源 URL 的 HTTP 头
  • optionsForAllow():发送 HTTP OPTIONS 请求,返回对特定 URL 的 Allow 头信息
  • postForLocation():POST 数据到一个 URL,返回新创建资源的 URL
  • put():PUT 资源到特定的 URL

2,安装配置

(1)首先编辑 pom.xml 文件,导入 springboot 的 web 包。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

(2)接着创建一个 RestTemplate 的配置类,同时设置连接池大小、超时时间、重试机制等等。

@Configuration
public class RestTemplateConfig {
 
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory){
        return new RestTemplate(factory);
    }
 
    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(15000); // 连接超时
        factory.setReadTimeout(5000); // 数据读取超时时间
        return factory;
    }
}

3,使用样例

(1)要发起网络请求时直接将 RestTemplate 对象注入使用即可。这里我们请求一个网络接口,并将结果以字符串的形式打印出来。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public String test() {
        String url = "http://jsonplaceholder.typicode.com/posts/1";
        return restTemplate.getForObject(url, String.class);
    }
}

二、GET 请求1:getForObject() 方法的使用

1,方法介绍

​ getForObject() 用于发送一个 HTTP GET 请求。它和 getForEntity() 用法几乎相同。区别在于 getForObject() 返回值返回的是响应体,省略了很多 response 的信息。

2,获取 String 结果数据

下面代码将响应结果映射为一个 String 字符串,并打印出来。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        String url = "http://jsonplaceholder.typicode.com/posts/1";
        String str = restTemplate.getForObject(url, String.class);
        System.out.println(str);
        return;
    }
}

3,将结果转换为对象

(1)由于 getForObject() 包含了将 HTTP 结果转成 POJO 的功能,所以我们可以将其转换成自定义的实体类对象。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        String url = "http://jsonplaceholder.typicode.com/posts/1";
        PostBean postBean = restTemplate.getForObject(url, PostBean.class);
        System.out.println(postBean.toString());
        return;
    }
}

(2)其中定义的实体 Bean 代码如下:

@Getter
@Setter
@ToString
public class PostBean {
    private int userId;
    private int id;
    private String title;
    private String body;
}

4,将结果转成数组

(1)假设接口返回的是一个 json 数组,内容如下:

在这里插入图片描述

(2)我们也可以将其转成对应的 Bean 数组

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        String url = "http://jsonplaceholder.typicode.com/posts";
        PostBean[] arr = restTemplate.getForObject(url, PostBean[].class);
        System.out.println("结果数:" + arr.length);
        return;
    }
}

5,参数传递的几种方式

下面 3 种方式的结果都是一样的。

(1)使用占位符的形式传递参数:

String url = "http://jsonplaceholder.typicode.com/{1}/{2}";
PostBean postBean = restTemplate.getForObject(url, PostBean.class, "posts", 1);

(2)另一种使用占位符的形式:

String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
String type = "posts";
int id = 1;
PostBean postBean = restTemplate.getForObject(url, PostBean.class, type, id);

(3)我们也可以使用 map装载参数:

String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
Map<String,Object> map = new HashMap<>();
map.put("type", "posts");
map.put("id", 1);
PostBean postBean = restTemplate.getForObject(url, PostBean.class, map);

三、GET 请求2:getForEntity() 方法的使用

1,方法介绍

​ getForEntity() 同样用于发送一个 HTTP GET 请求。它和上面介绍的 getForObject() 用法几乎相同。区别在于 getForEntity() 返回的是 ResponseEntity:

  • ResponseEntity 是 Spring 对 HTTP 请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。
  • 其中响应消息体可以通过 ResponseEntity 对象的 getBody() 来获取。

2,基本用法

下面代码请求一个网络接口,并将响应体、响应头、响应码打印出来。其中响应体的类型设置为 String。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        String url = "http://jsonplaceholder.typicode.com/posts/5";
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
        String body = responseEntity.getBody(); // 获取响应体
        HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
        int statusCodeValue = responseEntity.getStatusCodeValue(); // 获取响应码值
        HttpHeaders headers = responseEntity.getHeaders(); // 获取响应头
 
        System.out.println("body:" + body);
        System.out.println("statusCode:" + statusCode);
        System.out.println("statusCodeValue:" + statusCodeValue);
        System.out.println("headers:" + headers);
        return;
    }
}

3,将消息体转换为对象

(1)除了可以返回 String 类型的消息体,也可以返回一个自定义类型的对象。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        String url = "http://jsonplaceholder.typicode.com/posts/5";
        ResponseEntity<PostBean> responseEntity = restTemplate.getForEntity(url, PostBean.class);
        PostBean postBean = responseEntity.getBody(); // 获取响应体
        System.out.println(postBean);
        return;
    }
}

(2)其中定义的实体 Bean 代码如下:

@Getter
@Setter
@ToString
public class PostBean {
    private int userId;
    private int id;
    private String title;
    private String body;
}

4,将结果转成数组

(1)假设接口返回的是一个 json 数组,内容如下:

(2)我们也可以将响应体转成对应的 Bean 数组:

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        String url = "http://jsonplaceholder.typicode.com/posts";
        ResponseEntity<PostBean[]> responseEntity = restTemplate.getForEntity(url, PostBean[].class);
        PostBean[] arr = responseEntity.getBody(); // 获取响应体
        System.out.println("结果数:" + arr.length);
        return;
    }
}

5,参数传递的几种方式

下面 3 种方式的结果都是一样的。

(1)使用占位符的形式传递参数:

String url = "http://jsonplaceholder.typicode.com/{1}/{2}";
ResponseEntity<PostBean> responseEntity = restTemplate.getForEntity(url, PostBean.class, "posts", 1);

(2)另一种使用占位符的形式:

String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
String type = "posts";
int id = 1;
ResponseEntity<PostBean> responseEntity = restTemplate.getForEntity(url, PostBean.class, type, id);

(3)我们也可以使用map装载参数:

String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
Map<String,Object> map = new HashMap<>();
map.put("type", "posts");
map.put("id", 1);
ResponseEntity<PostBean> responseEntity = restTemplate.getForEntity(url, PostBean.class, map);

四、POST 请求1:postForObject() 方法的使用

1,方法介绍

​ postForObject() 用于发送一个 HTTP POST 请求。它和 postForEntity() 用法几乎相同。区别在于 postForObject() 返回值返回的是响应体,省略了很多 response 的信息。

2,发送一个 JSON 格式数据

(1)下面代码使用 post 方式发送一个 Bean 对象,并将结果打印出来(以字符串的形式)。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        // 请求地址
        String url = "http://jsonplaceholder.typicode.com/posts";
 
        // 要发送的数据对象
        PostBean postBean = new PostBean();
        postBean.setUserId(222);
        postBean.setTitle("abc");
        postBean.setBody("航歌");
 
        // 发送post请求,并输出结果
        String result = restTemplate.postForObject(url, postBean, String.class);
        System.out.println(result);
        return;
    }
}

(2)上面发送的 Bean 对象实际上会转成如下格式的 JSON 数据提交:

3,使用 Form 表单的形式提交数据

(1)下面样例使用 POST 方式发送 multipart/form-data 格式的数据:

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        // 请求地址
        String url = "http://jsonplaceholder.typicode.com/posts";
 
        // 请求头设置
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
 
        //提交参数设置
        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("title", "abc");
        map.add("body", "航歌");
 
        // 组装请求体
        HttpEntity<MultiValueMap<String, String>> request =
                new HttpEntity<MultiValueMap<String, String>>(map, headers);
 
        // 发送post请求,并输出结果
        String result = restTemplate.postForObject(url, request, String.class);
        System.out.println(result);
        return;
    }
}

(2)上面代码最终会通过如下这种 form 表单方式提交数据:

4,将结果转成自定义对象

​ 上面样例我们都是将响应结果以 String 形式接收,其实 RestTemplate 还可以自动将响应结果转成自定的对象或则数组。具体可以参考前面写的文章:

5,设置 url 参数

(1)如果 url 地址上面需要传递一些参数,可以使用占位符的方式:

String url = "http://jsonplaceholder.typicode.com/{1}/{2}";
String url = "http://jsonplaceholder.typicode.com/{type}/{id}";

五、POST 请求2:postForEntity()方法的使用

1,方法介绍

​ postForEntity() 用于发送一个 HTTP POST 请求。它和上面的 postForObject() 用法几乎相同。区别在于 getForEntity() 返回的是 ResponseEntity:

  • ResponseEntity 是 Spring 对 HTTP 请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。
  • 其中响应消息体可以通过 ResponseEntity 对象的 getBody() 来获取。

2,发送一个 JSON 格式数据

(1)下面代码使用 post 方式发送一个 Bean 对象,并将响应体、响应头、响应码打印出来。其中响应体的类型设置为 String。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        // 请求地址
        String url = "http://jsonplaceholder.typicode.com/posts";
 
        // 要发送的数据对象
        PostBean postBean = new PostBean();
        postBean.setUserId(222);
        postBean.setTitle("abc");
        postBean.setBody("航歌");
 
        // 发送post请求,并输出结果
        ResponseEntity<String> responseEntity
                = restTemplate.postForEntity(url, postBean, String.class);
        String body = responseEntity.getBody(); // 获取响应体
        HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
        int statusCodeValue = responseEntity.getStatusCodeValue(); // 获取响应码值
        HttpHeaders headers = responseEntity.getHeaders(); // 获取响应头
 
        System.out.println("body:" + body);
        System.out.println("statusCode:" + statusCode);
        System.out.println("statusCodeValue:" + statusCodeValue);
        System.out.println("headers:" + headers);
        return;
    }
}

(2)上面发送的 Bean 对象实际上会转成如下格式的 JSON 数据提交:

3,使用 Form 表单的形式提交数据

(1)下面样例使用 POST 方式发送 multipart/form-data 格式的数据:


(2)上面代码最终会通过如下这种form表单方式提交数据:

4,将结果转成自定义对象

​ 上面样例我们都是将响应结果以 String 形式接收,其实 RestTemplate 还可以自动将响应结果转成自定的对象或则数组。具体可以参考我前面写的文章:

5,设置 url 参数

(1)如果 url 地址上面需要传递一些参数,可以使用占位符的方式:

String url = "http://jsonplaceholder.typicode.com/{1}/{2}";
String url = "http://jsonplaceholder.typicode.com/{type}/{id}";

(2)具体的用法可以参考我前面写的文章:

六、POST 请求3:postForLocation() 方法的使用

1,方法介绍

(1)postForLocation() 也是通过 Post 方式提交新资源,postForLocation() 方法的参数和前面两种(postForObject、postForEntity)的参数基本一致。

(2)区别在于 postForLocation() 方法的返回值为 Uri,这个只需要服务提供者返回一个 Uri 即可,该 Uri 表示新资源的位置。

2,使用样例

(1)比如通常登录或者注册都是 post 请求,而这些操作完成之后大部分都是跳转到别的页面去了。这种场景下,就可以使用 postForLocation 了,提交数据,并获取返回的 URI。

(2)下面是一个简单的样例代码:

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        // 请求地址
        String url = "http://jsonplaceholder.typicode.com/posts";
 
        // 要发送的数据对象
        MultiValueMap<String, String> request = new LinkedMultiValueMap<>();
        request.add("username", "hangge");
        request.add("password", "123456");
 
        // 发送post请求,并输出结果
        URI uri = restTemplate.postForLocation(url, request);
        System.out.println(uri);
        return;
    }
}

七、通用请求方法:exchange

1,方法介绍

(1)exchange 的用法同前面介绍的 getForEntity、postForEntity 差不多,且返回的都是 ResponseEntity:

  • ResponseEntity 是 Spring 对 HTTP 请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。
  • 其中响应消息体可以通过 ResponseEntity 对象的 getBody() 来获取。

(2)不同在于 exchange 方法提供统一的方法模板,可以通过指定不同的 HTTP 请求类型,实现 POST、PUT、DELETE、GET 四种请求。

2,Get 请求样例

(1)下面代码使用 Get 方式请求一个网络接口,并将响应体、响应头、响应码打印出来。其中响应体的类型设置为 String。

关于请求更详细的用法(包括参数传递、结果转成自定义对象),可以参考我之前写的文章:

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        String url = "http://jsonplaceholder.typicode.com/posts/5";
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
                null, String.class);
        String body = responseEntity.getBody(); // 获取响应体
        HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
        int statusCodeValue = responseEntity.getStatusCodeValue(); // 获取响应码值
        HttpHeaders headers = responseEntity.getHeaders(); // 获取响应头
 
        System.out.println("body:" + body);
        System.out.println("statusCode:" + statusCode);
        System.out.println("statusCodeValue:" + statusCodeValue);
        System.out.println("headers:" + headers);
        return;
    }
}

3,Post 请求样例

(1)下面代码使用 post 方式发送一个 JSON 格式的数据,并将响应体、响应头、响应码打印出来。其中响应体的类型设置为 String。

关于请求更详细的用法(包括 form表单方式提交数据、将结果转成自定义对象),可以参考之前写的文章:

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        // 请求地址
        String url = "http://jsonplaceholder.typicode.com/posts";
 
        // 请求头设置
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
 
        //提交参数设置
        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("userId", "222");
        map.add("title", "abc");
        map.add("body", "航歌");
 
        // 组装请求体
        HttpEntity<MultiValueMap<String, String>> request =
                new HttpEntity<MultiValueMap<String, String>>(map, headers);
 
        // 发送post请求,并输出结果
        ResponseEntity<String> responseEntity
                = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
        String body = responseEntity.getBody(); // 获取响应体
        HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
        int statusCodeValue = responseEntity.getStatusCodeValue(); // 获取响应码值
        HttpHeaders responseHeaders = responseEntity.getHeaders(); // 获取响应头
 
        System.out.println("body:" + body);
        System.out.println("statusCode:" + statusCode);
        System.out.println("statusCodeValue:" + statusCodeValue);
        System.out.println("headers:" + responseHeaders);
        return;
    }
}

八、文件下载

1,简单的文件下载

(1)最简单的下载文件方式就是使用的是 restTemplate 调用 getForEntity 获取到字节数组,再将字节数组通过 java 8 的 Files 工具类的 write 方法,直接写到目标文件。

​ 该方式的缺点:由于需要将文件的字节数组全部放入内存中,极其消耗资源。当遇到大文件时,内存加载可能会造成 OutOfMemoryError。

(2)下面是一个简单的样例,下载一个网络上的图片并保存到本地。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        // 记录下开始下载时的时间
        Instant now = Instant.now();
        // 待下载的文件地址
        String url = "http://www.hangge.com/blog/images/logo.png";
        ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class);
        System.out.println("状态码:" + rsp.getStatusCode());
        try {
            // 将下载下来的文件内容保存到本地
            String targetPath = "/Users/hangge/Desktop/logo.png";
            Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(),
                    "未获取到下载文件"));
        } catch (IOException e) {
            System.out.println("文件写入失败:" + e.getMessage());
        }
        System.out.println("文件下载完成,耗时:" + ChronoUnit.MILLIS.between(now, Instant.now())
                + " 毫秒");
        return;
    }
}

2,大文件的下载

​ 对于大文件的下载,建议使用流的方式来解决。即每次接收到一部分数据就直接写入到文件。这里我使用使用 Files 的 copy 方法来处理流。。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        // 记录下开始下载时的时间
        Instant now = Instant.now();
        // 待下载的文件地址
        String url = "http://www.hangge.com/blog/images/logo.png";
        // 文件保存的本地路径
        String targetPath = "/Users/hangge/Desktop/logo.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;
        });
              System.out.println("文件下载完成,耗时:" + ChronoUnit.MILLIS.between(now, Instant.now())
                + " 毫秒");
        return;
    }
}

九、文件上传

1,效果图

(1)下面通过样例演示如何使用 RestTemplate 上传文件。这里使用 Form 表单的方式进行提交,上传时除了一个文件外还附带有两个自定义参数。

(2)接收端收到文件后会打印出相关参数、以及文件相关数据,并返回成功信息。

(3)发送方收到反馈后将反馈信息打印出来:

2,样例代码

(1)文件发送端代码如下:

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public void test() {
        // 上传接口
        String url = "http://localhost:8080/upload";
        // 待上传的文件
        String filePath = "/Users/hangge/Desktop/test.txt";
 
        // 封装请求参数
        FileSystemResource resource = new FileSystemResource(new File(filePath));
        MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
        param.add("myFile", resource);
        param.add("param1", "12345");
        param.add("param2", "hangge");
 
        // 发送请求并输出结果
        System.out.println("--- 上传文件 ---");
        String s = restTemplate.postForObject(url, param, String.class);
        System.out.println(s);
    }
}

(2)文件接收端代码如下:

​ 为方便演示,接收端这边的代码比较简单。如果想要进一步操作,比如:文件重命名、文件保存、相关上传参数的配置,参数配置

@RestController
public class HelloController {
 
    @PostMapping("/upload")
    public String upload(String param1, String param2, MultipartFile myFile) {
        System.out.println("--- 接收文件 ---");
        System.out.println("param1:" + param1);
        System.out.println("param2:" + param2);
        String originalFilename = myFile.getOriginalFilename();
        System.out.println("文件原始名称:" + originalFilename);
        try {
            String string = new String(myFile.getBytes(), "UTF-8");
            System.out.println("文件内容:" + string);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 处理文件内容...
        return "OK";
    }
}

当我们使用 RestTemplate 发送请求时, 如果接口返回的不是 200 状态(而是 4xx、5xx 这样的异常状态),则会抛出异常报错。

​ 但在实际接口对接中,我们可能希望获取接口返回的异常信息并返回(比如返回到前端)。这个可以通过自定义 RestTemplate 异常的处理来实现,下面通过样例进行演示。

十、请求异常处理

1,简单的样例代码

(1)首先我们需要创建一个自己的异常处理控制器(ExceptionHandler 类),该类实现 ResponseErrorHandler 接口。

public class RestThrowErrorHandler implements ResponseErrorHandler {
 
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        // 返回false表示不管response的status是多少都返回没有错
        // 这里可以自己定义那些status code你认为是可以抛Error
        return false;
    }
 
    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        // 这里面可以实现你自己遇到了Error进行合理的处理
    }
}

(2)接着在RestTemplate配置类中,指定使用我们前面自定义的异常处理控制。

@Configuration
public class RestTemplateConfig {
 
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory){
        RestTemplate restTemplate = new RestTemplate(factory);
        //Response status code 4XX or 5XX to the client.
        restTemplate.setErrorHandler(new RestThrowErrorHandler());
        return restTemplate;
    }
 
    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(15000); // 连接超时
        factory.setReadTimeout(5000); // 数据读取超时时间
        return factory;
    }
}

(3)最后是一个简单的请求样例。经过上面设置后,无论请求成功或者失败都会返回(不会抛异常),所以我们需要通过状态码来判断请求是否成功。

@RestController
public class HelloController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/test")
    public String test() {
        String url = "http://localhost:8080/xxxxxx";
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
        // 判断请求是否发生异常
        if(!responseEntity.getStatusCode().is2xxSuccessful()){
            // 返回异常信息
            return "请求失败,异常信息:" + responseEntity.getBody();
        }
        // 没有异常的话则返回正常的响应结果
        return responseEntity.getBody();
    }
}

(4)由于代码中我们请求了一个不存在的接口,并且在请求失败时将错误信息返回到前端,因此前端会显示如下内容:

2,代码改进

(1)通常来说我们不会直接把业务代码写在 Controller 里,而是通过 Service 实现。首先我们创建一个 Service,里面调用 RestTemplate 进行网络请求,当请求异常时直接抛出异常。

@Service
public class UserService {
 
    @Autowired
    private RestTemplate restTemplate;
 
    public String getInfo() {
        String url = "http://localhost:8080/xxxxxx";
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
        // 判断请求是否发生异常
        if(!responseEntity.getStatusCode().is2xxSuccessful()){
            // 抛出异常
            throw new RestClientException(responseEntity.getBody());
        }
        // 没有异常的话则返回正常的响应结果
        return responseEntity.getBody();
    }
}

(2)Controller 这边很简单,就是调用 Service 发起请求,然后返回结果。

@RestController
public class HelloController {
 
    @Autowired
    private UserService userService;
 
    @GetMapping("/test")
    public String test() {
        return userService.getInfo();
    }
}

(3)由于前面 Service 中我们将异常抛出了,所以要定义一个全局的异常处理类,捕获这个异常,并返回给前端处理的结果。

关于全局异常处理更详细用法可以参考我之前写的文章:

@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(RestClientException.class)
    public ResponseEntity<String> throwRestException(RestClientException restClientException){
        return new ResponseEntity<String>(restClientException.getMessage(),
                HttpStatus.BAD_REQUEST);
    }
}

(4)测试一下,可以看到异常信息已经返回到前端页面。

​ 有时当我们调用一个接口可能由于网络等原因造成第一次请求失败,如果再去尝试可能就成功了,这就是重试机制。下面演示如何结合 Spring Retry 实现请求发生异常时自动进行重试(重新发起请求)。

十一、请求异常自动重试

1,安装配置

(1)编辑项目 pom.xml 文件,添加 Spring Retry 相关依赖。

<!-- 重试机制 -->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

(2)在主类上加入 @EnableRetry 注解,启用重试功能。

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

2,使用样例

(1)由于这是前一篇文章关于请求异常处理的补充,首先我同样要创建一个自己的异常处理控制器(RestThrowErrorHandler)并在 RestTemplate 配置类中进行配置。目的是让 4XX、5XX 这样的请求也能成功返回到客户端。具体代码参考之前的文章:

(2)首先修改前文的 Service 类,在需要重试的方法上添加 @Retryable 和 @Backoff 注解,使其在发生异常时能够自动重试。

(1)@Retryable 注解的方法在发生异常时会重试,参数说明:

  • value:当指定异常发生时会进行重试
  • include:和 value 一样,默认空。如果 exclude 也为空时,所有异常都重试
  • exclude:指定异常不重试,默认空。如果 include 也为空时,所有异常都重试
  • maxAttemps:最大重试次数,默认 3
  • backoff:重试等待策略,默认没有

(2)@Backoff 注解为重试等待策略,参数说明:

  • delay:指定重试的延时时间,默认为 1000L
  • multiplier:指定延迟的倍数,默认为 0。比如 delay=5000l,multiplier=2 时,第一次重试为 5 秒后,第二次为 10 秒,第三次为 20 秒。
@Service
public class UserService {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @Retryable(value = RestClientException.class, maxAttempts = 3,
            backoff = @Backoff(delay = 5000l,multiplier = 1))
    public String getInfo() {
        String url = "http://localhost:8080/xxxxxx";
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
        // 判断请求是否发生异常
        if(!responseEntity.getStatusCode().is2xxSuccessful()){
            System.out.println("请求失败...");
            // 抛出异常
            throw new RestClientException(responseEntity.getBody());
        }
        // 没有异常的话则返回正常的响应结果
        return responseEntity.getBody();
    }
}

(2)然后 Contoller 会调用这个 Service,这边代码同前文一样:
注意:由于 retry 用到了 aspect 增强,所以会有 aspect 的坑,就是方法内部调用,会使 aspect 增强失效,那么 retry 当然也会失效。
比如这里重试方法是定义在 Service 类里面,Controller 调用 Service 的这个方法,重试机制是没问题的。
但如果重试方法直接定义在这个 Controller 里面,也就同一个类里面内部调用,那么重试机制就会失效。

@RestController
public class HelloController {
 
    @Autowired
    private UserService userService;
 
    @GetMapping("/test")
    public String test() {
        return userService.getInfo();
    }
}

(3)全局的异常处理类和前文一样,当超过重试次数是异常会被抛出,这个全局的异常处理类会捕获这个异常,并返回给前端处理的结果。

@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(RestClientException.class)
    public ResponseEntity<String> throwRestException(RestClientException restClientException){
        return new ResponseEntity<String>(restClientException.getMessage(),
                HttpStatus.BAD_REQUEST);
    }
}

(4)测试一下,由于我们使用 RestTemplate 请求一个不存在的接口,可以看到 UserService 方法重复执行3次(每次间隔5秒)。

(5)超过重试次数后异常信息才返回到前端页面。

附:同时指定多个异常

@Retryable 注解的 value 属性可以同时设置多个异常类型,只要其中某个异常发生时,被注解的方法就会进行重试。

@Service
public class UserService {
 
    @Autowired
    RestTemplate restTemplate;
 
    @Retryable(value = {RestClientException.class, ConnectException.class}, maxAttempts = 3,
            backoff = @Backoff(delay = 5000l,multiplier = 1))
    public String getInfo() {
        String url = "http://localhost:8080/xxxxxx";
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
        // 判断请求是否发生异常
        if(!responseEntity.getStatusCode().is2xxSuccessful()){
            System.out.println("请求失败...");
            // 抛出异常
            throw new RestClientException(responseEntity.getBody());
        }
        // 没有异常的话则返回正常的响应结果
        return responseEntity.getBody();
    }
}

版权声明:本文为weixin_35688430原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。