Thiết kế Module Quản lý Sách - Hướng dẫn Tích hợp SSM Nhanh chóng (Phần 2)

Thiết kế Giao thức Truyền dữ liệu giữa Presentation Layer và Frontend

Bối cảnh

Backend trả về dữ liệu không đồng nhất, gây khó khăn cho frontend khi truy cập dữ liệu, cần chuẩn hóa định dạng

Giải pháp

Tạo lớp model kết quả để đóng gói dữ liệu vào thuộc tính data

Cải tiến 1.0

  1. Trong case cơ bản, tất cả thao tác CRUD trả về true, không dễ phân biệt
  2. Thêm thuộc tính code vào model kết quả (sử dụng 200X cho loại thao tác, 0/1 cho thành công/thất bại)

Cải tiến 2.0

  1. Khi query rỗng, data trả về null, cần hiển thị gì cho người dùng?
  2. Thêm thuộc tính message để xử lý trường hợp này

Đóng gói dữ liệu Presentation Layer

  1. Tạo lớp kết quả trả về dữ liệu thống nhất
  2. Các trường trong lớp Result có thể tùy chỉnh, cung cấp nhiều constructor

Thực hiện Giao thức Truyền dữ liệu

Tạo lớp Result trong package controller:

public class ApiResponse {
    private Object payload;
    private Integer status;
    private String notification;
    
    public ApiResponse() {
    }
    
    public ApiResponse(Integer status, Object payload) {
        this.payload = payload;
        this.status = status;
    }
    
    public ApiResponse(Integer status, Object payload, String notification) {
        this.payload = payload;
        this.status = status;
        this.notification = notification;
    }
    
    // getters và setters
}

Tạo lớp Status trong package controller:

public class Status {
    public static final Integer CREATE_SUCCESS = 20011;
    public static final Integer REMOVE_SUCCESS = 20021;
    public static final Integer MODIFY_SUCCESS = 20031;
    public static final Integer FETCH_SUCCESS = 20041;
    
    public static final Integer CREATE_FAILED = 20010;
    public static final Integer REMOVE_FAILED = 20020;
    public static final Integer MODIFY_FAILED = 20030;
    public static final Integer FETCH_FAILED = 20040;
}

Chỉnh sửa BookController:

@RestController
@RequestMapping("/books")
public class BookController {
    
    @Autowired
    private BookService bookService;
    
    @PostMapping
    public ApiResponse create(@RequestBody Book book) {
        boolean result = bookService.save(book);
        return new ApiResponse(result ? Status.CREATE_SUCCESS : Status.CREATE_FAILED, result);
    }
    
    @PutMapping
    public ApiResponse modify(@RequestBody Book book) {
        boolean result = bookService.update(book);
        return new ApiResponse(result ? Status.MODIFY_SUCCESS : Status.MODIFY_FAILED, result);
    }
    
    @DeleteMapping("/{id}")
    public ApiResponse remove(@PathVariable Integer id) {
        boolean result = bookService.delete(id);
        return new ApiResponse(result ? Status.REMOVE_SUCCESS : Status.REMOVE_FAILED, result);
    }
    
    @GetMapping("/{id}")
    public ApiResponse fetchById(@PathVariable Integer id) {
        Book book = bookService.getById(id);
        Integer code = book != null ? Status.FETCH_SUCCESS : Status.FETCH_FAILED;
        String msg = book != null ? "" : "Không tìm thấy dữ liệu, vui lòng thử lại!";
        return new ApiResponse(code, book, msg);
    }
    
    @GetMapping
    public ApiResponse fetchAll() {
        List<Book> books = bookService.getAll();
        Integer code = books != null ? Status.FETCH_SUCCESS : Status.FETCH_FAILED;
        String msg = books != null ? "" : "Lấy dữ liệu thất bại, vui lòng thử lại!";
        return new ApiResponse(code, books, msg);
    }
}

Xử lý Exception

Bối cảnh

Chương trình không thể tránh khỏi exception, ảnh hưởng đến business và trải nghiệm người dùng

Phân loại Exception

  1. Exception từ framework: sử dụng không đúng cách
  2. Exception từ data layer: lỗi server bên ngoài
  3. Exception từ business layer: lỗi logic nghiệp vụ
  4. Exception từ presentation layer: lỗi thu thập/kiểm tra dữ liệu
  5. Exception từ utility classes: code không robust

Exception Handler

