Back to Spring Boot Course

Spring Boot Course

Note: RestTemplate is in maintenance mode as of Spring Framework 5.0. It remains fully supported but no new features will be added. For new projects, prefer WebClient (reactive, non-blocking) or Spring Cloud OpenFeign (declarative HTTP clients). RestTemplate is still widely used in existing codebases and perfectly fine for synchronous, blocking use cases.

REST Consumer API :: RestTemplate

If you need to call remote REST services from your application, you can use the Spring Framework's RestTemplate class. Since RestTemplate instances often need to be customized before being used, Spring Boot does not provide any single auto-configured RestTemplate bean. It does, however, auto-configure a RestTemplateBuilder, which can be used to create RestTemplate instances when needed. The auto-configured RestTemplateBuilder ensures that sensible HttpMessageConverters are applied to RestTemplate instances.

RestTemplateBuilder includes a number of useful methods that can be used to quickly configure a RestTemplate. For example, to add BASIC auth support, you can use builder.basicAuthentication("user", "password").build() (note: basicAuthorization was deprecated; use basicAuthentication in Spring Boot 2.1+).

REST Consumer API :: WebClient

If you have Spring WebFlux on your classpath, you can also choose to use WebClient to call remote REST services. Compared to RestTemplate, this client has a more functional feel and is fully reactive. You can create your own client instance with the builder, WebClient.create().

Spring Boot creates and pre-configures such a builder for you. For example, client HTTP codecs are configured in the same fashion as the server ones.

There are three main approaches to WebClient customization, depending on how broadly you want the customizations to apply.

To make the scope of any customizations as narrow as possible, inject the auto-configured WebClient.Builder and then call its methods as required. WebClient.Builder instances are stateful: Any change on the builder is reflected in all clients subsequently created with it. If you want to create several clients with the same builder, you can also consider cloning the builder with WebClient.Builder other = builder.clone();.

To make an application-wide, additive customization to all WebClient.Builder instances, you can declare WebClientCustomizer beans and change the WebClient.Builder locally at the point of injection.

Finally, you can fall back to the original API and use WebClient.create(). In that case, no auto-configuration or WebClientCustomizer is applied.

Example

Lets develop Covid Service which calls external service https://api.rootnet.in/covid19-in/stats/latest using Rest Template and WebClient.

Include the below dependencies in pom.xml

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

SpringBoot Main class

@SpringBootApplication public class CovidAlertServiceApplication { public static void main(String[] args) { SpringApplication.run(CovidAlertServiceApplication.class); } @Bean public OpenAPI customOpenAPI(ApplicationConfig applicationConfig) { return new OpenAPI().components(new Components()) .info(new Info().title(applicationConfig.getOpenApiTitle()) .description(applicationConfig.getOpenApiDescription()) .contact(new Contact().name("Praveen Oruganti").email("praveenoruganti@gmail.com") .url("https://www.praveenoruganti.in")) .termsOfService("https://www.praveenoruganti.in") .license(new License().name("GNU General Public License v3.0").url("https://www.gnu.org/licenses"))); } @Bean RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } }

Controller

@RestController @Validated @Tag(description = "Covid Alert Stats", name = "Covid Alert Stats") @RequestMapping("/api/v1_0") @Slf4j public class CovidAlertController { private final CovidAlertService alertService; public CovidAlertController(CovidAlertService alertService) { this.alertService = alertService; } @GetMapping("/summary/state/{state}") @Operation(description = "Covid Alert by State", summary = "Covid Alert by State") public AlertStatusResponse getAlertAboutState(@PathVariable("state") @State String state) { log.info(Constants.INITAL_REQUEST, v("state", state)); AlertStatusResponse alertStatusResponse = alertService.getAlertAboutState(state); log.info(Constants.FINAL_RESPONSE, kv(Constants.RESPONSE, alertStatusResponse)); return alertStatusResponse; } @GetMapping("/summary") @Operation(description = "Covid Alert Overall Summary", summary = "Covid Alert Overall Summary") public SummaryDataResponse getOverAllSummary() { log.info(Constants.INITAL_REQUEST); SummaryDataResponse summaryDataResponse = alertService.getOverAllSummary(); log.info(Constants.FINAL_RESPONSE, kv(Constants.RESPONSE, summaryDataResponse)); return summaryDataResponse; } @GetMapping("/stats") @Operation(description = "Covid Alert Overall Stats", summary = "Covid Alert Overall Stats") public CountryDataResponse getOverAllStats() { log.info(Constants.INITAL_REQUEST); CountryDataResponse countryDataResponse = alertService.getOverAllStats(); log.info(Constants.FINAL_RESPONSE, kv(Constants.RESPONSE, countryDataResponse)); return countryDataResponse; } }

WebClient Configuration

@Configuration public class WebClientConfiguration { private ApplicationConfig appConfig; public WebClientConfiguration(ApplicationConfig appConfig) { this.appConfig = appConfig; } @Bean public WebClient covidStatsLookup() { return WebClient.builder().clientConnector(new ReactorClientHttpConnector(createHttpClient())) .baseUrl(appConfig.getCovidStatsLookupApiHost() + appConfig.getCovidStatsLookupApiEndpoint()) .build(); } private HttpClient createHttpClient() { return HttpClient.newConnection().resolver(DefaultAddressResolverGroup.INSTANCE) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, appConfig.getConnectionTimeout()).doOnConnected( connection -> connection.addHandlerLast(new ReadTimeoutHandler(appConfig.getReadTimeout(), TimeUnit.MILLISECONDS))); } }

Service

