Tool usage set | WebClient & RestTemplate & Feign of remote calling tools

remote calling tool

foreword

In work, besides recording the call log through the facet method or operating through the Filter provided by MVc, is there any other way? Some classes carried by the remote calling tool can realize some similar functions. Record the use of various remote calling tools. I want to have an overall understanding of them and think that I can use them smoothly.

WebClient

Non-blocking and responsive: WebClient provides a non-blocking programming model based on Reactor, which can handle a large number of concurrent requests, reduce the blocking waiting time of threads, and provide responsive operators and data flow processing.

  • Non-blocking means that in the IO operation, the thread will not be blocked waiting for the operation to complete, and can continue to perform other tasks. Reactive is a programming paradigm based on event flow, focusing on the processing and transformation of data flow.

Asynchronous and synchronous requests: WebClient supports asynchronous and synchronous request methods. Using asynchronously, the response can be processed by returning a Mono (single value) or a Flux (multiple values). The synchronous method will block the current thread until a complete response is received.

  • Synchronous request:

    • After the request is initiated, the current thread will block waiting for the server to respond.
    • Execution of subsequent code will not continue until a complete response has been received.
    • Suitable for simple request scenarios where the relationship between request and response is one-to-one.
    • Request processing and response processing occur serially.
  • Asynchronous request:

    • After the request is initiated, the current thread will not block waiting for the server to respond.
    • You can register a callback to process the response or obtain the response result by way of callback function, Future/Promise or reactive programming.
    • It is suitable for scenarios where multiple requests are initiated at the same time, multiple requests are processed in parallel, or there is no need to wait for a response immediately.
    • Request processing and response processing are performed in parallel.

MonoRepresents an asynchronous sequence containing zero or one elements. Think of it as a single-valued asynchronous result, similar to that in Java 8 CompletableFuture. MonoUseful when dealing with a single value, such as fetching a single resource or performing a single action.

Flux 表示一个包含零个或多个元素的异步序列。可以将它理解为一个多值的异步结果,类似于 Java 8 中的 StreamFlux 在处理多个值的情况下非常有用,例如处理多个资源或批量操作。

功能丰富的 API:WebClient 提供了丰富的方法和操作符来构建和处理 HTTP 请求和响应。你可以设置请求方法、添加请求头、设置请求体、处理响应状态码、处理响应体等。

  1. 发起请求:

    • 使用 get()post()put()delete() 等方法来指定 HTTP 方法。
    • 可以使用 uri() 方法设置请求的 URI。
    • 通过 headers() 方法设置请求头。
    • 可以使用 body() 方法设置请求体。
  2. 发送请求并处理响应:

    • 使用 exchange() 方法发送请求并获取响应。
    • 使用 retrieve() 方法获取响应体,可以通过 toEntity()toFlux()toMono() 等方法将响应转换为不同的数据类型。
    • 可以使用 bodyToXxx() 方法将响应体转换为指定的数据类型,如 bodyToMono()bodyToFlux()bodyToEntity() 等。
  3. 异常处理:

    • 使用 onStatus() 方法可以处理特定的 HTTP 状态码,并进行相应的处理逻辑。
    • 可以使用 onError() 方法处理请求过程中的错误情况。
    • 通过 onStatus()onError() 等方法可以使用操作符链式组合多个异常处理逻辑。
  4. 响应处理:

    • 使用操作符(如 map()flatMap()filter() 等)对响应进行处理和转换。
    • 可以使用操作符进行响应流的合并、转换、过滤、排序等操作。
    • 可以使用 doOnXxx() 方法对响应进行副作用操作,如记录日志、统计信息等。
  5. 超时和重试:

    • 使用 timeout() 方法可以设置请求的超时时间。
    • 可以使用 retry() 方法进行请求重试,可以自定义重试条件和重试策略。
  6. 文件上传和下载:

    • 使用 multipart() 方法可以进行文件上传,支持单个文件或多个文件的上传。
    • 可以使用 body(BodyInserters.fromResource()) 方法下载文件,将响应体保存为文件。
  7. 请求拦截器和过滤器:

    • 可以通过 filter() 方法添加请求拦截器,对请求进行预处理或修改请求参数。
    • 通过自定义 ExchangeFilterFunction 可以添加全局的请求过滤器,对所有请求进行统一的处理。
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public class GlobalRequestFilterExample {
    public static void main(String[] args) {
        WebClient webClient = WebClient.builder()
                .baseUrl("https://api.example.com")
                .filter(globalRequestFilter())
                .build();
        webClient.get()
                .uri("/users")
                .retrieve()
                .toEntity(String.class)
                .subscribe(response -> {
                    HttpStatus statusCode = response.getStatusCode();
                    HttpHeaders headers = response.getHeaders();
                    String body = response.getBody();
                    System.out.println("Status: " + statusCode);
                    System.out.println("Headers: " + headers);
                    System.out.println("Body: " + body);
                });
    }
    private static ExchangeFilterFunction globalRequestFilter() {
        return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
            ClientRequest.Builder builder = ClientRequest.from(clientRequest);
            // 添加全局请求头
            builder.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
            // 修改请求方法
            builder.method(HttpMethod.GET);
            // 返回修改后的请求
            return Mono.just(builder.build());
        });
    }
}
复制代码

