- You must have maven 3 installed.
- You must have the Java 8 SDK installed.
Create a project directory and put the following in a file named pom.xml in the root of your project directory.
<?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>
<!-- Information about your RESTful API -->
<groupId>com.timjstewart</groupId>
<artifactId>cool-rest-api</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
</parent>
<dependencies>
<!-- Main dependency for SpringBoot apps -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- For testing SpringBoot web applications -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- For accessing MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- enables reloading web application when change is
detected on the CLASSPATH. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- HATEOAS Support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</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>
<!-- When classes on the CLASSPATH change, load the changes. -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.6.RELEASE</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
$ mkdir -p src/{test,main}/java/blogs
This should go in src/main/java/blogs/BlogsController.java.
package blogs;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class BlogsController {
@RequestMapping("/blogs")
public String index() {
return "Greetings from Spring Boot!";
}
}
This should go in src/main/java/blogs/Application.java
package blogs;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
$ mvn spring-boot:run
When your application boots up, you will be able to run the following curl command:
$ curl http://localhost:8080/blogs
Put the following code in src/main/java/blogs/Blog.java
package blogs;
public class Blog {
private final String title;
private final String description;
public Blog(String title, String description) {
this.title = title;
this.description = description;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
}
Modify BlogsController.java by adding these imports:
import java.util.List;
import java.util.ArrayList;
and changing the index method to:
@RequestMapping("/blogs")
public List<Blog> index() {
List<Blog> blogs = new ArrayList<>();
blogs.add(new Blog("Bits 'n Bytes", "Random musings of a programmer."));
return blogs;
}
I had to restart the application before the following curl command returned the correct JSON.
$ curl localhost:8080/blogs
Resulting JSON:
[
{
"title": "Bits 'n Bytes",
"description": "Random musings of a programmer."
}
]
Here we have a choice between making our Blog resource a Bean with getters and setters (which makes our immutable Blog object mutable), or we can add some attributes to our BlogObject so that Spring/Jackson can deserialize JSON into Blog objects. I like immutability more than I mind adding annotations.
Add the following imports to Blog.java
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
Then tag the Blog constructor and constructor parameters with JsonCreator and JsonProperty annotations:
@JsonCreator
public Blog(@JsonProperty("title") String title,
@JsonProperty("description") String description) {
this.title = title;
this.description = description;
}
We'll be adding another route that has the same /blogs path so we need to disambiguate our routes by specifying HTTP methods. When we add the HTTP method specification, we need to explicitly name our path property for the RequestMapping annotation. Add the imports for those annotations:
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestBody;
Change the annotation for the index() method to:
@RequestMapping(path = "/blogs", method = RequestMethod.GET)
Add the new POST route:
@RequestMapping(path = "/blogs", method = RequestMethod.POST)
public Blog createBlog(@RequestBody Blog blog) {
System.out.println("Blog created");
return blog;
}
Now you can run the following curl command:
$ curl -XPOST localhost:8080/blogs -H 'Content-Type: application/json' -d '{
"title": "Hi", "description": "Describe me"
}'
and get the following output:
{"title":"Hi","description":"Describe me"}
Add the following import to the BlogsController.java file:
import org.springframework.web.bind.annotation.PathVariable;
Add the new route method:
@RequestMapping(path = "/blogs/{blogTitle}", method = RequestMethod.GET)
public Blog getBlogByTitle(@PathVariable String blogTitle) {
System.out.println(blogTitle);
return new Blog(blogTitle, "No description");
}
Now you can execute the following curl command to get a specific Blog:
$ curl localhost:8080/blogs/tim
and get the following output:
{
"title":"tim",
"description":"No description"
}
Make Blog derive from Resource Support by adding this import to Blog.java:
import org.springframework.hateoas.ResourceSupport;
and adding a base class:
public class Blog extends ResourceSupport {
Add the following imports to BlogsController.java:
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
Modify the getBlogByTitle() method thusly:
First change the return type to: HttpEntity
Add links to the blog object before returning it:
blog.add(linkTo(methodOn(BlogsController.class).getBlogByTitle(blog.getTitle())).withSelfRel());
Then return the blog object wrapped in a ResponseEntity:
return new ResponseEntity<>(blog, HttpStatus.OK);
Now the GET request:
$ curl -XGET http://localhost:8080/blogs/tim
returns the following payload:
{
"title": "tim",
"description": "No description",
"_links": {
"self": {
"href": "http://localhost:8080/blogs/tim"
}
}
}
We're using title in some places and name in others with respect to the blog. I prefer title so let's just make all references to name refer to title.
I had to restart the spring app manually.
The following goes into src/test/java/blogs/BlogsControllerTest.java
package blogs;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class BlogsControllerTest {
@Autowired
private BlogsController controller;
@Test
public void contexLoads() throws Exception {
assertThat(controller).isNotNull();
}
}
$ mvn test
The test should pass.
For more information on the TestRestTemplate, read the JavaDocs.
Add the following import to src/test/java/blogs/BlogsControllerTest.java:
import org.springframework.boot.test.web.client.TestRestTemplate;
In the same file add the following instance variable:
@Autowired
private TestRestTemplate restTemplate;
And add a test like the following:
@Test
public void canCreateBlog() throws Exception {
final Blog blog = restTemplate.postForEntity("/blogs", new Blog("title", "description"), Blog.class).getBody();
assertThat(blog.getTitle()).isEqualTo("title");
}
Just to keep things embarrassingly simple, lets add a scheduled task that every 5 seconds logs, "Hello World!".
Create a file named src/main/java/blogs/ScheduledTask.java and put the following in it:
package blogs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTask {
private static final Logger log = LoggerFactory.getLogger(ScheduledTask.class);
@Scheduled(fixedRate = 5000)
public void sayHelloWorld() {
log.info("Hello, World!");
}
}
Add the following import to Application.java
import org.springframework.scheduling.annotation.EnableScheduling;
Add the following annotation to Application's class.
@EnableScheduling
Now when you run the web application, every 5 seconds "Hello, World!" will be logged at the INFO level.
Add the following import to the BlogsController.java file:
import org.springframework.web.bind.annotation.RequestParam;
Add the following parameters to the index() method:
@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "10") int limit
Instead of using RequestMapping and having to specify the HTTP method you can use method-specific mapping annotations like:
- PutMapping
- PostMapping
Delete the following imports from BlogsController.java:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
and add the following imports to BlogsController.java:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
Then replace the previous RequestMapping annotations with the following annotations:
@GetMapping("/blogs")
@PostMapping("/blogs")
@GetMapping("/blogs/{blogTitle}")
I had to break the connection between Blog and its base class ResourceSupport for this to work. I was getting Jackson serialization errors.
I think I'll return to this section later... I'm also concerned that I won't be able to inject any business logic into the routes. It seems like this technique just exposes a MongoDB collection.
Add this import to Blog.java
import org.springframework.data.annotation.Id;
Add an id field to Blog.java:
@Id private String id;
In a new file BlogRepository.java add the following:
package blogs;
import java.util.List;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(collectionResourceRel = "blogs", path = "blogs2")
public interface BlogRepository extends MongoRepository<Blog, String> {
List<Blog> findByTitle(@Param("title") String title);
}
Read and incorporate these guides into these notes: