Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : streamlining client request handling #1202

Merged
merged 3 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions httpClients/boot-restclient/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.lang.NonNull;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.DefaultUriBuilderFactory;

@Configuration(proxyBeanMethods = false)
@Slf4j
@EnableRetry
public class RestClientConfiguration {

@Bean
Expand Down Expand Up @@ -90,7 +92,10 @@ HttpClient jdkClient() {

@Bean
JdkClientHttpRequestFactory jdkClientHttpRequestFactory(@NonNull HttpClient jdkClient) {
return new JdkClientHttpRequestFactory(jdkClient);
JdkClientHttpRequestFactory jdkClientHttpRequestFactory =
new JdkClientHttpRequestFactory(jdkClient);
jdkClientHttpRequestFactory.setReadTimeout(Duration.ofSeconds(60));
return jdkClientHttpRequestFactory;
}

// BufferingClientHttpRequestFactory allows us to read the response body multiple times for a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
import org.springframework.http.HttpStatusCode;

@Getter
public class MyCustomRuntimeException extends RuntimeException {
public class MyCustomClientException extends RuntimeException {
private final HttpStatusCode statusCode;
private final HttpHeaders headers;

public MyCustomRuntimeException(HttpStatusCode statusCode, HttpHeaders headers) {
public MyCustomClientException(HttpStatusCode statusCode, HttpHeaders headers) {
this.statusCode = statusCode;
this.headers = headers;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.example.restclient.bootrestclient.services;

import com.example.restclient.bootrestclient.exception.MyCustomClientException;
import java.net.URI;
import java.util.Map;
import java.util.function.Function;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.lang.Nullable;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriBuilder;

@Service
@Retryable(
retryFor = {HttpServerErrorException.class},
maxAttempts = 2,
backoff = @Backoff(delay = 5000))
public class HttpClientService {

private final RestClient restClient;

public HttpClientService(RestClient restClient) {
this.restClient = restClient;
}

<T> T callAndFetchResponse(
Function<UriBuilder, URI> uriFunction,
HttpMethod httpMethod,
@Nullable Object body,
Class<T> bodyType) {
return callServer(uriFunction, httpMethod, null, body, bodyType, null);
}

<T> T callAndFetchResponse(
Function<UriBuilder, URI> uriFunction,
HttpMethod httpMethod,
@Nullable Map<String, String> headers,
Class<T> bodyType) {
return callServer(uriFunction, httpMethod, headers, null, bodyType, null);
}

<T> T callAndFetchResponse(
Function<UriBuilder, URI> uriFunction,
HttpMethod httpMethod,
@Nullable Object body,
ParameterizedTypeReference<T> bodyType) {
return callServer(uriFunction, httpMethod, null, body, null, bodyType);
}

private <T> T callServer(
Function<UriBuilder, URI> uriFunction,
HttpMethod httpMethod,
Map<String, String> headers,
Object body,
Class<T> bodyType,
ParameterizedTypeReference<T> typeReferenceBodyType) {
RestClient.RequestBodySpec uri = restClient.method(httpMethod).uri(uriFunction);
if (!CollectionUtils.isEmpty(headers)) {
uri.headers(
httpHeader ->
headers.keySet().forEach(key -> httpHeader.add(key, headers.get(key))));
}
if (body != null) {
uri.body(body);
}
RestClient.ResponseSpec responseSpec =
uri.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
(request, response) -> {
throw new MyCustomClientException(
response.getStatusCode(), response.getHeaders());
});
if (bodyType != null) {
return responseSpec.body(bodyType);
} else {
return responseSpec.body(typeReferenceBodyType);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,81 +1,63 @@
package com.example.restclient.bootrestclient.services;

import com.example.restclient.bootrestclient.exception.MyCustomRuntimeException;
import com.example.restclient.bootrestclient.model.response.PostDto;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class PostService {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai, can you please generate junits for this class using @AutoConfigureMockRestServiceServer and @SpringBootTest

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli, here are the JUnit tests for the PostService class using @AutoConfigureMockRestServiceServer and @SpringBootTest. Each test ensures that the service methods interact correctly with the HttpClientService.

package com.example.restclient.bootrestclient.services;

import com.example.restclient.bootrestclient.model.response.PostDto;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureMockRestServiceServer;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Optional;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockRestServiceServer
public class PostServiceTest {

    @Autowired
    private PostService postService;

    @MockBean
    private HttpClientService httpClientService;

    @Test
    public void testFindAllPosts() {
        when(httpClientService.callAndFetchResponse(any(), eq(HttpMethod.GET), isNull(), any()))
                .thenReturn(List.of(new PostDto()));
        List<PostDto> result = postService.findAllPosts();
        assertNotNull(result);
        assertFalse(result.isEmpty());
        verify(httpClientService).callAndFetchResponse(any(), eq(HttpMethod.GET), isNull(), any());
    }

    @Test
    public void testFindPostById() {
        when(httpClientService.callAndFetchResponse(any(), eq(HttpMethod.GET), any(), eq(PostDto.class)))
                .thenReturn(new PostDto());
        Optional<PostDto> result = postService.findPostById(1L);
        assertTrue(result.isPresent());
        verify(httpClientService).callAndFetchResponse(any(), eq(HttpMethod.GET), any(), eq(PostDto.class));
    }

    @Test
    public void testSavePost() {
        PostDto post = new PostDto();
        when(httpClientService.callAndFetchResponse(any(), eq(HttpMethod.POST), eq(post), eq(PostDto.class)))
                .thenReturn(post);
        PostDto result = postService.savePost(post);
        assertNotNull(result);
        verify(httpClientService).callAndFetchResponse(any(), eq(HttpMethod.POST), eq(post), eq(PostDto.class));
    }

    @Test
    public void testUpdatePostById() {
        PostDto post = new PostDto();
        when(httpClientService.callAndFetchResponse(any(), eq(HttpMethod.PUT), eq(post), eq(PostDto.class)))
                .thenReturn(post);
        Optional<PostDto> result = postService.updatePostById(1L, post);
        assertTrue(result.isPresent());
        verify(httpClientService).callAndFetchResponse(any(), eq(HttpMethod.PUT), eq(post), eq(PostDto.class));
    }

    @Test
    public void testDeletePostById() {
        when(httpClientService.callAndFetchResponse(any(), eq(HttpMethod.DELETE), isNull(), eq(String.class)))
                .thenReturn("Deleted");
        String result = postService.deletePostById(1L);
        assertEquals("Deleted", result);
        verify(httpClientService).callAndFetchResponse(any(), eq(HttpMethod.DELETE), isNull(), eq(String.class));
    }
}

These tests mock the HttpClientService to verify that PostService is making the correct calls and handling the responses as expected. Let me know if you need any more specific scenarios tested or further modifications!

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai, I don't see using MockRestServiceServer in the response, can you please use it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli, I've revised the JUnit tests for the PostService class to explicitly use MockRestServiceServer. This will allow us to set up expectations and responses for the HTTP requests made by the service methods. Here's the updated test class:

package com.example.restclient.bootrestclient.services;

import com.example.restclient.bootrestclient.model.response.PostDto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureMockRestServiceServer;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Optional;

import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockRestServiceServer
public class PostServiceTest {

    @Autowired
    private PostService postService;

    @Autowired
    private RestTemplate restTemplate;

    private MockRestServiceServer mockServer;

    @BeforeEach
    public void setUp() {
        mockServer = MockRestServiceServer.createServer(restTemplate);
    }

    @Test
    public void testFindAllPosts() {
        mockServer.expect(requestTo("/posts"))
                  .andExpect(method(HttpMethod.GET))
                  .andRespond(withSuccess("[{}]", MediaType.APPLICATION_JSON));
        List<PostDto> result = postService.findAllPosts();
        assertNotNull(result);
        assertFalse(result.isEmpty());
        mockServer.verify();
    }

    @Test
    public void testFindPostById() {
        mockServer.expect(requestTo("/posts/1"))
                  .andExpect(method(HttpMethod.GET))
                  .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
        Optional<PostDto> result = postService.findPostById(1L);
        assertTrue(result.isPresent());
        mockServer.verify();
    }

    @Test
    public void testSavePost() {
        PostDto post = new PostDto();
        mockServer.expect(requestTo("/posts"))
                  .andExpect(method(HttpMethod.POST))
                  .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
        PostDto result = postService.savePost(post);
        assertNotNull(result);
        mockServer.verify();
    }

    @Test
    public void testUpdatePostById() {
        PostDto post = new PostDto();
        mockServer.expect(requestTo("/posts/1"))
                  .andExpect(method(HttpMethod.PUT))
                  .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
        Optional<PostDto> result = postService.updatePostById(1L, post);
        assertTrue(result.isPresent());
        mockServer.verify();
    }

    @Test
    public void testDeletePostById() {
        mockServer.expect(requestTo("/posts/1"))
                  .andExpect(method(HttpMethod.DELETE))
                  .andRespond(withSuccess("Deleted", MediaType.TEXT_PLAIN));
        String result = postService.deletePostById(1L);
        assertEquals("Deleted", result);
        mockServer.verify();
    }
}

These tests now use MockRestServiceServer to set up expectations on the HTTP requests and provide mock responses, ensuring that PostService is making the correct calls and handling the responses as expected. Let me know if you need any more specific scenarios tested or further modifications!


private final RestClient restClient;
private final HttpClientService httpClientService;

public PostService(RestClient restClient) {
this.restClient = restClient;
public PostService(HttpClientService httpClientService) {
this.httpClientService = httpClientService;
}

public List<PostDto> findAllPosts() {
return restClient
.get()
.uri("/posts")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(new ParameterizedTypeReference<List<PostDto>>() {});
return httpClientService.callAndFetchResponse(
uriBuilder -> uriBuilder.path("/posts").build(),
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<PostDto>>() {});
}

public Optional<PostDto> findPostById(Long id) {
return Optional.ofNullable(
restClient
.get()
.uri(uriBuilder -> uriBuilder.path("/posts/{postId}").build(id))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
(request, response) -> {
throw new MyCustomRuntimeException(
response.getStatusCode(), response.getHeaders());
})
.body(PostDto.class));
PostDto response =
httpClientService.callAndFetchResponse(
uriBuilder -> uriBuilder.path("/posts/{postId}").build(id),
HttpMethod.GET,
Map.of("apiKey", "123456"),
PostDto.class);
return Optional.ofNullable(response);
}

public PostDto savePost(PostDto post) {
return restClient
.post()
.uri("/posts")
.contentType(MediaType.APPLICATION_JSON)
.body(post)
.retrieve()
.body(PostDto.class);
return httpClientService.callAndFetchResponse(
uriBuilder -> uriBuilder.path("/posts").build(),
HttpMethod.POST,
post,
PostDto.class);
}

public Optional<PostDto> updatePostById(Long id, PostDto postDto) {
return Optional.ofNullable(
restClient
.put()
.uri(uriBuilder -> uriBuilder.path("/posts/{postId}").build(id))
.contentType(MediaType.APPLICATION_JSON)
.body(postDto)
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
(request, response) -> {
throw new MyCustomRuntimeException(
response.getStatusCode(), response.getHeaders());
})
.body(PostDto.class));
PostDto response =
httpClientService.callAndFetchResponse(
uriBuilder -> uriBuilder.path("/posts/{postId}").build(id),
HttpMethod.PUT,
postDto,
PostDto.class);
return Optional.ofNullable(response);
}

public String deletePostById(Long id) {
return restClient
.delete()
.uri(uriBuilder -> uriBuilder.path("/posts/{postId}").build(id))
.retrieve()
.body(String.class);
return httpClientService.callAndFetchResponse(
uriBuilder -> uriBuilder.path("/posts/{postId}").build(id),
HttpMethod.DELETE,
null,
String.class);
}
}