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.
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)
Great tutorial, thanks, really helped me get started with groovy and spring!
I noticed a couple of minor issues:
INSERT INTO wild_pokemon VALUES
(1, 2000, 1, 1), (2, 2100, 4, 2), (7, 2000, 7, 1), (8, 600, 1, 2);
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)
}
Entering my MySQL password into application.properties? Isn't this unsafe?