<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-bo ot-starter-web</artifactId>
</dependency>
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
ServletUriComponentsBuilder.fromCurrentRequest().path("{id}").buildAndExpand(savedUser.getId()).toUri();
ResponseEntity
and return the ResponseEntity
object.
return ResponseEntity.created(location).body(savedUser);
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
logger.debug("User to save : {}", user);
User savedUser = userDaoService.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("{id}")
.buildAndExpand(savedUser.getId()).toUri();
return ResponseEntity.created(location).body(savedUser);
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
imports
import jakarta.validation.Valid;
Annotate the method parameter for validation.
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
logger.debug("User to save : {}", user);
var savedUser = userDaoService.save(user);
var location = ServletUriComponentsBuilder.fromCurrentRequest().path("{id}").buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).body(savedUser);
}
imports
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Size;
Add validation in the properties of the bean.
public class User {
private Integer id;
@Size(min = 3, max = 20, message = "Name must be more than 2 characters.")
private String name;
@Past(message = "Birth date should be in past.")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate birthDate;
// constructors, setter-getters and other methods.
}
Notes:
jakarta-validation
API.jakarta.validation.constraints.*
for more validation classes.
@Valid
annotation:
@Size
annotation
@Past
annotation
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.7.0</version>
</dependency>
None
.No code change required to enable swagger documentation
.POM.xml
https://github.com/springdoc/springdoc-openapi/blob/main/springdoc-openapi-starter-webmvc-ui
https://springdoc.org/#getting-started
<!-- XML conversion -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
None
Accept: application/xml
header.jackson-dataformat-xml
API dependency, if found bean will be transformed to xml. <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
import org.springframework.context.MessageSource;
@RestController
public class HelloWorldI18n {
private MessageSource messageSource;
// autowire (constructor injection) messageSource.
public HelloWorldI18n(MessageSource messageSource) {
super();
this.messageSource = messageSource;
}
@GetMapping("/say-hello-i18n")
public String sayHello() {
var locale = LocaleContextHolder.getLocale();
return this.messageSource.getMessage("good.morning", null, "Default - Good Morning", locale);
}
}
messages[-<locale>].properties
messages.properties
messages_es.properties
messages_ger.properties
messages
& suffix is .properties
.Accept-Language
Header from HTTP Request
and replaces it with <locale>
when locating messsages[-<locale>].properties
file.None
public class PersonV1 {
private String name;
public PersonV1(String name) {
super();
this.name = name;
}
// getter-setters
}
public class PersonV2 {
private Name name;
public PersonV2(Name name) {
super();
this.name = name;
}
// getter-setters
}
public class Name {
private String firstName;
private String lastName;
public Name(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
// getters-setters
}
import org.springframework.web.bind.annotation.GetMapping;
@RestController
public class UriVersioningPersonController {
/**
* Version 1
* @return
*/
@GetMapping("/v1/person")
public PersonV1 getPersonV1() {
return new PersonV1("URI Versioning v1");
}
/**
* Version 2
* @return
*/
@GetMapping("/v2/person")
public PersonV2 getPersonV2() {
return new PersonV2(new Name("URI", "Versioning V2"));
}
}
import org.springframework.web.bind.annotation.GetMapping;
@RestController
public class RequestParamVersioningController {
/**
* Version 1
* @return
*/
@GetMapping(path = "/person/param", params = "version=1")
public PersonV1 getPersonV1() {
return new PersonV1("Request Param versioning v1");
}
/**
* Version 2
* @return
*/
@GetMapping(path = "/person/param", params = "version=2")
public PersonV2 getPersonV2() {
return new PersonV2(new Name("Request Parama", "Versioning v2"));
}
}
import org.springframework.web.bind.annotation.GetMapping;
@RestController
public class CustomHeaderVersioning {
/**
* Version 1
* @return
*/
@GetMapping(path = "/person/header", headers = "X-API-VERSION=1")
public PersonV1 getPersonV1() {
return new PersonV1("Custom Header Versioning v1");
}
/**
* Version 2
* @return
*/
@GetMapping(path = "/person/header", headers = "X-API-VERSION=2")
public PersonV2 getPersonV2() {
return new PersonV2(new Name("Custom Header", "Versioning v2"));
}
}
Content negotiation
or Accept header
versioning.Accept
HTTP header.Accept: application/vnd.comp.app-v2+json
import org.springframework.web.bind.annotation.GetMapping;
@RestController
public class MediaTypeVersioning {
/**
* Version 1
* @return
*/
@GetMapping(path = "/person/accept", produces = "application/vnd.comp.app-v1+json")
public PersonV1 getPersonV1() {
return new PersonV1("Mediatype Versioning v1.");
}
/**
* Version 2
* @return
*/
@GetMapping(path = "/person/accept", produces = "application/vnd.comp.app-v2+json")
public PersonV2 getPersonV2() {
return new PersonV2(new Name("Media type", "Versioning v2"));
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
/**
* Retrieve the users.
* @return
*/
@GetMapping(path = "/users/{id}", produces = {"application/json", "application/xml"})
public EntityModel<User> retrieveUser(@PathVariable Integer id) {
User user = userDaoService.findById(id);
if (user == null) {
throw new UserNotFoundException(String.format("No user exists with id : %s", id));
}
// Hateoas: Create link to method
var link = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(this.getClass()).retrieveAllUsers());
// EntityModel object supports Model and allows to add links
final EntityModel<User> entityModel = EntityModel.of(user);
//Hateoas: Add link to Model response object.
entityModel.add(link.withRel("all-users"));
return entityModel;
}
Spring HATEOAS
provides some APIs to ease creating REST representations that follow the HATEOAS principle when working with Spring and especially Spring MVC.core problem it tries to address
is link creation and representation assembly.https://spring.io/projects/spring-hateoas
Purpose / Feature
jackson-annotations-x.x.jar
, which is added as part of spring-boot-starter-web
dependency.Maven / External dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Code changes
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/** Ignore all specified properties from the class. */
@JsonIgnoreProperties(value = {"property4", "property6"})
public class SomeBean {
private String property1;
// Ignore this property
@JsonIgnore
private String property2;
private String property3;
// @JsonIgnoreProperties - Ignore this
private String property4;
private String property5;
// @JsonIgnoreProperties - Ignore this
private String property6;
// constructors, setter-getters and utility methods
}
Notes:
Static Filtering
.@JsonIgnore
annotation:
@JsonIgnoreProperties
annotation
References:
TBU
Purpose / Feature
jackson-annotations-x.x.jar
, which is added as part of spring-boot-starter-web
dependency.Maven / External dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Code changes
filter name
create inside controller.filter name
.import com.fasterxml.jackson.annotation.JsonFilter;
/** Dynamically exclude properties as per the specified filter set by the API. */
@JsonFilter("dyna-filter-for-somebean")
public class SomeBeanDynamicFilter {
private String field1;
private String field2;
private String field3;
private String field4;
private String field5;
private String field6;
// constructors, setter-getters and utility methods
}
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.srvivek.sboot.mservices.bean.SomeBeanDynamicFilter;
@RestController
public class DynamicFilteringController {
@GetMapping("/dyna-filtering")
public MappingJacksonValue filtering() {
// Dynamic filtering
final SimpleBeanPropertyFilter simpleBeanPropertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("field2",
"field4", "field6");
final SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider()
.addFilter("dyna-filter-for-somebean", simpleBeanPropertyFilter);
// Construct resposnse bean
final SomeBeanDynamicFilter SomeBeanDynamicFilter = new SomeBeanDynamicFilter("Value-1", "Value-2", "Value-3",
"Value-4", "Value-5", "Value-6");
final MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(SomeBeanDynamicFilter);
mappingJacksonValue.setFilters(simpleFilterProvider);
return mappingJacksonValue;
}
@GetMapping("/dyna-filtering-list")
public MappingJacksonValue filteringList() {
// Dynamic filtering
SimpleBeanPropertyFilter simpleBeanPropertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("field1",
"field3", "field5", "field6");
FilterProvider simpleFilterProvider = new SimpleFilterProvider().addFilter("dyna-filter-for-somebean",
simpleBeanPropertyFilter);
// Construct list of resposnse beans
List<SomeBeanDynamicFilter> SomeBeanDynamicFilterList = Arrays.asList(
new SomeBeanDynamicFilter("Value-1", "Value-2", "Value-3", "Value-4", "Value-5", "Value-6"),
new SomeBeanDynamicFilter("Value-11", "Value-22", "Value-33", "Value-44", "Value-55", "Value-66"),
new SomeBeanDynamicFilter("Value-111", "Value-222", "Value-333", "Value-444", "Value-555",
"Value-666"));
final MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(SomeBeanDynamicFilterList);
mappingJacksonValue.setFilters(simpleFilterProvider);
return mappingJacksonValue;
}
}
Notes:
Dynamic Filtering
.@JsonFilter
annotation:
dyna-filter-for-somebean
is created and update in API method of controller class.References:
TBU
Purpose / Feature
Maven / External dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Code changes
None
public class ErrorDetails {
private LocalDateTime timestamp;
private String message;
private String details;
// Constructor, setter-getters
}
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
private static final long serialVersionUID = 4882099180124262207L;
public UserNotFoundException(String message) {
super(message);
}
}
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import jakarta.annotation.Priority;
@ControllerAdvice
//@ControllerAdvice(basePackages = "com.srvivek.x.y.z") //for specific package
//@Order(value = 1) // for defining ordering
//@Priority(value = 1) // for defining ordering
//@RestControllerAdvice(basePackages = "com.srvivek.x.y.z") // @ControllerAdvice + @ResponseBody
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomizedResponseEntityExceptionHandler.class);
/**
* Generic exception handling.
* @param ex
* @param request
* @return
* @throws Exception
*/
@ExceptionHandler(exception = Exception.class)
public final ResponseEntity<ErrorDetails> handleAllException(Exception ex, WebRequest request) throws Exception {
logger.error("Error stacktrace: {}", ex);
ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<ErrorDetails>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Return HTTP 404 for UserNotFoundException.
* @param ex
* @param request
* @return
* @throws Exception
*/
@ExceptionHandler(exception = UserNotFoundException.class)
public final ResponseEntity<ErrorDetails> handleUserNotFoundException(Exception ex, WebRequest request)
throws Exception {
logger.error("Error stacktrace: {}", ex);
ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<ErrorDetails>(errorDetails, HttpStatus.NOT_FOUND);
}
}
Notes:
References:
TBU
<!-- Spring boot HAL explorer -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-explorer</artifactId>
</dependency>
- http://localhost:8080/explorer
- http://localhost:8080/explorer/index.html#
##a13-sboot-ms-h2-jpa [TODO]
##a13-sboot-ms-mysql-jpa [TODO]
docker run --detach --env MYSQL_ROOT_PASSWORD=dummypassword --env MYSQL_USER=social-media-user --env MYSQL_PASSWORD=dummypassword --env MYSQL_DATABASE=social-media-database --name mysql --publish 3306:3306 mysql:8-oracle
mysqlsh
\connect social-media-user@localhost:3306
\sql
use social-media-database
select * from user_details;
select * from post;
\quit
<!-- Use this for Spring Boot 3.1 and higher -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- Use this if you are using Spring Boot 3.0 or lower
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
-->
#spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
spring.datasource.url=jdbc:mysql://localhost:3306/social-media-database
spring.datasource.username=social-media-user
spring.datasource.password=dummypassword
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
##a13-sboot-ms-mysql-jpa-JDBC-template [TODO]
##a14-sboot-sc-basic-authentication [TODO]
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Note: If facing any issue while starting the application, try following - Stop the server. - Update maven project (Alt + f5). - Start the server.
Default user is 'user'.
Get auto generated password from log.
Configuring user and password in application properties
spring.security.user.name=vivek
spring.security.user.password=welcome
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SpringSecurityConfiguration {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
/*
* All requests must be authorized.
*
* Else return HTTP 403, it doesn't prompt for user creds.
*/
httpSecurity.authorizeHttpRequests(
authorizationManagerRequestMatcherRegistryCustomizer -> authorizationManagerRequestMatcherRegistryCustomizer
.anyRequest().authenticated());
/*
* Prompt for authentication if request is not authorized.
*
* Using default customizer
*/
httpSecurity.httpBasic(Customizer.withDefaults());
/*
* Disabling CSRF as it may cause issue with HTTP methods - POST & PUT.
*
* if enabled, Keep prompting for user credentials for post request.
*/
httpSecurity.csrf(csrf -> csrf.disable());
return httpSecurity.build();
}
}