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,174