Contents
Introduction
The database structure needed for collecting data in our Ersa software is very uncomplicated. At this point we don’t need any authentication mechanism — we simply need to submit some sensor data to the server and be able to retrieve it. Normally I would chose a lightweight framework (like Flask) for such a task, but after over five years of Android development I felt the urge to explore a different part of the Java world. Enter Spring Boot.
I tend to be reluctant to use frameworks that require naming conventions and a plethora of annotations. Besides, Spring applications are memory hogs. I needed a huge swap file in my DO droplet to even start it up. That said, I created my first web service in a manner of minutes. Literally. That’s marvellous!
The code is on GitHub: https://github.com/Pygmalion69/ErsaApi
Gradle
First, we’ll add the required dependencies to our build script and apply the Boot plugin.
build.gradle
apply plugin: 'java' sourceCompatibility = '1.8' [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' if (!hasProperty('mainClass')) { ext.mainClass = 'eu.sergehelfrich.ersaapi.Application' } buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.9.RELEASE") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' jar { baseName = 'ersaapi' version = '0.1.0' } repositories { mavenCentral() } dependencies { compile("org.springframework.boot:spring-boot-starter-data-rest") compile("org.springframework.boot:spring-boot-starter-web") compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile 'mysql:mysql-connector-java' testCompile('org.springframework.boot:spring-boot-starter-test') testCompile group: 'junit', name: 'junit', version: '4.10' }
Application and controller
We’ll create an Application class that will run the Spring Application.
Application.java
@SpringBootApplication public class Application { /** * @param args the command line arguments */ public static void main(String[] args) { SpringApplication.run(Application.class, args); } } |
Dependency injection is at core of Spring, so for a REST controller all we have to to do is create a class and annotate it with @Controller, so it gets injected by the framework as needed. We also annotate a @RequestMapping defining the root of our API.
MainController.java
@Controller @RequestMapping(path="/ersa") public class MainController { } |
We’ll have to add at least a Repository for data access and endpoint definitions for our API calls.
Data objects
The data source is defined in application.properties, in this case a MySQL database:
application.properties
spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://services.mydomain.org:3306/ersa spring.datasource.username=..... spring.datasource.password=.....
Hibernate will create all the tables we need (ORM) from our data objects, annotated with @Entity. We want to store sensor readings, including an identifier of their origin (e.g. a sensor node id).
Reading.java
@Entity public class Reading { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private String origin; private Long timestamp; private Double temperature; private Double humidity; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getOrigin() { return origin; } public void setOrigin(String origin) { this.origin = origin; } public Long getTimestamp() { return timestamp; } public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } public Double getTemperature() { return temperature; } public void setTemperature(Double temperature) { this.temperature = temperature; } public Double getHumidity() { return humidity; } public void setHumidity(Double humidity) { this.humidity = humidity; } } |
Repository
Data access take place over a so-called Repository. We define it as an interface, in this case extending JpaRepository. A lot of magic goes on here. The framework knows about our entities and fields, and methods can be defined here with the Spring naming convention that work out of the box. We don’t need to implement them ourselves.
ReadingRepository.java
@RepositoryRestResource public interface ReadingRepository extends JpaRepository<Reading,Long> { List<Reading> findByOrigin(@Param("origin") String origin); List<Reading> findTop100ByOriginOrderByTimestampDesc(@Param("origin") String origin); } |
We obtain a reference to the repository in the controller and annotate it with @Autowired, so that the generated bean gets injected.
MainController.java
@Autowired private ReadingRepository readingRepository; |
GET mappings
Now all we have to to is something like this, e.g. to obtain a JSON array of the latest 100 readings upon a valid HTTP GET request:
MainController.java
@GetMapping(path="/latest100") public @ResponseBody List<Reading> getLatest(@RequestParam String origin) { return readingRepository.findTop100ByOriginOrderByTimestampDesc(origin); } |
The @ResponseBody annotation indicates that the return value will be serialized in the JSON response. The @RequestParam tells the framework that the variable is obtained from a URL parameter.
POST mappings
Readings are posted to the API as a simple JSON object: {“origin”:”111″,”temperature”:22.6,”humidity”:60} . What we need is a request object to represent it. It’s class:
ReadingRequest.java
public class ReadingRequest { public String origin; public double temperature; public double humidity; } |
The POST method accepts this request and returns the record id upon successful insertion.
MainController.java
@PostMapping(path="/reading") public @ResponseBody long addReading(@RequestBody ReadingRequest request) { Reading reading = new Reading(); reading.setTimestamp(System.currentTimeMillis() / 1000L); reading.setOrigin(request.origin); reading.setTemperature(request.temperature); reading.setHumidity(request.humidity); readingRepository.save(reading); readingRepository.flush(); return reading.getId(); } |
Custom query
Now we need a more complex query to retrieve the latest reading by origin. I use a native MySQL query since I couldn’t figure out how to define an alias for the inner SELECT in JPA. We place it in the interface ReadingRepositoryCustom.
ReadingRepositoryCustom.java
public interface ReadingRepositoryCustom { @Query(value = "SELECT * FROM `reading` INNER JOIN(SELECT origin, MAX(`timestamp`) AS maxtimestamp FROM `reading` GROUP BY `origin`) mts ON reading.origin = mts.origin AND timestamp = maxtimestamp", nativeQuery=true) public List<Reading> findLatest(); } |
Have ReadingRepository extend this custom interface:
ReadingRepository.java
@RepositoryRestResource public interface ReadingRepository extends JpaRepository<Reading,Long>, ReadingRepositoryCustom { ... } |
Finally we can have our GET mapping:
MainController.java
@GetMapping(path="/latest") public @ResponseBody List<Reading> getLatest() { return readingRepository.findLatest(); } |
One thought on “A REST API with Spring Boot”
Comments are closed.