Overview
The Spring Restful services could be categorized into 3 components:
1. Controller
2. Model
3. Service
The process workflow of restful request can be describe as following
We are going to review following topic to minimized the coding effort in this workflow
1. Validation -> Customized Validation
2. Global Exception Controller -> Customized Exception
3. Global Format Restful Response Object
Use Case
The use case is very simple:
1. Frontend send in User object
2. Validate the name, password and email
3. If failed, send error message back to Frontend
4. If success, call UserService to create user in database.
Source Code link
Components
User Object
User object has 3 fields: name, password and email. The Validation are controlled by User Object self:
@Entity
public class User {
@Id @GeneratedValue
Long Id;
@NotEmpty(message = "User Name can not be empty")
@Size(min = 4, max = 20, message = "user name has to be 4 - 20")
private String name;
@NotEmpty(message = "Password can not be empty")
@Size(min = 4, max = 20, message = "password has to be 4 - 20")
private String password;
@NotEmpty(message = "Email can not be emtpy")
@Email
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
@Size
annotation define the min/max and error message for the field
@NotEmpty
annotation restric the field can not be empty.
In this case, the Validation works move from Controller to Model object, so the Controller object will look super clean.
If Validation failed, Spring will throw a MethodArgumentNotValidException
Exception for Controller to handle.
UserController
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
private String[] s;
@PostMapping("/addUser")
public User addUser(@RequestBody @Valid User user) {
return userService.addUser(user);
}
}
The Controller is the simplest class in this flow, the Validation works has been assigned to User
object and the insert work has been assigned to UserService
. There are other logics has been hidden:
Global Exception Handling
Global Format Response
Global Exception Handling
As we saw, the Controller doesn’t have exception handling code, so who is doing the “dirty” work? the answer is @RestControllerAdvice
. This Object does like @RestController
but different is this Advice
is Global.
And with @ExceptionHandler
annotation on method, we can create Global Exception Handler cross the board.
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ObjectResponse<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return new ObjectResponse(1001, "Input Paramter Invalid", objectError.getDefaultMessage());
}
@ExceptionHandler(SQLException.class)
public ObjectResponse<String> SQLExceptionHandler(SQLException sqle){
return new ObjectResponse(1001, "Database Error", sqle.getMessage());
}
}
Ignore the ObjectResponse
for now, we will introduce next, in this Global Exception Handler class, it handle 2 type of exception, the MethodArgumentNotValidException
is the input parameter error, so we retrieve ObjectError
from BindResult
and construct the response object.
The SQLException
is the internal exception, general speaking, we don’t suggest to send internal error message direct back to frontend. but here, using as example, we just put getMessage()
in the Response Object.
Glabal Format Response
In ExceptionControllerAdvice
object, it return ObjectResponse
, this is an universal response object that server return in this sample project. It actually is Wrapper object, that can put any object into it. It has 3 attributes:
- Status Code
- Message.
- Object that each controller want to return, in our case, is
User
object
public class ObjectResponse<T> {
// Status Code
private int code;
//message
private String msg;
//return data
private T data;
public ObjectResponse(T data) {
this(1000, "success", data);
}
public ObjectResponse(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
With genetic T
, we can wrap any object in that, in the ExceptionControllerAdvice
object, the T
is String
. You might be wonder how come UserController
not return ObjectResponse
object? yes, we can let UserController
return ObjectResponse
object:
@PostMapping("/getUser")
public ObjectResponse<User> addUser(@RequestBody @Valid User user) {
user = userService.addUser(user);
return new ObjectResponse(1000, "success", user);
}
but there is better way to handle this.
Wrapper of RestController
to provice global format response
in UserController
, the addUser
method return User
object, we can use following wrapper to “wrap” the UserController
, so the return object will be ObjectResponse
object.
@RestControllerAdvice(basePackages = {"com.r0ngsh3n.restful.template.controoler"})
public class GlobalResponseControllerAdvice implements ResponseBodyAdvice<Object> {
/**
* @param returnType
* @param aClass
* @return
*
* This method is used to check if the @RestController already return ObjectResponse, then no need to
* wrap another layer, just return as is
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
return !returnType.getGenericParameterType().equals(ObjectResponse.class);
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
/**
* SPECIAL HANDLING for String: need use ObjectMapper to wrap and then output
* to String
*/
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(new ObjectResponse<>(data));
} catch (JsonProcessingException e) {
throw new RuntimeException("some error message");
}
}
return new ObjectResponse<>(data);
}
}
This GlobalResponseControllerAdvice
object can be considered as Post
processor, it will wrap whatever object into ObjectResponse
object and return to frontend:
- The
@RestControllerAdvice
annotation hasbasePackage
that will ONLY scan controller in this package and then handle the response for them. - The
support()
method will check the normal controller return object first, if it found already isObjectResponse
object, then no need to wrap again. - The String object need to special taken care by ObjectMapper.
Conclusion
- Validation should leave to Model (User object in this case) object.
- There should be a center point to handle all the exception cross the board.
- The Response object should be an unified format.
- Controller Advice is solution for handle request/response cross the board, with this function, we can handle Exception and Response cross the board.