Create a JSON API REST Service With Spring Boot and Elide
Learn how to make a HSON API REST service using Elide and Spring Boot in this awesome tutorial. The JSON API is a great specification for developers, and is getting much more popular.
Join the DZone community and get the full member experience.
Join For FreeJSON API is a specification that developers can reference to create rich JSON REST APIs. The specification is growing in popularity, and there are a number of open source libraries being developed to make implementing and consuming a JSON API compliant REST service quick and easy.
One of these libraries is Elide, which is being developed by Yahoo engineers to expose Java JPA entities through a JSON API compatible interface. The nice thing about Elide is if you already have JPA entities, then with just a few lines of code you can expose them through a JSON API REST interface.
To demonstrate this, I’m going to show you how to build a read-only JSON API RESTful service using JPA, Elide and Spring Boot.
Our application will be a stock standard Spring Boot REST application. I’m not going to go into too much detail for the boilerplate classes that make up a Spring Boot application because the Spring documentation already does a great job for that. I’ll run through these classes quickly, highlighting any customizations that were made to facilitate the integration with Elide.
Application.java
package com.matthewcasperson.elidetest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* The entry point to our spring boot application
*/
@SpringBootApplication
@EnableTransactionManagement // Elide requires transaction support, which we enable here
public class Application {
public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}
}
This is the entry point to the Spring boot application. The only change required to this class is the addition of the @EnableTransactionManagement annotation, which configures our application with transaction support. Transactions are required by Elide to function correctly.
ElideTest.java
package com.matthewcasperson.elidetest;
import com.yahoo.elide.Elide;
import com.yahoo.elide.ElideResponse;
import com.yahoo.elide.audit.Logger;
import com.yahoo.elide.audit.Slf4jLogger;
import com.yahoo.elide.core.DataStore;
import com.yahoo.elide.datastores.hibernate5.HibernateStore;
import org.hibernate.SessionFactory;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.HandlerMapping;
import javax.persistence.EntityManagerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.util.Map;
/**
* The rest interface
*/
@RestController
public class ElideTest {
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ElideTest.class);
@Autowired
private EntityManagerFactory emf;
/**
* Converts a plain map to a multivalued map
* @param input The original map
* @return A MultivaluedMap constructed from the input
*/
private MultivaluedMap<String, String> fromMap(final Map<String, String> input) {
return new MultivaluedHashMap<String, String>(input);
}
@CrossOrigin(origins = "*")
@RequestMapping(
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE,
value={"/{entity}", "/{entity}/{id}/relationships/{entity2}", "/{entity}/{id}/{child}", "/{entity}/{id}"})
@Transactional
public String jsonApiGet(@RequestParam final Map<String, String> allRequestParams, final HttpServletRequest request) {
/*
This gives us the full path that was used to call this endpoint.
*/
final String restOfTheUrl = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
/*
Elide works with the Hibernate SessionFactory, not the JPA EntityManagerFactory.
Fortunately we san unwrap the JPA EntityManagerFactory to get access to the
Hibernate SessionFactory.
*/
final SessionFactory sessionFactory = emf.unwrap(SessionFactory.class);
/*
Elide takes a hibernate session factory
*/
final DataStore dataStore = new HibernateStore(sessionFactory);
/*
Define a logger
*/
final Logger logger = new Slf4jLogger();
/*
Create the Elide object
*/
final Elide elide = new Elide(logger, dataStore);
/*
Convert the map to a MultivaluedMap, which we can then pass to Elide
*/
final MultivaluedMap<String, String> params = fromMap(allRequestParams);
/*
There is a bug in Elide on Windows that will convert a leading forward slash into a backslash,
which then displays the error "token recognition error at: '\\'".
*/
final String fixedPath = restOfTheUrl.replaceAll("^/", "");
/*
This is where the magic happens. We pass in the path, the params, and a place holder security
object (which we won't be using here in any meaningful way, but can be used by Elide
to authorize certain actions), and we get back a response with all the information
we need.
*/
final ElideResponse response = elide.get(fixedPath, params, new Object());
/*
Return the JSON response to the client
*/
return response.getBody();
}
}
This is our REST interface, and it is where we glue Spring and Elide together.
There are a few important things to note about this class.
The first is that we have one single method handling a number of JSON API endpoints. As you can see, we have one method responding to the following URL patterns:
/{entity}, which returns a collection of entities
/{entity}/{id}, which returns a single entity
/{entity}/{id}/{child}, which returns the child entities of a parent entity
/{entity}/{id}/relationships/{entity2}, which returns the details of the children of a parent
It’s important to note that we don’t have to do anything more than respond to these URLs. Elide takes the URL and returns the correct information. All we do is take the request made via Spring, pass it to Elide, and return the result.
The second thing to note is that we have added the @Transactional annotation to the method. This instructs Spring to create a transaction that Elide will then use.
The third important step we take here is converting the JPA EntityManagerFactory to a Hibernate SessionFactory. Elide specifically uses the Hibernate SessionFactory, which thankfully is trivial to convert from the EntityManagerFactory that Spring has injected for us.
Finally, you’ll note that I have altered the path to strip off the leading forward slash. This is because of a bug in Elide under Windows (specifically with the Elide.parse() method and its use of Paths.get(path)) that will convert the leading forward slash to a backslash, which then leads to an exception. Altering the path in this way was a simple workaround that allowed me to validate my code on a Windows machine before uploading it to a Linux server (Linux is not affected by this bug).
Child.java
package com.matthewcasperson.elidetest.jpa;
import com.fasterxml.jackson.annotation.JsonBackReference;
import java.io.Serializable;
import javax.persistence.*;
/**
* The persistent class for the child database table.
*
*/
@Entity
@Table(name="child")
@NamedQuery(name="Child.findAll", query="SELECT c FROM Child c")
public class Child implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String description;
private String name;
private Parent parent;
public Child() {
}
@Id
@Column(unique=true, nullable=false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@Column(length=45)
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
@Column(length=45)
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
//bi-directional many-to-one association to Parent
@ManyToOne
@JoinColumn(name="parentId", nullable=false)
@JsonBackReference
public Parent getParent() {
return this.parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
This class is a stock standard JPA entity. The only customization here is the addition of the Jackson @JsonBackReference annotation to prevent a serialization cycle from throwing an exception.
Note that you do need to put the JPA annotations on the methods rather that the fields. If you annotate the fields (which is completely legal JPA syntax), Elide will not function correctly.
Parent.java
package com.matthewcasperson.elidetest.jpa;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.io.Serializable;
import javax.persistence.*;
import java.util.List;
/**
* The persistent class for the parent database table.
*
*/
@Entity
@Table(name="parent")
@NamedQuery(name="Parent.findAll", query="SELECT p FROM Parent p")
public class Parent implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String description;
private String name;
private List<Child> children;
public Parent() {
}
@Id
@Column(unique=true, nullable=false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@Column(length=45)
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
@Column(length=45)
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
//bi-directional many-to-one association to Child
@OneToMany(mappedBy="parent")
@JsonManagedReference
public List<Child> getChildren() {
return this.children;
}
public void setChildren(List<Child> children) {
this.children = children;
}
public Child addChild(Child child) {
getChildren().add(child);
child.setParent(this);
return child;
}
public Child removeChild(Child child) {
getChildren().remove(child);
child.setParent(null);
return child;
}
}
Again, this is a pretty standard JPA entity class. We have added a Jackson @JsonManagedReference to match the annotation we defined in the Child class.
package-info.java
/**
* The JPA entities that are exposed to Elide via the @Include annotation
*/
@Include(rootLevel=true)
package com.matthewcasperson.elidetest.jpa;
import com.yahoo.elide.annotation.Include;
In order for Elide to have visibility on the JPA entities, we add a @Include(rootLevel=true) annotation on the package documented in the package-info.java file.
application.properties
server.port = 8080
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/elidetest
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
spring.datasource.max-active=5
spring.datasource.max-idle=1
spring.datasource.min-idle=1
spring.datasource.initial-size=1
spring.datasource.testOnBorrow=true
spring.datasource.validationQuery=SELECT 1
This is a file used by Spring boot to configure aspects of the web server. We have used it here to define a connection to the database.
The last few settings (from line 9 onward) are optional and were added to allow the application to work when deployed to Heroku. You don’t need these if you are running the app locally. You can find more information about these settings in the Spring documentation.
Procfile
web: java -Dserver.port=$PORT $JAVA_OPTS -jar build/libs/spring-boot-jpa-elide-0.0.1.jar
This is a file use by Heroku to launch a Spring Boot application. You can get more information on this file from the Heroku documentation.
Viewing the Results
Let’s take a look at the various JSON API endpoints that we have just exposed. I have taken the liberty of uploading a version of this application to Heroku, so you can see the results of these URLs live.
https://elide-test.herokuapp.com/parent will display a list of the parent entities
https://elide-test.herokuapp.com/parent/1 will display the details of a single parent entity
https://elide-test.herokuapp.com/parent/1/relationships/children will display the type and id of the children assigned to a single parent
https://elide-test.herokuapp.com/parent/1/children will display the complete details of the children assigned to a single parent
We also get a number of useful query parameters that can be used to customize the output:
https://elide-test.herokuapp.com/parent?fields[parent]=name will display only the name attribute of the parent entities (this is known as sparse fieldsets in the JSON API spec)
https://elide-test.herokuapp.com/parent?filter[parent.id]=2 will return the parent entity with the id of 2
https://elide-test.herokuapp.com/parent?filter[parent.name][prefix]=Jane will return the parent entities with the name attribute starting with “Jane”
https://elide-test.herokuapp.com/parent?sort=-name will return the parent entities sorted by the name attribute in descending order
You can find more information on the format of these query parameters in the Elide documentation.
The fact that you get all this functionality for free using Elide is incredibly cool, and will save you an immense amount of work. Believe me, before discovering Elide I went about implementing my own JSON API service, and it was not an easy task. So to get this functionality with just a few dozen lines of code is awesome.
You can download the source code for this application from GitHub.
Opinions expressed by DZone contributors are their own.
Comments