Exception là một trong những tính năng cực kỳ mạnh của java, nó cho phép các nhà phát triển xử lý lỗi khi cần thiết thay vì lúc nào cũng phải xử lý lỗi, ví dụ thay vì code thế này:
public void step1() {
    if (correct) {
        return result;
    }
    return error;
}

public void step2() {
    var result, error = step1();
    if (error != null) {
        return error;
    }
    return process2(result);
}

public void step3() {
    var result, error = step2();
    if (error != null) {
        return error;
    }
    return process3(result);
}

Thì bạn chỉ cần viết thế này:

public void step1() {
    if (correct) {
        return result;
    }
    throw new Exception("somthing");
}

public void step2() {
    return process2(step1());
}

public void step3() {
    try {
        return process3(result);
    } catch(Exception e) {
        // process error
    }
}

Mã nguồn sẽ ngắn gọn hơn rất nhiều. Với EzyPlatform thậm chí mọi việc còn đơn giản hơn thế khi các Exception được xử lý tập trung ở các lớp global, sẽ tiết kiệm công sức rất nhiều cho các nhà phát triển.

Đường đi của một exception

Tương ứng với đường đi của một yêu cầu đến và được xử lý bởi EzyPlatform thì exception cũng sẽ được sinh ra trong quá trình này:

Xử lý exception.png
  1. Tại servlet cũng có thể ném ra các exception hoặc các lỗi như không tìm thấy URI nào khớp với yêu cầu từ client gửi đến.
  2. Tại các interceptor cũng có thể ném ra các exception để không cho phép yêu cầu được đến controller.
  3. Tại các controller các nhà phát triển có thể ném ra các exception tương ứng với nghiệp vụ mà mình xử lý.

Khi một exception được ném ra thì nó sẽ không rơi vào hố đen hay gây crash chương trình bởi vì EzyPlatform được thừa hưởng từ EzyHTTP và EzyFox Server sẽ điều hướng toàn bộ các exception này đến các lớp xử lý tập trung. Nếu exception không được xử lý thì nó sẽ được coi là lỗi và được xử lý bởi các lớp cài đặt giao diện UnhandledErrorHandler, nếu không tồn tại lớp xử lý lỗi thì mặc định EzyPlatform sẽ trả về trạng thái INTERNAL_SERVER_ERROR.

Một số lớp xử lý exception do EzyPlatform cung cấp

Ở admin đã có sẵn lớp xử lý exception chung có tên AdminGlobalExceptionHandler với mã nguồn như sau:

@ExceptionHandler
@AllArgsConstructor
public class AdminGlobalExceptionHandler extends EzyLoggable {

    private final RequestURIManager requestUriManager;

    @TryCatch(ProjectMissingDependenciesException.class)
    public Object handle(ProjectMissingDependenciesException e) {
        logger.info("{}({})", e.getClass().getSimpleName(), e.getMessage());
        return ResponseEntity.builder()
            .status(StatusCodes.FORBIDDEN)
            .body(
                singletonMap(
                    "projectMissingDependencies",
                    e.getMissingDependencies()
                )
            )
            .build();
    }

    // các hàm xử lý khác.

    @TryCatch(DeserializeValueException.class)
    public Object handle(
        HttpServletRequest request,
        RequestArguments arguments,
        DeserializeValueException e
    ) {
        logger.info("{}({})", e.getClass().getSimpleName(), e.getMessage(), e);
        HttpMethod method = arguments.getMethod();
        String uriTemplate = arguments.getUriTemplate();
        if (requestUriManager.isApiURI(method, uriTemplate)) {
            return ResponseEntity.badRequest(
                singletonMap(
                    e.getValueName(),
                    "invalid"
                )
            );
        }
        return Redirect.to(addLanguageToUri(request, "/bad-request"));
    }
}

Lớp này xử lý gần toàn bộ các exception thông thường mà bạn hay dùng ví dụ như PermissionDeniedException, ForbiddenActionException vậy nên khi phát triển các plugin phần admin bạn cũng sẽ gần như không bao giờ phải xử lý exception nữa.

