Tuesday, August 22, 2017

JavaEE: Cách Handle Message ExceptionMapper Cho RestAPI

Trong môi trường micro serivce ta có rất nhiều gói war và nhiều khi mỗi gói war sẽ có một cách xử lí exception riêng cái này cũng là điểm mạnh của micro service tai nó để các module tự quyết định các hành vi của mình kể cả tự quyết định công nghệ và quyết định database lun.
Nhưng nếu mình xài JavaEE và mình muốn có chung một đặc tả để xử lí exception thì mình sẽ đạc tả thế nào là đúng để những module khác chỉ cần sử dụng đặc tả này và không cần phải cài xư lí exception tư đầu.
Đây là cách mình đủa ra hai đặc tả chính trong project
Một là: Các lỗi về business ví dụ như, require field, validation, cần phải làm cái gì trươc khi làm cái gì vơi nhưng lỗi như thế này thì làm sao chúng ta trả về dữ liệu cho người dùng một cách rõng ràng nhất và tiện lợi nhât có thể tại vì những lỗi này là chúng ta đã biết nó hết.
Hai là: Lỗi do chúng ta không biết trước được rất khó xử lí như là database connection, internet connection, file system, permission...
Vì thế bên dưới mình đưa ra hai đặc tả cho hai loại lỗi trên.
1. Yêu Cầu
Ví dụ ta có yêu cầu xử lí Exception cho một business nào đó xem đoạn code bên dưới.
if(studentNotExisting(studentEntity)) {
 throw new MyException("student.notfound");
}
Và mong muốn của chúng ta là hệ thống tự động build tất cả các message theo ngôn ngữ của và trả v ề json string như sau
{
 code: 500,
 createdTime: "18/08/2017 15:30:30",
 message: "Student not found"
}
2. Cách Xử Lí 
Đầu tiên ta tạo lớp BusinessException kế thừa RuntimeException
public class BusinessException extends RuntimeException {

 private static final long serialVersionUID = 1L;

 private String code;
 private String resouceBundlePath;
 private Object[] params;
 
 public String getLocalizedMessage(Locale preferredLanguage) {
  return ErrorMessageBundle
    .byPath(this.resouceBundlePath)
    .inClassPath(this.getClass().getClassLoader())
    .build()
    .get(code, preferredLanguage, params);
 }
 
 public BusinessException(String code, String resourceBundlePath, Object... params) {
  this.code = code;
  this.resouceBundlePath = resourceBundlePath;
  this.params = params;
 }
 
 public String getCode() {
  return code;
 }

 public void setCode(String code) {
  this.code = code;
 }


 public String getResouceBundlePath() {
  return resouceBundlePath;
 }

 public void setResouceBundlePath(String resouceBundlePath) {
  this.resouceBundlePath = resouceBundlePath;
 }
}
Ở trong class này ta có truy cập tới BundleMessage mục địch của bundle message là dùng các file property để tạo ra các ngôn ngữ khác nhau
Tiếp đến ta tạo class ErrorMessageBundle như sau.
import java.text.MessageFormat;
import java.util.Locale;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ErrorMessageBundle {

 public static class Builder {
  private String bundlePath;
  private Optional<ClassLoader> classLoader = Optional.empty();

  public Builder(String bundlePath) {
   this.bundlePath = bundlePath;
  }

  public Builder inClassPath(ClassLoader classLoader) {
   this.classLoader = Optional.of(classLoader);
   return this;
  }

  public ErrorMessageBundle build() {
   return new ErrorMessageBundle(this.bundlePath, classLoader.orElse(Thread.currentThread().getContextClassLoader()));
  }
 }
 
 private static final String EMPTY_MESSAGE = "";
 private String resouceBundlePath;
 private ClassLoader classLoader;

 public static Builder byPath(String bundlePath) {
        return new Builder(bundlePath);
    }
 
 private ErrorMessageBundle(String resouceBundlePath, ClassLoader classLoader) {
  this.resouceBundlePath = resouceBundlePath;
  this.classLoader = classLoader;
 }

 public String get(String key, Locale preferredLanguage, Object... params) {
  try {
   Locale languageToUse = Optional.ofNullable(preferredLanguage).orElse(Locale.ENGLISH);
   ResourceBundle bundle = ResourceBundle.getBundle(this.resouceBundlePath, languageToUse, this.classLoader);
   if(bundle.containsKey(key)) {
    MessageFormat message = new MessageFormat(bundle.getString(key));
    String result = message.format(params);
    return result.replaceAll("\\{[0-9]+\\}", "");
   } 
   return EMPTY_MESSAGE;
  } catch (Exception slientAllErrors) {
   Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, slientAllErrors.getMessage(),
     slientAllErrors);
   return EMPTY_MESSAGE;
  }
 }

}
- Tiếp ta tạo class MyException kế thừa từ BusinessException trên như sau.
@ApplicationException
public class MyException extends BusinessException {
 