@Service public class CovidAlertService { private final CovidStatsLookupService covidStatsLookupService; public CovidAlertService(CovidStatsLookupService covidStatsLookupService) { this.covidStatsLookupService = covidStatsLookupService; } public AlertStatusResponse getAlertAboutState(String state) { AlertStatus alertStatus = new AlertStatus(); CovidApiData covidApiData = covidStatsLookupService.fetchStatsUsingRestTemplate(); StateData stateData = Arrays.stream(covidApiData.getData().getRegional()) .filter(e -> e.getLoc().equalsIgnoreCase(state)).findAny().orElse(new StateData()); alertStatus.setSummaryData(stateData); if (stateData.getTotalConfirmed() < 1000) { alertStatus.setAlertLevel("GREEN"); alertStatus.setMeasuresToBeTaken(Arrays.asList("Everything is Normal !!")); } else if (stateData.getTotalConfirmed() > 1000 && stateData.getTotalConfirmed() < 10000) { alertStatus.setAlertLevel("ORANGE"); alertStatus.setMeasuresToBeTaken(Arrays.asList("Only Essential services are allowed", "List of services that come under essential service")); } else { alertStatus.setAlertLevel("RED"); alertStatus.setMeasuresToBeTaken( Arrays.asList("Absolute lock-down", "Only Medical and food services are allowed here")); } return AlertStatusResponse.builder().status("Success").data(alertStatus).build(); } public SummaryDataResponse getOverAllSummary() { CovidApiData covidApiData = covidStatsLookupService.fetchStatsUsingWebClient(); SummaryData summaryData = covidApiData.getData().getSummary(); summaryData.setUpdateTime(covidApiData.getLastRefreshed()); return SummaryDataResponse.builder().status("Success").data(summaryData).build(); } public CountryDataResponse getOverAllStats() { CovidApiData covidApiData = covidStatsLookupService.fetchStatsUsingWebClient(); CountryData countryData = covidApiData.getData(); countryData.getSummary().setUpdateTime(covidApiData.getLastRefreshed()); return CountryDataResponse.builder().status("Success").data(countryData).build(); } }
@Service @Slf4j public class CovidStatsLookupService { private final ApplicationConfig appConfig; private final WebClient covidStatsLookup; private final RestTemplate restTemplate; public CovidStatsLookupService(ApplicationConfig appConfig, WebClient covidStatsLookup, RestTemplate restTemplate) { this.appConfig = appConfig; this.covidStatsLookup = covidStatsLookup; this.restTemplate = restTemplate; } public CovidApiData fetchStatsUsingWebClient() { log.info("Covid Stats Lookup Service Request", v("Host", appConfig.getCovidStatsLookupApiHost()), v("Path", appConfig.getCovidStatsLookupApiEndpoint())); CovidApiData covidApiData = null; try { StopWatch stopWatch = new StopWatch("Covid Stats Lookup Service Call"); stopWatch.start(); covidApiData = covidStatsLookup.get().retrieve().bodyToMono(CovidApiData.class).retryWhen( Retry.backoff(appConfig.getRetryAttempts(), Duration.ofMillis(appConfig.getBackOffTimeInMillisec())) .filter(throwable -> isConnectionTimeout(throwable))) .block(); stopWatch.stop(); log.info("Covid Stats Lookup Service Execution Times", v("Host", appConfig.getCovidStatsLookupApiHost()), v("Path", appConfig.getCovidStatsLookupApiEndpoint()), v("TotalTimeTaken", stopWatch.getTotalTimeMillis())); } catch (WebClientResponseException webClientResponseException) { log.error("Covid Stats Lookup Failure Response", v("webClientResponse", webClientResponseException.getResponseBodyAsString())); throw new ExternalServiceException("covidStatsLookupException-" + webClientResponseException.getMessage()); } catch (Exception exception) { log.error("Covid Stats Lookup Failure Response", v("webClientResponse", exception.getMessage())); throw new ExternalServiceException("covidStatsLookupException-" + exception.getMessage()); } return covidApiData; } public CovidApiData fetchStatsUsingRestTemplate() { log.info("Covid Stats Lookup Service Request", v("Host", appConfig.getCovidStatsLookupApiHost()), v("Path", appConfig.getCovidStatsLookupApiEndpoint())); CovidApiData covidApiData = null; try { StopWatch stopWatch = new StopWatch("Covid Stats Lookup Service Call"); stopWatch.start(); covidApiData = restTemplate.getForObject( appConfig.getCovidStatsLookupApiHost() + appConfig.getCovidStatsLookupApiEndpoint(), CovidApiData.class); stopWatch.stop(); log.info("Covid Stats Lookup Service Execution Times", v("Host", appConfig.getCovidStatsLookupApiHost()), v("Path", appConfig.getCovidStatsLookupApiEndpoint()), v("TotalTimeTaken", stopWatch.getTotalTimeMillis())); } catch (Exception exception) { log.error("Covid Stats Lookup Failure Response", v("webClientResponse", exception.getMessage())); throw new ExternalServiceException("covidStatsLookupException-" + exception.getMessage()); } return covidApiData; } private boolean isConnectionTimeout(Throwable throwable) { boolean isConnectionTimeout = false; boolean isUnknownHost = false; if (throwable instanceof WebClientRequestException) { WebClientRequestException excep = (WebClientRequestException) throwable; if (excep.contains(ConnectTimeoutException.class)) { isConnectionTimeout = true; } else if (excep.contains(UnknownHostException.class)) { isUnknownHost = true; } } else { isConnectionTimeout = throwable instanceof ConnectTimeoutException; isUnknownHost = throwable instanceof UnknownHostException; } return isConnectionTimeout || isUnknownHost; } }