Skip to content

Commit 057e937

Browse files
committed
adding controlleradvice for common exceptions
1 parent eeeaa0d commit 057e937

12 files changed

+146
-19
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,25 @@ HATEOAS.
1717
#### Content Negotiation:
1818
Content Negotiation is one of the most powerful features of HTTP: the same service may serve clients
1919
that accept different protocols.
20+
21+
#### Little Bite of Servlet Container Terminology:
22+
In a typical Servlet Container implementation (Like Apache tomcat, ...) each http request is handled
23+
via a thread that is obtained from that Servlet Container's thread pool.
24+
25+
Threads pool are necessary to control the amount of the threads that are being executed simultaneously.
26+
27+
```
28+
In a regular basis, each thread consumes ~1MB of memory just to allocate a single thread stack,
29+
so 1000 simultaneous requests could use ~1GB of memory only for the thread stacks. Therefore,
30+
thread pool comes as a solution to limit the amount of threads being executed, fitting the application
31+
to a scenario where it doesn’t throw an OutOfMemoryError.
32+
```
33+
34+
#### Error Handling:
35+
@ControllerAdvie is a special type of component that may introduce behavior (and respond to exceptions) for
36+
any number of controllers. They are natural place to stick centralized @ExceptionHandler handlers.
37+
38+
39+
### Useful Resources:
40+
* [Using Asynchronuous Calls With Spring MVC](https://adrianobrito.github.io/2018/01/11/using-callable-responses-in-spring-mvc/)
41+
<!-- * [the difference between Callable and WebAsyncTask in Spring MVC]() -->

pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@
3737
<artifactId>spring-data-rest-hal-browser</artifactId>
3838
</dependency>
3939

40+
<dependency>
41+
<groupId>com.google.protobuf</groupId>
42+
<artifactId>protobuf-java</artifactId>
43+
<version>3.11.4</version>
44+
</dependency>
45+
<dependency>
46+
<groupId>com.googlecode.protobuf-java-format</groupId>
47+
<artifactId>protobuf-java-format</artifactId>
48+
<version>1.4</version>
49+
</dependency>
50+
4051
<dependency>
4152
<groupId>org.springdoc</groupId>
4253
<artifactId>springdoc-openapi-ui</artifactId>

protos/customer.proto

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.nodom.cnj;
2+
3+
option java_package = "io.nodom.cnj.customer.proto";
4+
5+
message Customer {
6+
optional int64 id = 1;
7+
required string firstName = 2;
8+
required string lastName = 3;
9+
}
10+
11+
message Customers {
12+
repeated Customer customer = 1;
13+
}
14+

protos/gen.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
3+
SRC_DIR='pwd'
4+
DST_DIR='pwd/../src/main/'
5+
6+
function ensure_implementations() {
7+
gem list | grep ruby-protocol-buffers || gem install ruby-protocol-buffers
8+
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
9+
}
10+
11+
function gen() {
12+
D=$1
13+
echo $D
14+
OUT=$DST_DIR/$D
15+
mkdir -p $OUT
16+
protoc -I=$SRC_DIR --${D}_out=$OUT $SRC_DIR/customer.proto
17+
}
18+
19+
ensure_implementations
20+
21+
gen java
22+
gen python
23+
gen ruby

src/main/java/io/nodom/cnj/CloudNativeJavaRestApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
56

7+
@EnableWebMvc
68
@SpringBootApplication
79
public class CloudNativeJavaRestApplication {
810

src/main/java/io/nodom/cnj/customer/controller/CustomerController.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.nodom.cnj.customer.controller;
22

33

4+
import io.nodom.cnj.customer.exception.CustomerNotFoundException;
45
import io.nodom.cnj.customer.model.Customer;
56
import io.nodom.cnj.customer.service.CustomerService;
67
import java.net.URI;
@@ -46,7 +47,9 @@ public ResponseEntity<List<Customer>> getAllCustomer() {
4647
@GetMapping(value = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE,
4748
MediaType.APPLICATION_XML_VALUE})
4849
public ResponseEntity<Customer> getCustomerById(@PathVariable Long id) {
49-
return ResponseEntity.ok(this.customerService.findCustomerById(id));
50+
Customer customer = this.customerService.findCustomerById(id)
51+
.orElseThrow(() -> new CustomerNotFoundException(id));
52+
return ResponseEntity.ok(customer);
5053
}
5154

5255
@PostMapping(produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.nodom.cnj.customer.controller;
2+
3+
import io.nodom.cnj.customer.exception.CustomerNotFoundException;
4+
import java.util.Optional;
5+
import org.springframework.hateoas.mediatype.vnderrors.VndErrors;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.ExceptionHandler;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
import org.springframework.web.bind.annotation.RestController;
11+
import org.springframework.web.bind.annotation.RestControllerAdvice;
12+
13+
14+
@RequestMapping(produces = "application/vnd.error+json")
15+
@RestControllerAdvice(annotations = RestController.class)
16+
public class CustomerControllerAdvice {
17+
18+
@ExceptionHandler(CustomerNotFoundException.class)
19+
ResponseEntity<VndErrors> notFoundException(CustomerNotFoundException ex) {
20+
return this.error(ex, HttpStatus.NOT_FOUND, ex.getCustomerId());
21+
}
22+
23+
@ExceptionHandler(IllegalArgumentException.class)
24+
ResponseEntity<VndErrors> assertionException(IllegalArgumentException ex) {
25+
return this.error(ex, HttpStatus.BAD_REQUEST, 0L);
26+
}
27+
28+
private <E extends Exception> ResponseEntity<VndErrors> error(E error, HttpStatus httpSatatus,
29+
Long logref) {
30+
String msg = Optional.of(error.getMessage()).orElse(error.getClass().getSimpleName());
31+
return new ResponseEntity<>(new VndErrors("CustomerId: ".concat(logref.toString()), msg), httpSatatus);
32+
}
33+
34+
}

src/main/java/io/nodom/cnj/customer/controller/CustomerProfilePhotoController.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,30 @@ public ResponseEntity<Resource> getCustomerProfilePhoto(@PathVariable Long id) {
3636
return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).body(customerPhoto);
3737
}
3838

39-
@RequestMapping(method = {RequestMethod.POST, RequestMethod.PUT}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
39+
/**
40+
* It could be an intensive IO Operation, for this reason, we decided to do it the non-blocking
41+
* way, using Asynchronous Calls, note that is recommended to use only the heavy operation inside
42+
* the callable lambda - {@link Callable}.
43+
*
44+
* @param id of the customer's profile.
45+
* @param photo to be uploaded.
46+
* @return a response with the HTTP Status.
47+
*/
48+
@RequestMapping(method = {RequestMethod.POST,
49+
RequestMethod.PUT}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
4050
public Callable<ResponseEntity<?>> uploadImage(@PathVariable Long id,
4151
@RequestParam MultipartFile photo) {
4252
log.info(
43-
String.format("====== upload start: /customer/%s/photo (%S bytes)", id, photo.getSize()));
53+
String.format("====== upload start: /customer/%s/photo (%S bytes) ======", id,
54+
photo.getSize()));
55+
return () -> {
56+
Long customerId = this.customerProfilePhotoService.uploadPhoto(id, photo);
4457

45-
Long customerId = this.customerProfilePhotoService.uploadPhoto(id, photo);
58+
URI location = ServletUriComponentsBuilder.fromCurrentContextPath().path("{id}")
59+
.buildAndExpand(customerId).toUri();
4660

47-
URI location = ServletUriComponentsBuilder.fromCurrentContextPath().path("{id}")
48-
.buildAndExpand(customerId).toUri();
49-
50-
return () -> ResponseEntity.created(location).build();
61+
return ResponseEntity.created(location).build();
62+
};
5163
}
5264

5365
}
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package io.nodom.cnj.customer.exception;
22

3+
4+
import lombok.Getter;
5+
6+
@Getter
37
public class CustomerNotFoundException extends RuntimeException {
48

5-
private String message;
9+
private final Long customerId;
610

7-
public CustomerNotFoundException(String message) {
8-
super(message);
9-
this.message = message;
11+
public CustomerNotFoundException(Long customerId) {
12+
super("cannot find customer with id: " + customerId);
13+
this.customerId = customerId;
1014
}
1115
}

src/main/java/io/nodom/cnj/customer/service/CustomerProfilePhotoService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public Resource readPhoto(Long customerId) {
3333
File file = new File(this.rootFile, Arrays.toString(this.encode(customer.getId())));
3434
return new FileSystemResource(file);
3535
}).orElseThrow(
36-
() -> new CustomerNotFoundException("No Customer found with Id: " + customerId));
36+
() -> new CustomerNotFoundException(customerId));
3737
}
3838

