DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Integrate Spring With Open AI
  • Upgrade Guide To Spring Boot 3.0 for Spring Data JPA and Querydsl
  • Spring Boot: User Login API Test Client Using Rest Assured
  • Spring Boot Delete User Details API Test Client Using Rest Assured | API Testing Using Rest Assured

Trending

  • AI Agents: A New Era for Integration Professionals
  • Useful System Table Queries in Relational Databases
  • System Coexistence: Bridging Legacy and Modern Architecture
  • Simpler Data Transfer Objects With Java Records
  1. DZone
  2. Coding
  3. Frameworks
  4. How Spring Boot Starters Integrate With Your Project

How Spring Boot Starters Integrate With Your Project

The article provides an overview of Spring Boot starters, which are pre-packaged sets of dependencies that simplify adding functionality to Spring Boot applications.

By 
Taras Ivashchuk user avatar
Taras Ivashchuk
·
Feb. 05, 25 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
4.6K Views

Join the DZone community and get the full member experience.

Join For Free

When developers set up and integrate services, they often face challenges that can take up a lot of time. Starters help simplify this process by organizing code and making it easier to manage. Let's take a look at creating two starters, configuring their settings automatically, and using them in a service.

So, what are Spring Boot Starters, exactly? What benefits do they provide?

A cartoon about Spring Boot project


Spring Boot Starters are like packages that streamline the process of incorporating libraries and components into Spring projects, making it simpler and more efficient to manage dependencies while cutting down development time significantly.

Benefits of Using Spring Boot Starter

Integration of Libraries

  • Starters include all the dependencies needed for technologies. For example spring-boot-starter-web provides everything for building web applications, while spring-boot-starter-data-jpa helps with JPA database work.
  • By adding these starters to a project, developers can start working with the desired technology without worrying about compatibility issues or version differences.

Focus on Business Logic

  • Developers can concentrate on creating business logic for dealing with infrastructure code.
  • This approach speeds up development and feature deployment, ultimately boosting team productivity.

Using Configurations

  • Using predefined setups helps ensure consistency in setting up and organizing projects, making it easier to maintain and advance code. Moreover, it aids in onboarding team members to the project by offering a code structure and setup.

Project Enhancements

  • Furthermore, using starters that include known libraries simplifies updating dependencies and integrating Spring Boot versions.
  • The support from the Spring team community linked with these starters also guarantees to resolve any questions or obstacles that might come up during development.

Task Description

In this article, we will address the issue of consolidating data from sources such as REST and GraphQL services. This problem is often encountered in projects with microservice architecture, where it is necessary to combine data coming from different services.

When it comes to solutions in a microservices setup, it’s possible to establish microservices for each integration. This approach is justifiable when the integration is extensive, and there are resources for its maintenance. However, in scenarios like working with a monolith or lacking the resources for multiple microservices support, opting for starters could be more practical.

The rationale behind selecting a library starter includes:

  • Business logic segmentation. Starters facilitate the separation of business logic and integration configuration.
  • Following the SOLID principles. Breaking down functionality into modules aligns with principles enhancing code maintainability and scalability.
  • Simplified setup. Starters streamline the process of configuring services by minimizing the required amount of configuration code.
  • Ease of use. Integrating a service becomes more straightforward by adding a dependency and configuring essential parameters.

Our Scenario

The scenario


Let's illustrate the solution with an example involving a tour aggregator that gathers data from tour operators and merges them. To start off, we will develop two starters (tour-operator-one-starter and tour-operator-two-starter) both of which will use a shared module (common-model) containing fundamental models and interfaces. These starter libraries will connect to the aggregator service (tour-aggregator).

Creating tour-operator-one-starter

Starter is designed to integrate with the tour operator and fetch data via the REST API.

All official starters use the naming scheme spring-boot-starter-*, where * denotes a specific type of application. Third-party starters should not start with spring-boot as it is reserved for official starters from the Spring team.

Typically, third-party starters begin with the project name. For example, my starter will be named tour-operator-one-spring-boot-starter.

1. Create pom.xml

Add dependencies.

XML
 
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.common.model</groupId>
            <artifactId>common-model</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


2. Create TourOperatorOneProperties

These are the properties we will set in tour-aggregator to configure our starter.

XML
 
