DEV Community

Cover image for Spring Boot + Groovy: From Zero to Hero
Jorge Ramón
Jorge Ramón

Posted on

Spring Boot + Groovy: From Zero to Hero

Java... ugghh...

I don't like to use Java (and please don't use it) but sometimes I have to... and that was the case.

A few weeks ago I started a new project at Indigo (please follow them!) and the chosen framework to do the job was Spring Boot and this time I had no option since the other person who is developing with me loves the combination Java + Spring Boot.

But why do I said "the combination"? 🤔 Well, there are some programming languages that work on the JVM such as Kotlin, Scala and my favorite Groovy!.

What is Groovy?

According to it's official page:

Apache Groovy is a powerful, optionally typed and dynamic language, with static-typing and static compilation capabilities, for the Java platform aimed at improving developer productivity thanks to a concise, familiar and easy to learn syntax. It integrates smoothly with any Java program, and immediately delivers to your application powerful features, including scripting capabilities, Domain-Specific Language authoring, runtime and compile-time meta-programming and functional programming.

In other words, you can do better stuff, write less code and get some cool features that Java cannot.

What will be our example?

This time we will create a Pokemon API!! Let's catch'em all

Our API must:

  • Get the information of all trainers
  • Get the information of a specific trainer
  • Get all caught pokemon of a specific trainer

What will be using to solve that?

  • Java 8
  • Gradle
  • Spring Boot
  • Groovy
  • MySQL
  • Flyway DB

What do I need to install?

  • Java 8 or greater
  • Gradle
  • MySQL

I highly recommend you to install Java and Gradle via SDKMAN.

Let's get started!

1. Create the base project with Gradle

Create a directory and then create a file named build.gradle. This file will:

  • Download all the dependencies
  • Compile the entire project
  • Run Spring Boot server
  • Run tests if needed

So, let's add the following lines:

buildscript {
  ext {
    springBootVersion = '2.0.6.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'groovy'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.pokemon'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
  implementation('org.codehaus.groovy:groovy:2.5.3')
}

The lines tell Gradle to configure Maven Central as the default repository, download and use the spring-boot-gradle-plugin and download the following dependencies:

  • Spring Boot Web: Exposes REST services
  • Groovy: Compiles and interprets all Groovy/Java code

Also our base package name will be com.pokemon so every class with this package name will be compiled by Gradle and used by Spring Boot.

2. Create the base code and run it for the first time

Create the directories src/main/groovy/com/pokemon in the root directory and then create a file named Application.groovy:

package com.pokemon

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

// In groovy every class is public by default
// so you don't need to write "public class" anymore
@SpringBootApplication
class Application {

    // Every method is public as well
    static void main(String[] args) {

        // You can omit the last method call parenthesis
        // This is the same as .run(Application, args)
        // also you can omit ;
        SpringApplication.run Application, args
    }
}

Open your terminal, go to your project directory and type the command gradle bootRun to get your application running like this:

3. SQL tables with Flyway DB

Working with SQL databases is often a pain.

How do you share the database structure with your team?

A way to deal with it is create a SQL script and share it with all the developers.

Sounds good, right?

But, when you need to add changes (like modifying existing tables or adding new ones) to the script, What do you do now? How do the dev team get notified from those changes?

Framework such as PHP Laravel introduce the migration concept. Every change to the database (creating a table, adding a field, changing a field type, etc) is a migration. Migrations are executed in certain order and keep the database always up-to-date.

Flyway DB will help us to solve the problem but in Java (Groovy).

According to it's official page:

Version control for your database.
Robust schema evolution across all your environments.
With ease, pleasure and plain SQL.

So now let's add Flyway DB, Spring Data and MySQL Connector to our project.

Edit build.gradle file and the following dependencies:

implementation('org.flywaydb:flyway-core:5.2.0')
implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
implementation('mysql:mysql-connector-java:8.0.13')

And now create a new file in src/main/resources directory named application.properties:

spring.flyway.url=jdbc:mysql://localhost/
spring.flyway.user=your-username
spring.flyway.password=your-password
spring.flyway.schemas=your-database-name

spring.datasource.url=jdbc:mysql://localhost/your-database-name
spring.datasource.username=your-username
spring.datasource.password=your-password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

Don't forget to change your-username, your-password and your-database-name values.

In my case, the database name is pokemon_example.

spring.flyway credentials let FlywayDB connect to the database and install the lastest changes every time you run the project.

spring.datasource credentials let Spring Data connect to the database and run queries.

To create the initial tables create a new file in src/main/resources/db/migration directory named V1__creating_initial_tables.sql:

CREATE TABLE trainers(
  id INTEGER AUTO_INCREMENT,
  name VARCHAR(100) NOT NULL,
  level SMALLINT NOT NULL DEFAULT 1,
  PRIMARY KEY (id)
);

CREATE TABLE pokemon(
  id INTEGER AUTO_INCREMENT,
  name VARCHAR(20) NOT NULL,
  number SMALLINT NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE wild_pokemon(
  id INTEGER AUTO_INCREMENT,
  combat_power SMALLINT NOT NULL DEFAULT 0,
  pokemon_id INTEGER NOT NULL,
  trainer_id INTEGER,
  PRIMARY KEY (id),
  FOREIGN KEY (pokemon_id) REFERENCES pokemon (id)
    ON DELETE CASCADE,
  FOREIGN KEY (trainer_id) REFERENCES trainers (id)
    ON DELETE SET NULL
);

Flyway DB search for all the files in db/migration directory with the pattern VN__any_name.sql where N is a unique version, for example:

  • 1
  • 001
  • 5.2
  • 1.2.3.4.5.6.7.8.9
  • 205.68
  • 20130115113556
  • 2013.1.15.11.35.56
  • 2013.01.15.11.35.56

Personally I prefer to use this versioning:

  • V1__whatever.sql
  • V2__whatever.sql
  • etc.

For last, run the project with gradle bootRun command and then take a look to your database.

4. Entity classes (Domain)

Back to code, after creating the database successfully it's time to map those tables to classes.

In src/main/groovy/com/pokemon directory, create a new one named entity and create 3 new files:

  • Trainer.groovy
  • Pokemon.groovy
  • WildPokemon.groovy

Trainer.groovy

package com.pokemon.entity

import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.GeneratedValue
import javax.persistence.Table
import javax.persistence.GenerationType
import javax.validation.constraints.NotNull
import javax.persistence.Column

@Entity
@Table(name = "trainers")
class Trainer {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  Integer id

  @NotNull
  @Column(nullable = false)
  String name

  @NotNull
  @Column(nullable = false)
  Short level
}

Pokemon.groovy

package com.pokemon.entity

import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.GeneratedValue
import javax.persistence.Table
import javax.persistence.GenerationType
import javax.validation.constraints.NotNull
import javax.persistence.Column

@Entity
@Table(name = "pokemon")
class Pokemon {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  Integer id

  @NotNull
  @Column(nullable = false)
  String name

  @NotNull
  @Column(nullable = false)
  Short number
}

WildPokemon.groovy

package com.pokemon.entity

import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.GeneratedValue
import javax.persistence.Table
import javax.persistence.GenerationType
import javax.validation.constraints.NotNull
import javax.persistence.Column
import javax.persistence.ManyToOne
import javax.persistence.JoinColumn

@Entity
@Table(name = "wild_pokemon")
class WildPokemon {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  Integer id

  @NotNull
  @Column(name = "combat_power", nullable = false)
  Integer combatPower

  @ManyToOne
  @JoinColumn(name = "pokemon_id", referencedColumnName = "id", nullable = false)
  Pokemon pokemon

  @ManyToOne
  @JoinColumn(name = "trainer_id", referencedColumnName = "id", nullable = true)
  Trainer trainer
}

Before continue, remember!! because we are using Groovy there's no need to add the boring getters and setters:

// Groovy version
class Example {

  String property
}

Example ex = new Example(property: "hello world")
ex.property = "hola inmundo"

// -----------------------------------------------------

// Java version
public class Example {

  private String property;

  public Example(String property) {
    this.property = property;
  }

  public String getProperty() {
    return property;
  }

  public void setProperty(String property) {
    this.property = property;
  }
}

Example ex = new Example("hello world")
ex.setProperty("hola inmundo")

Writing less code FTW! 🤙

As you can see, it's easy to create entity classes based on tables. In the WildPokemon case we declared 2 many to one relationships, one to Pokemon class (or table) and the other one to Trainer class (or table). Only Trainer relationship can be null.

5. Repository classes (Persistence layer)

A Repository class is the place to define our queries. It's very easy and magical at the same time.

Remember our API goal:

  • Get the information of all trainers
  • Get the information of a specific trainer
  • Get all caught pokemon of a specific trainer

Okay, let's translate those features into SQL queries:

-- Get the information of all trainers
SELECT * FROM trainers;

-- Get the information of a specific trainer
SELECT * FROM trainers WHERE id = ?;

-- Get all caught pokemon of a specific trainer
SELECT * FROM wild_pokemon WHERE trainer_id = ?;

Seems legit.

But what if the queries were written in human readable text?

# Get the information of all trainers
find all trainers

# Get the information of a specific trainer
find trainer by id

# Get all caught pokemon of a specific trainer
find wild pokemon by trainer id

And believe or not, using JpaRepository interface from Spring Data our queries are:

  • findAll
  • findById
  • findByTrainerId

Very easy!! 🎉🎉🎉🎉🎉

Let's code them, in src/main/groovy/com/pokemon directory create a new one named repository and create 2 new files:

  • TrainerRepository.groovy
  • WildPokemonRepository.groovy

TrainerRepository.groovy

package com.pokemon.repository

import com.pokemon.entity.Trainer
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface TrainerRepository extends JpaRepository<Trainer, Integer> {

  List<Trainer> findAll()

  Trainer findById(Integer id)
}

WildPokemonRepository.groovy

package com.pokemon.repository

import com.pokemon.entity.WildPokemon
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface WildPokemonRepository extends JpaRepository<WildPokemon, Integer> {

  List<WildPokemon> findByTrainerId(Integer trainerId)
}

Simple as that, Spring Data will search for all the classes annotated with @Repository and verify the method signature.

6. Service classes (Business layer)

Our API doesn't need business logic at all but still is a good idea to create service classes and use repositories in them.

In src/main/groovy/com/pokemon directory create a new one named service and then inside service create a new one named impl.

Our service directory will contain interfaces and impl their implementation.

Wait, what?

It has its explanation.

Suppose you have to implement a class that stores files:

class FileStorage {

  String saveFile(String base64) {
    // storing file in disk...
  }
}

Pretty simple, but then your client needs to store the files in Amazon S3 and Azure Blob too. What is the easiest way to solve the problem?

You can create a interface, because in-disk storage, Amazon S3 storage and Azure Blob storage do the same thing, save a file:

interface Storage {

  String saveFile(String base64)
}

And then you implement the class for each storage type.

class S3Storage implements Storage {
}

class BlobStorage implements Storage {
}

class DiskStorage implements Storage {
}

And finally you can use polymorphism to change the storage type whenever you need it:

// Save the file in Amazon S3
Storage storage = new S3Storage()

// Save the file in Azure Blob
Storage storage = new BlobStorage()

// Save the file in disk
Storage storage = new DiskStorage()

That's why we create interfaces, because the business logic can change anytime or whenever the client wants to.

So, let's create our 2 interfaces and their implementation:

TrainerService.groovy

package com.pokemon.service

import com.pokemon.entity.Trainer

interface TrainerService {

  List<Trainer> findAll()

  Trainer findById(int id)
}

TrainerServiceImpl.groovy

package com.pokemon.service.impl

import com.pokemon.entity.Trainer
import com.pokemon.service.TrainerService
import com.pokemon.repository.TrainerRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

@Service
class TrainerServiceImpl implements TrainerService {

  @Autowired
  private final TrainerRepository trainerRepository

  @Override
  List<Trainer> findAll() {
    trainerRepository.findAll()
  }

  @Override
  Trainer findById(int id) {
    trainerRepository.findById(id)
  }
}

WildPokemonService implementation is very similar.

Our implementation class has @Service annotation because Spring will create an instance for us when another class uses @Autowired annotation.

In TrainerServiceImpl we use @Autowired annotation to create an instance of TrainerRepository class.

Annotations are very important because they tell Spring what they are and what to do with them.

7. Controller classes

Finally it's time to create the controller with 3 endpoints:

  • /trainers - Get all trainers
  • /trainers/{ id } - Get a specific trainer
  • /trainers/{ id }/pokemon - Get all caught pokemon of a specific trainer

In src/main/groovy/com/pokemon directory create a new one named controller and then create a new file named TrainerController.groovy:

package com.pokemon.controller

import com.pokemon.entity.Trainer
import com.pokemon.entity.WildPokemon
import com.pokemon.service.TrainerService
import com.pokemon.service.WildPokemonService
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.PathVariable

@RestController
@RequestMapping('/trainers')
class TrainerController {

  @Autowired
  private final TrainerService trainerService

  @Autowired
  private final WildPokemonService wildPokemonService

  @RequestMapping(method = RequestMethod.GET)
  List<Trainer> findAll() {
    trainerService.findAll()
  }

  @RequestMapping(value = '/{id}/pokemon', method = RequestMethod.GET)
  List<Trainer> findCaughtPokemon(@PathVariable('id') int id) {
    wildPokemonService.findByTrainer id
  }

  @RequestMapping(value = '/{id}', method = RequestMethod.GET)
  Trainer findById(@PathVariable('id') int id) {
    trainerService.findById id
  }
}

@RequestMapping annotation configures the prefix of all endpoints defined in that controller. Also configures method type (GET, POST, PUT, DELETE, etc).

@Autowired annotation creates instances of service classes for us.

@PathVariable annotation passes the specified URL parameter to the method call.

For last, let's insert dummy data into MySQL using Flyway DB. Create a new migration (in src/main/resources/db/migration) named V2__inserting_example_data.sql:

INSERT INTO trainers VALUES (1, 'Red', 40), (2, 'Ash Ketchum', 10);

INSERT INTO pokemon VALUES
  (1, 'Bulbasaur', 1), (2, 'Ivysaur', 2), (3, 'Venosaur', 3), (4, 'Charmander', 4),
  (5, 'Charmeleon', 5), (6, 'Charizard', 6), (7, 'Squirtle', 7), (8, 'Wartortle', 8),
  (9, 'Blastoise', 9);

INSERT INTO wild_pokemon VALUES
  (1, 2000, 1, 3), (2, 2100, 4, 6), (7, 2000, 7, 9), (8, 600, 1, 2);

Run the project with gradle bootRun command, go to your favorite browser and navigate to http://localhost:8080/trainers/1/pokemon to get:

[
   {
      "id":1,
      "combatPower":2000,
      "pokemon":{
         "id":3,
         "name":"Venosaur",
         "number":3
      },
      "trainer":{
         "id":1,
         "name":"Red",
         "level":40
      }
   },
   {
      "id":2,
      "combatPower":2100,
      "pokemon":{
         "id":6,
         "name":"Charizard",
         "number":6
      },
      "trainer":{
         "id":1,
         "name":"Red",
         "level":40
      }
   },
   {
      "id":7,
      "combatPower":2000,
      "pokemon":{
         "id":9,
         "name":"Blastoise",
         "number":9
      },
      "trainer":{
         "id":1,
         "name":"Red",
         "level":40
      }
   }
]

This is our final project directory structure:

And that's all, folks!

Thank you so much for reading this long post.

In the next post we will improve our pokemon API adding more features and things like Data Transfer Objects, Exception Handler, Swagger, Testing and more!

Stay tuned.

GitHub logo jorgeramon / spring-groovy-pokemon

Pokemon API example using Spring Boot + Groovy.

Pokemon API v1.0

Spring Boot + Groovy example for Dev.to

Requirements

  • Java 8 or greater
  • MySQL
  • Gradle

Configuration

Edit the file application.properties and change your-username, your-password and your-database values.

Running the project will create the database, tables and inserting example data automatically.

Running the project

In terminal type the command gradle bootRun.

Top comments (2)

Collapse
 
masaltzman profile image
DegoyDegoy

Great tutorial, thanks, really helped me get started with groovy and spring!

I noticed a couple of minor issues:

  • in the sample data INSERT statements you provided, the wild_pokemon insert will fail because it's trying to create wild pokemon owned by trainer with id = 3, but the trainer inserts only create ids 1 and 2. Easy to modify the statement like this, changing the trainer_id to match the two existing trainers:

INSERT INTO wild_pokemon VALUES
(1, 2000, 1, 1), (2, 2100, 4, 2), (7, 2000, 7, 1), (8, 600, 1, 2);

  • you didn't include WildPokemonService and WildPokemonServiceImpl, but they were very straightforward to create from the TrainerService and TrainerServiceImpl:

class WildPokemonServiceImpl implements WildPokemonService {

@Autowired
private final WildPokemonRepository wildPokemonRepository

// @Override
// List findAll() {
// wildPokemonRepository.findAll()
// }

@Override
List findByTrainerId(int id) {
wildPokemonRepository.findByTrainerId(id)
}
}

package com.pokemon.service

import com.pokemon.entity.WildPokemon

interface WildPokemonService {

//List findAll()

List findByTrainerId(int id)
}

Collapse
 
jamesberes profile image
jamesberes

Entering my MySQL password into application.properties? Isn't this unsafe?