数据转换和编解码:WebClient 支持数据转换,可以将请求和响应体转换为各种数据类型,如 JSON、XML 等。它使用 Jackson、Gson、XML 编解码器等来实现数据的序列化和反序列化。

  1. 响应体的数据类型转换:

    • 使用 toEntity() 方法将响应体转换为 ResponseEntity 对象,包含了响应的状态码、响应头和响应体。
    • 可以使用 toFlux() 方法将响应体转换为 Flux 对象,用于处理响应体为多个元素的情况。
    • 可以使用 toMono() 方法将响应体转换为 Mono 对象,用于处理响应体为单个元素的情况。
  2. 响应体的数据格式转换:

    • WebClient 支持将响应体从 JSON、XML、HTML、文本等格式转换为对象或字符串。
    • 可以使用 bodyToXxx() 方法将响应体转换为指定的数据类型,如 bodyToMono()bodyToFlux()bodyToEntity() 等。
    • WebClient 默认集成了 JSON 和 XML 的数据转换器,可以将响应体直接转换为 Java 对象。
  3. 请求体的数据类型转换:

    • 使用 bodyValue() 方法可以设置请求体的数据,WebClient 会根据请求的 Content-Type 进行适当的编码。
    • 可以使用 body() 方法指定请求体的数据类型,WebClient 会根据数据类型进行编码,如 JSON、XML、表单等。
    • 可以使用 body(BodyInserters.fromXxx()) 方法将 Java 对象转换为请求体的数据,如 body(BodyInserters.fromValue())body(BodyInserters.fromFormData()) 等。
  4. 数据编解码器的自定义:

    • 可以通过自定义 EncoderDecoder 实现数据的自定义编解码。
    • WebClient 提供了 codecs() 方法来配置自定义的编解码器,可以设置 JSON、XML、表单等数据格式的编解码器。
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.AbstractJackson2Decoder;
import org.springframework.http.codec.json.AbstractJackson2Encoder;
import org.springframework.http.codec.json.Jackson2CodecSupport;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public class CustomCodecExample {
    public static void main(String[] args) {
        WebClient webClient = WebClient.builder()
                .exchangeStrategies(customExchangeStrategies())
                .build();
        webClient.get()
                .uri("https://api.example.com/users")
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(User.class)
                .subscribe(user -> {
                    System.out.println("User: " + user);
                });
    }
    static class User {
        private String name;
        private int age;
        // 构造方法、getter 和 setter 省略
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + ''' +
                    ", age=" + age +
                    '}';
        }
    }
    private static ExchangeStrategies customExchangeStrategies() {
        return ExchangeStrategies.builder()
                .codecs(configurer -> {
                    configurer.defaultCodecs().jackson2JsonEncoder(new CustomJsonEncoder());
                    configurer.defaultCodecs().jackson2JsonDecoder(new CustomJsonDecoder());
                })
                .build();
    }
    private static class CustomJsonEncoder extends AbstractJackson2Encoder {
        CustomJsonEncoder() {
            super(new Jackson2CodecSupport() {
                @Override
                public void setSerializedType(ResolvableType type) {
                    // 自定义序列化的类型
                    super.setSerializedType(ResolvableType.forClass(User.class));
                }
            }, MediaType.APPLICATION_JSON);
        }
    }
    private static class CustomJsonDecoder extends AbstractJackson2Decoder {
        CustomJsonDecoder() {
            super(new Jackson2CodecSupport() {
                @Override
                public void setSerializedType(ResolvableType type) {
                    // 自定义反序列化的类型
                    super.setSerializedType(ResolvableType.forClass(User.class));
                }
            }, MediaType.APPLICATION_JSON);
        }
    }
}
复制代码

连接池管理:WebClient 内置了连接池管理,可以重用连接,减少资源消耗和连接建立的时间。

  • WebClient 在默认情况下使用一个共享的连接池,这意味着多个 WebClient 实例将共享同一个连接池。这样可以减少资源的占用,并提高性能。
  • WebClient 默认会重用连接,如果一个请求完成后仍然有其他请求需要发起,WebClient 会尝试重用现有的连接,而不是关闭它。
  • 连接池管理还涉及连接的保持时间。当连接在一段时间内没有被使用时,连接可能会被关闭或释放。
  • 当请求完成后,连接可以被释放回连接池以供其他请求使用。默认情况下,WebClient 会自动释放连接。
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
    @Bean
    public WebClient webClient(WebClient.Builder builder) {
        // 设置连接池最大连接数和每个主机的最大连接数
        builder.clientConnector(new ReactorClientHttpConnector(HttpClient.create().wiretap(true)
                .tcpConfiguration(tcpClient -> tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                        .doOnConnected(conn -> conn
                                .addHandlerLast(new ReadTimeoutHandler(5))
                                .addHandlerLast(new WriteTimeoutHandler(5))))).build();
        
        // 设置连接的保持时间
        builder.keepAlive(Duration.ofMinutes(5));
        
        // 可以根据需要进行其他的配置,如数据转换器、错误处理等
        builder.exchangeStrategies(ExchangeStrategies.builder()
                .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
                .build());
        
        return builder.build();
    }
    @Bean
    public WebClientCustomizer webClientCustomizer() {
        return webClientBuilder -> {
            // 可以在这里进行全局的 WebClient 配置
            webClientBuilder.baseUrl("https://api.test.com");
            // ...
        };
    }
}
复制代码

错误处理:WebClient 提供了错误处理机制,可以处理请求失败、超时、错误状态码等情况,并支持自定义错误处理逻辑。

  1. 异常处理:

    • 使用 onStatus() 方法可以指定处理特定的 HTTP 状态码,如 onStatus(HttpStatus::isError, errorHandler) 可以处理错误状态码。
    • 使用 onError() 方法可以指定处理请求过程中的异常情况,如网络连接失败、连接超时等。
  2. 错误处理器:

    • 使用 exchange() 方法可以获取完整的响应信息,包括状态码、响应头和响应体。
    • 使用 bodyToMono()bodyToFlux() 方法将响应体转换为相应的 MonoFlux 对象。
    • 可以使用 onErrorResume() 方法设置自定义的错误处理逻辑,返回一个备用的 MonoFlux 对象。
  3. 超时处理:

    • 使用 timeout() 方法可以设置请求的超时时间,当请求超过指定时间仍未完成时,会触发超时异常。
    • 使用 retry() 方法可以进行请求的重试,可以设置最大重试次数和重试条件,用于处理请求失败的情况。
  4. 错误信号转换:

    • 使用 onErrorMap() 方法可以将错误信号转换为不同的异常类型,以便在错误处理时进行更精细的判断和处理。
    • 使用 onErrorResume() 方法可以根据不同的错误类型返回不同的备用数据,用于灵活处理不同类型的错误情况。
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public class ErrorHandlingExample {
    public static void main(String[] args) {
        WebClient webClient = WebClient.create();
        webClient.get()
                .uri("https://api.example.com/users/123")
                .retrieve()
                .onStatus(HttpStatus::isError, response -> {
                    if (response.statusCode() == HttpStatus.NOT_FOUND) {
                        return Mono.error(new UserNotFoundException("User not found"));
                    } else {
                        return Mono.error(new RuntimeException("Request failed with status code: " + response.statusCode()));
                    }
                })
                .bodyToMono(User.class)
                .doOnError(UserNotFoundException.class, ex -> {
                    System.out.println("User not found: " + ex.getMessage());
                })
                .doOnError(RuntimeException.class, ex -> {
                    System.out.println("Request failed: " + ex.getMessage());
                })
                .subscribe(user -> {
                    System.out.println("User: " + user);
                });
    }
    static class User {
        private String name;
        private int age;
        // 构造方法、getter 和 setter 省略
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + ''' +
                    ", age=" + age +
                    '}';
        }
    }
    static class UserNotFoundException extends RuntimeException {
        public UserNotFoundException(String message) {
            super(message);
        }
    }
}
复制代码

可扩展性和定制性:WebClient 可以通过配置不同的 ExchangeStrategies、过滤器、拦截器等来进行定制和扩展,以满足不同的需求。

配置过滤

