Skip to content

Tutorial: Speedment Spring Boot Integration

Per Minborg edited this page Aug 28, 2020 · 80 revisions

Having covered basic Speedment code generation and application logic in previous tutorials, this tutorial builds on that knowledge to showcase how to create a REST API to a Speedment application using Spring Boot.

Step 1: Setup the database

The Sakila database can be downloaded here. Installation instructions are here.

Step 2: Create a new Maven project

As in previous tutorials, create a new Java 8 Maven project and the Speedment dependencies to the pom.xml-file. For this tutorial, we also add the Spring Boot framework dependencies. In the following, we assume a MySQL database since the Sakila database linked above is MySQL.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.0.RELEASE</version>
</parent>

<build>
    <plugins>
        ...
        <plugin>
            <groupId>com.speedment</groupId>
            <artifactId>speedment-maven-plugin</artifactId>
            <version>3.2.10</version>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        ...
    </plugins>
</build>
<dependencies>
    ...
    <dependency>
        <groupId>com.speedment</groupId>
        <artifactId>runtime</artifactId>
        <version>3.2.10</version>
        <type>pom</type>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>Z5.1.46</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jdk8</artifactId>
    </dependency>
    ...
</dependencies>

Step 3: Spring integration

The starting point of a Spring Boot based application is the SpringApplication.run method. The following main class is all that is needed to bootstrap the application.

@SpringBootApplication
public class Main {
    public static void main(String[] args) throws UnsupportedEncodingException, FileNotFoundException {
        SpringApplication.run(Main.class, args);
    }
}

To add any meaningful functionality to the application, we start out with creating the RestController. Using Spring annotations, we declare a class that will manage the server side of the REST endpoints for films. The controller defines four different endpoints; the list of all films (/), film lookup by ID (/{id}), film lookup via any actor having a given name (/byactor/{actorName}), film lookup grouped on actor names (/byactors/{actorName}) and filtered on an actor name part.

@RestController
@RequestMapping("/film")
public class FilmsController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final FilmActorManager filmActors;
    private final ActorManager actors;
    private final FilmManager films;

    public FilmsController(SakilaApplication app) {
        films = app.getOrThrow(FilmManager.class);
        filmActors = app.getOrThrow(FilmActorManager.class);
        actors = app.getOrThrow(ActorManager.class);
    }

    @GetMapping("")
    List<Film> getFilms() {
        return films.stream().collect(toList());
    }

    @GetMapping("{id}")
    Film getFilmById(@PathVariable int id) {
        return films.stream().filter(Film.FILM_ID.equal(id)).findAny().orElse(null);
    }

    @GetMapping("byactor/{actorName}")
    List<Film> getFilmByActor(@PathVariable String actorName) {
        Integer actorId = actors.stream()
                .filter(Actor.LAST_NAME.equalIgnoreCase(actorName))
                .mapToInt(Actor.ACTOR_ID)
                .boxed()
                .findAny()
                .orElse(null);

        return actorId == null ? emptyList() : filmActors.stream()
                .filter(FilmActor.ACTOR_ID.equal(actorId))
                .map(films.finderBy(FilmActor.FILM_ID))
                .collect(toList());
    }

    @GetMapping("byactors/{actorName}")
    Map<String, List<String>> getFilmByActors(@PathVariable String actorName) {
        Set<Integer> actorIds = actors.stream()
                .filter(Actor.LAST_NAME.containsIgnoreCase(actorName)
                    .or(Actor.FIRST_NAME.containsIgnoreCase(actorName)))
                .mapToInt(Actor.ACTOR_ID)
                .distinct()
                .boxed()
                .collect(toSet());

        return actorIds.isEmpty() ? Collections.EMPTY_MAP : filmActors.stream()
                .filter(FilmActor.ACTOR_ID.in(actorIds))
                .collect(
                        groupingBy(actors.finderBy(FilmActor.ACTOR_ID)
                                .andThen(a -> a.getFirstName() + " " + a.getLastName()),
                                mapping(
                                        films.finderBy(FilmActor.FILM_ID)
                                            .andThen(GeneratedFilm::getTitle),
                                        toList()
                                )
                        )
                );
    }
}