Sử dụng AOP, Spring cung cấp @RestControllerAdvice và @ExceptionHandler

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(SystemException.class)
    public ApiResponse handleSystemException(SystemException ex) {
        // Ghi log
        // Gửi thông báo cho ops team
        return new ApiResponse(ex.getCode(), null, ex.getMessage());
    }
    
    @ExceptionHandler(BusinessException.class)
    public ApiResponse handleBusinessException(BusinessException ex) {
        return new ApiResponse(ex.getCode(), null, ex.getMessage());
    }
    
    @ExceptionHandler(Exception.class)
    public ApiResponse handleOtherException(Exception ex) {
        // Ghi log
        // Gửi thông báo cho dev team
        return new ApiResponse(Status.SYSTEM_UNEXPECTED_ERROR, null, "Hệ thống đang bận, vui lòng thử lại sau!");
    }
}

Xử lý Exception trong Project

Phân loại Exception Project

  1. BusinessException: từ hành vi người dùng
  2. SystemException: lỗi hệ thống không thể tránh khỏi
  3. Exception: lỗi không dự đoán được

Tạo lớp SystemException:

public class SystemException extends RuntimeException {
    private Integer errorCode;
    
    public Integer getErrorCode() {
        return errorCode;
    }
    
    public void setErrorCode(Integer errorCode) {
        this.errorCode = errorCode;
    }
    
    public SystemException(Integer errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public SystemException(Integer errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
}

Tạo lớp BusinessException:

public class BusinessException extends RuntimeException {
    private Integer errorCode;
    
    public Integer getErrorCode() {
        return errorCode;
    }
    
    public void setErrorCode(Integer errorCode) {
        this.errorCode = errorCode;
    }
    
    public BusinessException(Integer errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public BusinessException(Integer errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
}

Thêm mã lỗi vào lớp Status:

public static final Integer SYSTEM_ERROR = 50001;
public static final Integer SYSTEM_TIMEOUT = 50002;
public static final Integer SYSTEM_UNKNOWN = 59999;
public static final Integer BUSINESS_ERROR = 60002;

Simulate exception trong BookServiceImpl:

public Book getById(Integer id) {
    if (id == 1) {
        throw new BusinessException(Status.BUSINESS_ERROR, "Vui lòng không thử thách kiên nhẫn của tôi!");
    }
    
    try {
        int i = 1 / 0;
    } catch (Exception e) {
        throw new SystemException(Status.SYSTEM_TIMEOUT, "Server timeout, vui lòng thử lại!", e);
    }
    
    return bookDao.getById(id);
}

Tích hợp Frontend-Backend cho CRUD

Tạo SpringMvcSupport trong package config:

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/js/**").addResourceLocations("/js/");
        registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
    }
}

Cập nhật SpringMvcConfig:

@Configuration
@ComponentScan({"org.example.controller", "org.example.config"})
@EnableWebMvc
public class SpringMvcConfig {
}

JavaScript cho fetch all:

getAll() {
    axios.get("/books").then((res) => {
        this.dataList = res.data.payload;
    });
}

JavaScript cho create:

handleCreate() {
    axios.post("/books", this.formData).then((res) => {
        if (res.data.status === 20011) {
            this.dialogFormVisible = false;
            this.$message.success("Thêm thành công");
        } else if (res.data.status === 20010) {
            this.$message.error("Thêm thất bại");
        } else {
            this.$message.error(res.data.notification);
        }
    }).finally(() => {
        this.getAll();
    });
}

JavaScript cho update:

handleUpdate(row) {
    axios.get("/books/" + row.id).then((res) => {
        if (res.data.status === 20041) {
            this.formData = res.data.payload;
            this.dialogFormVisible4Edit = true;
        } else {
            this.$message.error(res.data.notification);
        }
    });
}

handleEdit() {
    axios.put("/books", this.formData).then((res) => {
        if (res.data.status === 20031) {
            this.dialogFormVisible4Edit = false;
            this.$message.success("Sửa thành công");
        } else if (res.data.status === 20030) {
            this.$message.error("Sửa thất bại");
        } else {
            this.$message.error(res.data.notification);
        }
    }).finally(() => {
        this.getAll();
    });
}

JavaScript cho delete:

handleDelete(row) {
    this.$confirm("Xóa vĩnh viễn dữ liệu này, tiếp tục?", "Cảnh báo", {
        type: 'info'
    }).then(() => {
        axios.delete("/books/" + row.id).then((res) => {
            if (res.data.status === 20021) {
                this.$message.success("Xóa thành công");
            } else {
                this.$message.error("Xóa thất bại");
            }
        }).finally(() => {
            this.getAll();
        });
    }).catch(() => {
        this.$message.info("Hủy xóa");
    });
}

Thẻ: SSM Spring SpringMVC mybatis Java

Đăng vào ngày 29 tháng 7 lúc 03:28