Before we start, this post is mostly aimed at Java developers, specifically Java developers who use the Spring umbrella of frameworks for development. If you are not a Java developer, you can still read on, chances are you haven't found the love of your life - in terms of languages - yet.
Also, I apologise in advance for the markup mishaps that will pop up here and there in various code sections. You could check out the original blog post here Original Post if you have trouble understanding/copy pasting the code.
First off, what really is Spring?
If you are a java developer, you've probably stumbled upon the term 'Spring' at some point in your journey, and possibly fell face first at either its learning curve, or its complexity. One thing most developers fail to understand - I also struggled with this at first - is that Spring represents a collection of frameworks that are tailored to meet specific development needs. As an example, if you're a java web developer, Spring provides the Web Servlet Framework for web development where Spring MVC (Included in this framework) is built on top of the Servlet API. Therefore, you need not learn all the frameworks that Spring provides, but rather the frameworks that fit your specific use case. Yeah, that's a shortcut, and yes, you're welcome.
If you've never heard about Spring before, Spring is an Inversion of Control and Dependency injection framework. This are fairly big terms but this comprehensive post will help you understand the meanings of these two concepts: IoC and Dependency Injection
Now onto Spring Boot
If you have used Spring MVC before, you've definitely have had to wrestle with Spring MVC's pre-configurations like Setting up the Dispatcher Servlet etc. etc. before you were able to get the framework up and running. This is where Spring Boot comes in. Spring Boot is an auto-configuration tool for setting up your Spring-powered applications. You can now put away those boxing gloves cause you might not need to wrestle with Spring Boot.
To help you understand Spring Boot further, and shine a light on why you should be using it if you already aren't, we'll build a simple Netflix API that allows client devices to register themselves, suggest movies and query movies.
Let us begin
Step 1: Setting up Spring Boot on your application.
Spring offers a project initializer, Spring Initialzr that allows you to select your project specifications and download an already configured Spring Boot project as a zip file or a maven build file. You could skip to step 2 if you have done this.
If you're a more of a hands on type of person who enjoys understanding what's happening under the hood, you can continue with this step.
Folder Structure.
Create a new Java project with you favourite IDE and configure your folder structure to mimic the following design:
└── src
└── main
└── controllers
└── models
└── repositories
└── resources
└── templates
└── error.html
└── application.properties
└── Application.java
contollers - This folder will contain the controllers we define for this project
repositories - This folder will contain the repositories we'll define for our models that will be used to fetch data from the database.
resources - this folder will contain our project resources. The templates folder contains our template files that will be rendered by Spring. You can include other folders like static which will be used to server static content like javascript and css files.
Maven dependencies
Spring Boot allows us to include in our pom.xml file all the Spring dependencies that we'll use in our project. Copy paste the following dependencies, together with the Spring Boot Maven Plugin to your pom.xml.
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-spring-boot</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencies>
<!--Spring dependencies-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!--Spring JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Our dependencies overview:
1. spring-boot-starter-web - When building web applications using java, we often need other external dependencies that we include in our pom.xml like tomcat and Spring MVC. What spring-boot-starter-web does is add all these dependencies through one single dependency.
2. spring-boot-starter-thymeleaf - If you've never used thymeleaf before, thymeleaf is a templating engine for processing and creating HTML, XML, JavaScript, CSS, and text whose template files retain the .html extension and therefore a better alternative to JSPs (Java Server Pages). What this basically means is that you can run thymeleaf template files like normal web pages without a backend server for template processing as in the case of JSPs.
3. spring-boot-devtools - These tools grease your gears of development therefore making the overall development process more bearable. To learn more about what these tools offer, you can check out this link: spring-boot-devtools
4. mysql-connector-java - These is the MySQL JDBC implementation that we'll use to make connections to our MySQL database.
5. spring-boot-starter-data-jpa - Most if not all web applications need some form of persistence, which in java cases, is often JPA (Java Persistence API). If spring-boot-data-jpa is in the classpath, Spring boot will automatically configure our data-source through reading our database configuration from the application.properties file that we will configure next.
Note that we've set our java version to 1.8 since JDK 11 does not offer a lot of things out of the box and therefore you may run into errors like: springboot: org.hibernate.MappingException: Could not get constructor for org.hibernate.persister.entity.SingleTableEntityPersister
Application.properties file
Spring boot automatically reads configuration settings from this file and configures our spring boot environment accordingly. We'll configure our database here and also at the same time disable Spring boot's whitelabel error page which we'll replace with our own custom error page. You can copy paste all this into your own application.properties file if you do not intended to make any changes.
## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url = jdbc:mysql://localhost:3306/netflix?useSSL=false
spring.datasource.username = netflix
spring.datasource.password = netflix
## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
#Disabling the whitelabel error page
server.error.whitelabel.enabled=false
In the above application.properties file, We've configured our database, username and password to netflix . You can configure this if you want to. Spring JPA automatically uses the Hibernate implementation of JPA.
We've set spring.jpa.hibernate.ddl-auto
to update
which will ensure that any changes we make to our models will be reflected in our Database, which also includes creating a new model. Please note that this option is only suitable for development environments rather than production environments. For more information, you can check this link: Database Initialization.
We've also set server.error.whitelabel.enabled to false to disable Spring boot's whitelabel error pages which we'll replace with our own custom error page.
Configuring our Application.java file
This file will contain the main method which we'll use to ignite our Spring Application with. Copy paste the following to your Application.java file:
package main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableJpaRepositories(basePackages="main.repositories")
@EnableTransactionManagement
@EnableJpaAuditing
@EntityScan(basePackages={"main.entities","main.models"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@SpringBootApplication is a combination of the following more specific spring annotations -
1. @Configuration : Any class annotated with @Configuration annotation is bootstrapped by Spring and is also considered as a source of other bean definitions.
2. @EnableAutoConfiguration : This annotation tells Spring to automatically configure your application based on the dependencies that you have added in the pom.xml file. For example, If spring-data-jpa or spring-jdbc is in the classpath, then it automatically tries to configure a DataSource by reading the database properties from application.properties file.
3. @ComponentScan : It tells Spring to scan and bootstrap other components defined in the current package (main) and all the sub-packages.
@EnableJpaAuditing is used to support the automatic filling of fields that we'll annotate with @CreatedDate.
@EnableJpaRepositories tells Spring where to find our defined Repositories, since we'll not be using the @Repository
annotation.
Step 2. Coding our Controllers.
We'll create only 3 Contollers namely: CustomErrorController
that we'll use to format and serve our custom error page, MoviesController
that will perform movie related functions and UsersContoller
that will perform user related functions.
CustomErrorController
In this controller, we'll register a route error
that will be mapped to our renderErrorPage
method. Therefore all requests made through the error
route will be recieved by our method.
Note that here we'll use the @Controller
annotation since we'd like to return a view rather than plain text and therefore our method returning a string will return the name of the view. To return plain text rather than views, use the @RestController
annotation.
We will also format our error messages to make them more user friendly when we display them on our error page.
We've also implemented the ErrorController
interface and overridden the getErrorPath()
method which will automatically be invoked when Spring encounters an error.
@Controller
public class CustomErrorController implements ErrorController {
@RequestMapping(value = "error",produces = "application/json;charset=UTF-8")
public String renderErrorPage(HttpServletRequest request, Model model) {
String errorMsg = "";
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
int httpErrorCode = 404;
if(status != null){
httpErrorCode = Integer.valueOf(status.toString());
}
switch (httpErrorCode) {
case 400: {
errorMsg = "Http Error Code: 400. Bad Request";
break;
}
case 401: {
errorMsg = "Http Error Code: 401. Unauthorized";
break;
}
case 404: {
errorMsg = "Http Error Code: 404. Resource not found";
break;
}
case 500: {
errorMsg = "Http Error Code: 500. Internal Server Error";
break;
}
}
model.addAttribute("error",errorMsg);
return "error";
}
@Override
public String getErrorPath() {
return "/error";
}
}
MoviesController
As we have stated earlier, this Controller will store functionalities related to our movies.
Since we are creating an api, we'll map api requests to url patterns that start with /api
. Therefore, we add a @RequestMapping annotation on top of the class, rather than the method so that every request url we map on our methods will be appended to /api
.
@RestController
@RequestMapping(value = "/api",produces = "application/json;charset=UTF-8") //All our api request URLs will start with /api and will return Json
public class MoviesController {
private MoviesRepository moviesRepository;
private CategoriesRepository categoriesRepository;
private UserRepository userRepository;
@Autowired
public MoviesController(MoviesRepository moviesRepository, CategoriesRepository categoriesRepository, UserRepository userRepository){
this.moviesRepository = moviesRepository;
this.categoriesRepository = categoriesRepository;
this.userRepository = userRepository;
}
//Suggest A movie
@GetMapping(value = "/suggestMovie")
public String suggestMovie(@RequestParam(name = "category_id") Long categoryId,@RequestParam(name = "name")String name
,@RequestParam(name = "suggested_by")Long suggestedBy){
//Movies added through this API route are automatically marked as suggested.
String movieType = Movies.MovieType.SUGGESTED.getMovieType();
Movies movies = new Movies();
//Provided category id should be in our categories table.
if(categoriesRepository.findById(categoryId).isPresent()){
if(userRepository.findById(suggestedBy).isPresent()){
movies.setCategoryId(categoryId);
movies.setName(name);
movies.setType(movieType);
movies.setSuggestedBy(suggestedBy);
return moviesRepository.save(movies).toString();
} else {
return "{'error':'The specified user id does not exist.'}";
}
} else {
return "{'error':'The specified category id does not exist.'}";
}
}
//delete a suggested movie
@GetMapping(value = "/deleteMovie")
public String deleteMovie(@RequestParam(name = "movie_id") Long movieId,@RequestParam(name = "user_id")Long userId) {
if(userRepository.findById(userId).isPresent()){
Optional<Movies> movies = moviesRepository.findById(movieId);
if(movies.isPresent()){
List<Movies> movie = moviesRepository.findBySuggestedByEqualsAndIdEquals(userId,movieId);
if(movie.size()>0){
moviesRepository.delete(movie.get(0));
return movie.toString();
} else {
return generateErrorResponse("The user specified cannot delete this movie");
}
} else {
return generateErrorResponse("Specified movie id does not exist");
}
} else {
return generateErrorResponse("Specified user id does not exist");
}
}
//update a suggested movie. Supports only updating of the movie name or category.
@GetMapping(value = "/updateMovie/{movie_id}")
public String updateMovie(@PathVariable(name = "movie_id") Long movieId,@RequestParam(name = "user_id")Long userId,
@RequestParam(name = "movie_name",required = false)String movieName, @RequestParam(name = "movie_category",required = false) Long movieCategory) {
List<Movies> movie = moviesRepository.findBySuggestedByEqualsAndIdEquals(userId,movieId);
if(!(movie.size()>0)){
return generateErrorResponse("The user specified cannot update this movie");
}
if(moviesRepository.findById(movieId).isPresent()){
Movies movies = moviesRepository.findById(movieId).get();
if(movieName != null && !movieName.isEmpty()){
movies.setName(movieName);
}
if(movieCategory != null && categoriesRepository.findById(movieCategory).isPresent()){
movies.setCategoryId(movieCategory);
}
return moviesRepository.save(movies).toString();
} else {
return generateErrorResponse("The specified movie id does not exist");
}
}
//query available movies
@GetMapping(value = "/queryMovies/{categoryId}")
public String queryMovies(@PathVariable Long categoryId,@RequestParam(name = "type") String type){
JsonObjectBuilder jsonResponse = Json.createObjectBuilder();
JsonObjectBuilder temp = Json.createObjectBuilder();
int count = 0;
for(Movies movie:moviesRepository.findAllByCategoryIdEqualsAndTypeEquals(categoryId,type)){
temp.add("id",movie.getId());
temp.add("name",movie.getName());
temp.add("type",movie.getType());
temp.add("category_id",movie.getCategoryId());
temp.add("created_at",movie.getCreatedAt().toString());
jsonResponse.add(count + "",temp);
temp = Json.createObjectBuilder();
count++;
}
return jsonResponse.build().toString();
}
private String generateErrorResponse(String message){
return "{\"error\":\"" + message + "\"";
}
//add categories
@GetMapping(value = "/addCategories")
public String addCategories(@RequestParam(name = "name") String name){
Categories categories = new Categories();
categories.setName(name);
return categoriesRepository.save(categories).toString();
}
}
In this Class, you may have noticed annotations that you might have not seen before. Let's go through them quickly:
1. @Autowired
- As the annotation itself suggests, this annotation automatically injects an implementation of the movies, users and categories repository interface which we assign the the field variables we have declared. As we mentioned earlier, you need a repository to be able to access database contents, which explains these three repositories. I'll explain this further when we reach the repositories section.
2. @GetMapping
- This annotation is the same as @RequestMapping
except that it only maps get requests to the specified url.
3. @RequestParam
- This annotation automatically injects the specified query parameter name to this variable.
4. @PathVariable` - This annotation automatically injects the path value - enclosed in curly braces - to this variable.
Users Controller
This controller will contain functionalities related to users. In this case, we'll define only a single method that will be responsible for creating a user.
`
@RestController
@RequestMapping(value = "/api",produces = "application/json;charset=UTF-8") //All our api request URLs will start with /api and return Json
public class UsersController {
private UserRepository userRepository;
@Autowired
public UsersController(UserRepository userRepository){
this.userRepository = userRepository;
}
@GetMapping(path = "/addUser")
public String addUser(@RequestParam(name = "id")Long id, @RequestParam(name="name") String name) {
Users users = new Users();
users.setId(id);
users.setName(name);
users = userRepository.save(users);
return users.toString();
}
}
`
Our user IDs in this case will not be auto-generated but instead, we'll provide users with an option to define their own IDs.
Step 3. Defining our Repositories
Repositories will be used by our models to query data from the Database. spring-jpa comes with a JpaRepository
interface that defines all CRUD operations that we can perform on an Entity. We'll use the CrudRepository
implementation of JpaRespository
as it offers many CRUD operations out of the box through methods like findAll(), save() etc. At the same time, CrudRepository
automatically generated for us dynamic queries based on method names as we'll see in the following example.
We'll define three repositories for our three entities: CategoriesRepository
, MoviesRepository
and UsersRepository
, which will all be interfaces extending CrudRepository
.
CategoriesRepository
public interface CategoriesRepository extends CrudRepository<Categories,Long> {
}
MoviesRepository
`
public interface MoviesRepository extends CrudRepository {
List<Movies> findAllByCategoryIdEqualsAndTypeEquals(Long categoryId,String type);
List<Movies> findBySuggestedByEqualsAndIdEquals(Long suggestedBy,Long movieId);
}
`
In this repository, notice the abstract methods we have defined. Extending CrudRepository
will automatically compel Spring to create an implementation of these methods automatically at run-time just from the definition of the method name. To add Custom methods, we can add them in the following ways:
We can start our query method names with
find...By
,read...By
,query...By
,count...By
, andget...By
. BeforeBy
we can add expression such asDistinct
. After By we need to add property names of our entity.To get data on the basis of more than one property we can concatenate property names using
And
andOr
while creating method names.If we want to use completely custom name for our method, we can use
@Query
annotation to write query.
UsersRepository
`
@Repository
public interface UserRepository extends CrudRepository {
}
`
Final Step: Defining our models.
The models (Entities) that we define will be used to store our table structures as will be defined in the database. We will therefore have three models for our three tables: Categories
, Movies
and Users
.
Categories Model
`
@Entity
@Table(name = "categories")
public class Categories {
@Id
@GeneratedValue
private Long id;
@NotBlank
private String name;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
JsonObjectBuilder builder = Json.createObjectBuilder();
//serialize to Json only if the data was persisted.
if(!Objects.isNull(id)){
builder.add("id",id);
}
if(!Objects.isNull(name)){
builder.add("name",name);
}
return builder.build().toString();
}
}
`
An entity is a plain old Java object (POJO) class that is mapped to the database and configured for usage through JPA using annotations and/or XML.
Note that we've included a @Table
annotation to explicitly define the name of our table. The @Id
annotation automatically declares the created field as a primary key for our table in our database. At the same time, the @GeneratedValue
annotation will automatically generate a value and store it in the database during saving of a record, pretty much like an auto-increment field. The @NotBlank
annotation will automatically validate values that will be inserted into the name
variable we defined and ensure that this field is not blank.
We've also defined our own toString
method (overriding the superclass's toString method) that will convert our model to a Json string that we return as a response in our controllers.
Movies Model
`
@Entity
@Table(name = "movies")
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(value = {"createdAt"},allowGetters = true)
public class Movies implements Serializable {
@Id
@GeneratedValue
private Long id;
private Long categoryId;
@NotBlank
private String type;
@NotBlank
private String name;
private Long suggestedBy;
@Column(nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP)
@CreatedDate
private Date createdAt; //Stores the date at which a user was created.
@PrePersist
public void prePersist(){
createdAt = new Date();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCategoryId() {
return categoryId;
}
public void setCategoryId(Long categoryId) {
this.categoryId = categoryId;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreatedAt() {
return createdAt;
}
@Override
public String toString() {
JsonObjectBuilder builder = Json.createObjectBuilder();
//serialize to Json only if the data was persisted.
if(!Objects.isNull(id)){
builder.add("id",id);
}
if(!Objects.isNull(name)){
builder.add("name",name);
}
if(!Objects.isNull(categoryId)){
builder.add("category_id",categoryId);
}
if(!Objects.isNull(createdAt)) {
builder.add("created_at",createdAt.toString());
}
return builder.build().toString();
}
public Long getSuggestedBy() {
return suggestedBy;
}
public void setSuggestedBy(Long suggestedBy) {
this.suggestedBy = suggestedBy;
}
public enum MovieType{
SUGGESTED("suggested"),ORIGINAL("original");
private String movieType;
MovieType(String movieType){
this.movieType = movieType;
}
public String getMovieType() {
return movieType;
}
}
}
`
In this model, note the annotations below:
1. @EntityListeners(AuditingEntityListener.class)
- This will attach an entity listener to our model class that will automatically fill the fields we've annotated with @CreatedAt
.
2. `@PrePersist - This annotation will ensure that the automatically generated value for the createdAt field is stored in this field whenever we'll need access. For more information on Database Auditing you can check this link: Database Auditing
Users Model
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(value = {"createdAt"},
allowGetters = true)
public class Users implements Serializable {
private static final long serialVersionUID = 2L;
@Column(updatable = false)
@Id
private Long id;
@NotBlank(message = "The field 'name' is mandatory.")
private String name;
@Column(nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP)
@CreatedDate
private Date createdAt; //Stores the date at which a user was created.
@PrePersist
public void prePersist(){
createdAt = new Date();
}
public void setId(long id) {
this.id = id;
}
public long getId() {
return id;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
JsonObjectBuilder builder = Json.createObjectBuilder();
//serialize to Json only if the data was persisted.
if(!Objects.isNull(id)){
builder.add("id",id);
}
if(!Objects.isNull(name)){
builder.add("name",name);
}
if(!Objects.isNull(createdAt)) {
builder.add("created_at",createdAt.toString());
}
return builder.build().toString();
}
public Date getCreatedAt() {
return createdAt;
}
}
The Custom Error Page Template
In the templates folder we defined, create a html page and name it error.html and copy paste the following code into it:
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Error</title>
</head>
<body>
<div>Web Application. Error : th:text="${error}"</div>
</body>
</html>
thymeleaf will automatically parse this html page and render our error message by replacing the th:text
attribute.
Finally
Run your Application.java's main method and test out your netflix api on your browser by navigation to localhost:8080/. You should be able to see your json responses on your browser. Alternatively, you can check out my git repository for the source code and a client you can test your code with: github repo
Conclusion
You've successfully made a netflix api using Spring boot, mysql and JPA. Congrats! For more posts like this, you could check out my personal blog @ balysnotes.com
Top comments (0)