Rest with Spring Part-2
Guiding Principles of Rest API
- Client–server
- Stateless
- Cacheable
- Uniform Interface
- identification of resource
- Manipulation through represenatition
- Layered System
For more information visit the link below:
https://restfulapi.net/
Serializing enums
package com.springdemo.springrestdochateos.enums;
public enum Gender {
MALE("male"),FEMALE("female");
private String displayName;
Gender(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
}
@GetMapping("/genders")
public List<Gender> getGenderList(){
return Arrays.asList(Gender.values());
}
Enum as Json
package com.springdemo.springrestdochateos.enums;
import com.fasterxml.jackson.annotation.JsonFormat;
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum Gender {
MALE("male"),FEMALE("female");
private String displayName;
Gender(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
}
Serializing an Enum
package com.springdemo.springrestdochateos.enums;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
public class GenderSerializer extends StdSerializer<Gender> {
protected GenderSerializer(Class<Gender> t) {
super(t);
}
protected GenderSerializer() {
super(Gender.class);
}
@Override
public void serialize(Gender value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeFieldName("name");
gen.writeObject(value.name());
gen.writeFieldName("displayName");
gen.writeObject(value.getDisplayName());
gen.writeEndObject();
}
}
package com.springdemo.springrestdochateos.enums;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = GenderSerializer.class)
public enum Gender {
MALE("male"),FEMALE("female");
private String displayName;
Gender(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
}
Exercise 1
- Create an enum Permission with 3 value ADMIN, CUSTOMER and VENDOR
- Create a rest full API to expose Permission values
- Convert ENUM to JSON
- Serialize ENUM to show the custom value.
Spring Data Rest
Spring Data Rest builds on top of Spring Data repositories and automatically export those as Rest Resource.
Repositories Detection Strategy
Spring Data REST uses Repositories Detection Strategy to determine if a repository will be exported as Rest resource or not.
Name | Description |
---|---|
Default | Expose all public repositories consider exported flag of Rest Resource |
All | Expose all repositories |
Annotations | Only repositories annotated with @RestResource |
Visibility | Only public repositories annotated are exposed |
Changing base URI
spring.data.rest.basePath=/api
Spring data Rest serves up the rest resource at the Root URI "/".
In order to change this default setting just set the following value in application.properties.
Name | Description |
---|---|
basepath | Root URI for Spring Data Rest |
defaultPageSize | Default number of items served in single page |
pageParamName | Name of query param for page |
limitParamName | Name of query param for items to show in page |
sortParamName | Name of query param for sorting |
returnBodyOnCreate | body should return created entity |
returnBodyOnUpdate | body should return updated entity |
Repository Resources Fundamentals
Spring Data REST uses HAL to render Responses. HAL defines links to be contained in a property of the returned document.
build.gradle
buildscript {
ext {
springBootVersion = '2.0.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile('org.springframework.boot:spring-boot-starter-hateoas')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('mysql:mysql-connector-java')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.restdocs:spring-restdocs-mockmvc')
}
Employee Repository
package com.springdemo.springrestdochateos.repositories;
import com.springdemo.springrestdochateos.entities.Employee;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface EmployeeRepository extends CrudRepository<Employee,Long> {
}
package com.springdemo.springrestdochateos.entities;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Employee {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private Integer salary;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getSalary() {
return salary;
}
public void setSalary(Integer salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", age='" + age + '\'' +
", salary=" + salary +
'}';
}
}
Employee Entity
Bootstrap Data
package com.springdemo.springrestdochateos.events;
import com.springdemo.springrestdochateos.entities.Employee;
import com.springdemo.springrestdochateos.repositories.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.stream.IntStream;
@Component
public class Bootstrap {
@Autowired
EmployeeRepository employeeRepository;
@EventListener(ApplicationStartedEvent.class)
public void init(){
if(!employeeRepository.findAll().iterator().hasNext()) {
IntStream.range(1,51).forEach(e->{
Employee employee = new Employee();
employee.setAge(20+e);
employee.setName("name "+e);
employee.setSalary(20000+(e*1000));
employeeRepository.save(employee);
});
}
}
}
package com.springdemo.springrestdochateos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.springdemo.springrestdochateos.repositories")
@EntityScan(basePackages = "com.springdemo.springrestdochateos.entities")
public class SpringRestDocHateosApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestDocHateosApplication.class, args);
}
}
Config file for Repository Detection Strategy
package com.springdemo.springrestdochateos.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
@Configuration
public class SpringRestConfigurerAdapter extends RepositoryRestConfigurerAdapter {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.setRepositoryDetectionStrategy(RepositoryDetectionStrategy
.RepositoryDetectionStrategies.ANNOTATED);
}
}
Request
http://localhost:8080/api
Reponse
{
"_links" : {
"employees" : {
"href" : "http://localhost:8080/api/employees"
},
"profile" : {
"href" : "http://localhost:8080/api/profile"
}
}
}
- Get all employee <Verb : GET> http://localhost:8080/api/employees
- Get one employee <Verb : GET>
http://localhost:8080/api/employees/{id}
- Delete employee < Verb :DELETE> http://localhost:8080/api/employees/{id}
- Create employee <Verb : POST>
{"name":"Peter","age":27,"salary":26000}
- Update employee <Verb : PUT>
{"name":"Peter","age":27,"salary":26000}
Note: Content-Type should be application/json or application/hal+json
Default Operations
Provide pagination and Sorting
package com.springdemo.springrestdochateos.repositories;
import com.springdemo.springrestdochateos.entities.Employee;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface EmployeeRepository extends PagingAndSortingRepository<Employee,Long> {
}
Extend your repository with PaginationAndSortingRepository
Now you can hit the url below to get the sorted and paginated data:
http://localhost:8080/api/employees?page=0&size=5&sort=age,desc
Expose Ids
package com.springdemo.springrestdochateos.config;
import com.springdemo.springrestdochateos.entities.Employee;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
@Configuration
public class SpringRestConfigurerAdapter extends RepositoryRestConfigurerAdapter {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.setRepositoryDetectionStrategy(RepositoryDetectionStrategy
.RepositoryDetectionStrategies.ANNOTATED)
.exposeIdsFor(Employee.class);
}
}
Ignore attribute
In order to ignore the attribute on an entity just place @JsonIgnore annotation on the entity
Additional methods
package com.springdemo.springrestdochateos.repositories;
import com.springdemo.springrestdochateos.entities.Employee;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
import java.util.List;
@RepositoryRestResource
public interface EmployeeRepository extends PagingAndSortingRepository<Employee,Long> {
List<Employee> findAllByAge(Integer age);
Employee findByName(String name);
}
Now make a GET request on following url:
http://localhost:8080/api/employees/search/
(Cont.)
Response
{
"_links" : {
"findByName" : {
"href" : "http://localhost:8080/api/employees/search/findByName{?name}",
"templated" : true
},
"findAllByAge" : {
"href" : "http://localhost:8080/api/employees/search/findAllByAge{?age}",
"templated" : true
},
"self" : {
"href" : "http://localhost:8080/api/employees/search/"
}
}
}
find employee by age
http://localhost:8080/api/employees/search/findByAge?age=23
Change the path of your resource
package com.springdemo.springrestdochateos.repositories;
import com.springdemo.springrestdochateos.entities.Employee;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(path = "/persons")
public interface EmployeeRepository extends PagingAndSortingRepository<Employee,Long> {
Employee findByAge(@Param("age") Integer age);
Employee findByName(@Param("name") String name);
}
Now employee will be found on : http://localhost:8080/api/persons
Hide a search
package com.springdemo.springrestdochateos.repositories;
import com.springdemo.springrestdochateos.entities.Employee;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
@RepositoryRestResource(path = "/persons")
public interface EmployeeRepository extends PagingAndSortingRepository<Employee,Long> {
@RestResource(exported = false)
Employee findByAge(@Param("age") Integer age);
Employee findByName(@Param("name") String name);
}
Hide RestResource
package com.springdemo.springrestdochateos.repositories;
import com.springdemo.springrestdochateos.entities.Employee;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
@RepositoryRestResource(path = "/persons",exported = false)
public interface EmployeeRepository extends PagingAndSortingRepository<Employee,Long> {
Employee findByAge(@Param("age") Integer age);
Employee findByName(@Param("name") String name);
}
Exercise 2
- Create an entity student and with the help of Spring Data Rest expose CRUD operations for Student Resource.
- Apply Pagination and Sorting with Spring Data Rest for Student Resource
- Expose the id for the Resource
- Create a search method to search a student on the basis of name
- Hide one of the fields of Student Resource in Rest API
HATEOAS
- Hypermedia as the Engine of Application State.
- HATEOAS is one of the architectural constraints in the REST architecture.
- To follow HATEOAS principles you need to incorporate links into your resource representation.
- Spring HATEOAS provides a set of useful types to ease working with those.
Lets take simple example of shopping items online to understand HATEOAS
Lets create a class Item
package com.springdemo.springrestdochateos.entities;
public class Item {
private Integer itemId;
private String name;
private Integer price;
public Item(Integer itemId, String name, Integer price) {
this.itemId = itemId;
this.name = name;
this.price = price;
}
public Integer getItemId() {
return itemId;
}
public void setItemId(Integer itemId) {
this.itemId = itemId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
}
package com.springdemo.springrestdochateos.entities;
import java.util.ArrayList;
import java.util.List;
public class Cart {
private List<Item> list= new ArrayList<>();
public List<Item> getList() {
return list;
}
public void setList(List<Item> list) {
this.list = list;
}
}
Now Lets create a shopItemController
package com.springdemo.springrestdochateos.controllers;
import com.springdemo.springrestdochateos.entities.Cart;
import com.springdemo.springrestdochateos.entities.Item;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/item")
public class ShopItemsController {
Item item1 = new Item(1,"Item1",25);
Item item2 = new Item(2,"Item2",30);
List<Item> items=Arrays.asList(item1,item2);
Cart cart= new Cart();
@GetMapping("/{id}")
public Item getItem1(@PathVariable("id") Integer id){
Item selectedItem=items.stream().filter(item->item.getItemId()==id).findFirst().get();
return selectedItem;
}
@GetMapping("/addToCart/{id}")
public Cart addToCart(@PathVariable("id") Integer id){
Item selectedItem=items.stream().filter(item->item.getItemId()==id).findFirst().get();
cart.getList().add(selectedItem);
return cart;
}
@GetMapping("/removeFromCart/{id}")
public Cart removeCart(@PathVariable("id") Integer id){
Item selectedItem=items.stream().filter(item->item.getItemId()==id).findFirst().get();
cart.getList().remove(selectedItem);
return cart;
}
@GetMapping("/cart")
public Cart getCart(){
return cart;
}
}
In order to Provide the support of introducing links in the resource. We do it by extending ResourceSupport class
package com.springdemo.springrestdochateos.entities;
import org.springframework.hateoas.ResourceSupport;
public class Item extends ResourceSupport {
private Integer itemId;
private String name;
private Integer price;
public Item(Integer itemId, String name, Integer price) {
this.itemId = itemId;
this.name = name;
this.price = price;
}
public Integer getItemId() {
return itemId;
}
public void setItemId(Integer itemId) {
this.itemId = itemId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
}
Returning Item response with HATEOAS Links
package com.springdemo.springrestdochateos.controllers;
import com.springdemo.springrestdochateos.entities.Cart;
import com.springdemo.springrestdochateos.entities.Item;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/item")
public class ShopItemsController {
Item item1 = new Item(1,"Item1",25);
Item item2 = new Item(2,"Item2",30);
List<Item> items=Arrays.asList(item1,item2);
Cart cart= new Cart();
@GetMapping("/{id}")
public Item getItem1(@PathVariable("id") Integer id){
Link selfLink= ControllerLinkBuilder
.linkTo(ShopItemsController.class)
.slash(id).withSelfRel();
Link addToCart=ControllerLinkBuilder
.linkTo(ShopItemsController.class)
.slash("addToCart").slash(id).withRel("addToCart");
Link removeFromCart=ControllerLinkBuilder
.linkTo(ShopItemsController.class)
.slash("removeFromCart").slash(id).withRel("removeFromCart");
Item selectedItem=items.stream().filter(item->item.getItemId()==id).findFirst().get();
selectedItem.getLinks().clear();
selectedItem.add(selfLink);
selectedItem.add(addToCart);
selectedItem.add(removeFromCart);
return selectedItem;
}
@GetMapping("/addToCart/{id}")
public Cart addToCart(@PathVariable("id") Integer id){
Item selectedItem=items.stream().filter(item->item.getItemId()==id).findFirst().get();
cart.getList().add(selectedItem);
return cart;
}
@GetMapping("/removeFromCart/{id}")
public Cart removeCart(@PathVariable("id") Integer id){
Item selectedItem=items.stream().filter(item->item.getItemId()==id).findFirst().get();
cart.getList().remove(selectedItem);
return cart;
}
@GetMapping("/cart")
public Cart getCart(){
return cart;
}
}
Exercise 3
- Create API's for the following scenario (Online food order)
- You can select the food you wish
- You can also deselect from the list
- Place order
- Apply Hateoas on you APIs
Swagger
Swagger can be used to document our rest APIs
Following are the steps to configure swagger in our application:
- Include following dependencies in build.gradle
compile('io.springfox:springfox-swagger2:2.2.2')
compile('io.springfox:springfox-swagger-ui:2.2.2')
-
@EnableSwagger2 and Register the Docket Bean
package com.springdemo.springrestdochateos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.time.LocalDate;
import java.util.Arrays;
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.springdemo.springrestdochateos.repositories")
@EntityScan(basePackages = "com.springdemo.springrestdochateos.entities")
@EnableAsync
@EnableCaching
@EnableSwagger2
public class SpringRestDocHateosApplication {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build().pathMapping("/")
.directModelSubstitute(LocalDate.class,String.class)
.genericModelSubstitutes(ResponseEntity.class)
.apiInfo(metaData());
}
private ApiInfo metaData() {
ApiInfo apiInfo = new ApiInfo(
"Spring Boot REST API",
"Spring Boot REST API for Online Store",
"1.0",
"Terms of service",
"Spring-Developer",
"Apache License Version 2.0",
"https://www.apache.org/licenses/LICENSE-2.0");
return apiInfo;
}
@Bean
public TaskExecutor myTaskExecutor(){
return new ThreadPoolTaskExecutor();
}
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("CacheResource")
));
return cacheManager;
}
public static void main(String[] args) {
SpringApplication.run(SpringRestDocHateosApplication.class, args);
}
}
In order to display swagger UI make the following configuration (Configure this class only if your swagger html is not rendering )
package com.springdemo.springrestdochateos.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
public class CustomWebSecurityAdapter extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
Following are the URLs for swagger
For swagger doc
http://localhost:8080/v2/api-docs
For swagger-UI
http://localhost:8080/swagger-ui.html
You can use @ApiOperation on Resource to be more descriptive
@ApiOperation(value = "Get the item")
Exercise 4
Introduce swagger for you Restful API
Evolving API without breaking client
Understanding and anticipating change
- Backward Compatible Change
- Breaking Change
A clear message to clients : Ignore any unexpected data in the interaction with the API
Why don't just version your API?
- Leads to an explosion of the URL surface area of the API
- Is disruptive to client
- Is complex to maintain Long term
Types of changes
- Adding data to a resource
- Changing the type of data in a resource
- Changing the structure of a field in resource
- Removing a data from a resource
- Splitting one resource into two
Adding new data to a resource
- On an Output Resource - If it is non breaking Clients should ignore unknown data
- On an Input Resource - If the new data is optional then its a non breaking change
Changing data type in an Output resource
- Change the type from broad to specific e.g Integer to Long is not breaking
- Changing the type from specific to broad e.g Long to Integer is breaking
Change data type in an input resource
- Change the type from broad to specific is breaking change because now the API accepts less data than it did before
- Change the type from specific to broad is non-breaking because the API still accepts everything that it did before
Change data structure in a resource
- a name field becomes {firstname, lastname}
- on an output resource - non breaking if the old clients will ignore it with the condition that the API keep sending name
- on an input resource - non breaking with the condition that the API will treat the new structure as optional
- But if any of the above condition is not true then the change is breaking
Breaking change
- for the 4 scenarios that do involve breaking change
- Instead of changing the type of field :
- add a new field (with a new type)
- deprecate the existing field
Remove data from a resource
- On an output resource breaking change
- On an input resource non breaking because API ignores extra data
- Alternative don't remove just deprecate
Deprecation Policy and Sunsetting
- Beyond field deprecation
- sometimes it's necessary to provide a new resource
- and to sunset the old resource
- Deprecation and sunset policy should be well documented
Fundamentals of monitoring with boot
Dependency required for actuator
compile('org.springframework.boot:spring-boot-starter-actuator')
In order to activate end points for actuator set following property in application.properties
management.endpoints.web.exposure.include=*
/actuator will give you the details on the endpoints available
- actuator/beans : it gives the description of the beans defined in the application
- actuator/configprops : provides description of the configurations in the app
- actuator/env : It gives the information of the environment in which application is set up
- actuator/mappings : Display collated list of request mapping paths
- actuator/threaddump : Performs a thread dump.
- actuator/loggers : Shows and modifies the configurations of loggers in the application.
- actuator/health : Shows health of the app
- (POST) actuator/shutdown : Shutdown the app
Some customisations in application.properties
management.endpoints.web.exposure.include=* (To enable all the endpoints by default) management.endpoint.shutdown.enabled=true (To enable single endpoint) info.app.name=My App info.build.version=1.0.2 (Specify app info)
management.endpoint.health.show-details=always
(Gives full details about disk space and DM try shutting down Mysql and running health check again)
Getting git details in info
Introduce following plugin in build.gradle
plugins { id "com.gorylenko.gradle-git-properties" version "1.5.1" }
In application.properties set following value
management.info.git.mode=full
Custom Heath check
package com.springdemo.springrestdochateos.config;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.stereotype.Component;
@Component
public class CustomHealthCheck extends AbstractHealthIndicator {
public boolean checkCachingLayerErrors(){
return true;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if(checkCachingLayerErrors()){
builder.down().withDetail("Caching layer message","It is down for some reason");
}
}
}
Hit actuator/health to see the output
Using Project properties for /info
group = 'com.spring-demo'
version = '0.0.1-SNAPSHOT'
description="This is a spring project"
processResources{
expand(project.properties)
}
Please make sure you have following property set in build.gradle
Do the following entries in application.properties
# INFO ENDPOINT CONFIGURATION
info.app.name=${project.name}
info.app.description=${project.description}
info.app.version=${project.version}
/metrics
In order to get the options available for metrics just hit the link below
http://localhost:8080/actuator/metrics
To get the details of a metrics hit the link below:
http://localhost:8080/actuator/metrics/{requiredMetricName}
ETags
- At high level Etags or entity tags are used for HTTP caching with conditional requests
- Using an If* header turns a standard GET request in conditional GET.
- Following are the headers which can be used with Etags:
- if-none-match
- if-match
If the condition doesn't match then it returns 304 Not Modified status
package com.springdemo.springrestdochateos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import javax.servlet.Filter;
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.springdemo.springrestdochateos.repositories")
@EntityScan(basePackages = "com.springdemo.springrestdochateos.entities")
public class SpringRestDocHateosApplication {
@Bean
public Filter filter(){
ShallowEtagHeaderFilter filter=new ShallowEtagHeaderFilter();
return filter;
}
public static void main(String[] args) {
SpringApplication.run(SpringRestDocHateosApplication.class, args);
}
}
Configuring SwallowEtagHeaderFilter bean
Now lets make a get request to the employee resource
http://localhost:8080/person/1
Now in the response header we will get ETag value. Now again hit the previous resource but this time pass request header If-None-Match: "ETag value in double codes"
You will notice that HttpStatus 304 is returned with no response
Exercise 5
- Try out actuator options for your API
- Implement ETags for one of your API
Long Running requests
@GetMapping("/longRunningThread")
public Callable<String> getResponse() throws Exception{
System.out.println("Request Recieved");
Callable<String> stringCallable= new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000L);
System.out.println("Execute Long running something");
return "Done";
}
};
System.out.println("Request Completed");
return stringCallable;
}
@Bean
public TaskExecutor myTaskExecutor(){
return new ThreadPoolTaskExecutor();
}
Deferred Execution
@GetMapping("/Deferred")
public DeferredResult<String> getDeferredResult(){
DeferredResult<String> deferredResult= new DeferredResult<>();
CompletableFuture.supplyAsync(()->{
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Deferred Result";
}).whenComplete((result,ex)->{ deferredResult.setResult(result);});
return deferredResult;
}
Async Operation
@GetMapping("/asyncOperation")
public String asyncOperation(){
System.out.println("recieved request");
dummy.longAsyncOperation();
return "Done" ;
}
package com.springdemo.springrestdochateos.services;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class Dummy {
@Async
public void longAsyncOperation() {
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("long asyn operation");
}
}
Enable async via @EnableAsyn annotation
Caching the API response
@EnableCaching
compile('org.springframework.boot:spring-boot-starter-cache')
Include following dependency in build.gradle
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("CacheResource")
));
return cacheManager;
}
@Cacheable(value = "CacheResource")
@GetMapping("/cachedData")
public String cache() throws InterruptedException {
Thread.sleep(4000L);
return "Hello Cache";
}
@GetMapping("/evictCaching")
@CacheEvict(value = "CacheResource")
public String cacheEvict(){
return "Cache Cleared";
}
Rest with Spring Part-2
By Pulkit Pushkarna
Rest with Spring Part-2
- 1,153