import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public class ErrorHandlingExample {
    public static void main(String[] args) {
        WebClient webClient = WebClient.builder()
                .baseUrl("https://api.example.com")
                .filter(new ErrorHandlingFilter())
                .build();
        webClient.get()
                .uri("/users/123")
                .retrieve()
                .bodyToMono(User.class)
                .subscribe(user -> {
                    System.out.println("User: " + user);
                });
    }
    static class User {
        private String name;
        private int age;
        // 构造方法、getter 和 setter 省略
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + ''' +
                    ", age=" + age +
                    '}';
        }
    }
    static class ErrorHandlingFilter implements ExchangeFilterFunction {
        @Override
        public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
            return next.exchange(request)
                    .flatMap(response -> {
                        if (response.statusCode().isError()) {
                            if (response.statusCode() == HttpStatus.NOT_FOUND) {
                                return Mono.error(new UserNotFoundException("User not found"));
                            } else {
                                return Mono.error(new RuntimeException("Request failed with status code: " + response.statusCode()));
                            }
                        }
                        return Mono.just(response);
                    });
        }
    }
    static class UserNotFoundException extends RuntimeException {
        public UserNotFoundException(String message) {
            super(message);
        }
    }
}
复制代码

实战:

添加所需的依赖到 pom.xml 文件中:

<dependencies>
    <!-- Spring Boot Webflux -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <!-- Spring Boot Starter Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <!-- Jackson JSON Processor -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>
复制代码

注意:

Spring WebFlux中的WebClient,它的底层实现可以使用不同的HTTP客户端,包括Apache HttpClient和OkHttp。

显式地使用Apache HttpClient作为WebClient的底层实现,可以通过添加spring-webflux-httpclient依赖来实现,同时需要排除默认的Reactor Netty依赖。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<exclusions>
   <exclusion>
       <groupId>io.projectreactor.netty</groupId>
       <artifactId>reactor-netty-http</artifactId>
   </exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<exclusions>
   <exclusion>
       <groupId>io.projectreactor.netty</groupId>
       <artifactId>reactor-netty</artifactId>
   </exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<exclusions>
   <exclusion>
       <groupId>io.netty</groupId>
       <artifactId>netty-transport-native-epoll</artifactId>
   </exclusion>
   <exclusion>
       <groupId>io.netty</groupId>
       <artifactId>netty-transport-native-unix-common</artifactId>
   </exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
复制代码

WebClient 配置为 @Configuration 类:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
    @Bean
    public WebClient webClient() {
        ExchangeStrategies strategies = ExchangeStrategies.builder()
                .codecs(this::customCodecs)
                .build();
        return WebClient.builder()
                .exchangeStrategies(strategies)
                .build();
    }
    private void customCodecs(ClientCodecConfigurer configurer) {
        configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder());
        configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder());
        configurer.defaultCodecs().maxInMemorySize(1024 * 1024);
        configurer.defaultCodecs().defaultContentType(MediaType.APPLICATION_JSON);
    }
}
复制代码

创建一个 UserController 类,用于定义 REST API 的处理器方法:

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    private final UserService userService;
    public UserController(UserService userService) {
        this.userService = userService;
    }
    @GetMapping("/{id}")
    public Mono<User> getUserById(@PathVariable String id) {
        return userService.getUserById(id);
    }
    @PostMapping
    public Mono<User> createUser(@RequestBody @Validated CreateUserRequest request) {
        return userService.createUser(request);
    }
}
复制代码

创建一个 UserService 类,用于处理用户相关的业务逻辑:

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class UserService {
    public Mono<User> getUserById(String id) {
        // Simulate fetching user data from external service
        WebClient webClient = WebClient.create();
        return webClient.get()
                .uri("https://api.example.com/users/{id}", id)
                .retrieve()
                .bodyToMono(User.class);
    }
    public Mono<User> createUser(CreateUserRequest request) {
        // Simulate creating a user by sending a POST request
        WebClient webClient = WebClient.create();
        return webClient.post()
                .uri("https://api.example.com/users")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(request)
                .retrieve()
                .bodyToMono(User.class);
    }
}
复制代码

创建一个 User 类,作为数据模型:

public class User {
    private String id;
    private String name;
    private int age;
    // 构造方法、getter 和 setter 省略
    @Override
    public String toString() {
        return "User{" +
                "id='" + id + ''' +
                ", name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}
复制代码

创建一个 CreateUserRequest 类,用于接收创建用户的请求:

import javax.validation.constraints.NotEmpty;
public class CreateUserRequest {
    @NotEmpty(message = "Name cannot be empty")
    private String name;
    private int age;
    // 构造方法、getter 和 setter 省略
    @Override
    public String toString() {
        return "CreateUserRequest{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}
​
复制代码

RestTemplate

RestTemplate 是 Spring 3.x 版本引入的,并在 Spring 5.x 版本中仍然可用。它的设计受到了传统的 Spring MVC 的影响,并提供了许多方便的方法和功能。

RestTemplate 是一个同步的、阻塞式的 HTTP 客户端,它主要以同步方式发送和接收 HTTP 请求。在默认情况下,RestTemplate 的方法会阻塞当前线程,直到收到响应或发生超时。

RestTemplate 的主要特点和功能如下:

发送 HTTP 请求:RestTemplate 提供了多种方法,例如 GET、POST、PUT、DELETE 等,用于发送不同类型的 HTTP 请求。

getForObject(): 发送 GET 请求,并将响应体转换为指定类型的对象。

User user = restTemplate.getForObject("http://example.com/api/user/{id}", User.class, 1);
复制代码

getForEntity(): 发送 GET 请求,并将响应体转换为 ResponseEntity 对象,包含响应的状态码、头部信息和响应体。

ResponseEntity<User> response = restTemplate.getForEntity("http://example.com/api/user/{id}", User.class, 1);
User user = response.getBody();
HttpStatus status = response.getStatusCode();
复制代码

postForObject(): 发送 POST 请求,并将请求体转换为指定类型的对象。可以传递 URL 参数作为变量。

User user = restTemplate.postForObject("http://example.com/api/user", newUser, User.class);
复制代码

postForEntity(): 发送 POST 请求,并将请求体转换为 ResponseEntity 对象,包含响应的状态码、头部信息和响应体。

ResponseEntity<User> response = restTemplate.postForEntity("http://example.com/api/user", newUser, User.class);
User createdUser = response.getBody();
HttpStatus status = response.getStatusCode();
复制代码

put(): 发送 PUT 请求,用于更新资源。可以传递 URL 参数作为变量。

restTemplate.put("http://example.com/api/user/{id}", updatedUser, 1);
复制代码

delete(): 发送 DELETE 请求,用于删除资源。可以传递 URL 参数作为变量。

restTemplate.delete("http://example.com/api/user/{id}", 1);
复制代码

exchange() 方法是 RestTemplate 提供的灵活且强大的方法,它可以发送任意类型的 HTTP 请求,并且可以自定义请求和响应的处理。

RestTemplate restTemplate = new RestTemplate();
String username = "username";
String password = "password";
String credentials = username + ":" + password;
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());

HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Basic " + encodedCredentials);
HttpEntity<String> requestEntity = new HttpEntity<>(headers);

ResponseEntity<User> response = restTemplate.exchange("http://example.com/api/user/{id}", HttpMethod.GET, requestEntity, User.class, 1);


Resource fileResource = new FileSystemResource("/path/to/file.jpg");
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", fileResource);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

ResponseEntity<String> response = restTemplate.exchange("http://example.com/api/upload", HttpMethod.POST, requestEntity, String.class);
复制代码

请求和响应处理:RestTemplate 支持将请求参数设置为 URL 查询参数、表单参数或 JSON 请求体。它还能够将响应转换为对象或者是原始的 HTTP 响应数据。

请求参数的设置:

  • 将参数作为 URL 查询参数:可以通过将参数拼接到 URL 中来设置查询参数。例如:http://example.com/api/user?id=123.
  • 将参数作为表单参数:可以使用 MultiValueMapMap 对象作为请求体中的表单参数。例如:
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", "john");
formData.add("password", "secret");
复制代码
  • 将参数作为 JSON 请求体:可以将对象转换为 JSON 字符串,并将其作为请求体发送。例如:
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
User user = new User("john", "secret");
HttpEntity<User> requestEntity = new HttpEntity<>(user, headers);
复制代码

请求体的处理:

  • 设置请求体的类型和编码:可以通过设置请求头的 Content-Type 来指定请求体的类型和编码。例如:headers.setContentType(MediaType.APPLICATION_JSON).
  • 将请求体转换为 JSON 字符串:可以使用 JSON 库将对象转换为 JSON 字符串,并将其作为请求体发送。

响应的处理:

  • 将响应转换为对象:可以使用 exchange()getForObject() 方法将响应转换为指定的对象类型。例如:User user = restTemplate.getForObject("http://example.com/api/user/{id}", User.class, 1).
  • 获取原始的 HTTP 响应数据:可以使用 exchange() 方法获取包含响应状态码、响应头和响应体的 ResponseEntity 对象。例如:
ResponseEntity<String> response = restTemplate.getForEntity("http://example.com/api/user/{id}", String.class, 1);
String responseBody = response.getBody();
复制代码

错误处理:RestTemplate 可以处理请求过程中发生的错误,包括网络异常、HTTP 状态码错误等。可以根据需要自定义错误处理逻辑。

抛出异常:

try {
    ResponseEntity<String> response = restTemplate.getForEntity("http://example.com/api/resource", String.class);
    // 处理成功响应
} catch (RestClientException ex) {
    // 处理异常情况
    if (ex instanceof HttpClientErrorException) {
        HttpClientErrorException httpClientErrorException = (HttpClientErrorException) ex;
        HttpStatus statusCode = httpClientErrorException.getStatusCode();
        // 处理特定的 HTTP 状态码错误
    } else if (ex instanceof HttpServerErrorException) {
        HttpServerErrorException httpServerErrorException = (HttpServerErrorException) ex;
        HttpStatus statusCode = httpServerErrorException.getStatusCode();
        // 处理服务器端错误
    } else if (ex instanceof ResourceAccessException) {
        ResourceAccessException resourceAccessException = (ResourceAccessException) ex;
        // 处理网络异常等其他错误
    } else {
        // 处理其他类型的异常
    }
}
复制代码

自定义异常处理器:

public class CustomRestTemplate extends RestTemplate {
    public CustomRestTemplate() {
        setErrorHandler(new CustomResponseErrorHandler());
    }
}
public class CustomResponseErrorHandler extends DefaultResponseErrorHandler {
    @Override
    protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
        if (statusCode.is4xxClientError()) {
            // 处理客户端错误
        } else if (statusCode.is5xxServerError()) {
            // 处理服务器端错误
        } else {
            // 处理其他类型的错误
        }
    }
}
复制代码

自定义了一个 CustomRestTemplate 类,继承自 RestTemplate,并在构造函数中设置了自定义的异常处理器 CustomResponseErrorHandler。在自定义异常处理器中,我们可以根据不同的 HTTP 状态码来处理对应的错误情况。

URI 变量替换:RestTemplate 支持将 URI 中的占位符替换为实际的值,以便动态构建请求 URL。

RestTemplate restTemplate = new RestTemplate();
// 定义 URI 模板,其中 {id} 是占位符
String uriTemplate = "http://example.com/api/resource/{id}";
// 创建 URI 变量,设置实际的值
Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("id", "123");
// 使用 URI.expand() 方法进行 URI 变量替换
URI uri = UriComponentsBuilder.fromUriString(uriTemplate)
        .buildAndExpand(uriVariables)
        .toUri();
// 发送 GET 请求
ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class);
复制代码

拦截器和过滤器:RestTemplate 允许注册请求和响应的拦截器和过滤器,用于在发送请求前和处理响应时进行额外的处理。

拦截器(Interceptor)是一种针对请求和响应的拦截处理机制,可以在发送请求前和处理响应时执行预定义的逻辑。你可以实现 ClientHttpRequestInterceptor 接口,并将其注册到 RestTemplate 中的 interceptors 集合中,以便在请求发送之前和响应处理之后进行拦截。

RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(List.of(new LoggingClientHttpRequestInterceptor()));
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {
        // 在发送请求前的拦截处理
        logRequest(request, body);
        // 继续执行请求
        ClientHttpResponse response = execution.execute(request, body);
        // 在处理响应后的拦截处理
        logResponse(response);
        return response;
    }
    private void logRequest(HttpRequest request, byte[] body) {
        // 记录请求日志的逻辑
        System.out.println("Sending request: " + request.getMethod() + " " + request.getURI());
        System.out.println("Request body: " + new String(body, StandardCharsets.UTF_8));
    }
    private void logResponse(ClientHttpResponse response) throws IOException {
        // 记录响应日志的逻辑
        System.out.println("Received response: " + response.getStatusCode());
        System.out.println("Response body: " + StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8));
    }
}
复制代码