@ConfigurationProperties(prefix = "tour-operator.one.service")
public class TourOperatorOneProperties {

    private final Boolean enabled;
    private final String url;
    private final Credentials credentials;

    public TourOperatorOneProperties(
            Boolean enabled,
            String url,
            Credentials credentials) {
        this.enabled = enabled;
        this.url = url;
        this.credentials = credentials;
    }

    //getters

    public static class Credentials {
        private final String username;
        private final String password;

        public Credentials(String username, String password) {
            this.username = username;
            this.password = password;
        }

        //getters
    }
}


3. Create TourOperatorOneAutoConfiguration

  • @AutoConfiguration – indicates that this class is a configuration class for Spring Boot auto-configuration.
  • @ConditionalOnProperty – activates the configuration if the property tour-operator.one.service.enabled is set to true. If the property is missing, the configuration is also activated due to matchIfMissing = true.
  • @EnableConfigurationProperties(TourOperatorOneProperties.class) – enables support for @ConfigurationProperties annotations for the TourOperatorOneProperties class.
XML
 
@AutoConfiguration
@ConditionalOnProperty(prefix = "tour-operator.one.service", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(TourOperatorOneProperties.class)
public class TourOperatorOneAutoconfiguration {

    private static final Logger log = LoggerFactory.getLogger(TourOperatorOneAutoconfiguration.class);
    private final TourOperatorOneProperties properties;

    public TourOperatorOneAutoconfiguration(TourOperatorOneProperties properties) {
        this.properties = properties;
    }

    @Bean("operatorOneRestClient")
    public RestClient restClient(RestClient.Builder builder) {
        log.info("Configuration operatorRestClient: {}", properties);
        return builder
                .baseUrl(properties.getUrl())
                .defaultHeaders(httpHeaders -> {
                    if (null != properties.getCredentials()) {
                        httpHeaders.setBasicAuth(
                                properties.getCredentials().getUsername(),
                                properties.getCredentials().getPassword());
                    }
                })
                .build();
    }

    @Bean("tourOperatorOneService")
    public TourOperatorOneServiceImpl tourOperatorService(TourOperatorOneProperties properties,
                                                          @Qualifier("operatorOneRestClient") RestClient restClient) {
        log.info("Configuration tourOperatorService: {} and restClient: {}", properties, restClient);
        return new TourOperatorOneServiceImpl(restClient);
    }
}


In this example, I use @ConditionalOnProperty, but there are many other conditional annotations:

  1. @ConditionalOnBean – generates a bean when a specified bean exists in the BeanFactory
  2. @ConditionalOnMissingBean – facilitates creating a bean if a particular bean is not found in the BeanFactory
  3. @ConditionalOnClass – produces a bean when a specific class is present, in the classpath
  4. @ConditionalOnMissingClass – acts oppositely to @ConditionalOnClass

You should choose what suits your needs best. You can learn more about conditional annotations here.

4. Create TourOperatorOneServiceImpl

In this class, we implement the base interface and lay down the main business logic for retrieving data from the first tour operator and standardizing it according to the common interface.

Plain Text
 
public class TourOperatorOneServiceImpl implements TourOperatorService {

    private final RestClient restClient;

    public TourOperatorOneServiceImpl(@Qualifier("operatorOneRestClient") RestClient restClient) {
        this.restClient = restClient;
    }

    @Override
    public TourOperatorResponse makeRequest(TourOperatorRequest request) {
        var tourRequest = mapToOperatorRequest(request); // transformation of our request into the one that the tour operator will understand

        var responseList = restClient
                .post()
                .body(tourRequest)
                .retrieve()
                .toEntity(new ParameterizedTypeReference<List<TourProposition>>() {
                });

        return TourOperatorResponse.builder()
                .deals(responseList
                        .getBody()
                        .stream()
                        .map(ModelUtils::mapToCommonModel)
                        .toList())
                .build();
    }
}


5. Create Auto-Configuration File

To register auto-configurations, we create the file resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports .

Plain Text
 
com.tour.operator.one.autoconfiguration.TourOperatorOneAutoConfiguration


This file contains a collection of configurations. In my scenario, one configuration is listed. If you have multiple configurations, make sure that each configuration is listed on a separate line.

By creating this file, you are informing Spring Boot that it should load and utilize the TourOperatorOneAutoConfiguration class for setup when certain conditions specified by the @ConditionalOnProperty annotation are satisfied.

Thus, we have established the setup for collaborating with the tour operator by developing configuration classes and beans and leveraging properties.

Creating tour-operator-two-starter 

Up is creating tour-operator-two-starter a kit designed to integrate with the second tour operator and retrieve data from a GraphQL server through a straightforward HTTP request.

Let's proceed with the process used for tour-operator-one-starter.

1. Create pom.xml

Add dependencies.

XML
 
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.common.model</groupId>
            <artifactId>common-model</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


2. Create TourOperatorTwoProperties

These are the properties we will set in tour-aggregator to configure our starter.

@ConfigurationProperties(prefix = "tour-operator.two.service")
public class TourOperatorTwoProperties {

    private final Boolean enabled;
    private final String url;
    private final String apiKey;

    public TourOperatorTwoProperties(
            Boolean enabled,
            String url,
            String apiKey) {
        this.enabled = enabled;
        this.url = url;
        this.apiKey = apiKey;
    }

    //getters
}


2. Create TourOperatorOneAutoConfiguration

Java
 
@AutoConfiguration
@ConditionalOnProperty(prefix = "tour-operator.two.service", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(TourOperatorTwoProperties.class)
public class TourOperatorTwoAutoconfiguration {

    private static final Logger log = LoggerFactory.getLogger(TourOperatorTwoAutoconfiguration.class);
    private final TourOperatorTwoProperties properties;

    public TourOperatorTwoAutoconfiguration(TourOperatorTwoProperties properties) {
        log.info("Configuration with: {}", properties);
        this.properties = properties;
    }

    @Bean("operatorTwoRestClient")
    public RestClient restClient(RestClient.Builder builder) {
        log.info("Configuration operatorRestClient: {}", properties);
        return builder
                .baseUrl(properties.getUrl())
                .defaultHeaders(httpHeaders -> {
                    httpHeaders.set("X-Api-Key", properties.getApiKey());
                })
                .build();
    }

    @Bean("tourOperatorTwoService")
    public TourOperatorTwoServiceImpl tourOperatorService(TourOperatorTwoProperties properties,
                                                          @Qualifier("operatorTwoRestClient") RestClient restClient) {
        log.info("Configuration tourOperatorService: {} and restClient: {}", properties, restClient);
        return new TourOperatorTwoServiceImpl(restClient);
    }
}


3. Create TourOperatorOneServiceImpl

Receiving data from the second tour operator.

Java
 
public class TourOperatorTwoServiceImpl implements TourOperatorService {

    private static final String QUERY =
            """
                    query makeTourRequest($request: TourOperatorRequest) {
                                      makeTourRequest(request: $request) {
                                        id
                                        startDate
                                        endDate
                                        price
                                        currency
                                        days
                                        hotel {
                                          hotelName
                                          hotelRating
                                          countryCode
                                        }
                                      }
                                    }
                    """;

    private final RestClient restClient;

    public TourOperatorTwoServiceImpl(@Qualifier("operatorTwoRestClient") RestClient restClient) {
        this.restClient = restClient;
    }

    @Override
    public TourOperatorResponse makeRequest(TourOperatorRequest request) {
        var tourRequest = mapToOperatorRequest(request);
        var variables = Map.ofEntries(Map.entry("request", tourRequest));
        var requestBody = Map.ofEntries(
                Map.entry("query", QUERY),
                Map.entry("variables", variables));

        var response = restClient
                .post()
                .body(requestBody)
                .retrieve()
                .toEntity(QueryResponse.class);

        return TourOperatorResponse.builder()
                .deals(response.getBody()
                        .data()
                        .makeTourRequest()
                        .stream()
                        .map(ModelUtils::mapToCommonModel).toList())
                .build();
    }
}


Create Auto-Configuration File

Create the file resources/META-INF/spring/org.springframework.boot. autoconfigure.AutoConfiguration.imports.

Plain Text
 
com.tour.operator.two.autoconfiguration.TourOperatorTwoAutoconfiguration


Creating and Using the Aggregator Service

An aggregator service is designed to gather data from tour operators. This involves linking starters, configuring parameters, and using beans with a shared interface.

1. Connect Starter Libraries

Include dependencies for the two libraries in the pom.xml.

XML
 
<dependencies>
    ...
        <dependency>
            <groupId>com.tour.operator</groupId>
            <artifactId>tour-operator-one-spring-boot-starter</artifactId>
            <version>0.0.2-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.tour.operator</groupId>
            <artifactId>tour-operator-two-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
       ...
    </dependencies>


Configure Parameters in application.yaml

Specify the necessary data, such as URLs and connection parameters, in the application.yaml.

YAML
 
spring:
  application:
    name: tour-aggregator
tour-operator:
  one:
    service:
      enabled: true
      url: http://localhost:8090/api/tours
      credentials:
        username: user123
        password: pass123
  two:
    service:
      enabled: true
      url: http://localhost:8091/graphql
      api-key: 11d1de45-5743-4b58-9e08-f6038fe05c8f


Use Services

We use the established beans, which implement the TourOperatorService interface within the TourServiceImpl class. This class outlines the process of retrieving and aggregating data from various tour operators.

Java
 
@Service
public class TourServiceImpl implements TourService {

    private static final Logger log = LoggerFactory.getLogger(TourServiceImpl.class);
    private final List<TourOperatorService> tourOperatorServices;
    private final Executor tourOperatorExecutor;
    private final Integer responseTimeout;

    public TourServiceImpl(List<TourOperatorService> tourOperatorServices,
                           @Qualifier("tourOperatorTaskExecutor") Executor tourOperatorExecutor,
                           @Value("${app.response-timeout:5}") Integer responseTimeout) {
        this.tourOperatorServices = tourOperatorServices;
        this.tourOperatorExecutor = tourOperatorExecutor;
        this.responseTimeout = responseTimeout;

    }


    public List<TourOffer> getTourOffers(@RequestBody TourOperatorRequest request) {
        log.info("Send request: {}", request);

        var futures = tourOperatorServices.stream()
                .map(tourOperator -> CompletableFuture.supplyAsync(() -> tourOperator.makeRequest(request), tourOperatorExecutor)
                        .orTimeout(responseTimeout, TimeUnit.SECONDS)
                        .exceptionally(ex -> TourOperatorResponse.builder().deals(List.of()).build())
                )
                .toList();

        var response = futures.stream()
                .map(CompletableFuture::join)
                .map(TourOperatorResponse::getDeals)
                .filter(Objects::nonNull)
                .flatMap(List::stream)
                .toList();

        return response;
    }
}


Allocating Resources for Calls

It’s good practice to allocate separate resources for calls, allowing better thread management and performance optimization.

Java
 
@Configuration
public class ThreadPoolConfig {

    private final Integer threadCount;

    public ThreadPoolConfig(@Value("${app.thread-count:5}") Integer threadCount) {
        this.threadCount = threadCount;
    }

    @Bean(name = "tourOperatorTaskExecutor")
    public Executor tourOperatorTaskExecutor() {
        return Executors.newFixedThreadPool(threadCount);
    }
}


This code ensures efficient management of asynchronous tasks and helps avoid blocking the main thread, thereby improving overall system performance.

Conclusion

In this article, we’ve created two starters for reaching out to tour operators through REST and GraphQL technology interfaces. These steps include all the configurations and elements to simplify their usage. Afterward, we merged them into a system that communicates with them in an asynchronous manner and aggregates data.

This approach solved several problems:

  • Simplified integration and setup. By using auto-configuration and properties of coding, we saved time during development.
  • Improved flexibility and usability. Separating functions into starters improved code structure and simplified maintenance.
  • System flexibility. We can easily add new integrations without breaking the existing logic.

Now, our system is better equipped to adapt and scale effortlessly while being easier to manage, leading to enhancements in its architecture and performance.

Here’s the full code.

I appreciate you reading this article. I look forward to hearing your thoughts and feedback!

API Spring Framework Data (computing) Spring Boot

Opinions expressed by DZone contributors are their own.

Related

  • Integrate Spring With Open AI
  • Upgrade Guide To Spring Boot 3.0 for Spring Data JPA and Querydsl
  • Spring Boot: User Login API Test Client Using Rest Assured
  • Spring Boot Delete User Details API Test Client Using Rest Assured | API Testing Using Rest Assured

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!