In a previous article, I talked about an environment, I prepared on my Windows laptop, with a guest Operating System, Docker and Minikube available within an Oracle VirtualBox appliance.
In this article, I will create two versions of a RESTful Web Service Spring Boot application that, later on (in another article), I will be running in Minikube.
The Spring Boot application I create in this article is a book service. With this service you can add, update, delete and retrieve books from a catalog.
The application uses an H2 in-memory database but is also prepared for using an external MySQL database. For demo purposes I created a 1.0 and 2.0 version of the application.
The latter has some additional fields representing a book.
In a next article I will be describing how these applications will be used in Minikube, together with an external “Dockerized” MySQL database.
Minikube
Minikube is a tool that makes it easy to run Kubernetes locally. Minikube runs a single-node Kubernetes cluster for users looking to try out Kubernetes or develop with it day-to-day.
[https://kubernetes.io/docs/setup/minikube/]
Spring Boot
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.
[https://spring.io/projects/spring-boot]
Spring Initializr
To bootstrap my application, I used Spring Initializr.
I left most as default and filled in:
- Group: nl.amis.demo.services
- Artifiact: books_service
- Dependencies: Web
If you switch to the full version, the following is shown:
Remark:
Because the things you fill in, are being placed in a maven pom.xml file, I had a look at the Guide to naming conventions on groupId, artifactId, and version.
[https://maven.apache.org/guides/mini/guide-naming-conventions.html]
Then I clicked the “Generate Project” button, which downloaded a books_service.zip to my laptop.
Next, I created a subdirectory named env on my Windows laptop. In this directory I created an applications subdirectory and extracted the zip file in this directory.
Pom.xml
The pom.xml that is created, has the following content:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>nl.amis.demo.services</groupId> <artifactId>books_service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>books_service</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Because I selected Web as a technology for the project, the following were added as a dependency:
- spring-boot-starter-web
Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container.
[https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web] - spring-boot-starter-test
Starter for testing Spring Boot applications with libraries including JUnit, Hamcrest and Mockito
[https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test]
In the pom.xml, I changed the version from:
<version>0.0.1-SNAPSHOT</version>
to
<version>1.0.0-SNAPSHOT</version>
Importing the project into IntelliJ IDEA
Within IntelliJ IDEA, I imported the project and selected directory: C:\My\AMIS\env\applications\books_service. Next, I chose “Import project from external model: Maven” and kept the rest as default.
BooksServiceApplication.java
Spring Initizalizr has generated an application class named BooksServiceApplication.java. It contains a method called main, which is the entry point of the program.
package nl.amis.demo.services.books_service; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BooksServiceApplication { public static void main(String[] args) { SpringApplication.run(BooksServiceApplication.class, args); } }
The BooksService, will be a catalog of books. For now, an in-memory database is used to add, update, delete and retrieve books. Later on, also an external MySQL database is used.
In my application I used a layered architecture. This means that I organized the project structure into to following main categories:
- Presentation
Contains all of the classes responsible for presenting the UI to the end-user or sending the response back to the client. - Application
Contains all the logic that is required by the application to meet its functional requirements. The application layer mostly consists of services orchestrating the domain objects. - Domain
Represents the underlying domain, mostly consisting of domain entities and business rules. - Infrastructure (also known as the persistence layer)
Contains all the classes responsible for doing the technical stuff, like persisting the data in the database.
Each of the layers contains objects related to the particular functionality it provides.
[https://dzone.com/articles/layered-architecture-is-good]
Book.java
To model the book representation, I created a resource representation class. Providing a plain old java object with fields, constructors, and accessors.
Therefor I added a new package to ‘nl.amis.demo.services.books_service’ and named it ‘domain’.
Next, I added a new Java class to the domain package and named it Book.java.
package nl.amis.demo.services.books_service.domain; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class Book { @Id private String id = ""; private String title = ""; private String author = ""; private String type = ""; private double price = 0; private int numOfPages = 0; private String language = ""; private String isbn13 = ""; public Book() { } public Book(String id, String title, String author, String type, double price, int numOfPages, String language, String isbn13) { this.id = id; this.title = title; this.author = author; this.type = type; this.price = price; this.numOfPages = numOfPages; this.language = language; this.isbn13 = isbn13; } public String getId() { return id; } public String getTitle() { return title; } public String getAuthor() { return author; } public String getType() { return type; } public double getPrice() { return price; } public int getNumOfPages() { return numOfPages; } public String getLanguage() { return language; } public String getIsbn13() { return isbn13; } }
The Book class is annotated with @Entity, indicating that it is a JPA entity. For lack of a @Table annotation, it is assumed that this entity will be mapped to a table named Book.
The Book’s id property is annotated with @Id so that JPA will recognize it as the object’s ID.
[https://spring.io/guides/gs/accessing-data-jpa/]
Spring Data JPA focuses on using JPA to store data in a relational database. Its most compelling feature is the ability to create repository implementations automatically, at runtime, from a repository interface.
[https://spring.io/guides/gs/accessing-data-jpa/]
BookRepository.java
By using a Spring Data repository you get a lot of (CRUD)functionality with a minimum of effort.
The goal of the Spring Data repository abstraction is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores.
[https://docs.spring.io/spring-data/jpa/docs/current/reference/html/]
See below a list of the methods that become available when using a CrudRepository.
I added a new package to ‘nl.amis.demo.services.books_service’ and named it ‘infrastructure.persistence’.
Next, I added a new Java class to the persistence package and named it BookRepository.java.
package nl.amis.demo.services.books_service.infrastructure.persistence; import nl.amis.demo.services.books_service.domain.Book; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface BookRepository extends CrudRepository { }
BookService.java
I added a new package to ‘nl.amis.demo.services.books_service’ and named it ‘application’.
Next, I added a new Java class to the application package and named it BookService.java.
This class is, more or less, a wrapper class, exposing some of the methods of the CrudRepository.
package nl.amis.demo.services.books_service.application; import nl.amis.demo.services.books_service.domain.Book; import nl.amis.demo.services.books_service.infrastructure.persistence.BookRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service public class BookService { @Autowired private BookRepository bookRepository; public List getAllBooks() { List books = new ArrayList<>(); bookRepository.findAll().forEach(books::add); return books; } public Book getBook(String id) { return bookRepository.findById(id).orElseGet(Book::new); } public void addBook(Book whiskey) { bookRepository.save(whiskey); } public void updateBook(String id, Book whiskey) { bookRepository.save(whiskey); } public void deleteBook(String id) { bookRepository.deleteById(id); } }
Changes made to pom.xml
Because I used the CrudRepository, I added the following dependency in the pom.xml:
- spring-boot-starter-data-jpa
Starter for using Spring Data JPA with Hibernate.
[https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa]
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
Because I used the H2 in-memory database, I added the following dependency in the pom.xml:
- h2
H2 Database Engine.
[https://mvnrepository.com/artifact/com.h2database/h2]
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>
Because I also want to use the MySQL database, I added the following dependency in the pom.xml:
- mysql-connector-java
JDBC Type 4 driver for MySQL.
[https://mvnrepository.com/artifact/mysql/mysql-connector-java]
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
Remark:
The same could be achieved in Spring Initializr, by selecting the following Dependencies:
BookController.java
In Spring’s approach to building RESTful web services, HTTP requests are handled by a controller. These components are easily identified by the @RestController annotation.
I added a new package to ‘nl.amis.demo.services.books_service’ and named it ‘presentation’.
Next, I added a new Java class to the presentation package and named it BookController.java.
package nl.amis.demo.services.books_service.presentation; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BookController { @GetMapping("/books") public List getAllBooks() { return bookservice.getAllbooks(); } @GetMapping("/books/{id}") public Book getBook(@PathVariable String bookId){ return bookservice.getBook(bookId); } @PostMapping("/books") public void addBook(@RequestBody Book book) { bookservice.addBook(book); } @PutMapping("/books/{id}") public void updateBook(@PathVariable String bookId, @RequestBody Book book) { bookservice.updateBook(bookId, book); } @DeleteMapping("/books/{id}") public void deleteBook(@PathVariable String bookId) { bookservice.deleteBook(bookId); } }
Id is a so-called path variable and needs to be mapped onto the corresponding method parameter, which is bookId in this case. For the mapping you use the annotation @PathVariable.
The @GetMapping annotation is for mapping HTTP GET requests onto specific handler methods.
Specifically, @GetMapping is a composed annotation that acts as a shortcut for @RequestMapping(method = RequestMethod.GET).
[https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/GetMapping.html]
The @PostMapping annotation is for mapping HTTP POST requests onto specific handler methods.
Specifically, @PostMapping is a composed annotation that acts as a shortcut for @RequestMapping(method = RequestMethod.POST).
[https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/PostMapping.html]
The @PutMapping annotation is for mapping HTTP PUT requests onto specific handler methods.
Specifically, @PutMapping is a composed annotation that acts as a shortcut for @RequestMapping(method = RequestMethod.PUT).
[https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/PutMapping.html]
The @DeleteMapping annotation for mapping HTTP DELETE requests onto specific handler methods.
Specifically, @DeleteMapping is a composed annotation that acts as a shortcut for @RequestMapping(method = RequestMethod.DELETE).
[https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/DeleteMapping.html]
The @RequestBody annotation indicates a method parameter should be bound to the body of the web request. The body of the request is passed through an HttpMessageConverter to resolve the method argument depending on the content type of the request.
[https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestBody.html]
HTTP Methods
For your convenience, below I added an overview of the HTTP Methods:
HTTP Method | Description |
GET | GET is used to request data from a specified resource |
POST | POST is used to send data to a server to create/update a resource |
PUT | PUT is used to send data to a server to create/update a resource |
HEAD | HEAD is almost identical to GET, but without the response body |
DELETE | The DELETE method deletes the specified resource |
PATCH | PATCH requests are to make partial update on a resource |
OPTIONS | The OPTIONS method describes the communication options for the target resource |
The difference between POST and PUT is that PUT requests are idempotent. That is, calling the same PUT request multiple times will always produce the same result. In contrast, calling a POST request repeatedly have side effects of creating the same resource multiple times.
[https://www.w3schools.com/tags/ref_httpmethods.asp]
Running the BooksServiceApplication on port 8080
Within IntelliJ IDEA, I chose the BooksServiceApplication | Run BooksServiceApplication main(). The response from running the application is:
As you can see, Tomcat is started by default on port 8080 (http).
Example filling
As an example filling for the book catalog, in this article (and the next one), I will be using some of the data provided in the following table:
Title | PublishDate | Author | Type | Price | NumOfPages | Publisher | Language | ISBN-13 |
The Threat: How the FBI Protects America in the Age of Terror and Trump | February 19, 2019 | Andrew G. McCabe | Hardcover | $17.99 | 288 | St. Martin’s Press | English | 978-1250207579 |
Becoming | November 13, 2018 | Michelle Obama | Hardcover | $17.88 | 448 | Crown Publishing Group; First Edition edition | English | 978-1524763138 |
Five Presidents: My Extraordinary Journey with Eisenhower, Kennedy, Johnson, Nixon, and Ford | May 2, 2017 | Clint Hill, Lisa McCubbin | Paperback | $11.09 | 464 | Gallery Books; Reprint edition | English | 978-1476794143 |
Where the Crawdads Sing | August 14, 2018 | Delia Owens | Hardcover | $16.20 | 384 | G.P. Putnam’s Sons; First Edition, First Printing edition | English | 978-0735219090 |
[https://www.amazon.com/best-sellers-books-Amazon/zgbs/books]
Postman
From Postman I invoked a request named “PostBook1Request” (with method “POST” and URL “http://locahost:8080/books”) and a response with “Status 200 OK” was shown:
Next, from Postman I invoked a request named “PostBook2Request” (with method “POST” and URL “http://locahost:8080/books”) and a response with “Status 200 OK” was shown:
Then I wanted to check if the books were added to the catalog.
From Postman I invoked a request named “GetAllBooksRequest” (with method “GET” and URL “http://locahost:8080/books”) and a response with “Status 200 OK” was shown:
As you can see, both books that I added, were returned from the catalog.
So now let’s make some changes to the application code.
Running the BooksServiceApplication on port 9090
I opened the file application.properties, which already existed in the project folder, and added the following line:
Within IntelliJ IDEA, I chose the BooksServiceApplication | Run BooksServiceApplication main(). The response from running the application is:
Run/Debug Configurations
Within IntelliJ IDEA, via Run | Edit Configurations… , you can edit the Run/Debug Configurations.
In the Main class field, specify the class that contains the main() method.
In the VM options field, type optional VM arguments, for example the heap size, garbage collection options, file encoding, etc.
In the Program arguments field, type optional list of arguments that should be passed to the main() method.
In the Environment variables field, create variables and specify their values.
[https://www.jetbrains.com/help/idea/setting-configuration-options.html]
Profiles
In Spring you can have different profiles. I created a development profile and a testing profile. In the development profile I want to quickly test with an H2 in-memory database, while in the testing profile I want to connect to an external MySQL database.
You can set up different application-{profile}.properties files and set the environment variable spring.profiles.active equal to the activated profile. The activated profile will overwrite the properties of the default application.properties file.
I changed the content of application.properties to:
logging.level.root=INFO server.port=9090
Next, I added a new application-development.properties:
logging.level.root=INFO server.port=9090 nl.amis.environment=development
Next, I added a new application-testing.properties:
logging.level.root=INFO server.port=9091 nl.amis.environment=testing spring.datasource.url=jdbc:mysql://localhost:3306/test?allowPublicKeyRetrieval=true&useSSL=false spring.datasource.username=root spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect spring.jpa.hibernate.ddl-auto=create
Adding logging to BooksServiceApplication.java
In order, to be able to see what VM options, Program arguments and Environment variables are set, I added logging to BooksServiceApplication.java.
package nl.amis.demo.services.books_service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; @SpringBootApplication public class BooksServiceApplication implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(BooksServiceApplication.class); @Autowired private Environment environment; @Override public void run(String... args) throws Exception { logger.info("\n----Begin logging BooksServiceApplication----"); logger.info("----System Properties from VM Arguments----"); logger.info("server.port: " + System.getProperty("server.port")); logger.info("----Program Arguments----"); for (String arg : args) { logger.info(arg); } if (environment != null) { getActiveProfiles(); logger.info("----Environment Properties----"); logger.info("server.port: " + environment.getProperty("server.port")); logger.info("nl.amis.environment: " + environment.getProperty("nl.amis.environment")); logger.info("spring.datasource.url: " + environment.getProperty("spring.datasource.url")); logger.info("spring.datasource.username: " + environment.getProperty("spring.datasource.username")); logger.info("spring.datasource.password: " + environment.getProperty("spring.datasource.password")); logger.info("spring.jpa.database-platform: " + environment.getProperty("spring.jpa.database-platform")); logger.info("spring.jpa.hibernate.ddl-auto: " + environment.getProperty("spring.jpa.hibernate.ddl-auto")); } logger.info("----End logging BooksServiceApplication----"); } private void getActiveProfiles() { for (final String profileName : environment.getActiveProfiles()) { logger.info("Currently active profile - " + profileName); } } public static void main(String[] args) { SpringApplication.run(BooksServiceApplication.class, args); } }
Within IntelliJ IDEA, I chose the BooksServiceApplication | Run BooksServiceApplication main(). The response from running the application is:
To test the logging, next I added some program arguments (space separated).
Within IntelliJ IDEA, I chose the BooksServiceApplication | Run BooksServiceApplication main(). The response from running the application is:
To test the logging even further, next I added an environment variable.
Within IntelliJ IDEA, I chose the BooksServiceApplication | Run BooksServiceApplication main(). The response from running the application is:
As you can see by using an environment variable, you can also change the port that the BooksServiceApplication is running on.
To test the logging even further, next I added an environment variable to set the profile to developement.
Within IntelliJ IDEA, I chose the BooksServiceApplication | Run BooksServiceApplication main(). The response from running the application is:
To test the logging even further, next I added an environment variable to set the profile to testing.
Within IntelliJ IDEA, I chose the BooksServiceApplication | Run BooksServiceApplication main(). The response from running the application is:
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174) ~[mysql-connector-java-8.0.15.jar:8.0.15]
As expected this results in an error, because the MySQL database is not yet up and running. This will be addressed when we are going to use Minikube (see my next article).
Creating an Executable Jar
From IntelliJ, via Maven Projects | package | Run ‘books_service [package]’, I created an Executable Jar.
This creates a jar file:
…\ applications\books_service\target\books_service-1.0.0-SNAPSHOT.jar
With the following output:
Process finished with exit code 0
Creating another version of the BooksServiceApplication
For demo purposes I also wanted another version of the BooksServiceApplication with some code changes.
To keep it simple I only added some extra fields to Book.java.
package nl.amis.demo.services.books_service.domain; import javax.persistence.Entity; import javax.persistence.Id; import java.util.Date; @Entity public class Book { @Id private String id = ""; private String title = ""; private Date publishDate = null; private String author = ""; private String type = ""; private double price = 0; private int numOfPages = 0; private String publisher = ""; private String language = ""; private String isbn13 = ""; public Book() { } public Book(String id, String title, Date publishDate, String author, String type, double price, int numOfPages, String publisher, String language, String isbn13) { this.id = id; this.title = title; this.publishDate = publishDate; this.author = author; this.type = type; this.price = price; this.numOfPages = numOfPages; this.publisher = publisher; this.language = language; this.isbn13 = isbn13; } public String getId() { return id; } public String getTitle() { return title; } public Date getPublishDate() { return publishDate; } public String getAuthor() { return author; } public String getType() { return type; } public double getPrice() { return price; } public int getNumOfPages() { return numOfPages; } public String getPublisher() { return publisher; } public String getLanguage() { return language; } public String getIsbn13() { return isbn13; } }
In the pom.xml, I changed the version from:
<version>1.0.0-SNAPSHOT</version>
to
<version>2.0.0-SNAPSHOT</version>
Within IntelliJ IDEA I removed the program arguments and environment variables in the Run/Debug Configurations, so that the default application.properties file was going to be used.
From IntelliJ IDEA, via Maven Projects | package | Run ‘books_service [package]’, I created an Executable Jar.
This creates a jar file:
…\ applications\books_service\target\books_service-2.0.0-SNAPSHOT.jar
With the following output:
Process finished with exit code 0
Postman
From Postman I invoked a request named “PostBook1Request” (with method “POST” and URL “http://locahost:9090/books”) and a response with “Status 200 OK” was shown:
Next, from Postman I invoked a request named “PostBook2Request” (with method “POST” and URL “http://locahost:9090/books”) and a response with “Status 200 OK” was shown:
Then I wanted to check if the books were added to the catalog.
From Postman I invoked a request named “GetAllBooksRequest” (with method “GET” and URL “http://locahost:9090/books”) and a response with “Status 200 OK” was shown:
As you can see, both books that I added, were returned from the catalog.
With these final checks I conclude this article.
Version 1.0 and 2.0 of the BooksServiceApplication executable jar are created and ready to be used in Minikube.
But more about that, you can read in my next article.
I got this error:
Local Exception Stack:
Exception [EclipseLink-4002] (Eclipse Persistence Services – 2.7.7.v20200504-69f2c2b80d): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: org.h2.jdbc.JdbcSQLSyntaxErrorException:
Syntax error in SQL statement “SELECT SEQ_AUTORISATION_TAURUS.NEXTVAL FROM[*] DUAL”; expected “identifier”; SQL statement:
SELECT SEQ_AUTORISATION_TAURUS.NEXTVAL FROM DUAL [42001-200]
Error Code: 42001
Call: SELECT SEQ_AUTORISATION_TAURUS.NEXTVAL FROM DUAL
Query: ValueReadQuery(sql=”SELECT SEQ_AUTORISATION_TAURUS.NEXTVAL FROM DUAL”)
A good guide, but including politics in it was distracting, uncomfortable, and unnecessary.
Thank you Marc, very nice tutorial. For learning purposes, it would be nice to load initial data from a sql file, this can be accomplished by putting a “import.sql” file, having the inserts, in the resources folder, or adding the property “spring.datasource.data=classpath:setup.sql” to application.properties
please share me your source code please
nattaphon@javaverse.dev
Here you can find the code of the RESTful Web Service Spring Boot application belonging to my article:
https://github.com/marclameriks/amis-technology-blog-2019-02-2-books-service-2.0
This code represents version 2.0 of the application.
Good luck with trying it out.
Hi Marc, can you provide/share your code to compare and test the application?
Hello Rodrigo,
Please send me an email (at: marc.lameriks@amis.nl) and I will provide you with the source code of the RESTful Web Service Spring Boot application belonging to my article.
Your currently provided email address is rejected.