过滤器(Filter)是一种更细粒度的请求处理机制,它可以在请求的各个阶段添加自定义逻辑。

可以通过使用 ClientHttpRequestFactory 来注册过滤器。下面是一个示例,展示如何实现一个简单的过滤器来添加请求头信息:

public class HeaderFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 添加自定义的请求头
        request.addHeader("X-Request-Id", UUID.randomUUID().toString());
        // 继续执行过滤器链
        filterChain.doFilter(request, response);
    }
}
复制代码

创建了一个名为 HeaderFilter 的过滤器类,继承了 Spring Framework 提供的 OncePerRequestFilter 抽象类。在 doFilterInternal() 方法中,我们添加了一个自定义的请求头,将一个随机生成的请求 ID 添加到请求中。

需要创建一个自定义的 ClientHttpRequestFactory,并在其中注册过滤器。下面是一个示例:

public class CustomHttpRequestFactory extends SimpleClientHttpRequestFactory {
    private final List<Filter> filters;
    public CustomHttpRequestFactory(List<Filter> filters) {
        this.filters = filters;
    }
    @Override
    protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
        if (filters != null && !filters.isEmpty()) {
            for (Filter filter : filters) {
                if (filter instanceof OncePerRequestFilter) {
                    OncePerRequestFilter oncePerRequestFilter = (OncePerRequestFilter) filter;
                    oncePerRequestFilter.doFilter(null, null, null);
                }
            }
        }
        super.prepareConnection(connection, httpMethod);
    }
}
复制代码

创建了一个名为 CustomHttpRequestFactory 的自定义请求工厂类,继承了 SimpleClientHttpRequestFactory。在 prepareConnection() 方法中,我们遍历注册的过滤器列表,并调用过滤器的 doFilter() 方法进行处理。

使用这个自定义请求工厂,可以在创建 RestTemplate 实例时进行设置,示例如下:

List<Filter> filters = List.of(new HeaderFilter());
RestTemplate restTemplate = new RestTemplate(new CustomHttpRequestFactory(filters));
复制代码

引申一下基于 spring 的过滤

使用 javax.servlet.Filter 接口来创建过滤器:

@WebFilter(urlPatterns = "/*") //通过注解的方式
public class HeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
       throws IOException, ServletException {
   HttpServletRequest httpRequest = (HttpServletRequest) request;
   HttpServletResponse httpResponse = (HttpServletResponse) response;
   // 添加自定义的请求头
   httpRequest.addHeader("X-Request-Id", UUID.randomUUID().toString());
   // 继续执行过滤器链
   chain.doFilter(httpRequest, httpResponse);
}
// 其他方法...
}
复制代码

web.xml 配置

<web-app>
<!-- 其他配置... -->
<filter>
   <filter-name>headerFilter</filter-name>
   <filter-class>com.example.HeaderFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>headerFilter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 其他配置... -->
</web-app>
复制代码

实战:

