Complete Spring Restful Service Design I


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

Picture 1

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.

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:

  1. Status Code
  2. Message.
  3. 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:

  1. The @RestControllerAdvice annotation has basePackage that will ONLY scan controller in this package and then handle the response for them.
  2. The support() method will check the normal controller return object first, if it found already is ObjectResponse object, then no need to wrap again.
  3. 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.