SpringBoot - mrbmrb1122/SpringBootCloud GitHub Wiki
Welcome to the SpringBootCloud wiki!
-
Best Practices :
- Consumer First: Resources naming (give proper name), documentation.
- Make use of HTTP Request methods: GET, POST, PUT, DELETE. And also use proper HTTP response codes: 200,201,404,500etc
- No Secure info in URI (like ssn no and etc)
- Use Plurals :
- prefer /users instead of /user
- prefer /users/1 instead of /user/1
-
Create user- 201 user created. Response.created().
-
Delete User-> 200 success. No content method. Response.nocontent();
@PostMapping("/jpa/users")
public ResponseEntity<Object> createUser(@Valid @RequestBody User user) {
User savedUser = userRepository.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
}
}
public class ExceptionResponse {
private Date timestamp;
private String message;
private String details;
public ExceptionResponse(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
}
}
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable int id) {
User user = service.deleteById(id);
if(user==null)
throw new UserNotFoundException("id-"+ id);
}import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import com.in28minutes.rest.webservices.restfulwebservices.user.UserNotFoundException;
@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), "Validation Failed",
ex.getBindingResult().toString());
return new ResponseEntity(exceptionResponse, HttpStatus.BAD_REQUEST);
}
}
@Entity
public class User {
@Id
@GeneratedValue
private Integer id;
@Size(min=2, message="Name should have atleast 2 characters")
@ApiModelProperty(notes="Name should have atleast 2 characters")
private String name;
}
@PostMapping("/jpa/users")
public ResponseEntity<Object> createUser(@Valid @RequestBody User user) {
User savedUser = userRepository.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}Like @size we have some other java validations where we can use:
@AssertFalse
@AssertTrue
@DecimalMax
@DecimalMin
@Digits
@Future (for dates)
@Past (for dates)
@Max, @Min, @Null, @NotNull, @Pattern, @SizeHATEOAS (Hypermedia as the Engine of Application State) is a constraint of the REST application architecture that keeps the RESTful style architecture unique from most other network application architectures. The term “hypermedia” refers to any content that contains links to other forms of media such as images, movies, and text.
http://api.domain.com/management/departments/10
{
"departmentId": 10,
"departmentName": "Administration",
"locationId": 1700,
"managerId": 200,
"links": [
{
"href": "10/employees",
"rel": "employees",
"type" : "GET"
}
]
}
Implementation: pox.xml dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
@GetMapping("/jpa/users/{id}")
public Resource<User> retrieveUser(@PathVariable int id) {
Optional<User> user = userRepository.findById(id);
if (!user.isPresent())
throw new UserNotFoundException("id-" + id);
// "all-users", SERVER_PATH + "/users"
// retrieveAllUsers
Resource<User> resource = new Resource<User>(user.get());
ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
resource.add(linkTo.withRel("all-users"));
// HATEOAS
return resource;
}- LocaleResolver
- Default Locale - Locale.US
- ResourceBundleMessageSource
Implementation:
/src/main/resources/messages.properties
good.morning.message=Good Morning
/src/main/resources/messages_fr.properties
good.morning.message=Bonjour
/src/main/resources/application.properties
spring.messages.basename=messages
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
@SpringBootApplication
public class RestfulWebServicesApplication {
public static void main(String[] args) {
SpringApplication.run(RestfulWebServicesApplication.class, args);
}
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
return localeResolver;
}
}
@GetMapping(path = "/hello-world-internationalized")
public String helloWorldInternationalized() {
return messageSource.getMessage("good.morning.message", null,
LocaleContextHolder.getLocale());
}If we implement/add below dependency, it will automatically convert to XML format.
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency> Swagger
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.4.0</version>
</dependency>import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
public static final Contact DEFAULT_CONTACT = new Contact(
"Ranga Karanam", "http://www.in28minutes.com", "[email protected]");
public static final ApiInfo DEFAULT_API_INFO = new ApiInfo(
"Awesome API Title", "Awesome API Description", "1.0",
"urn:tos", DEFAULT_CONTACT,
"Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0");
private static final Set<String> DEFAULT_PRODUCES_AND_CONSUMES =
new HashSet<String>(Arrays.asList("application/json",
"application/xml"));
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(DEFAULT_API_INFO)
.produces(DEFAULT_PRODUCES_AND_CONSUMES)
.consumes(DEFAULT_PRODUCES_AND_CONSUMES);
}
}
@ApiModel @ApiModelProperty
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@ApiModel(description="All details about the user.")
@Entity
public class User {
@Id
@GeneratedValue
private Integer id;
@Size(min=2, message="Name should have atleast 2 characters")
@ApiModelProperty(notes="Name should have atleast 2 characters")
private String name;
@Past
@ApiModelProperty(notes="Birth date should be in the past")
private Date birthDate;
}<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>How to ignore few fields of the bean whcih is more secure and doesn't want to show in response. There are 2 ways:
- @JsonIgnore
- @JsonIgnoreProperties
Implementation:
- 1st way: @JsonIgnore
@Entity
public class Post {
@Id
@GeneratedValue
private Integer id;
private String description;
@JsonIgnore
private User user;
}- 2nd way: @JsonIgnoreProperties
@Entity
@JsonIgnoreProperties("sno", "password")
public class Post {
@Id
@GeneratedValue
private Integer id;
private String description;
private User user;
}Dynamically how to filter particular bean properties in run time:
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@RestController
public class FilteringController {
// field1,field2
@GetMapping("/filtering")
public MappingJacksonValue retrieveSomeBean() {
SomeBean someBean = new SomeBean("value1", "value2", "value3");
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("field1", "field2");
FilterProvider filters = new SimpleFilterProvider().addFilter("SomeBeanFilter", filter);
MappingJacksonValue mapping = new MappingJacksonValue(someBean);
mapping.setFilters(filters);
return mapping;
}
// field2, field3
@GetMapping("/filtering-list")
public MappingJacksonValue retrieveListOfSomeBeans() {
List<SomeBean> list = Arrays.asList(new SomeBean("value1", "value2", "value3"),
new SomeBean("value12", "value22", "value32"));
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("field2", "field3");
FilterProvider filters = new SimpleFilterProvider().addFilter("SomeBeanFilter", filter);
MappingJacksonValue mapping = new MappingJacksonValue(list);
mapping.setFilters(filters);
return mapping;
}
}
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("SomeBeanFilter")
public class SomeBean {
......
}- Versioning
- http://localhost:8080/v1/person
- http://localhost:8080/v2/person
-
http://localhost:8080/person/param
- params=[version=1]
-
http://localhost:8080/person/param
- params=[version=2]
-
http://localhost:8080/person/header
- headers=[X-API-VERSION=1]
-
http://localhost:8080/person/header
- headers=[X-API-VERSION=2]
-
http://localhost:8080/person/produces
- produces=[application/vnd.company.app-v1+json]
-
http://localhost:8080/person/produces
- produces=[application/vnd.company.app-v2+json]
Types:
- Media type versioning (a.k.a “content negotiation” or “accept header”)
- GitHub
- (Custom) headers versioning
- Microsoft
- URI Versioning
- Request Parameter versioning
- Amazon
- Factors
- URI Pollution (with URI Versioning we are polluting URI)
- Misuse of HTTP Headers (Headers are not intended for Versioning)
- Caching (Caching is difficult with the first 2 options because URI not come into the picture to cache. But with other 2 we can easily do cache since its all URI level)
- Can we execute the request on the browser? First options, we have to have some technical knowledge to execute becuase consumer needs send header params. But where as in other, this is not a problem.
- API Documentation : With Last 2 options, even generating documentation is not easy.
- No Perfect Solution : We have to prepare at the time of creating first rest service itself.
- https://www.mnot.net/blog/2011/10/25/web_api_versioning_smackdown
- http://urthen.github.io/2013/05/09/ways-to-version-your-api/
- http://stackoverflow.com/questions/389169/best-practices-for-api-versioning
- http://www.lexicalscope.com/blog/2012/03/12/how-are-rest-apis-versioned/
- https://www.3scale.net/2016/06/api-versioning-methods-a-brief-reference/
- Basic Authentication
- OAuth
To Enable Basic Authentication:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Instead of default user ids and password, we can set our own user Ids and password in spring properties file also.-
Level 0: Expose SOAP services in REST Style Like below URLs
-
Level 1: Correct URI but improper use of Http methods
-
Level 2: Level 1 + proper use of Http methods.
-
Level 3: Level 2+ HATEOAS Data + next possible actions.
Exception Handling proper use of Http Error codes:
Get: @PathVariable: users/{1}
Post: Created: 201 created
void createUser(@RequestBody User user)
Post: with htt code 201 and newly created URI @PostMapping("/users") public ResponseEntity createUser(@Valid @RequestBody User user) { User savedUser = service.save(user); // CREATED // /user/{id} savedUser.getId()
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedUser.getId()).toUri();
return ResponseEntity.created(location).build();
}
Exception Handling: This class handles 404, 400 Bad request and all internal servers errors. And also @Valid errors and @Max, @Min attribute errors (handleMethodArgumentNotValid).
public class ExceptionResponse { private Date timestamp; private String message; private String details;
public ExceptionResponse(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
//setter and getters }
User not found exception Useage in controller: @GetMapping("/jpa/users/{id}") public Resource retrieveUser(@PathVariable int id) { Optional user = userRepository.findById(id);
if (!user.isPresent())
throw new UserNotFoundException("id-" + id);
}}
@Valid and Min length validation @PostMapping("/jpa/users") public ResponseEntity createUser(@Valid @RequestBody User user) { User savedUser = userRepository.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}
Bean Class: @Entity public class User {
@Id
@GeneratedValue
private Integer id;
@Size(min=2, message="Name should have atleast 2 characters")
private String name;
}
MainException class
@ControllerAdvice @RestController public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), "Validation Failed",
ex.getBindingResult().toString());
return new ResponseEntity(exceptionResponse, HttpStatus.BAD_REQUEST);
}
}
Delete: Either we have to return noContent or success. NoContent means there is nothing to return but its deleted successfully.
Validation: Different Validation annotations @Past, @Min, @Max, @Size, @Null, @NotNull, @Digits, @Pattern, @Future, @DecimalMin, @DecimalMax