使用 RestTemplate 进行 GET 请求,并添加自定义的请求头、处理响应结果以及错误处理:

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        // 添加自定义的请求拦截器
        restTemplate.getInterceptors().add(new CustomHeaderInterceptor());
        // 设置连接超时时间和读取超时时间
        restTemplate.setRequestFactory(requestFactory());
        // 设置错误处理器
        restTemplate.setErrorHandler(new CustomResponseErrorHandler());
        // 设置消息转换器,例如处理 JSON 数据
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        return restTemplate;
    }
    @Bean
    public HttpComponentsClientHttpRequestFactory requestFactory() {
        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
        requestFactory.setConnectTimeout(5000); // 设置连接超时时间为5秒
        requestFactory.setReadTimeout(5000); // 设置读取超时时间为5秒
        return requestFactory;
    }
}
@Component
public class CustomHeaderInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {
        HttpHeaders headers = request.getHeaders();
        headers.add("X-Custom-Header", "Custom Value");
        return execution.execute(request, body);
    }
}
@Component
public class CustomResponseErrorHandler extends DefaultResponseErrorHandler {
    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        // 获取错误的 HTTP 状态码
        HttpStatus statusCode = response.getStatusCode();
        if (statusCode.is4xxClientError()) {
            // 处理客户端错误,例如 4xx 状态码
            if (statusCode == HttpStatus.NOT_FOUND) {
                throw new ResourceNotFoundException("Requested resource not found");
            } else if (statusCode == HttpStatus.UNAUTHORIZED) {
                throw new UnauthorizedException("Unauthorized access");
            } else {
                throw new RuntimeException("Client error occurred");
            }
        } else if (statusCode.is5xxServerError()) {
            // 处理服务器错误,例如 5xx 状态码
            if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR) {
                throw new ServerErrorException("Internal server error");
            } else if (statusCode == HttpStatus.SERVICE_UNAVAILABLE) {
                throw new ServiceUnavailableException("Service unavailable");
            } else {
                throw new RuntimeException("Server error occurred");
            }
        } else {
            // 其他错误处理
            super.handleError(response);
        }
    }
}
@Service
public class ApiService {
    private final RestTemplate restTemplate;
    public ApiService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    public String fetchData(String url) {
        try {
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
            if (response.getStatusCode().is2xxSuccessful()) {
                return response.getBody();
            } else {
                throw new RuntimeException("Request failed with status code: " + response.getStatusCodeValue());
            }
        } catch (RestClientException ex) {
            // 处理异常情况
            throw new RuntimeException("Request failed: " + ex.getMessage(), ex);
        }
    }
}
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        ApiService apiService = context.getBean(ApiService.class);
        String data = apiService.fetchData("http://example.com/api/resource");
        System.out.println("Response data: " + data);
    }
}
复制代码

使用Apache HttpClient作为RestTemplate的底层实现:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
复制代码
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
@EnableConfigurationProperties
public class CustomRestTemplateConfig {
@Autowired(required = false)
private ClientHttpRequestFactory requestFactory;
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
   return new RestTemplate(getRequestFactory());
}
private ClientHttpRequestFactory getRequestFactory() {
   if (this.requestFactory != null) {
       return this.requestFactory;
   } else {
       return new HttpComponentsClientHttpRequestFactory(httpClient());
   }
}
@Bean
@ConditionalOnMissingBean
public HttpClient httpClient() {
   return HttpClientBuilder.create().build();
}
}
@SpringBootApplication(exclude = {RestTemplateAutoConfiguration.class})
public class MyApplication {
// ...
}
复制代码

Feign

Feign是一个声明式的HTTP客户端库,它简化了与RESTful服务进行交互的过程。

声明式API:Feign允许使用注解方式定义HTTP请求的接口,而无需编写具体的实现代码。通过编写接口,可以声明请求的URL、HTTP方法、请求参数等信息。

Feign提供了一系列注解来帮助定义请求接口,其中常用的注解包括:

  1. @FeignClient:用于标识一个Feign客户端,指定服务的名称或URL。
  2. @RequestMapping:类似于Spring MVC中的注解,用于指定请求的URL路径和HTTP方法。
  3. @PathVariable:用于将URL路径中的变量绑定到方法参数。
  4. @RequestParam:用于将请求参数绑定到方法参数。
  5. @RequestBody:用于将请求体绑定到方法参数。
  6. @RequestHeader:用于将请求头信息绑定到方法参数。
@FeignClient(name = "example-service")
public interface ExampleFeignClient {

    @RequestMapping(value = "/api/resource/{id}", method = RequestMethod.GET)
    ResourceDTO getResource(@PathVariable("id") String id, @RequestParam("param") String param);

    @RequestMapping(value = "/api/resource", method = RequestMethod.POST)
    void createResource(@RequestBody ResourceDTO resource);

    @RequestMapping(value = "/api/resource/{id}", method = RequestMethod.PUT)
    void updateResource(@PathVariable("id") String id, @RequestBody ResourceDTO resource);

    @RequestMapping(value = "/api/resource/{id}", method = RequestMethod.DELETE)
    void deleteResource(@PathVariable("id") String id);
}
复制代码

使用Feign声明式API时,还需要在Spring Boot应用程序的配置文件中启用Feign客户端。可以通过添加@EnableFeignClients注解或feign.enable=true的配置来实现。

内部集成了Ribbon和Hystrix:Feign内部集成了Netflix Ribbon和Netflix Hystrix,可以提供负载均衡和容错功能。Feign可以通过服务名称自动解析服务实例,并实现请求的负载均衡。

  1. Ribbon负载均衡:Feign集成了Ribbon,可以通过服务名称自动解析服务实例,并实现请求的负载均衡。只需要在Feign客户端接口上使用@FeignClient注解指定服务名称,Feign就能够根据服务名称从服务注册中心(如Eureka)获取服务实例列表,并根据负载均衡策略选择一个实例发送请求。

  2. Hystrix容错:Feign还集成了Hystrix,可以提供容错功能,防止远程调用的失败影响整个系统。Hystrix可以对远程调用进行熔断、降级和限流等操作,以保护系统的稳定性。在使用Feign时,可以通过配置feign.hystrix.enabled=true启用Hystrix支持,并在Feign客户端接口的方法上添加@HystrixCommand注解来定义容错逻辑。

    • 熔断(Circuit Breaker)是Hystrix的核心功能之一,它可以在调用失败或达到一定的错误阈值时,打开断路器,阻止继续向该服务发起请求,从而快速失败,避免资源的浪费和雪崩效应。当断路器打开后,可以定义一个降级逻辑,返回一个预先设定的默认值或错误信息,以提供给调用方使用。
    • 降级(Fallback)是在熔断状态下触发的操作,用于替代实际调用的逻辑,返回一个备选的响应。降级逻辑可以是从缓存中获取数据、返回静态数据、调用本地方法等。通过降级处理,可以在服务不可用时提供一个有意义的响应,保证系统的可用性。
    • 限流(Rate Limiting)是指控制对服务的并发请求量或并发连接数,以防止系统过载。Hystrix提供了线程池隔离和信号量隔离两种限流模式,可以根据具体需求选择合适的限流策略。
<dependencies>
    <!-- Feign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <!-- Ribbon -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>
    
    <!-- Hystrix -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
</dependencies>
复制代码

在Spring Boot的启动类上添加@EnableFeignClients注解,启用Feign客户端:

@SpringBootApplication
@EnableFeignClients
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
复制代码

创建一个Feign客户端接口,使用@FeignClient注解指定服务名称,并使用@RibbonClient注解启用Ribbon负载均衡:

@FeignClient(name = "my-service")
@RibbonClient(name = "my-service")
public interface MyServiceClient {
    @GetMapping("/api/resource")
    String getResource();
}
复制代码

在配置文件中配置Ribbon的服务列表和Hystrix的相关配置:


# Ribbon配置
my-service:
  ribbon:
    listOfServers: example.com:8081, example.com:8082
    
# Hystrix配置
hystrix:
  command:
    default:
      execution.isolation.thread.timeoutInMilliseconds: 5000
复制代码