As seen in the constructor of the controller, it relies on an instantiated Speedment SakilaApplication. Without any further instructions on how to do so, the Spring framework would try to instantiate such an object before creating the controller. As seen in previous tutorials, the Speedment application is created using a builder pattern and in order to tell Spring how to do that we create a configuration class. In the configuration class, we also supply a JSON formatting builder which will be used by Spring when JSON encoding the returned data structures from the controller class above. The application would work without it, but adding a formatter allows us to customize the formatting and for this tutorial, we want indented JSON.

@Configuration
public class Setup {
    @Bean
    public SakilaApplication createApplication() {
        return new SakilaApplicationBuilder()
                .withBundle(MySqlBundle.class)
                .withPassword("MyPassword")
                .build();
    }

    @Bean
    public Jackson2ObjectMapperBuilder jacksonBuilder() {
        return new Jackson2ObjectMapperBuilder().indentOutput(true);
    }
}

Having a Main-method as an executional starting point, a Controller for the logic behind the REST API and a Configuration class for setting things up, everything is provided for Spring to determine runtime dependencies and start an HTTP server that exposes our API to clients.

Example output

Querying for a single film by its ID can be done as follows.

curl http://localhost:8080/film/14

{
  "filmId": 14,
  "title": "ALICE FANTASIA",
  "description": "A Emotional Drama of a A Shark And a Database Administrator who must Vanquish a Pioneer in Soviet Georgia",
  "releaseYear": "2006-01-01",
  "languageId": 1,
  "originalLanguageId": null,
  "rentalDuration": 6,
  "rentalRate": 0.99,
  "length": 94,
  "replacementCost": 23.99,
  "rating": "NC-17",
  "specialFeatures": "Trailers,Deleted Scenes,Behind the Scenes",
  "lastUpdate": 1139979822000
}

Asking for all films featuring an actor with the last name Ball will yield a long list of films which we here abbreviate by means of greping for the film titles.

curl -s http://localhost:8080/film/byactor/ball | grep title
  "title" : "ALONE TRIP",
  "title" : "ANGELS LIFE",
  "title" : "ANTITRUST TOMATOES",
  "title" : "BALLOON HOMEWARD",
  "title" : "BINGO TALENTED",
  "title" : "BIRDCAGE CASPER",
  "title" : "BRIGHT ENCOUNTERS",
  "title" : "CABIN FLASH",
  "title" : "CAT CONEHEADS",
  "title" : "COMANCHEROS ENEMY",
  "title" : "DESERT POSEIDON",
  "title" : "DESPERATE TRAINSPOTTING",
  "title" : "EXTRAORDINARY CONQUERER",
  "title" : "GHOST GROUNDHOG",
  "title" : "GREEDY ROOTS",
  "title" : "HILLS NEIGHBORS",
  "title" : "HOTEL HAPPINESS",
  "title" : "HUNTER ALTER",
  "title" : "JADE BUNCH",
  "title" : "KING EVOLUTION",
  "title" : "LOVERBOY ATTACKS",
  "title" : "MAGNIFICENT CHITTY",
  "title" : "MASK PEACH",
  "title" : "NATURAL STOCK",
  "title" : "NONE SPIKING",
  "title" : "PATRIOT ROMAN",
  "title" : "PERDITION FARGO",
  "title" : "SCARFACE BANG",
  "title" : "SENSE GREEK",
  "title" : "TRAMP OTHERS",
  "title" : "TROUBLE DATE",
  "title" : "UNFAITHFUL KILL",
  "title" : "WIND PHANTOM",

The output from the lookup of films by several actors is briefer, so for example, listing the films of actors called Aykroyd yields the following result where each list of films has been truncated for readability.

curl -s http://localhost:8080/film/byactors/Akroyd
 
{
  "KIRSTEN AKROYD" : [ "BOULEVARD MOB", "BRAVEHEART HUMAN", "BUCKET BROTHERHOOD", ... ],
  "DEBBIE AKROYD" : [ "APOLLO TEEN", "CLUB GRAFFITI", "FAMILY SWEET", ... ],
  "CHRISTIAN AKROYD" : [ "BACKLASH UNDEFEATED", "BETRAYED REAR", "CAPER MOTIONS", ... ]
}