Tutorial für Custom Spring Boot Starter

Eine kurze Anleitung für einen Custom Spring Boot Starter für Spring Boot 3.2.

Spring Boot Starter

Spring Boot Starter sind Abhängigkeiten die in das Projekt eingebunden werden. Der große Vorteil ist, dass diese Abhängigkeiten mit einer starken Standardkonfiguration kommen, gleichzeitig aber sehr flexibel und konfigurierbar bleiben.

Custom Spring Boot Starter

Beispielhaft wurde das Pattern auf eine fiktive Bibliothek angewendet. Bei der Bibliothek handelt es sich um eine Applikation, die für Produkte (bspw. im E-Commerce-Kontext) Preise und Bestände über eine REST-Schnittstelle anlegen und persistieren kann. Die Bibliothek bietet für die Implementierung der Persistierung eine H2-Datenbank (in-memory) an.

In dem Guide gehe ich nur auf die für das Verständnis und den Spring Boot Starter wesentlichsten Teile ein. Das vollständige Beispiel findet ihr auf Gitlab.

Die Applikation wird dabei in die 3 Module app, autoconfigure und starter unterteilt:

productlistservice-app

In der App liegt die sowohl die fachliche Logik als auch angebotene Standard-Implementierung. Die wichtigsten Klassen sind hier ProductServiceController, der die Anfragen über eine REST-API entgegennimmt, das Interface ProductPersistenceService, das eine Schnittstelle zum Hinzufügen und Auslesen von Preisen und Beständen liefert sowie die in-memory Standard-Implementierung dieser Schnittstelle H2ProductPersistenceService:

@RestController
public class ProductServiceController {

    private final ProductPersistenceService productPersistenceService;

    public ProductServiceController(ProductPersistenceService productPersistenceService) {
        this.productPersistenceService = productPersistenceService;
    }

    @GetMapping("/stocks")
    public List<Stock> getStocks() {
        return productPersistenceService.getStocks();
    }

    @GetMapping("/stocks/{articleId}")
    public Stock getStock(@PathVariable String articleId) {
        return productPersistenceService.getStock(articleId);
    }

    @PostMapping("/stocks/add")
    @ResponseStatus(value = HttpStatus.NO_CONTENT)
    public void addStock(@RequestBody Stock stock) {
        productPersistenceService.saveStock(stock);
    }

    //Ähnlich für Preise

}
public interface ProductPersistenceService {

    Stock getStock(String articleId);

    List<Stock> getStocks();

    Stock saveStock(Stock stock);

    //Ähnlich für Preise

}
public class H2ProductPersistenceService implements ProductPersistenceService {

    private final H2PriceRepository h2PriceRepository;
    private final H2StockRepository h2StockRepository;

    public H2ProductPersistenceService(H2PriceRepository h2PriceRepository, H2StockRepository h2StockRepository) {
        this.h2PriceRepository = h2PriceRepository;
        this.h2StockRepository = h2StockRepository;
    }


    @Override
    public Stock getStock(String articleId) {
        return Mapper.toStock(h2StockRepository.getByArticleId(articleId).get(0));
    }

    @Override
    public List<Stock> getStocks() {
        return h2StockRepository.findAll().stream()
                .map(Mapper::toStock)
                .collect(Collectors.toList());
    }

    @Override
    public Stock saveStock(Stock stock) {
        return Mapper.toStock(h2StockRepository.save(Mapper.toStockEntity(stock)));
    }

    //Ähnlich für Preise
}

Damit alle Komponenten, JpaRepositories und Entities von Spring geladen werden eine Standardkonfiguration in der Applikation:

@Configuration
@ComponentScan(basePackageClasses = ProductServiceAppPackageMarker.class)
@EnableJpaRepositories(basePackageClasses = ProductServiceAppPackageMarker.class)
@EntityScan(basePackageClasses = ProductServiceAppPackageMarker.class)
@PropertySource("classpath:default-application.properties")
public class ProductServiceAppConfiguration {
}

default-application.properties mit notwendigen Standardeinstellungen:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.defer-datasource-initialization=true
spring.h2.console.enabled=true

productlistservice-autoconfigure

Das Modul kapselt eine Standardkonfiguration und hat daher eine Abhängigkeit auf die Applikation.

pom.xml:

    <dependencies>
        <dependency>
            <groupId>com.typosaurus</groupId>
            <artifactId>productservice-app</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>

Das Modul productlistservice-autoconfigure beinhaltet die Konfiguration, in der die H2-Implementierung als Standard definiert wird. Die Komponente wird dabei mit der Annotation @ConditionalOnMissingBean versehen, die dafür sorgt, dass das Bean H2ProductPersistenceService nur dann erstellt wird, wenn keine andere Implementierung der Schnittstelle ProductPersistenceService existiert. So können implementierende Anwendungen sehr einfach Beans überschreiben.

Ein sehr zentrale Klasse, die die H2-Implementierung als Standard definiert und importiert die ProductServiceConfiguration:

@Configuration
@Import(ProductServiceAppConfiguration.class)
public class ProductServiceConfiguration {

    @Bean
    @ConditionalOnMissingBean(ProductPersistenceService.class)
    public ProductPersistenceService productPersistenceService(H2PriceRepository h2PriceRepository, H2StockRepository h2StockRepository) {
        return new H2ProductPersistenceService(h2PriceRepository, h2StockRepository);
    }

}

Spring bietet viele weitere Möglichkeiten, u.a. Java-Version, ConditionalOnProperty oder ConditionalOnThreading, Bedingungen einzubauen. Die Bedingungen sind Teil der spring-boot-autoconfigure dependency.

Die Klasse ProductServiceConfiguration muss als Autokonfiguration hinzugefügt werden. Das wird durch die Datei src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (vor Spring Boot 3.0 spring.factories) gemacht, die den Pfad auf die Autokonfiguration enthält:

com.typosaurus.productservicespringbootautoconfigure.ProductServiceConfiguration

productlist-starter

Das Modul enthält nicht viel mehr als die pom.xml, in der das Modul productlist-autoconfigure als Abhängigkeit hinzugefügt wurde.

pom.xml:

    <dependencies>
        <dependency>
            <groupId>com.typosaurus</groupId>
            <artifactId>productservice-autoconfigure</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

Einbindung des Starters

Der Starter kann als Abhängigkeit zu einer Applikation hinzugefügt werden und stellt dann ohne weitere notwendige Konfiguration die Logik und die Standardimplementierung für die Persistierung zur Verfügung.

Eine SpringBootApplication dafür nur die productlist-starter als Abhängigkeit in der pom.xml:

    <dependencies>
        <dependency>
            <groupId>com.typosaurus</groupId>
            <artifactId>productservice-starter</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>

Nun kann die Applikation gestartet und Bestände hinzugefügt und abgefragt werden:

curl --location 'http://localhost:8080/stocks/add' \
--header 'Content-Type: application/json' \
--data '{
    "articleId" : "typosaurus",
    "stock" : 5
}'
curl --location 'http://localhost:8080/stocks'

[
    {
        "articleId": "typosaurus",
        "stock": 5
    }
]

Zusammenfassung:

In diesem Tutorial wurde gezeigt, wie einfach Custom Spring Boot Starter erstellt werden können und welche Vorteile sie für die einbindenden Applikationen bieten. Das vollständige Beispiel gibt’s bei Github.


Beitrag veröffentlicht

in

,

von