关于 Hystrix 请求超时需要注意的点:

  1. Feign请求超时:如果Feign请求设置的超时时间较长,而另一个项目使用HTTP请求的超时时间较短,那么在请求另一个项目时,Feign请求可能会等待较长时间才会超时。这可能导致Feign请求的响应时间较长,从而影响系统的性能和吞吐量。
  2. HTTP请求超时:如果Feign请求设置的超时时间较短,而另一个项目使用HTTP请求的超时时间较长,那么在请求另一个项目时,Feign请求可能会因为超时而中断,并触发熔断逻辑。此时,Feign可以根据熔断策略进行降级处理,例如返回默认值、调用备用接口等,以确保系统的可用性。

自动化的请求和响应转换:Feign通过集成Jackson等序列化库,自动处理请求和响应的数据转换。可以通过注解指定请求和响应的数据格式,例如JSON、XML等。

Feign会根据注解中指定的数据格式,自动将请求和响应的数据进行序列化和反序列化。默认情况下,Feign使用JSON作为数据的格式,但也可以通过其他注解(如@Produces@Consumes)来指定其他的数据格式,例如XML。

@FeignClient(name = "example-service", url = "http://example.com")
public interface ExampleFeignClient {

    @PostMapping(value = "/api/resource", consumes = MediaType.APPLICATION_XML_VALUE,
            produces = MediaType.APPLICATION_XML_VALUE)
    ExampleResponseDto postResource(@RequestBody ExampleRequestDto request);
}

@XmlRootElement
public class ExampleRequestDto {
    // 请求数据的字段
}

@XmlRootElement
public class ExampleResponseDto {
    // 响应数据的字段
}

@RestController
public class ExampleController {

    @Autowired
    private ExampleFeignClient feignClient;

    @PostMapping("/call-example-service")
    public ExampleResponseDto callExampleService(@RequestBody ExampleRequestDto request) {
        return feignClient.postResource(request);
    }
}
复制代码

consumes属性指定了请求数据的格式为XML,produces属性指定了响应数据的格式为XML。

使用@XmlRootElement注解标记了请求和响应的数据对象,以便进行XML序列化和反序列化。

定制化和扩展性:Feign提供了丰富的扩展点和自定义配置选项,允许根据需要进行定制化。可以自定义请求拦截器、错误处理器、编码器、解码器等,以满足特定的业务需求。

  • 请求拦截器(Request Interceptors):可以实现RequestInterceptor接口,编写自定义的请求拦截器,用于在发送请求前进行额外的处理。例如,可以在请求头中添加认证信息、日志记录等。通过注册请求拦截器,可以在全局范围或特定的Feign客户端中应用自定义逻辑。
  • 响应拦截器(Response Interceptors):类似于请求拦截器,可以实现ResponseInterceptor接口,编写自定义的响应拦截器,用于在处理响应数据前进行额外的处理。例如,可以对响应进行统一的处理、错误处理等。
  • 错误处理器(Error Handlers):通过实现ErrorDecoder接口,可以自定义错误处理器来处理特定的错误情况。例如,可以根据不同的HTTP状态码或响应内容来处理异常,实现定制化的错误处理逻辑。
  • 编码器(Encoders)和解码器(Decoders):Feign支持自定义编码器和解码器,用于处理请求和响应数据的编码和解码。可以实现Encoder接口和Decoder接口,根据需求选择合适的序列化和反序列化方式。这样可以适应不同的数据格式,如JSON、XML等。

请求拦截器(Request Interceptor):

public class CustomRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 在请求前进行额外处理,如添加请求头、认证信息等
        template.header("Authorization", "Bearer token");
    }
}
复制代码

响应拦截器(Response Interceptor):

public class CustomResponseInterceptor implements ResponseInterceptor {
    @Override
    public void apply(Response response) {
        // 在处理响应前进行额外处理,如统一处理响应数据、错误处理等
        if (response.status() == 404) {
            // 处理特定的HTTP状态码
            throw new NotFoundException("Resource not found");
        }
    }
}
复制代码

错误处理器(Error Decoder):

public class CustomErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        // 根据响应内容和状态码自定义异常处理逻辑
        if (response.status() == 500) {
            return new CustomServerException("Server Error");
        } else if (response.status() == 400) {
            return new CustomBadRequestException("Bad Request");
        }
        return new FeignException(response.status(), response.reason());
    }
}
复制代码

编码器(Encoder)和解码器(Decoder):

public class CustomJsonEncoder implements Encoder {
    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        // 自定义JSON编码逻辑,将对象转换为JSON格式的请求体
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            String jsonBody = objectMapper.writeValueAsString(object);
            template.body(jsonBody, MediaType.APPLICATION_JSON_VALUE);
        } catch (JsonProcessingException e) {
            throw new EncodeException("Error encoding object to JSON", e);
        }
    }
}

public class CustomJsonDecoder implements Decoder {
    @Override
    public Object decode(Response response, Type type) throws DecodeException, FeignException {
        // 自定义JSON解码逻辑,将响应体转换为对象
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            return objectMapper.readValue(response.body().asInputStream(), objectMapper.constructType(type));
        } catch (IOException e) {
            throw new DecodeException("Error decoding JSON response", e);
        }
    }
}
复制代码
@Configuration
public class FeignConfig {
    
    @Bean
    public CustomRequestInterceptor customRequestInterceptor() {
        return new CustomRequestInterceptor();
    }
    
    
    @Bean
    public CustomResponseInterceptor customResponseInterceptor() {
        return new CustomResponseInterceptor();
    }
    
    
    @Bean
    public CustomErrorDecoder customErrorDecoder() {
        return new CustomErrorDecoder();
    }
    
    
     @Bean
    public CustomJsonEncoder customJsonEncoder() {
        return new CustomJsonEncoder();
    }
    
    @Bean
    public CustomJsonDecoder customJsonDecoder() {
        return new CustomJsonDecoder();
    }
    
}

@FeignClient(name = "example-service", configuration = FeignConfig.class)
public interface ExampleClient {
    // 接口方法定义
}
复制代码

整合Spring Cloud:Feign是Spring Cloud生态系统的一部分,可以与其他Spring Cloud组件无缝集成,例如Eureka、Zuul等。它可以与Spring Boot应用程序无缝配合使用,简化微服务架构中的服务间通信。

HTTP 客户端库

Apache HttpClient

强大的功能:Apache HttpClient支持HTTP/1.1协议的各种特性,包括连接管理、请求和响应拦截、重定向、认证、代理等。它可以执行各种类型的HTTP请求,如GET、POST、PUT、DELETE等,并支持多种数据格式的处理,如JSON、XML等。

import org.apache.http.HttpHost;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HttpClientConfig {
    @Value("${http.maxTotalConnections}")
    private int maxTotalConnections;
    @Value("${http.defaultMaxPerRoute}")
    private int defaultMaxPerRoute;
    @Value("${http.connectionRequestTimeout}")
    private int connectionRequestTimeout;
    @Value("${http.connectTimeout}")
    private int connectTimeout;
    @Value("${http.socketTimeout}")
    private int socketTimeout;
    @Value("${http.proxyHost}")
    private String proxyHost;
    @Value("${http.proxyPort}")
    private int proxyPort;
    @Bean
    public CloseableHttpClient httpClient() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(maxTotalConnections);
        connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
        HttpHost proxy = new HttpHost(proxyHost, proxyPort);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(connectionRequestTimeout)
                .setConnectTimeout(connectTimeout)
                .setSocketTimeout(socketTimeout)
                .setProxy(proxy)
                .build();
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        // 设置认证信息
        return HttpClientBuilder.create()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .setDefaultCredentialsProvider(credentialsProvider)
                .build();
    }
}
复制代码