3939
public Long uploadPhoto(Long customerId, MultipartFile photo) {
@@ -42,7 +42,7 @@ public Long uploadPhoto(Long customerId, MultipartFile photo) {
4242
this.writeBinaryDateByCustomer(photo, customer.getId());
4343
return customer.getId();
4444
}).orElseThrow(
45-
() -> new CustomerNotFoundException("No Customer found with Id: " + customerId));
45+
() -> new CustomerNotFoundException(customerId));
4646
}
4747

4848
private void writeBinaryDateByCustomer(MultipartFile photo, Long customerId) {

src/main/java/io/nodom/cnj/customer/service/CustomerService.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ public List<Customer> findAllCustomers() {
2323
return this.customerRepository.findAll();
2424
}
2525

26-
public Customer findCustomerById(Long customerId) {
27-
return this.customerRepository.findById(customerId)
28-
.orElseThrow(() -> new CustomerNotFoundException("No Customer with Id: " + customerId));
26+
public Optional<Customer> findCustomerById(Long customerId) {
27+
return this.customerRepository.findById(customerId);
2928
}
3029

3130
public Customer createCustomer(@Valid Customer customer) {
@@ -35,7 +34,7 @@ public Customer createCustomer(@Valid Customer customer) {
3534

3635
public Customer deleteCustomerById(Long id) {
3736
Customer customer = this.customerRepository.findById(id)
38-
.orElseThrow(() -> new CustomerNotFoundException("No Customer With Id" + id));
37+
.orElseThrow(() -> new CustomerNotFoundException(id));
3938

4039
this.customerRepository.delete(customer);
4140
return customer;
@@ -50,6 +49,6 @@ public Customer updateCustomer(Customer customer) {
5049
existingCustomer.setLastName(customer.getLastName());
5150
this.customerRepository.save(existingCustomer);
5251
return customer;
53-
}).orElseThrow(() -> new CustomerNotFoundException("Customer Not Found"));
52+
}).orElseThrow(() -> new CustomerNotFoundException(customer.getId()));
5453
}
5554
}

src/test/restClient.http

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
GET localhost:8080/v1/customers/1
2+
3+
###

0 commit comments

Comments
 (0)