Spring Boot - Getting Started with RESTful service
Overview
This is a quick tutorial on getting started with creating RESTful service with Spring Boot. This tutorial would be covering following aspects :
- Generating Project using Spring Initializr.
- Creating RESTful Controller.
- DTO Validations for Requests.
- Service Layer.
- Spring JPA for Persistance using In Memory H2 DB.
- Database Entities and Validations.
- Exception handling using
@RestControllerAdvice
.
Setup
To follow along, as a prerequisite you’ll need to install following toolkits:
- JDK 17 or newer
- Maven 3.8.6 or newer
- IDE of your choice, IntelliJ IDEA, Spring Tools 4 supported for Eclipse, Visual Studio Code and Theia.
Creating Project
Navigate to Spring Initializr to generate project’s base structure.
Enter data for following fields :
- Group : Follows java package rules
- Artifact : Project Name
- Name : Name of the Applications Main Class
- Description : Project Description
- Package Name : Combination of Group and Artifact
Click on Add Dependencies
button and search/select following dependencies:
- Spring Web
- Spring HATEOAS
- Validation
- Spring Data JPA
- H2 Database
Spring Initializr window should look something like:
Now click on Generate
button to download project structure. Unzip it and open the project in an IDE.
Spring Boot Essentials
Project contains StudentServiceApplication
class which serves as the main and entry point for the Spring Boot application.
@SpringBootApplication
serves as primary application configuration. Behind the scene it is equivalent to @Configuration
, @EnableAutoConfiguration
and @ComponentScan
.
@Configuration
tags the class as a source of bean definitions for the application context.@EnableAutoConfiguration
enables Spring Boot to start adding beans based on classpath settings, other beans, and various property settings.@ComponentScan
enables Spring to look for other components, configurations, and services in thecom.abhijits.studentservice
package.
1
2
3
4
5
6
7
8
@SpringBootApplication
public class StudentServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StudentServiceApplication.class, args);
}
}
Run the application on Terminal/Console using mvn spring-boot:run
and stop using Ctrl+c
or use IDE’s start
and stop
button to run and stop application respectively.
Successful execution will produce following logs for application start up:
Controller
Let’s create a controller class StudentController
.
StudentController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@RestController
@RequestMapping("/student")
public class StudentController {
@GetMapping(
path = "/{uuid}",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<StudentDto> get(@PathVariable UUID uuid) {
return ResponseEntity.ok().body(new StudentDto().setUuid(uuid)
.setAge(18)
.setGender(Gender.MALE)
.setStandard(10)
.setFirstName("Amit")
.setLastName("Sharma")
);
}
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<StudentDto> create(@RequestBody StudentDto studentDto) {
studentDto.setUuid(UUID.randomUUID());
return ResponseEntity.created(
WebMvcLinkBuilder.linkTo(
WebMvcLinkBuilder.methodOn(StudentController.class)
.get(studentDto.getUuid())).toUri()
).body(studentDto);
}
@PatchMapping(
path = "/{uuid}",
consumes = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<StudentDto> update(@PathVariable UUID uuid, @RequestBody StudentDto studentDto) {
return ResponseEntity.ok()
.body(studentDto.setUuid(uuid));
}
@DeleteMapping(
path = "/{uuid}",
consumes = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity delete(@PathVariable UUID uuid) {
return ResponseEntity.ok().build();
}
}
@RestController
on line 1, tells spring that this class is a Rest Controller. It is a Convenience Annotation which includes@Controller
and@ResponseBody
@RequestMapping
- Maps HTTP request with request handler methods. On line 2@RequestMapping("/student")
is going to redirect all the requests call starting with/student
to methods contained in StudentController class.@GetMapping
on line 5 is mapping theget()
method to HTTP GET request with path variableuuid
. As there is a@RequestMapping
defined on class level so final path would be/student/{uuid}
where{uuid}
defines a place holder for actual uuid.produces = MediaType.APPLICATION_JSON_VALUE
on line 7 defines the response content type and is mached against theAccept
header of HTTP Request.@PathVariable
on line 9 maps the place holder defined in@GetMapping
on line 6 to corresponding java varialbeUUID uuid
. If name of the corresponding java variable name is different from the place holder, then it could be mapped using@PathVariable("uuid")
ResponseEntity
class is used to fully configure the complete HTTP response: status code, headers and body. On line 9@ResponseEntity<StudentDto>
defines that body of the response would containStudentDto
.@PostMapping
on line 16 is mappingcreate()
method to POST/student
request. No explicit path is defined on this annotation so path defined by@RequestMapping
on class level at line 1 would be mapped.WebMvcLinkBuilder
on line 26 is used to easily buildLink
instances pointing to Spring MVC controllers. The benefit of using this is that links are not hard coded and would be updated automatically if there are any changes in the method or the link associated with it.@RequestBody
on line 23 maps the body of the POST request toStudentDto
@PatchMapping
on line 32 is mappingupdate()
method to PATCH/student/{uuid}
request. For this particular request we are using both@PathVariable
and@RequestBody
annotation to map path variable and request body contained in PATCH request.@DeleteMapping
on line 41 is mappingdelete()
method to DELETE/student/{uuid}
.
Now let’s create StudentDto
and Gender
Enum used in StudentController
StudentDto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StudentDto {
private UUID uuid;
private String firstName;
private String lastName;
private double age;
private Gender gender;
private int standard;
public UUID getUuid() {
return uuid;
}
public StudentDto setUuid(UUID uuid) {
this.uuid = uuid;
return this;
}
// ... add getter/setter for rest of the fields using above pattern where setter is returning this
// ... add toString()
}
Gender Enum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public enum Gender {
MALE("MALE"),
FEMALE("FEMALE"),
OTHER("OTHER");
private String value;
private Gender(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public Gender setValue(String value) {
this.value = value;
return this;
}
@Override
public String toString() {
return "Gender{" +
"value='" + value + '\'' +
'}';
}
}
Test Run for Endpoints
Before trying run
the applicaton let’s comment following dependencies in our pom.xml
file as our DB is not yet configured and running the application would throw error.
1
2
3
4
5
6
7
8
9
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-jpa</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.h2database</groupId>-->
<!-- <artifactId>h2</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- </dependency>-->
Now let’s Run the application and test the REST endpoints. You can either use curl
to send http request or use Postman
or any other http request tool of you choosing. In this blog we are defining the curl
request for testing.
Getting familiarised with curl
early on is benefitial as it can be used for testing/debugging REST requests inside containers/servers as well.
Create Student
Request :
curl -v -X POST -d '{"firstName":"Varun","lastName":"Dube","age":18,"gender":"MALE","standard":10}' -H 'Content-Type: application/json' http://localhost:8080/student
Response :
1
2
3
4
5
6
7
8
< HTTP/1.1 201
< Location: http://localhost:8080/student/1d9a0b9d-0832-4198-97dd-4235f568265b
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 27 Sep 2022 07:36:14 GMT
<
* Connection #0 to host localhost left intact
{"uuid":"1d9a0b9d-0832-4198-97dd-4235f568265b","firstName":"Varun","lastName":"Dube","age":18.0,"gender":"MALE","standard":10}
Get Student
Request :
curl -v -X GET -H 'Accept: application/json' http://localhost:8080/student/2577edba-5ade-4783-b653-0294bc2703e2
Response :
1
2
3
4
5
6
7
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 27 Sep 2022 09:27:23 GMT
<
* Connection #0 to host localhost left intact
{"uuid":"2577edba-5ade-4783-b653-0294bc2703e2","firstName":"Amit","lastName":"Sharma","age":18.0,"gender":"MALE","standard":10}
Update Student
Request :
curl -v -X PATCH -d '{"firstName":"Varun","lastName":"Dube","age":40,"gender":"MALE","standard":12}' -H 'Content-Type: application/json' http://localhost:8080/student/fac201c6-353c-4c57-9349-0397df285901
Response :
1
2
3
4
5
6
7
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 27 Sep 2022 09:29:26 GMT
<
* Connection #0 to host localhost left intact
{"uuid":"fac201c6-353c-4c57-9349-0397df285901","firstName":"Varun","lastName":"Dube","age":40.0,"gender":"MALE","standard":12}
Delete Student
Request :
curl -v -X DELETE -H 'Content-Type: application/json' http://localhost:8080/student/fac201c6-353c-4c57-9349-0397df285901
Response :
1
2
3
4
5
< HTTP/1.1 200
< Content-Length: 0
< Date: Tue, 27 Sep 2022 09:31:12 GMT
<
* Connection #0 to host localhost left intact
DTO Validation
Now let’s try to execute following POST request where names are empty.
curl -v -X POST -d '{"firstName":"","lastName":"","age":18,"gender":"MALE","standard":10}' -H 'Content-Type: application/json' http://localhost:8080/student
Application provided following response:
1
{"uuid":"7974baf3-c6f2-4784-b74b-3d41ecb18b68","firstName":"","lastName":"","age":18.0,"gender":"MALE","standard":10}%
This is not a valid request as firstName
and lastName
should not be null or empty and should be a valid name.
Let’s add validations on our StudentDto
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StudentDto {
private UUID uuid;
@NotBlank(message = "'firstName' should not be Empty or null.")
@Pattern(regexp = "^[A-Za-z]+$", message = "'firstName' should only contain alphabets.")
private String firstName;
@NotBlank(message = "'firstName' should not be Empty or null.")
@Pattern(regexp = "^[A-Za-z]+$", message = "'firstName' should only contain alphabets.")
private String lastName;
@NotNull(message = "'age' should not be null.")
@Min(value = 4, message = "'age' minimum valid age is 4.")
@Max(value = 99, message = "'age' maximum valid age is 99.")
private double age;
@NotNull(message = "'gender' should not be null.")
private Gender gender;
@NotNull(message = "'standard' should not be null.")
@Min(value = 1, message = "'standard' minimum valid value is 1.")
@Max(value = 12, message = "'standard' maximum valid value is 12.")
private int standard;
}
@NotBlank
is applied on CharSequence fields and validates that the field is not null and trimmed length is greater than zero.@Pattern
is also applied to CharSequence fields and validates that field values match the provided regex.@NotNull
validates that the field is not null.@Min
and@Max
are used on number fields to validate minimum and maximum allowed values.
In order for these validations to be used we need to annotate DTO in our controller methods with @Valid
. Update the create()
and update()
methods as follows:
public ResponseEntity<StudentDto> create(@Valid @RequestBody StudentDto studentDto)
public ResponseEntity<StudentDto> update(@PathVariable UUID uuid, @Valid @RequestBody StudentDto studentDto)
Let’s not again run the application and fire post request with invalid data.
curl -v -X POST -d '{"firstName":"","lastName":"","age":18,"gender":"MALE","standard":10}' -H 'Content-Type: application/json' http://localhost:8080/student
Application is now returning Bad Request 400 in response.
1
2
3
4
5
6
7
8
< HTTP/1.1 400
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 02 Oct 2022 00:57:24 GMT
< Connection: close
<
* Closing connection 0
{"timestamp":"2022-10-02T00:57:24.809+00:00","status":400,"error":"Bad Request","path":"/student"}
Let’s not connect our applicaton with database, then later we will use @RestControllerAdvice
to return custom error response.
Database Configurations
Let’s add/uncomment JPA dependencies in pom.xml file.
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
You can either use application.properties
file or application.yml
and add following properties to configure in memory H2 database. For this blog we are using application.yml
file.
1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
url: jdbc:h2:mem:mydb
username: admin
password: password
driverClassName: org.h2.Driver
jpa:
database-platform: org.hibernate.dialect.H2Dialect
h2:
console:
enabled: true
- Above configs define the
url
,username
,password
anddriverClassName
used to access the database. spring.jpa.database-platform
is used to set the databaseDialect
.h2.console.enabled
is used to enable the h2 database console for debugging. After running the application console can be accessed athttp://localhost:8080/h2-console
Entity and Repository
Let’s create entity class to map with database table.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entity
public class Student {
@Id
@Column(columnDefinition = "UUID")
private UUID uuid = UUID.randomUUID();
@Column(nullable = false)
private String firstName;
@Column
private String lastName;
@Column(nullable = false)
private double age;
@Column(nullable = false)
@Enumerated(EnumType.ORDINAL)
private Gender gender;
@Column(nullable = false)
private int standard;
// Add getter's, setter's and toString() method
}
@Entity
on line 1 defines thatStudent
class is an Entity and is mapped to a database table.@Id
on line 4 specifies the primary key for the table.@Column
is used to map variable to corresponding column name in table.nullable
parameter defines if the column could havenull
values, default istrue
.columnDefinition
on line 5 is used to define database column type. This is not usually required but in current version of hibernate mapping for H2 databaseUUID
column type is not automatically inferred, so needs to be defined explicitly.
Now let’s create StudentRepository
class extending from JpaRepository
for accessing database methods.
1
2
public interface StudentRepository extends JpaRepository<Student, UUID> {
}
Extending StudentRepository
from JpaRepository
adds some basic CRUD methods and functionality.
Service and Transformer
Now let’s create a StudentService
class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
public class StudentService {
private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
public Student get(UUID uuid) {
return studentRepository.findById(uuid).orElseThrow(() -> new IllegalArgumentException("Student not found."));
}
public Student create(Student student) {
return studentRepository.save(student);
}
public void delete(UUID uuid) {
studentRepository.deleteById(uuid);
}
public Student update(Student student) {
return studentRepository.save(
get(student.getUuid())
.setFirstName(student.getFirstName())
.setLastName(student.getLastName())
.setGender(student.getGender())
.setAge(student.getAge())
.setStandard(student.getStandard())
);
}
}
@Service
annotation on line 1 is a specialization of@Component
annotation and defines that the class is a service layer and does not encapsulate any state.StudentService
class is usingconstructor
based injection forStudentRepository
.@Autowired
attonation is used to mark dependency injection, but this annotation is optional for constructor injection.
Let’s create Transformer
interface to convert DTO to Entity and vice-versa.
1
2
3
4
5
public interface Transformer<TDomain, TDto> {
TDto toDto (TDomain entity);
TDomain toEntity(TDto dto);
}
Now let’s extend Transformer
interface to create StudentTransformer
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class StudentTransformer implements Transformer<Student, StudentDto> {
@Override
public StudentDto toDto(Student student) {
return new StudentDto()
.setUuid(student.getUuid())
.setFirstName(student.getFirstName())
.setLastName(student.getLastName())
.setGender(student.getGender())
.setAge(student.getAge())
.setStandard(student.getStandard());
}
@Override
public Student toEntity(StudentDto studentDto) {
return new Student()
.setFirstName(studentDto.getFirstName())
.setLastName(studentDto.getLastName())
.setAge(studentDto.getAge())
.setGender(studentDto.getGender())
.setStandard(studentDto.getStandard());
}
}
Controller Update
Now let’s wire the StudentService
and StudentTransformer
in StudentController
and update the get()
, create()
, update()
and delete()
methods.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@RestController
@RequestMapping("/student")
public class StudentController {
private final StudentService studentService;
private final StudentTransformer studentTransformer;
public StudentController(StudentService studentService, StudentTransformer studentTransformer) {
this.studentService = studentService;
this.studentTransformer = studentTransformer;
}
@GetMapping(
path = "/{uuid}",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<StudentDto> get(@PathVariable UUID uuid) {
return ResponseEntity.ok().body(studentTransformer.toDto(studentService.get(uuid)));
}
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<StudentDto> create(@Valid @RequestBody StudentDto studentDto) {
StudentDto responseStudentDto = studentTransformer.toDto(studentService.create(studentTransformer.toEntity(studentDto)));
return ResponseEntity.created(
WebMvcLinkBuilder.linkTo(
WebMvcLinkBuilder.methodOn(StudentController.class)
.get(responseStudentDto.getUuid())).toUri()
).body(responseStudentDto);
}
@PatchMapping(
path = "/{uuid}",
consumes = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<StudentDto> update(@PathVariable UUID uuid, @Valid @RequestBody StudentDto studentDto) {
return ResponseEntity.ok()
.body(studentTransformer.toDto(studentService.update(studentTransformer.toEntity(studentDto).setUuid(uuid))));
}
@DeleteMapping(
path = "/{uuid}",
consumes = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity delete(@PathVariable UUID uuid) {
studentService.delete(uuid);
return ResponseEntity.ok().build();
}
}
Let’s run the application and navigate to http://localhost:8080/h2-console
in browser.
Enter database username
and password
set in application.yml
file and click on Connect
button.
Let’s now send a post request to application.
curl -v -X POST -d '{"firstName":"Varun","lastName":"Dube","age":18,"gender":"MALE","standard":10}' -H 'Content-Type: application/json' http://localhost:8080/student
Getting following response in console.
1
2
3
4
5
6
7
8
< HTTP/1.1 201
< Location: http://localhost:8080/student/b83f54d9-b9db-4bef-877f-43e50243c5ea
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 02 Oct 2022 01:49:47 GMT
<
* Connection #0 to host localhost left intact
{"uuid":"b83f54d9-b9db-4bef-877f-43e50243c5ea","firstName":"Varun","lastName":"Dube","age":18.0,"gender":"MALE","standard":10}
Now let verify in h2 console that data is actually entered in DB. Enter following query in SQL Statement
text box.
1
SELECT * FROM student;
Click on the Run
button.
Results in Database should match response in your POST request.
Error Handling
Let’s first create ErrorResponse
class. This would define the format returned when error has occured.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ErrorResponse {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
private ZonedDateTime timestamp;
private Integer statusCode;
private List<String> message;
private String stackTrace;
public static ErrorResponse createInstance() {
return new ErrorResponse().setTimestamp(ZonedDateTime.now());
}
public List<String> getMessage() {
return message;
}
public ErrorResponse setMessage(List<String> message) {
this.message = message;
return this;
}
public ErrorResponse addMessage(String message) {
if (this.message == null) {
this.message = new ArrayList<>();
}
this.message.add(message);
return this;
}
// Getter's/Setter's for remaining private fields
}
@JsonFormat
on line 3 defines the format to serialize timestamp field.
Let’s create a class GlobalExceptionHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Value("${stackTrace.enabled}")
private boolean printStackTrace;
@Override
public ResponseEntity<Object> handleExceptionInternal(
Exception ex,
Object body,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
return buildErrorResponse(ex, status);
}
@ExceptionHandler(value = { Exception.class })
protected ResponseEntity<Object> handleAllException(Exception ex) {
ResponseStatus responseStatus = ex.getClass().getAnnotation(ResponseStatus.class);
if (responseStatus != null)
return buildErrorResponse(ex, responseStatus.value());
return buildErrorResponse(ex, HttpStatus.INTERNAL_SERVER_ERROR);
}
private ResponseEntity<Object> buildErrorResponse(Exception exception, HttpStatus httpStatus) {
ErrorResponse errorResponse = ErrorResponse.createInstance()
.setStatusCode(httpStatus.value());
if (exception instanceof MethodArgumentNotValidException) {
errorResponse.setMessage(((MethodArgumentNotValidException) exception).getBindingResult().getAllErrors().stream().map(
MessageSourceResolvable::getDefaultMessage).collect(Collectors.toList()));
} else {
errorResponse.addMessage(exception.getMessage());
}
if (printStackTrace) {
errorResponse.setStackTrace(getStackTrace(exception));
}
return ResponseEntity.status(httpStatus).body(errorResponse);
}
private String getStackTrace(Exception exception) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
exception.printStackTrace(printWriter);
return stringWriter.toString();
}
}
@RestControllerAdvice
annotation on line 1 is specialization of@Component
annotation and is auto-detected via classpath scanning.@RestControllerAdvice
is the combination of both@ControllerAdvice
and@ResponseBody
which acts as interceptor that surrounds the logic in Controllers and allows us to apply some common logic to them.- Rest Controller Advice’s methods (annotated with
@ExceptionHandler
) are shared globally across multiple@Controller
components to capture exceptions and translate them to HTTP responses. @ExceptionHandler
on line number 18, markshandleAllException()
method for capturing exceptions and translating them to HTTP responses. This method is shared globally across multiple@Controller
s. The@ExceptionHandler
annotation indicates which type ofException
can be handled. The exception instance and the request is injected via method arguments.- By using two annotations together, we can:
- control the body of the response along with status code
- handle several exceptions in the same method
@Value
annotation on line 4, defines a property driven dependency injection whereprintStackTrace
varialbe recieves value from the property file.@GlobalExceptionHandler
is extended fromResponseEntityExceptionHandler
, which is a convenience base class for@ControllerAdvice
to provide centralized exception handling across all@RequestMapping
methods through@ExceptionHandler
methods.
Add following property to application.yml
file to enable/disable returning of stacktrace in error response.
1
2
stackTrace:
enabled: false
Let’s create a NotFoundException
and throw it from StudentService
class when get does not find anything.
1
2
3
4
5
6
7
8
9
10
11
@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
public NotFoundException() {
super();
}
public NotFoundException(String message) {
super(message);
}
}
@ResponseStatus
annotation defines the HttpStatus code which this exception is translated too.
Update StudentService
class to use NotFoundException
.
1
2
3
public Student get(UUID uuid) {
return studentRepository.findById(uuid).orElseThrow(() -> new NotFoundException("Student not found."));
}
Now let’s again try to test with Invalid POST request.
curl -v -X POST -d '{"firstName":"","lastName":"","age":18,"gender":"MALE","standard":20}' -H 'Content-Type: application/json' http://localhost:8080/student
Response is 400 Bad Request.
1
2
3
4
5
6
7
8
< HTTP/1.1 400
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 02 Oct 2022 01:25:39 GMT
< Connection: close
<
* Closing connection 0
{"timestamp":"02-10-2022 06:55:39","statusCode":400,"message":["'firstName' should not be Empty or null.","'lastName' should only contain alphabets.","'firstName' should only contain alphabets.","'standard' maximum valid age is 12.","'lastName' should not be Empty or null."],"stackTrace":null}%
Let’s enable
stackTrace in application.yml
1
2
stackTrace:
enabled: true
Get request for data is not available.
curl -v -X GET -H 'Accept: application/json' http://localhost:8080/student/2577edba-5ade-4783-b653-0294bc2703e2
Response is a 404 Not Found with complete stackTrace.
1
2
3
4
5
6
< HTTP/1.1 404
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 02 Oct 2022 01:34:56 GMT
<
{"timestamp":"02-10-2022 07:04:56","statusCode":404,"message":["Student not found."],"stackTrace":"com.abhijits.studentservice.errorhandling.exceptions.NotFoundException: Student not found.\n\tat com.abhijits.studentservice.service.StudentService.lambda$get$0(StudentService.java:23)\n\tat java.base/java.util.Optional.orElseThrow(Optional.java:403)\n\tat com.abhijits.studentservice.service.StudentService.get(StudentService.java:23)\n\tat com.abhijits.studentservice.controller.StudentController.get(StudentController.java:36)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:655)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:764)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)\n\tat org.apache.catalina.core.Standa* Connection #0 to host localhost left intact rdContextValve.invoke(StandardContextValve.java:97)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\n"}%
Summary
Congratulations! now you have successfully created a SpringBoot based REST Application which is able to perform basic CRUD operations, along with basic error handling.
Complete code for above tutorial could be found on Github.