通过在HttpClient的配置中设置代理信息,可以将请求通过代理服务器发送出去,而不是直接使用本机的IP和端口。在示例代码中,使用了HttpHost来定义代理的主机和端口,并将其设置到RequestConfig中。这样配置后,HttpClient会将请求发送到指定的代理服务器,然后由代理服务器转发请求到目标服务器,实现了通过代理发送请求的功能。

高级配置选项:Apache HttpClient提供了丰富的配置选项,可以根据需要进行定制化。可以设置连接池的大小、超时时间、重试策略等。它还支持连接的复用和持久连接,以提高性能和效率。

import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HttpClientConfig {

    @Bean
    public CloseableHttpClient httpClient() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(100);
        connectionManager.setDefaultMaxPerRoute(20);

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000)
                .setSocketTimeout(5000)
                .build();

        ConnectionKeepAliveStrategy keepAliveStrategy = (response, context) -> {
            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 30 * 1000;
        };

        return HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .setKeepAliveStrategy(keepAliveStrategy)
                .build();
    }
}
复制代码

请求和响应处理:Apache HttpClient提供了灵活的请求和响应处理机制。可以通过设置请求头、请求参数、请求体等来定制请求。对于响应,可以获取响应的状态码、响应头、响应体等信息,并对其进行处理。

import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.IOException;

public class HttpClientExample {
    
    public static void main(String[] args) throws IOException {
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 设置请求头
        String token = "your-token";
        String authorizationHeader = "Bearer " + token;

        // 发起GET请求
        HttpGet httpGet = new HttpGet("http://example.com/api/resource");
        httpGet.setHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
        HttpResponse getResponse = httpClient.execute(httpGet);
        printResponse(getResponse);

        // 发起POST请求
        HttpPost httpPost = new HttpPost("http://example.com/api/resource");
        httpPost.setHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
        httpPost.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
        String requestBody = "{"key": "value"}";
        httpPost.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
        HttpResponse postResponse = httpClient.execute(httpPost);
        printResponse(postResponse);

        // 发起DELETE请求
        HttpDelete httpDelete = new HttpDelete("http://example.com/api/resource/123");
        httpDelete.setHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
        HttpResponse deleteResponse = httpClient.execute(httpDelete);
        printResponse(deleteResponse);

        // 发起PUT请求
        HttpPut httpPut = new HttpPut("http://example.com/api/resource/123");
        httpPut.setHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
        httpPut.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
        String requestBody = "{"key": "value"}";
        httpPut.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
        HttpResponse putResponse = httpClient.execute(httpPut);
        printResponse(putResponse);

        // 关闭HttpClient
        httpClient.close();
    }

    private static void printResponse(HttpResponse response) throws IOException {
        System.out.println("Status Code: " + response.getStatusLine().getStatusCode());
        System.out.println("Response Body: " + EntityUtils.toString(response.getEntity()));
        // 可以根据需要处理响应的其他信息
    }
}
复制代码

并发执行:Apache HttpClient支持并发执行多个HTTP请求,通过使用连接池和线程池来提高并发性能。可以同时发送多个请求,并异步地获取它们的响应。

Apache HttpClient提供的异步执行机制来实现并发执行多个请求。通过将请求提交到线程池中,可以并发地发送请求,并通过回调或Future对象来获取每个请求的响应结果。

CloseableHttpAsyncClient httpClient = HttpAsyncClients.createDefault();

// 创建多个HttpGet请求
HttpGet request1 = new HttpGet("http://api.example.com/resource1");
HttpGet request2 = new HttpGet("http://api.example.com/resource2");
HttpGet request3 = new HttpGet("http://api.example.com/resource3");

// 提交请求到线程池,并异步执行
Future<HttpResponse> future1 = httpClient.execute(request1, null);
Future<HttpResponse> future2 = httpClient.execute(request2, null);
Future<HttpResponse> future3 = httpClient.execute(request3, null);

// 获取每个请求的响应结果
HttpResponse response1 = future1.get();
HttpResponse response2 = future2.get();
HttpResponse response3 = future3.get();

// 处理响应结果
// ...

// 关闭HttpClient
httpClient.close();
复制代码

有时候我们遇到一个场景:

下一个请求需要使用到上一个请求返回类的参数,虽然我们可以通过分两次请求,但是这样写代码不好看,并且不清晰。我们可以通过使用异步请求和回调函数的方式来处理。

CloseableHttpAsyncClient httpClient = HttpAsyncClients.createDefault();

// 第一个请求
HttpGet request1 = new HttpGet("http://api.example.com/resource1");
Future<HttpResponse> future1 = httpClient.execute(request1, null);

// 处理第一个请求的响应结果
future1.thenApply(response1 -> {
// 解析第一个请求的响应结果
String result1 = parseResponse(response1);

// 第二个请求,将第一个请求的结果作为参数
HttpGet request2 = new HttpGet("http://api.example.com/resource2?param=" + result1);
Future<HttpResponse> future2 = httpClient.execute(request2, null);

// 处理第二个请求的响应结果
future2.thenApply(response2 -> {
   // 解析第二个请求的响应结果
   String result2 = parseResponse(response2);

   // 执行下一步操作,使用第二个请求的结果
   performNextStep(result2);

   return null;
});

return null;
});

// 等待所有请求执行完成
CompletableFuture.allOf(future1).join();

// 关闭HttpClient
httpClient.close();
复制代码

实战:

import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//创建一个配置类 HttpClientConfig,用于配置 HttpClient 的相关参数:
@Configuration
public class HttpClientConfig {
    private static final int TIMEOUT = 5000; // 请求超时时间,单位:毫秒

    @Bean
    public CloseableHttpClient httpClient() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(TIMEOUT)
                .setConnectionRequestTimeout(TIMEOUT)
                .setSocketTimeout(TIMEOUT)
                .build();

        return HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .build();
    }
}

//创建一个使用 HttpClient 的服务类 HttpService,用于发起 HTTP 请求:
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class HttpService {
    private final CloseableHttpClient httpClient;

    @Autowired
    public HttpService(CloseableHttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public String get(String url) throws Exception {
        HttpGet httpGet = new HttpGet(url);
        HttpResponse response = httpClient.execute(httpGet);
        HttpEntity entity = response.getEntity();
        return EntityUtils.toString(entity);
    }
}

//需要使用 HttpClient 的地方,注入 HttpService 并调用其方法即可:
    
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {
    private final HttpService httpService;

    @Autowired
    public Application(HttpService httpService) {
        this.httpService = httpService;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        String response = httpService.get("http://api.example.com/resource");
        System.out.println("Response: " + response);
    }
}
复制代码

OkHttp

简洁的 API:OkHttp 提供了易于使用的 API,使发送 HTTP 请求变得简单和直观。可以使用链式调用的方式设置请求的 URL、方法、请求头、请求体等信息。

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class OkHttpExample {
    public static void main(String[] args) throws Exception {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("https://api.example.com/data")
                .get()
                .addHeader("Authorization", "Bearer token")
                .build();
        Response response = client.newCall(request).execute();
        String responseBody = response.body().string();
        System.out.println("Response: " + responseBody);
    }
}
//发送POST请求
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://api.example.com/users")
        .post(RequestBody.create(MediaType.parse("application/json"), requestBodyJson))
        .build();
Response response = client.newCall(request).execute();
//发送DELETE请求
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://api.example.com/users/123")
        .delete()
        .build();
Response response = client.newCall(request).execute();
//发送PUT请求:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://api.example.com/users/123")
        .put(RequestBody.create(MediaType.parse("application/json"), requestBodyJson))
        .build();
Response response = client.newCall(request).execute();
复制代码

高效性能:OkHttp 使用了连接池和复用连接的技术,可以有效地管理和重用连接,减少网络请求的延迟和资源消耗。它还支持异步请求和流式传输,以提高并发性能。

异步请求

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://api.example.com/data")
        .build();
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        // 处理响应结果
        String responseBody = response.body().string();
        System.out.println("Response: " + responseBody);
    }
    @Override
    public void onFailure(Call call, IOException e) {
        // 处理请求失败
        e.printStackTrace();
    }
});
复制代码