 private static final long serialVersionUID = 1L;
 private static final String RESOURCE_BUNDLE_PATH = "messages/error_message";
 
 public OrderManagementException(String code, Object... params) {
  super(code, RESOURCE_BUNDLE_PATH, params);
 }
}
- Tiếp đến là ta sẽ định nghĩa các file property tương ứng cho từng ngôn ngữ. Tao file error_message_vn.properties trong src/main/resources/messages với nội dung sau.
student.notfound = Sinh viên không tìm thấy
Tương tự ta tạo thêm một file error_message_en.properties cho ngôn ngữ tiếng anh. Như vậy thì khi ta throw new MyException("student.notfound"); làm sao hệ thống biết được mà tìm đến các file message mà ta đã config, chúng ta sẽ định nghĩa một class Provider để bắt tất cả các exception mà là con kế thừa của class BusinessException như sau
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {

 @Inject
 private HttpServletRequest httpServletRequest;

 @Override
 public Response toResponse(BusinessException businessException) {
  
  Map<String, Object> map = new HashMap<String, Object>();
  
  
  String codeMessage = businessException.getCode();
  String message = businessException.getLocalizedMessage(httpServletRequest.getLocale());
  
  map.put("code", codeMessage);
  map.put("createdTime", new SimpleDateFormat("dd/MM/yyyy H:m:s").format(new Date()));
  map.put("detail", message);
  
  return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON).entity(map).build();
  
 }
}
Và khí đó chúng ta sẽ có một số lỗi chung chung của hệ thống như, ngắt kết nối đến database thì ta phải làm sao, ta sẽ thêm một Provider nữa cho Exception thì tất cả các lỗi nào cũng sẽ được xử lí.
@Provider
public class SystemExceptionMapper implements ExceptionMapper<Exception> {

 private static final String CODE = "code";
 private static final String DETAIL = "detail";
 @Inject
 private HttpServletRequest httpServletRequest;
 
 @Override
 public Response toResponse(Exception exception) {
  Map<String, Object> map = new HashMap<>();
  UUID uuid = UUID.randomUUID();
  
  map.put("createdTime", new SimpleDateFormat("dd/MM/yyyy H:m:s").format(new Date()));
  map.put(DETAIL,"UUID: " + uuid + " " + exception.getMessage());

  Logger.getLogger(SystemExceptionMapper.class.getName()).log(Level.SEVERE, "UUID: " + uuid + exception.getMessage(), exception);
  
  //find business except in root cause
  Throwable cause = exception.getCause(); 
  while(cause != null && !(cause instanceof BusinessException)) {
   cause = cause.getCause();
  }
  if(cause != null && cause instanceof BusinessException) {
   String codeMessage = ((BusinessException)cause).getCode();
   String message = ((BusinessException)cause).getLocalizedMessage(httpServletRequest.getLocale());
   map.put(CODE, codeMessage);
   map.put(DETAIL, message);
   return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON).entity(map).build();
  }

  return Response.serverError().type(MediaType.APPLICATION_JSON).entity(map).build();
 }
 
}
Như vậy thì tất cả những lỗi sẽ được hệ thống xử lí và trả về 1 chuỗi json message cho client, chúng ta đã hoàn thành cách xử lí lỗi cho RestAPI của chúng ta.

No comments:

Post a Comment