Building Spring Boot RESTful Service + Spring Boot Actuator
Overview
What is REST?
REST(REpresentational State Transfer) is the architectural style the web is built on and has become a standard software design pattern used for web applications. The term Representational State Transfer was first used by Roy Fielding, the originator of REST and one of the principal authors of HTTP specification, in his doctoral dissertation.
There are many good references on REST including:
- Wikipedia
- Richardson Maturity Model
- How Ryan Tomayko Explained REST to His Wife
- Roy Fielding’s REST APIs Must Be Hypertext Driven
- Stackoverflow SOAP vs REST
This tutorial is based on Building Rest Services with Spring and the beginning of the tutorial has a good overview of REST as well.
What is Spring Boot Actuator?
Spring Boot Actuator is a sub-project of Spring Boot. It adds several production grade services to your application with minimal effort on your part.
Definition of Actuator
An actuator is a component responsible for moving or controlling a system.
The term actuator is not limited to Spring Boot; however, that is our focus here.
After Actuator is configured in your Spring Boot application, it allows you to interact and monitor your application by invoking different technology agnostic endpoints exposed by Spring Boot Actuator such as application health, beans, loggers, mappings, and trace. More are listed in this Spring doc.
0 – Spring Boot RESTful Web Service with Actuator Example Application
We will build an example RESTful web application with Spring Boot and Actuator.
The application will be a “username tracker.” In this application, a person has one account and their account may have many usernames.
View and Download the code from Github
1 – Project Structure
As usual, we have a normal Maven project structure.
2 – Project Dependencies
Besides typical Spring Boot dependencies, we are including HSQLDB for our embedded database and spring-boot-starter-actuator for all the Actuator dependencies.
<?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>com.michaelcgood</groupId> <artifactId>michaelcgood-springbootactuator</artifactId> <version>0.0.1</version> <packaging>jar</packaging> <name>Spring-Boot-Actuator-Example</name> <description>Michael C Good - Spring Boot Actuator Example</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.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-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3 – Run the Empty Application
Although we haven’t written any code, we will run the Spring Boot application.
Go to your terminal and follow along with the commands.
mikes-MacBook-Air:Spring-Boot-Actuator-Example mike$ curl localhost:8080 {"timestamp":1505235455245,"status":404,"error":"Not Found","message":"No message available","path":"/"}
We haven’t written any code yet, instead of a default container-generated HTML error response, Actuator produces you a JSON response from its /error endpoint.
mikes-MacBook-Air:Spring-Boot-Actuator-Example mike$ curl localhost:8080/health {"status":"UP"}
The Actuator /health endpoint will let you know if your application is up.
4 – Model
Now let’s define the fields of the models for our username tracker application.
- As mentioned, a person has one account and may have many usernames. So we map Setwith a @OneToMany annotation
- A username model will have a password and a username of course
- Our model will need an ID and we make it autogenerated
- We make a class construction to define an account may be made with a username and a password. Because of this custom constructor we also need to make a default one with no parameters.
Account.java
package com.michaelcgood.model; import java.util.HashSet; import java.util.Set; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany; import com.fasterxml.jackson.annotation.JsonIgnore; @Entity public class Account { public Set<Usernames> getUsernames() { return usernames; } public void setUsernames(Set<Usernames> usernames) { this.usernames = usernames; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @OneToMany(mappedBy= "account") private Set<Usernames> usernames = new HashSet<>(); @Id @GeneratedValue private Long id; @JsonIgnore public String password; public String username; public Account(String name, String password) { this.username = name; this.password = password; } Account(){ } }
Usernames.java
- As there is one account to many usernames, the reverse is true as well: there many usernames to one account. Therefore, we map Account with @ManyToOne annotation
- To track a username we need: the url and the username
- We once again define an autogenerated ID
- We define a custom class constructor that requires account, url, and username parameter. Once again we need to define a default constructor method to avoid an error being thrown.
package com.michaelcgood.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToOne; import com.fasterxml.jackson.annotation.JsonIgnore; @Entity public class Usernames { @JsonIgnore @ManyToOne private Account account; public Account getAccount() { return account; } public void setAccount(Account account) { this.account = account; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Id @GeneratedValue private Long id; public String url; public String username; Usernames(){ } public Usernames(Account account, String url, String username){ this.url=url; this.account=account; this.username=username; } }
5 – Repository
We create a repository for both models and create search functions using derived queries.
AccountRepository.java
package com.michaelcgood.dao; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.michaelcgood.model.Account; public interface AccountRepository extends JpaRepository<Account,Long> { Optional<Account> findByUsername(String username); }
UsernamesRepository.java
package com.michaelcgood.dao; import java.util.Collection; import org.springframework.data.jpa.repository.JpaRepository; import com.michaelcgood.model.Usernames; public interface UsernamesRepository extends JpaRepository<Usernames,Long> { Collection<Usernames> findByAccountUsername(String username); }
6 – Controller
In the controller, we define all the mapping that we will be using for our RESTful web service.
- We annotate our controller with @RestController rather than @Controller. As stated in the javadoc, it is “a convenience annotation that is itself annotated with @Controller and @ResponseBody.”
- We declare the variables for our UsernamesRepository and AccountRepository and make them final because we only want the value to be assigned once. We annotate them as @Autowired over the UsernamesRestController class constructor.
- {userId} and {usernamesId} are path variables. That means these values are provided in a URL. This will be shown in our demo.
- The Controller methods return POJOs (Plain Old Java Objects). Spring Boot automatically wires HttpMessageConverter to convert these generic objects to JSON.
UsernamesRestController.java
package com.michaelcgood.controller; import java.net.URI; import java.util.Collection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.michaelcgood.dao.AccountRepository; import com.michaelcgood.dao.UsernamesRepository; import com.michaelcgood.model.Usernames; @RestController @RequestMapping("/{userId}/usernames") public class UsernamesRestController { private final UsernamesRepository usernamesRepository; private final AccountRepository accountRepository; @Autowired UsernamesRestController(UsernamesRepository usernamesRepository, AccountRepository accountRepository){ this.usernamesRepository = usernamesRepository; this.accountRepository = accountRepository; } @GetMapping Collection<Usernames> readUsernames (@PathVariable String userId){ this.validateUser(userId); return this.usernamesRepository.findByAccountUsername(userId); } @PostMapping ResponseEntity<?> add(@PathVariable String userId,@RequestBody Usernames input){ this.validateUser(userId); return this.accountRepository.findByUsername(userId) .map(account -> { Usernames result = usernamesRepository.save(new Usernames(account,input.url,input.username)); URI url = ServletUriComponentsBuilder .fromCurrentRequest().path("/{id}") .buildAndExpand(result.getId()).toUri(); return ResponseEntity.created(url).build(); }) .orElse(ResponseEntity.noContent().build()); } @GetMapping(value="{usernamesId}") Usernames readUsername(@PathVariable String userId, @PathVariable Long usernameId){ this.validateUser(userId); return this.usernamesRepository.findOne(usernameId); } private void validateUser(String userId){ this.accountRepository.findByUsername(userId).orElseThrow( () -> new UserNotFoundException(userId)); } }
UserNotFoundException.java
Here we define the custom exception we used in our Controller class to explain a user could not be found.
package com.michaelcgood.controller; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { /** * */ private static final long serialVersionUID = 7537022054146700535L; public UserNotFoundException(String userId){ super("Sorry, we could not find user '" + userId +"'."); } }
7 – @SpringBootApplication
We use the CommandLineRunner to create accounts and insert usernames. Every account will have two usernames.
package com.michaelcgood; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import com.michaelcgood.dao.AccountRepository; import com.michaelcgood.dao.UsernamesRepository; import com.michaelcgood.model.Account; import com.michaelcgood.model.Usernames; import java.util.Arrays; @SpringBootApplication public class SpringBootActuatorExampleApplication { public static void main(String[] args) { SpringApplication.run(SpringBootActuatorExampleApplication.class, args); } @Bean CommandLineRunner init(AccountRepository accountRepository, UsernamesRepository usernamesRepository) { return (evt) -> Arrays.asList( "ricksanchez,mortysmith,bethsmith,jerrysmith,summersmith,birdperson,squanchy,picklerick".split(",")) .forEach( a -> { Account account = accountRepository.save(new Account(a, "password")); usernamesRepository.save(new Usernames(account, "http://example.com/login", a +"1")); usernamesRepository.save(new Usernames(account, "http://example2.com/login", "the_"+a)); }); } }
8 – Configuration
It is stated in the Spring documentation:
By default all sensitive HTTP endpoints are secured such that only users that have an ACTUATOR role may access them. Security is enforced using the standard HttpServletRequest.isUserInRole method.
We haven’t set up any security and user roles, as this is just an example. So, for ease of demonstration, I will be disabling the security requirement. Otherwise, we will get an “unauthorized” error as of right now, like the one shown below.
{"timestamp":1505321635068,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource.","path":"/beans"}
application.properties
Add this to your application.properties to disable the need for authentication.
management.security.enabled=false
9 – Demo
To retrieve the responses from the server, you could either visit the URL in your browser or use curl. For my demo, I am using curl.
REST queries for data in repositories
Query for usernames belonging to account jerrysmith.
mikes-MacBook-Air:Spring-Boot-Actuator-Example mike$ curl localhost:8080/jerrysmith/usernames [{"id":7,"url":"http://example.com/login","username":"jerrysmith1"},{"id":8,"url":"http://example2.com/login","username":"the_jerrysmith"}]
Query for usernames belonging to account picklerick
mikes-MacBook-Air:Spring-Boot-Actuator-Example mike$ curl localhost:8080/picklerick/usernames [{"id":15,"url":"http://example.com/login","username":"picklerick1"},{"id":16,"url":"http://example2.com/login","username":"the_picklerick"}]
Actuator queries
The response to this query is truncated because it is really, really long.
Beans
mikes-MacBook-Air:Spring-Boot-Actuator-Example mike$ curl localhost:8080/beans [{"context":"application","parent":null,"beans":[{"bean":"springBootActuatorExampleApplication","aliases":[],"scope":"singleton","type":"com.michaelcgood.SpringBootActuatorExampleApplication$$EnhancerBySpringCGLIB$$509f4984","resource":"null","dependencies":[]},{"bean":"org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory","aliases":[],"scope":"singleton","type":"org.springframework.core.type.classreading.CachingMetadataReaderFactory","resource":"null","dependencies":[]},{"bean":"usernamesRestController","aliases":[],"scope":"singleton","type":"com.michaelcgood.controller.UsernamesRestController","resource":"file [/Users/mike/javaSTS/Spring-Boot-Actuator-Example/target/classes/com/michaelcgood/controller/UsernamesRestController.class]","dependencies":["usernamesRepository","accountRepository"]},{"bean":"init","aliases":[],"scope":"singleton","type":"com.michaelcgood.SpringBootActuatorExampleApplication$$Lambda$11/889398176","resource":"com.michaelcgood.SpringBootActuatorExampleApplication", [...]
Metrics
mikes-MacBook-Air:Spring-Boot-Actuator-Example mike$ curl localhost:8080/metrics {"mem":350557,"mem.free":208275,"processors":4,"instance.uptime":213550,"uptime":240641,"systemload.average":1.6552734375,"heap.committed":277504,"heap.init":131072,"heap.used":69228,"heap":1864192,"nonheap.committed":74624,"nonheap.init":2496,"nonheap.used":73062,"nonheap":0,"threads.peak":27,"threads.daemon":23,"threads.totalStarted":30,"threads":25,"classes":9791,"classes.loaded":9791,"classes.unloaded":0,"gc.ps_scavenge.count":11,"gc.ps_scavenge.time":139,"gc.ps_marksweep.count":2,"gc.ps_marksweep.time":148,"httpsessions.max":-1,"httpsessions.active":0,"datasource.primary.active":0,"datasource.primary.usage":0.0,"gauge.response.beans":14.0,"gauge.response.info":13.0,"counter.status.200.beans":2,"counter.status.200.info":1}
9 – Conclusion
Congratulations, you have created a RESTful Web Service that can be monitored with Actuator. REST is really the most organic way for different clients to communicate because it works due to HTTP.
The source code is on Github
Published on Java Code Geeks with permission by Michael Good, partner at our JCG program. See the original article here: Building Spring Boot RESTful Service + Spring Boot Actuator Opinions expressed by Java Code Geeks contributors are their own. |