TỔNG QUAN VỀ CÔNG TY TMA BÌNH ĐỊNH SOLUTIONS VÀ CƠ SỞ LÝ THUYẾT VỀ BACK – END DEVELOPER
Giơ ́ i thiê ̣u tổng quan về công ty TMA Bình Định Solutions
1.1.1 Tổng quan về công ty
- TMA Solutions được thành lập năm 1997, với sự phát triển vững mạnh trong suốt
Trong suốt 25 năm phát triển, TMA đã khẳng định vị thế là công ty phần mềm hàng đầu tại Việt Nam, với 16 năm liên tiếp (2004-2019) đạt huy chương vàng xuất khẩu phần mềm TMA cũng nằm trong top 10 công ty FinTech, AI và IoT, hiện có hơn 4000 kỹ sư tài năng cùng nhau xây dựng hình ảnh năng động và chuyên nghiệp trên bản đồ công nghệ thông tin toàn cầu.
Hình 1 Công ty TMA Bình Định
1.1.2 Tầm nhìn và sứ mệnh
- Tầm nhìn: Trở thành một trong những công ty hàng đầu về cung cấp giải pháp phần mềm tại Việt Nam và trong khu vực
Sứ mệnh của chúng tôi là cung cấp cho khách hàng những sản phẩm và giải pháp phần mềm chất lượng cao với chi phí hợp lý, đồng thời xây dựng mối quan hệ tin cậy và uy tín với các đối tác trong lĩnh vực công nghệ thông tin nhằm hợp tác và phát triển bền vững.
Tổng quan về vi ̣ trí Back – End Developer
1.2.1 Giới thiệu về Back - End Developer
Là một chuyên gia phát triển phần mềm, tôi chuyên về xây dựng và duy trì phần back-end của ứng dụng hoặc website Back-end là phần không nhìn thấy trực tiếp bởi người dùng, nhưng đóng vai trò quan trọng trong việc xử lý yêu cầu từ người dùng, quản lý dữ liệu, xử lý logic và tương tác với các cơ sở dữ liệu cũng như dịch vụ bên ngoài.
1.2.2 Định hướng về Back – End Developer
Phát triển bản thân qua việc học hỏi và rèn luyện kỹ năng chuyên môn là rất quan trọng, bao gồm tham gia các khóa đào tạo và chứng chỉ, cũng như tham gia vào các dự án phức tạp Đóng góp tích cực vào công ty không chỉ giúp nâng cao hiệu quả và chất lượng dự án mà còn tạo cơ hội thăng tiến, đặc biệt sau khi hoàn thành giai đoạn thực tập với thành tích tốt Để tiến xa hơn trong sự nghiệp, cần tích cực trau dồi kiến thức của một Back-End Developer và mở rộng thêm kiến thức Front-End để trở thành một FullStack Developer.
1.2.3 Lộ trình thăng tiến của Back-End Developer
+ Được đào tạo cơ bản về ngôn ngữ lập trình, framework và các kiến thức cơ bản về phát triển phía sau
+ Tham gia vào các dự án nhỏ, học cách làm việc trong môi trường thực tế
+ Học cách sử dụng công cụ, quy trình và thực hành lập trình
+ Được giao nhiệm vụ xây dựng và duy trì các tính năng cơ bản của dự án
+ Học cách tối ưu hóa code, viết unit tests và tham gia code reviews
+ Bắt đầu hiểu về cách làm việc với cơ sở dữ liệu và các dịch vụ khác
+ Tham gia xây dựng các tính năng phức tạp và chịu trách nhiệm cho các phần cụ thể của dự án
+ Tham gia vào việc tối ưu hóa hiệu suất ứng dụng và đảm bảo bảo mật
+ Trở thành chuyên gia trong một số lĩnh vực cụ thể như tối ưu hóa hiệu suất, bảo mật hoặc quản lý cơ sở dữ liệu
+ Dẫn dắt và hướng dẫn các thành viên mới trong đội, tham gia vào việc đưa ra quyết định thiết kế quan trọng
+ Đóng góp vào việc thiết lập tiêu chuẩn phát triển và quy trình làm việc trong dự án.
Cơ sở lý thuyết về Back – End Developer
1.3.1 Java Basic a) Giới thiệu về Java
Kiến thức cơ bản về ngôn ngữ lập trình Java bao gồm các khái niệm, cú pháp, cấu trúc và thành phần thiết yếu Đây là nền tảng quan trọng để bắt đầu học và phát triển ứng dụng Java hiệu quả.
Hình 2 Java Basic b) Các tính năng của Java
Java có cú pháp dựa trên C++, giúp người học dễ dàng tiếp cận Ngôn ngữ này loại bỏ những đặc điểm gây bối rối như con trỏ tường minh Hơn nữa, Java tự động quản lý bộ nhớ, không yêu cầu người dùng phải xóa các đối tượng không còn tham chiếu.
Java là một ngôn ngữ lập trình độc lập với nền tảng, cho phép các ứng dụng được phát triển trên một nền tảng có thể dễ dàng chuyển giao và hoạt động trên nền tảng khác mà không gặp phải vấn đề tương thích.
Mọi thứ xung quanh chúng ta đều được xem như những đối tượng riêng biệt, mỗi đối tượng sở hữu các thuộc tính đặc trưng Tất cả các hoạt động diễn ra trong cuộc sống hàng ngày đều được thực hiện thông qua việc sử dụng những đối tượng này.
Bảo mật là yếu tố quan trọng trong lập trình, khi tất cả mã nguồn được chuyển đổi sang byteCode sau quá trình biên dịch, khiến nó không thể đọc được bởi con người Hệ thống chạy các chương trình trong môi trường Sandbox, ngăn chặn mọi hoạt động từ các nguồn không đáng tin cậy, từ đó phát triển các hệ thống và ứng dụng an toàn, không có virus hay giả mạo.
Linh hoạt trong khả năng thích ứng với môi trường phát triển giúp tối ưu hóa cấp phát bộ nhớ động, giảm thiểu lãng phí bộ nhớ và nâng cao hiệu suất của ứng dụng.
Java hỗ trợ phát triển ứng dụng phân tán thông qua tính năng gọi phương thức từ xa Tính năng này cho phép một chương trình gọi phương thức của một chương trình khác và nhận kết quả đầu ra Bằng cách này, người dùng có thể truy cập các tệp từ bất kỳ máy nào trên internet một cách dễ dàng.
- Mạnh mẽ: Java có một hệ thống quản lý bộ nhớ mạnh Nó giúp loại bỏ lỗi vì nó kiểm tra Code trong quá trình biên dịch và runtime.
Java mang lại hiệu suất cao nhờ vào việc sử dụng byteCode, cho phép dễ dàng chuyển đổi sang mã máy Sự hỗ trợ từ các trình biên dịch giúp Java tối ưu hóa hiệu năng, đảm bảo tốc độ và hiệu quả trong quá trình thực thi.
- Thông dịch: Java được biên dịch thành byteCode, được thông dịch bởi môi trường Java run-time.
Java hỗ trợ đa luồng, cho phép nhiều luồng thực thi đồng thời với các nguyên hàm đồng bộ hóa, giúp lập trình với các chủ đề trở nên dễ dàng và hiệu quả hơn.
Java OOP là phương pháp lập trình dựa trên các khái niệm của lập trình hướng đối tượng, trong đó chương trình được tổ chức thành các đối tượng riêng biệt Mỗi đối tượng có thuộc tính và phương thức riêng, cho phép chúng tương tác để thực hiện các tác vụ và chức năng OOP cung cấp nhiều khái niệm quan trọng, giúp cải thiện cấu trúc và khả năng bảo trì của mã nguồn.
Hình 3 Các tính chất của Java OOP
- Object: Một thực thể có trạng thái và hành vi Ví dụ như xe đạp, bàn, ghế, Nó có thể mang tính vật lý hoặc logic.
- Class: Một tập hợp các đối tượng Nó là một thực thể logic.
Tính kế thừa là quá trình mà một đối tượng nhận các thuộc tính và hành vi từ đối tượng cha, giúp tăng cường khả năng tái sử dụng mã nguồn Phương pháp này được áp dụng để đạt được tính đa hình trong thời gian chạy.
Tính đa hình là khả năng thực hiện một tác vụ theo nhiều cách khác nhau Trong Java, tính đa hình được hiện thực hóa thông qua hai kỹ thuật chính: nạp chồng phương thức (method overloading) và ghi đè phương thức (method overriding).
Trừu tượng là việc ẩn đi các chi tiết nội tại và chỉ hiển thị những tính năng cần thiết Trong Java, chúng ta áp dụng tính trừu tượng thông qua việc sử dụng lớp abstract và interface abstract.
- Encapsulation: Đó là gắn kết code và dữ liệu cùng với nhau vào trong một đơn vị
Trong java, chúng ta sử dụng phương thức getter, setter để thể hiện tính bao đóng.
1.3.3 Java Spring a) Giới thiệu về Spring Framework
Java Spring là một framework phát triển ứng dụng Java mạnh mẽ và phổ biến, cung cấp cách tiếp cận linh hoạt để xây dựng ứng dụng web và doanh nghiệp Được xây dựng trên ngôn ngữ Java, Spring là một phần của hệ sinh thái Spring với nhiều module hỗ trợ các khía cạnh khác nhau của phát triển ứng dụng.
Hình 4 Kiến thức về các tầng của Java Spring Framework b) Các tầng (modules) của Sping Framework
Kiểm thử thường không được coi là một tầng chính thức trong phát triển ứng dụng, nhưng lại đóng vai trò quan trọng, cung cấp các công cụ và lớp hỗ trợ cho việc kiểm thử với Junit và TestNG.
TRIỂN KHAI DỰ ÁN XÂY DỰNG HỆ THỐNG QUẢN LÝ CHI TIÊU SAVE MONEY
Yêu cầu hệ thống thông tin
Yêu cầu về thiết kế chức năng tối ưu trải nghiệm của người dùng
− Chức năng đơn giản, thân thiện , dễ dàng sử dụng
Để xây dựng một trang web quản lý bán hàng hiệu quả, cần đảm bảo các tính năng cơ bản như đăng nhập, tạo sản phẩm, thêm sản phẩm vào danh sách, cập nhật thông tin sản phẩm, xóa sản phẩm và chức năng tìm kiếm.
Phân tích hệ thống
a) Biều đồ UseCase tổng quan
Hình 12 Use Case của hệ thống b) Cơ sở dữ liệu
Hình 13 Database của hệ thống c) Mối quan hệ
Hình 14 Relationship của hệ thống d) Workflow
Hình 15 Workflow của hệ thống e) Yêu cầu đặc tả
Người dùng có thể đăng nhập vào hệ thống bằng tên người dùng và mật khẩu
Hệ thống phải cung cấp API Restful cho các hoạt động CRUD với các mục chi tiêu
Hệ thống phải hỗ trợ phân quyền cho người dùng, giới hạn quyền truy cập vào các chức năng dựa trên vai trò của họ
Yêu cầu phi chức năng:
Hiệu suất: Hệ thống phải đáp ứng trong vòng 3 giây khi thực hiện các hoạt động xem, thêm, sửa đổi hoặc xóa các dữ liệu
Hệ thống cần triển khai Keycloak để xác thực và phân quyền người dùng, nhằm đảm bảo rằng chỉ những người dùng được cấp quyền mới có thể truy cập vào các chức năng liên quan.
Độ tin cậy: Hệ thống cần đảm bảo tính ổn định.
Triển khai hệ thống
2.2.1 Tạo Spring project và thêm các thư viện
Hình 16 Tạo Spring project
Hình 17 Thêm thư viện vào dự án
Ngoài ra còn có các thư viện khác được thêm vào sau như:
2.2.2 Tạo Database từ PostgreSQL và kết nối vào dự án
Hình 18 Sử dụng Query để tạo Database
Hình 19 Kết nối Database với PostgreSQL
2.2.3 Triển khai mô hình MVC a) Tạo Entity
- Trong source code, tạo 1 package có tên entity để lưu Class card
- Tạo Class card theo mô hình OOP với các trường như: card_id, amount, card_number, symbol, status
Hình 20 Xây dựng Object card
- Tạo các Relationship tương ứng với Database cùng với các trường chứa khóa như card_brand, transactions và với các annotation như:
+ @OneToOne: Được sử dụng để tạo mối quan hệ một một giữa bảng card và card_brand
+ @OneToMany: Được sử dụng để tạo mối quan hệ một nhiều giữa bảng card và transactions
+ @JoinColumn: Xác định cột trong bảng card mà sẽ được sử dụng để lưu trữ khóa ngoại tới bảng card_brand
@JsonManagedReference được sử dụng trong quan hệ một-một để xác định thực thể quản lý của mối quan hệ Khi thực hiện serialize thành JSON, thông tin về card_brand sẽ được bao gồm trong quá trình này.
+ @JsonBackReference: Được áp dụng trong quan hệ một nhiều để xác định thực thể bị tham chiếu trong quan hệ Trong trường hợp này, khi serialize thành JSON,
- Các annotation trong Spring Boot như:
@Entity là chú thích dùng để đánh dấu lớp card như một thực thể trong cơ sở dữ liệu, giúp JPA (Java Persistence API) quản lý và lưu trữ đối tượng vào cơ sở dữ liệu một cách hiệu quả.
+ @NoArgsConstructor: Tự động tạo ra một constructor không có tham số (constructor mặc định) cho lớp card
+ @AllArgsConstructor: Tự động tạo ra một constructor chứa tất cả các tham số của lớp card
+ @Data: Đây là một annotation của Lombok, giúp tự động tạo các getter, setter, toString, equals và hashCode cho các trường trong lớp
+ @Builder: Đây là một annotation của Lombok, cho phép tạo ra các đối tượng thông qua builder pattern, giúp việc xây dựng đối tượng linh hoạt hơn
Trong cơ sở dữ liệu, bảng được đánh dấu bằng tên "card" để thuận tiện cho việc đặt tên khi tên mặc định không phù hợp Trường card_id được đánh dấu bằng @Id, xác định nó là khóa chính của bảng Để tự động tạo Id, chúng ta sử dụng @GeneratedValue Tiếp theo, cần tạo Repository để quản lý các thao tác với bảng này.
- Tiếp theo, tạo package repository với Interface cardRepository là nơi để truy xuất dữ liệu trong data
- Interface này kế thừa JpaRepository để hỗ trợ các phương thức có sẵn như: findAll(), save(), findById, deleteById
Annotation @Query cho phép khai báo phương thức truy vấn tùy chỉnh bằng ngôn ngữ SQL, thay vì sử dụng các phương thức tiêu chuẩn của JpaRepository Điều này giúp tạo ra phương thức findCardNotDeleted() một cách hiệu quả Bước tiếp theo là tạo Service để xử lý logic ứng dụng.
Tạo gói dịch vụ với Interface cardService để xử lý các logic kinh doanh, bao gồm quy tắc kinh doanh, tính toán phức tạp và quản lý dữ liệu hiệu quả.
getAllCard(): Phương thức này trả về danh sách tất cả các thẻ (card) trong hệ thống
saveCard(): Phương thức này được sử dụng để lưu hoặc cập nhật một đối tượng card vào cơ sở dữ liệu
getCard(): Phương thức này trả về thông tin về một card cụ thể dựa trên khóa chính cardId
getCardNotDeleted(): Phương thức này trả về danh sách các card có trạng thái không bị xóa (không bị đánh dấu là "deleted")
updateCard(): Phương thức này được sử dụng để cập nhật thông tin của một card cụ thể dựa trên khóa chính cardId và dữ liệu từ formData
- Tiếp theo, trong package service tạo package ServiceImp với Class cardServiceImp là nơi phục vụ các logic RestFul API.
+ @Service: Đánh dấu class cardServiceImp là một Spring Bean dịch vụ (service) Spring sẽ quản lý và quản lý vòng đời của bean này
+ @Autowired: Được sử dụng để tiêm (inject) phụ thuộc (dependency) vào class Trong constructor của cardServiceImp, cardRepository được tiêm vào để sử dụng
+ @Override: Đánh dấu các phương thức là việc ghi đè lên các phương thức trong interface cardService.
Hình 23 Xây dựng chi tiết API d) Tạo Controller
Next, create a Controller package that includes the CardController class, which serves as the layer for retrieving and modifying data for users We utilize annotations for handling requests in this project, such as @GetMapping, @PostMapping, and @PutMapping.
Hình 25 Xây dựng cardController
Hình 26 Xây dựng cardController
Hình 27 Xây dựng cardController
Tạo hàm findAllCard(): Xử lý request để tìm tất cả các card
Tạo hàm getCard(): Xử lý request để tìm một card dựa trên ID
Tạo hàm saveCard(): Xử lý request để tạo một card mới
Tạo hàm getCardNotDeleted (): Xử lý request để lấy danh sách các card chưa bị xóa
Tạo hàm updateCard(): Xử lý request để cập nhật thông tin một card dựa trên
2.2.4 Sử dụng Keycloak và phân quyền a) Cài đặt Keycloak
- Cài đặt thông qua gói quản lý gói của hệ điều hành Docker và chạy trên http://localhost:8080
Hình 28 Cài đặt Keycloak thông qua Docker b) Tạo Client
- Tạo một client có tên là hon-rest-api để xác định ứng dụng hoặc dịch vụ cần xác thực người dùng trước khi cho phép truy cập
Hình 29 Tạo client c) Tạo Realm roles
- Tạo hai realm roles là admin và user để đại diện cho các quyền truy cập cụ thể trong hệ thống
Hình 30 Tạo realm admin và user d) Tạo Users
Để tạo người dùng trong Realm cho việc đăng nhập vào các ứng dụng sử dụng Keycloak, hãy tạo hai tài khoản người dùng hoang và hon, tương ứng với hai realm là admin và user Tiếp theo, cần định cấu hình xác thực và ủy quyền để đảm bảo an toàn cho hệ thống.
Tạo một package security với class JwtAuthConverter, có nhiệm vụ chuyển đổi thông tin từ JWT thành đối tượng xác thực của Spring Security Class này cung cấp các quyền và thông tin nguyên tắc cần thiết để thực hiện ủy quyền và kiểm tra quyền truy cập trong ứng dụng.
Hình 32 Xây dựng JwtAuthConverter f) Cấu hình bảo mật
Tạo lớp SecurityConfig để cung cấp cấu hình bảo mật cho ứng dụng Spring Security, thực hiện xác thực và ủy quyền các yêu cầu HTTP dựa trên JWT và các quy tắc xác thực đã được thiết lập.
Hình 34 Xây dựng SecurityConfig g) Cấu hình để sử dụng Keycloak
Cấu hình Keycloak trong file application.properties là rất quan trọng cho việc thiết lập ứng dụng sử dụng Keycloak để xác thực và ủy quyền Ngoài ra, nó còn chỉ định cách mà JWT sẽ được sử dụng và xử lý bởi ứng dụng.
Hình 35 Cấu hình Keycloak trong properties h) Cấp quyền truy cập
- Cấp quyền cho các request nằm trong tầng Controller bằng annotation
@PreAuthorize: Xác định quyền truy cập cần thiết cho từng API
Hình 36 Cấp quyền truy cập
Hình 37 Cấp quyền truy cập
2.2.5 Sử dụng Log4j để ghi log a) Cấu hình để sử dụng Log4j
- Trong package resources tạo một file log4j.properties để cấu hình log4j vào dự án
Hình 38 Cấu hình Log4j trong log4j.properties b) Tạo class logger
Tạo một package log với class logger, cung cấp các phương thức đơn giản để ghi lại thông báo ở các mức độ log khác nhau như info, warn, error, fatal và debug.
Hình 39 Tạo class logger
Sử dụng Postman để kiểm tra kết quả triển khai
2.3.1 Đăng nhập vào trang web bằng quyền Admin a) Lấy Token của tài khoản Admin
- Đăng nhập bằng tài khoản Admin để lấy Token
Hình 40 Token của Admin b) Xem danh sách card
- Dùng phương thức GET để lấy thông tin danh sách card
Hình 41 Danh sách card mối quan hệ với card_brand (Admin) c) Xem card theo từng Id
- Dùng phương thức GET để lấy thông tin chi tiết từng card mà người dùng muốn xem
Hình 42 Thông tin card với Id 956 (Admin) d) Thêm card
- Dùng phương thức POST để thêm card mới e) Cập nhật card
- Dùng phương thức PUT để cập nhật thông tin của từng card nếu như sai sót hoặc có sự thay đổi
Hình 44 Cập nhật card với Id 1004 (Admin) f) Xóa card
- Khi mà dữ liệu/ các đối tượng trong bảng đã nối quan hệ với các đối tượng trong bảng khác thì không thể xóa trong dữ liệu được
Khi đối tượng đã có mối quan hệ, nó được coi là nguồn thông tin quan trọng cần được lưu trữ trong hệ thống dữ liệu Để tránh mất mát dữ liệu và bảo vệ giá trị của thông tin đối với doanh nghiệp, cần chỉ thực hiện việc chỉnh sửa trạng thái của đối tượng.
- Dùng phương thức PUT để cập nhật status thành inactive (ngừng hoạt động)
Hình 45 Cập nhật inactive cho card có Id 1004 (Admin)
- Dùng phương thức GET để xem danh sách các card active(hoạt động)
Hình 46 Xem danh sách active (Admin)
2.3.2 Đăng nhập vào trang web bằng quyền User a) Lấy Token của tài khoản User
Hình 47 Token của User (Admin) b) Xem danh sách card
- Dùng phương thức GET để lấy thông tin danh sách card
Hình 48 Danh sách card mối quan hệ với card_brand (User) c) Xem card theo từng Id
- Dùng phương thức GET để lấy thông tin chi tiết từng card mà người dùng muốn xem
Hình 49 Thông tin card với Id 955 (User) d) Thêm card
- Dùng phương thức POST để thêm card mới
Hình 50 Thêm card với Id 1052 (User)
- Vì User không có quyền để sử dụng PUT nên khi ta cố cập nhật giá trị sẽ bị lỗi 403 là mã lỗi "Forbidden" (Từ chối truy cập)
Hình 51 Cập nhật card lỗi 403 (User) f) Xóa card
- Khi mà dữ liệu/ các đối tượng trong bảng đã nối quan hệ với các đối tượng trong bảng khác thì không thể xóa trong dữ liệu được
Khi đối tượng đã có mối quan hệ, nó được coi là có đầy đủ thông tin và cần được lưu trữ trong dữ liệu Để tránh mất mát dữ liệu, điều này trở nên quan trọng đối với doanh nghiệp, vì vậy chỉ cần chỉnh sửa trạng thái của đối tượng.
Giống như phương thức PUT, người dùng sẽ không có quyền sử dụng DELETE, vì vậy khi cố gắng cập nhật giá trị, hệ thống sẽ trả về mã lỗi 403, tức là "Forbidden" (Từ chối truy cập).
Hình 52 Xóa card lỗi 403 (User)
- Khi sử dụng GET thành công trong Controller thì thông báo log hiển thị trên consolog
Hình 53 Các thông báo log hiển thị trong consolog
KẾT QUẢ ĐẠT ĐƯỢC
- Hiểu và tạo được một project Spring đơn giản
- Nắm được kiến thức Database, các Relationship và sử dụng Restful API để thực hiện các thao tác GET, POST, PUT, DELETE
- Thiết kế security và phân quyền cho một hệ thống bằng Keycloak
- Nâng cao khả năng tự học, tiếp thu kiến thức một cách chủ động
- Cải thiện kỹ năng giao tiếp, nâng cao hiểu biết của bản thân trong môi trường làm việc chuyên nghiệp của doanh nghiệp
- Được training những kỹ năng chuyên môn và một số kỹ năng mềm như: bảo mật trong doanh nghiệp, trao đổi trong các buổi seminar,…
- Nâng cao kỹ năng viết email, báo cáo, cũng như báo cáo trong các buổi họp team hàng tuần
- Kỹ năng làm việc theo đội nhóm, phân chia công việc rõ ràng
- Về Log4j vẫn chưa thể tạo File để lưu console log
- Vẫn chưa thể nối hai khóa Primary Key và Foreign Key của những bảng có mối quan hệ nhiều – nhiều
Do thời gian thực tập hạn chế, tôi chưa thể nắm vững toàn bộ kiến thức đã tiếp xúc và chưa hoàn thành tất cả các chức năng mà mentor yêu cầu, bao gồm Multithread, CompletableFuture và Transaction.
Đối với dự án xây dựng hệ thống quản lý chi tiêu Save Money, tôi hướng tới việc hoàn thiện nó thành một hệ thống phức tạp hơn với nhiều chức năng Để đạt được điều này, tôi cần nắm vững kiến thức về Back-End, đặc biệt là Framework Spring của Java Hiện tại, kỹ năng và kiến thức của tôi về phát triển web với Spring vẫn còn ở mức cơ bản, vì vậy tôi sẽ nỗ lực để phát triển hơn nữa trong tương lai Tôi rất mong muốn được học hỏi thêm về Java Web và Framework Spring Tôi xin chân thành cảm ơn doanh nghiệp, nhà trường và khoa Thống Kê – Tin Học đã tạo điều kiện cho tôi tiếp thu những kiến thức cơ sở này để từng bước hoàn thiện kỹ năng của một Developer Backend Java.