同步请求:

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://api.example.com/data")
        .build();
try (Response response = client.newCall(request).execute()) {
    // 处理响应结果
    String responseBody = response.body().string();
    System.out.println("Response: " + responseBody);
} catch (IOException e) {
    // 处理请求失败
    e.printStackTrace();
}
复制代码

文件下载:

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://example.com/file.pdf")
        .build();
try (Response response = client.newCall(request).execute()) {
    // 将响应结果写入文件
    InputStream inputStream = response.body().byteStream();
    FileOutputStream outputStream = new FileOutputStream("downloaded_file.pdf");
    byte[] buffer = new byte[4096];
    int bytesRead;
    while ((bytesRead = inputStream.read(buffer)) != -1) {
        outputStream.write(buffer, 0, bytesRead);
    }
    outputStream.close();
} catch (IOException e) {
    // 处理请求失败
    e.printStackTrace();
}
复制代码

请求和响应拦截器:OkHttp 提供了拦截器的机制,允许在发送请求和接收响应的过程中对其进行处理和修改。可以添加自定义的拦截器来实现身份验证、请求重试、日志记录等功能。

拦截器使用Interceptor接口进行定义,它包含了两个方法:intercept()chain.proceed()

记录请求和响应的日志:

class LoggingInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        long startTime = System.nanoTime();
        System.out.println("Sending request: " + request.url());
        Response response = chain.proceed(request);
        long endTime = System.nanoTime();
        System.out.println("Received response for " + request.url() + " in " + ((endTime - startTime) / 1e6) + " ms");
        return response;
    }
}
OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new LoggingInterceptor())
        .build();
复制代码

身份验证拦截器示例:

class AuthInterceptor implements Interceptor {
    private String authToken;
    public AuthInterceptor(String authToken) {
        this.authToken = authToken;
    }
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        // 添加身份验证的请求头
        Request authenticatedRequest = originalRequest.newBuilder()
                .header("Authorization", authToken)
                .build();
        return chain.proceed(authenticatedRequest);
    }
}
复制代码

请求重试拦截器示例:

class RetryInterceptor implements Interceptor {
    private int maxAttempts;
    private int currentAttempt = 0;
    public RetryInterceptor(int maxAttempts) {
        this.maxAttempts = maxAttempts;
    }
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = null;
        while (currentAttempt < maxAttempts) {
            try {
                response = chain.proceed(request);
                break;
            } catch (IOException e) {
                // 请求失败,进行重试
                currentAttempt++;
                if (currentAttempt >= maxAttempts) {
                    throw e; // 达到最大重试次数,抛出异常
                }
            }
        }
        return response;
    }
}
复制代码

支持同步和异步请求:OkHttp 提供了同步和异步两种方式发送请求。通过同步方式发送请求,可以直接获取到响应的结果;而通过异步方式发送请求,可以注册回调来处理响应的结果,这对于处理大量并发请求非常有用。

enqueue(Callback callback): 这是最常用的发送异步请求的方法。可以通过实现Callback接口来处理请求的响应结果和错误情况。

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://api.example.com/users")
        .build();
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        // 处理响应结果
        String responseBody = response.body().string();
        // ...
    }
    @Override
    public void onFailure(Call call, IOException e) {
        // 处理异常
        e.printStackTrace();
    }
});
复制代码

enqueue(Callback callback, Executor executor): 这个方法与上述方法类似,但是允许指定一个Executor来执行回调。这样可以在指定的线程池中处理回调,例如使用Executors.newCachedThreadPool()来创建线程池。

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://api.example.com/users")
        .build();
Executor executor = Executors.newCachedThreadPool();
client.newCall(request).enqueue(new Callback() {
    // ...
}, executor);
复制代码

newWebSocket(Request request, WebSocketListener listener): 如果需要进行WebSocket通信,可以使用此方法来创建一个WebSocket连接。需要实现WebSocketListener接口来处理WebSocket的事件和消息。

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("wss://api.example.com/socket")
        .build();
WebSocketListener listener = new WebSocketListener() {
    // ...
};
WebSocket webSocket = client.newWebSocket(request, listener);
复制代码

支持WebSocket:OkHttp 还支持 WebSocket 协议,可以使用 OkHttp 来创建 WebSocket 连接并进行双向通信。

使用OkHttp创建WebSocket连接:

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
    .url("ws://example.com/socket")
    .build();
WebSocket webSocket = client.newWebSocket(request, new WebSocketListener() {
    @Override
    public void onOpen(WebSocket webSocket, Response response) {
        // WebSocket连接已打开,可以进行通信
    }
    @Override
    public void onMessage(WebSocket webSocket, String text) {
        // 接收到WebSocket服务器发送的消息
    }
    @Override
    public void onClosed(WebSocket webSocket, int code, String reason) {
        // WebSocket连接已关闭
    }
    @Override
    public void onFailure(WebSocket webSocket, Throwable t, Response response) {
        // WebSocket连接出现异常
    }
});
// 发送文本消息
webSocket.send("Hello, server!");
// 关闭WebSocket连接
webSocket.close(1000, "Goodbye, server!");
复制代码

定制化和扩展性:OkHttp 提供了丰富的定制化选项,可以根据需要进行配置和扩展。可以自定义连接池、超时设置、缓存策略等,以满足特定的需求。

  1. 连接池定制化:可以通过 ConnectionPool 类设置连接池的最大连接数、保持时间等参数,以控制连接的复用和管理。例如:
ConnectionPool connectionPool = new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.MILLISECONDS);
OkHttpClient client = new OkHttpClient.Builder()
    .connectionPool(connectionPool)
    .build();
复制代码
  1. 超时设置:可以使用 OkHttpClient.Builder 中的方法来设置连接超时、读取超时和写入超时等参数。例如:
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)
    .build();
复制代码
  1. 缓存策略定制化:OkHttp 支持 HTTP 缓存,并提供了 CacheControl 类来设置缓存策略。可以根据具体需求设置缓存的有效期、条件等。例如:
CacheControl cacheControl = new CacheControl.Builder()
    .maxAge(1, TimeUnit.DAYS)
    .build();
Request request = new Request.Builder()
    .url("http://example.com")
    .cacheControl(cacheControl)
    .build();
复制代码
  1. 拦截器扩展:可以使用 Interceptor 接口来定义自定义的请求和响应拦截器,以对请求和响应进行处理和修改。例如,实现一个自定义的日志拦截器:
Interceptor loggingInterceptor = new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        long startTime = System.nanoTime();
        System.out.println("Sending request: " + request.url());
        Response response = chain.proceed(request);
        long endTime = System.nanoTime();
        long duration = endTime - startTime;
        System.out.println("Received response for: " + response.request().url() + " in " + duration + " nanoseconds");
        return response;
    }
};
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .build();
复制代码

Guess you like

Origin juejin.im/post/7229172559571402812