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
- Trong case cơ bản, tất cả thao tác CRUD trả về true, không dễ phân biệt
- 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
- Khi query rỗng, data trả về null, cần hiển thị gì cho người dùng?
- Thêm thuộc tính message để xử lý trường hợp này
Đóng gói dữ liệu Presentation Layer
- Tạo lớp kết quả trả về dữ liệu thống nhất
- 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
- Exception từ framework: sử dụng không đúng cách
- Exception từ data layer: lỗi server bên ngoài
- Exception từ business layer: lỗi logic nghiệp vụ
- Exception từ presentation layer: lỗi thu thập/kiểm tra dữ liệu
- 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
- BusinessException: từ hành vi người dùng
- SystemException: lỗi hệ thống không thể tránh khỏi
- 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");
});
}