Spring REST with JSON Example
RESTful web services using JSON data format address the problems with SOAP web services using XML. REST can output data in different formats like Comma Separated Values (CSV) or Really Simple Syndication (RSS), but the most popular format is JSON. Compared to XML, JSON is not only more human readable but also lightweight. It’s easier for the browser to take a JSON data structure and get its JavaScript structure and the processing time is lesser. Development with SOAP involves more code and at times becomes unwieldy. The biggest advantage for the REST + JSON mechanism is a smaller learning curve for newbies, which further gets accelerated with Spring Boot.
Table of Contents
1. Introduction
In this article, we will show how to build a RESTful web service that uses JSON format for data in the request and response to a Spring Boot application. The key aspect of the RESTful design is to conceptualize your data as resources. The resources could be anything from a map showing a location to a software download. The HTTP actions (verbs) are used in a meaningfully semantic manner in conjunction with the Universal Resource Indicator (URI) to deliver application functionality. This is best illustrated with database records.
2. Application
The application we will develop is a web service that handles Tickets as in a bug-tracking or task-tracking system. A Ticket
has a description and many comments
. A Comment
has a Text
field. In our application, data is persisted to an in-memory H2 database. We use Spring Data JPA for the database operations.
Thus, the web service will offer a RESTful interface to the database operations on a ticket. The Create (C), Read (R), Update (U) and Delete (D) operations are mapped to POST (Po), GET (G), PUT (Pu) and DELETE (D) actions of HTTP. To use the acronyms as a helpful mnemonic, the database CRUD is mapped to HTTP PoGPuD.
3. Environment
I have used the following technologies for this application:
- Java 1.8
- Spring Boot 1.5.9
- Maven 3.3.9
- Ubuntu 16.04 LTS
4. Source Code
This is a maven-based project, so all the project-level settings and dependencies are given in pom.xml file.
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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.javacodegeeks.webservices.rest</groupId> <artifactId>ticket</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>ticket</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </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>
application.properties
spring.h2.console.enabled=true
This configuration is required to enable browser access to the H2 database, since we are not using Spring Boot’s developer tools. Additionally, we made sure to include com.h2database:h2
is on the classpath via a dependency in pom.xml.
TicketApplication.java
package org.javacodegeeks.webservices.rest.ticket; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TicketApplication { public static void main(String[] args) { SpringApplication.run(TicketApplication.class, args); } }
This is the main class of the application that runs on the default Tomcat container of Spring Boot at port 8080.
Ticket.java
package org.javacodegeeks.webservices.rest.ticket.domain; import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Entity @Getter @Setter @NoArgsConstructor public class Ticket { @Id @GeneratedValue(strategy=GenerationType.AUTO) @Column(name="TICKET_ID") private Long ticketId; private String description; @OneToMany(mappedBy="ticket", cascade=CascadeType.ALL) private List comments = new ArrayList(); }
This is the chief domain class of the application. The @Entity
annotation specifies that this class is mapped to a database table and since we don’t have the @Table
annotation, the table name will be the same as the class name. The three lombok annotations, @Getter
, @Setter
, and @NoArgsConstructor
respectively create the getters and setters to the fields and a default no-argument constructor.
The field ticketId
is annotated with @Id
, @GeneratedValue(strategy=GenerationType.AUTO)
and @Column(name="TICKET_ID")
specifying that it is the key column with the name TICKET_ID
and whose value should be automatically generated.
A Ticket
has many comments
which are stored in an ArrayList
. The annotation @OneToMany(mappedBy="ticket", cascade=CascadeType.ALL)
specifies the database side of the relationship indicating that Ticket
is the owner of the bi-directional relationship and that changes to a Ticket
are to propagated to all the child records.
Comment.java
package org.javacodegeeks.webservices.rest.ticket.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Entity @Getter @Setter @NoArgsConstructor public class Comment { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(name="COMMENT_ID") private Long commentId; private String text; @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name="TICKET_ID") @JsonIgnore private Ticket ticket; }
As in Ticket
, this class also uses @Entity
, @Getter
, @Setter
, @NoArgsConstructor
, @Id
, @GeneratedValue
and @Column
annotations. The important annotation here is @ManyToOne
annotation indicating the reverse side of the relationship with Ticket
. The @JoinColumn
annotation specifies that the foreign key is TEXT_ID
. The @JsonIgnore
is used to avoid the parent record’s attributes getting parsed into the output.
TicketRepository.java
package org.javacodegeeks.webservices.rest.ticket.service; import org.javacodegeeks.webservices.rest.ticket.domain.Ticket; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface TicketRepository extends JpaRepository<Ticket, Long> { }
The @Repository
annotation on this interface allows it to import standard DAO routines into the runtime environment and also makes it eligible for Spring DataAccessException
translation.
CommentRepository.java
package org.javacodegeeks.webservices.rest.ticket.service; import org.javacodegeeks.webservices.rest.ticket.domain.Comment; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface CommentRepository extends JpaRepository<Comment, Long> { }
This is the repository interface for the Comment
class using the @Repository
annotation.
TicketEndpoint.java
package org.javacodegeeks.webservices.rest.ticket.endpoint; import java.util.List; import org.javacodegeeks.webservices.rest.ticket.domain.Comment; import org.javacodegeeks.webservices.rest.ticket.domain.Ticket; import org.javacodegeeks.webservices.rest.ticket.service.TicketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class TicketEndpoint { @Autowired private TicketService ticketService; // -------------------------------------------- // CRUD OPERATIONS FOR PARENT RECORDS (TICKETS) @PostMapping("/tickets") public Ticket createTicket(@RequestBody Ticket ticket) { Ticket savedTicket = ticketService.createTicket(ticket); return savedTicket; } @GetMapping("/tickets") public List getAllTickets() { return ticketService.findAll(); } @GetMapping("/tickets/{id}") public Ticket getTicket(@PathVariable long id) { return ticketService.findTicket(id); } @PutMapping("/tickets/{id}") public Ticket changeTicket(@PathVariable long id, @RequestBody Ticket ticket) { return ticketService.updateTicket(id, ticket); } @DeleteMapping("/tickets/{id}") public String deleteTicket(@PathVariable long id) { ticketService.deleteById(id); return String.format("Ticket id #%d successfully deleted", id); } // -------------------------------------------- // CRUD OPERATIONS FOR CHILD RECORDS (COMMENTS) @PostMapping("/tickets/{id}/comments") public Ticket createComment(@PathVariable long id, @RequestBody Comment comment) { return ticketService.createComment(id, comment); } @GetMapping("/tickets/{id}/comments") public List getAllComments(@PathVariable long id) { return ticketService.findAllComments(id); } @GetMapping("/tickets/comments/{id}") public Comment getComment(@PathVariable long id) { return ticketService.findComment(id); } @PutMapping("/tickets/comments/{id}") public Comment changeComment(@PathVariable long id, @RequestBody Comment comment) { return ticketService.updateComment(id, comment); } @DeleteMapping("/tickets/comments/{id}") public String deleteComment(@PathVariable long id) { ticketService.deleteCommentById(id); return String.format("Comment id %d successfully deleted", id); } }
This class is an end point for the REST clients as specified by the @RestController
annotation. A TicketService
bean is auto wired into this class with the @Autowired
annotation. The key design to note here is that it offers the endpoint to both the ticket and comment server side operations. The intuition behind this design is that comments do not have an independent existence; they belong to a Ticket
. Therefore in the service class, there are ten methods five each for the ticket and comments functionality. The create methods createTicket
and createComment
are annotated with @PostMapping
annotation, the read methods getAllTickets
, getTicket
, getAllComments
and getComment
are annotated with @GetMapping
annotation, the update methods changeTicket
and changeComment
are annotated with @PutMapping
annotation and finally, the delete methods deleteTicket
and deleteComment
are annotated with @DeleteMapping
annotation. To reiterate, database CRUD is mapped to HTTP PoGPuD.
The @PathVariable
annotation indicates the argument is part of the URI and the @RequestBody
annotation specifies which object the HTTP body is de-serialized to.
TicketService.java
package org.javacodegeeks.webservices.rest.ticket.service; import java.util.List; import org.javacodegeeks.webservices.rest.ticket.domain.Comment; import org.javacodegeeks.webservices.rest.ticket.domain.Ticket; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class TicketService { @Autowired private TicketRepository ticketRepository; // -------------------------------------------- // CRUD OPERATIONS FOR PARENT RECORDS (TICKETS) public Ticket createTicket(Ticket ticket) { return ticketRepository.save(ticket); } public List findAll() { return ticketRepository.findAll(); } public Ticket findTicket(long id) { return ticketRepository.findOne(id); } public Ticket updateTicket(long id, Ticket ticket) { Ticket updatedTicket = findTicket(id); if (!ticket.getDescription().equals(updatedTicket.getDescription())) { updatedTicket.setDescription(ticket.getDescription()); return ticketRepository.save(updatedTicket); } else return null; } public void deleteById(long id) { ticketRepository.delete(id); } @Autowired private CommentRepository commentRepository; // -------------------------------------------- // CRUD OPERATIONS FOR CHILD RECORDS (COMMENTS) public Ticket createComment(long ticketId, Comment comment) { Ticket ticket = findTicket(ticketId); comment.setTicket(ticket); ticket.getComments().add(comment); return ticketRepository.save(ticket); } public List findAllComments(long ticketId) { return findTicket(ticketId).getComments(); } public Comment findComment(long id) { return commentRepository.findOne(id); } public Comment updateComment(long commentId, Comment comment) { Comment savedComment = commentRepository.findOne(commentId); savedComment.setText(comment.getText()); commentRepository.save(savedComment); return savedComment; } public void deleteCommentById(long id) { commentRepository.delete(id); } }
This is the business service class specified with the @Service
annotation. It has two repository beans TicketRepository
and CommentRepository
autowired into it. The create methods invoke the repository save method. The findAll
method invokes the repository findAll
method. Similarly, the findTicket
and deleteById
method invoke the repository methods findOne
and delete
. The updateTicket
method takes in an id value, fetches the ticket record from the database, and if the description is not the same as the one passed in with the request body, it changes the description and saves the changed record back in the database.
For the comments, the createComment
method first fetches the parent ticket from the database, adds the comment to the ArrayList
of comments
and then invokes the repository save method to persist the record. The findAllComments
method fetches the parent ticket by calling the findTicket
method and returns the comments list by invoking the getter. The findComment
and deleteCommentById
methods invoke the repository methods findOne
and delete
respectively. The updateComment
method takes in an id
value, fetches the comment record from the database, sets the text to that passed in with the request body and saves the changed record back to the database.
5. How To Run and Test
In a terminal window, change directory to the root folder of the application ticket and enter
mvn spring-boot:run
This will start the application.
In another terminal window, change directory to ticket/src/main/resources
and run the file data.sh. This file uses curl
command to make POST requests “/tickets” and “/tickets/{id}/comments” to create three posts and three comments each for them.
You can check that these 12 records in the database. In a browser window, go the URL http://localhost:8080/h2-console/
. Make sure the JDBC URL is jdbc:h2:mem:testdb
. Hit Connect button.
In the next screen run the SQL statements SELECT * from TICKET;
and SELECT * FROM COMMENT;
to see the database records. The screenshots are given below.
For the next steps, we can use any REST client like the Advanced REST Client chrome extension or even SoapUI. I used the Postman application. Let’s test with three REST calls to
i) Delete the second ticket
ii) Modify the second comment of the first ticket
iii) Delete the third comment of the third ticket
For i) we send a DELETE
request to localhost:8080/tickets/2
. This will delete the child comments records too. You should see a confirmation message, “Ticket id #2 successfully deleted” as the response.
For ii) we send a PUT request to localhost:8080/tickets/comments/2
since the id of the comment record in the database is 2. In Postman, in the Body panel, select the radio option raw and from the drop down list at the right, select JSON (application/json). For the input, enter “text” : “First ticket, modified second comment” and click Send. You should see the changed comment in the response box. The screenshot for this step is given below:
For iii) we send a DELETE
request to localhost:8080/tickets/comments/9
since the id of the comment record in the database is 9.
After doing these three steps, the database should have two tickets and five comments, one of which is modified from its original value. You can check them in the browser via h2-console or in the terminal window by running
curl -X GET http://localhost:8080/tickets | jq .
This command’s output will be as shown in the following screenshot
6. Summary
In this article, we have seen how to use Spring framework to implement a RESTful web service that uses JSON for the request and response. We have seen how to map the HTTP actions to database operations. There are many other aspects of REST web services that are vital in real world implementations. These aspects are security, versioning, connectedness using HATEOAS (Hypermedia As The Engine Of Application State), meaningful messages in Exceptions, internationalization and so on. This article covers the basics and positions you to explore those advance areas.
7. Useful Links
8. Download the Source Code
You can download the full source code of this example here: ticket.zip