Đối với các exception hoặc các lỗi không được xử lý thì lớp AdminGlobalErrorHandler sẽ tiếp nhận và làm công việc của mình với mã nguồn như sau:

@EzySingleton
@AllArgsConstructor
public class AdminGlobalErrorHandler
    extends EzyLoggable
    implements UnhandledErrorHandler {

    private final AdminViewFactory viewFactory;
    private final AdminMenuManager menuManager;
    private final RequestURIManager requestUriManager;
    private final AdminEnvironmentManager environmentManager;

    @Override
    public Object processError(
        HttpMethod method,
        HttpServletRequest request,
        HttpServletResponse response,
        int errorStatusCode,
        Exception exception
    ) {
        if (environmentManager.isDebugMode()) {
            logger.info(
                "process error for request uri: {}, params: {}",
                request.getRequestURI(),
                request.getQueryString(),
                exception
            );
        }
        String matchedUri = (String) request.getAttribute(ATTRIBUTE_MATCHED_URI);
        if (matchedUri == null) {
            matchedUri = request.getRequestURI();
        }
        boolean isApiUri = method != HttpMethod.GET
            || requestUriManager.isApiURI(method, matchedUri);
        if (isApiUri) {
            return toResponseEntity(errorStatusCode, exception);
        }
        Menu menu = menuManager.getMenuByUri(matchedUri);
        if (menu == null) {
            MenuItem menuItem = menuManager.getMenuItemByUri(matchedUri);
            if (menuItem != null) {
                menu = menuManager.getMenuByFullName(menuItem.getMenuFullName());
            }
        }
        if (menu == null) {
            return processError(request, errorStatusCode, exception);
        }
        return viewFactory.newViewBuilder()
            .template("default-menus")
            .addVariable("menu", menu)
            .build();
    }

    private Object processError(
        HttpServletRequest request,
        int errorStatusCode,
        Exception exception
    ) {
        if (environmentManager.isDebugMode()) {
            return toResponseEntity(errorStatusCode, exception);
        }
        if (errorStatusCode == StatusCodes.NOT_ACCEPTABLE) {
            return ResponseEntity
                .status(StatusCodes.NOT_ACCEPTABLE)
                .build();
        } else if (errorStatusCode == StatusCodes.INTERNAL_SERVER_ERROR) {
            return Redirect.builder()
                .uri(addLanguageToUri(request, "/server-error"))
                .addAttribute(
                    "exceptionMessage",
                    exceptionToSimpleString(exception)
                )
                .build();
        } else if (errorStatusCode == StatusCodes.BAD_REQUEST) {
            return Redirect.to(addLanguageToUri(request, "/bad-request"));
        }
        return Redirect.to(addLanguageToUri(request, "/not-found"));
    }

    private ResponseEntity toResponseEntity(
        int errorStatusCode,
        Exception exception
    ) {
        return ResponseEntity.status(errorStatusCode)
            .body(
                Collections.singletonMap(
                    "error",
                    exception != null
                        ? exception.getClass().getName()
                        : String.valueOf(errorStatusCode)
                )
            )
            .build();
    }
}

Ở đây nó cũng không làm gì nhiều, chỉ đơn giản là kiểm tra xem loại lỗi là gì để trả về một trang mặc định hay trả về dữ liệu dạng json.

Đối với web thì EzyPlatform cũng cung cấp sẵn hai lớp WebGlobalExceptionHandler, WebGlobalErrorHandler các nhà phát triển có thể thừa kế hai lớp này hoặc khai báo các lớp mới để xử lý các exception do mình tạo ra, ví dụ:

@ExceptionHandler
public class ElearningGlobalExceptionHandler extends WebGlobalExceptionHandler {}
@EzySingleton
public class ElearningGlobalErrorHandler extends WebGlobalErrorHandler {}

Tổng kết lại

Xử lý exception là một trong những công việc tương đối dễ bỏ sót hoặc xử lý chồng chéo. Việc EzyPlatform gom lại toàn bộ các exception vào một nơi để xử lý sẽ giúp phần mềm của chúng ta an toàn hơn, hoạt động ổn định hơn, đồng thời cũng giúp cho việc phát triển nhanh chóng hơn.