Category: Java

  • Kiến trúc phân tầng (Layered Architecture) (Phần 2)

    Kiến trúc phân tầng (Layered Architecture) (Phần 2)

    Trong phần 1 chúng ta đã tìm hiều về Kiến trúc phân tầng và các khái niệm quan trọng nhất của nó. Trong phần 2 này chúng ta sẽ xem xét cách thức hoạt động của kiến trúc phân tầng và những điểm cần lưu ý khi sử dụng kiến trúc này.

    Ví dụ

    Để minh họa cách thức hoạt động của kiến trúc phân lớp, chúng ta cùng xem xét một yêu cầu từ người dùng doanh nghiệp muốn truy xuất thông tin khách hàng của một cá nhân cụ thể.

    Các mũi tên màu đen thể hiện luồng yêu cầu xuống cơ sở dữ liệu để lấy thông tin khách hàng. Các mũi tên màu đỏ thể hiện luồng phản hồi ngược trở lại màn hình để hiển thị dữ liệu. Trong ví dụ này, thông tin khách hàng bao gồm cả dữ liệu khách hàng và dữ liệu đơn hàng (đơn hàng do khách hàng đặt). Customer Screen có nhiệm vụ tiếp nhận yêu cầu và hiển thị thông tin khách hàng. Nó hoàn toàn không biết nơi dữ liệu được lư trữ, làm thế nào để lấy nó hoặc có bao nhiêu bảng cơ sở dữ liệu phải được truy vấn để lấy dữ liệu. Khi Customer Screen nhận được yêu cấu lấy thông tin khác hàng của một cá nhân cụ thể, nó sẽ chuyển tiếp yêu cầu đó tới mô đun Customer Delegate. Mô đun này có nhiệm vụ biết các mô đun ở tầng nghiệp vụ có thể xử lí yêu cầu đó cũng như làm thế nào để lấy các mô đun đó và dữ liệu nào mà nó cần (hợp đồng). Customer Object ở tầng nghiệp vụ có nhiệm vụ tổng hợp toàn bộ thông tin cần thiết bởi yêu cầu nghiệp vụ(trong trường hợp này là thông tin khách hàng). Mô đun này gọi ra Customer DAO (đối tượng truy cập dữ liệu) ở tầng lưu trữ để lấy dữ liệu khách hàng cùng với Order DAO để lấy thông tin đơn hàng. Các mô đun này lần lượt thực thi các câu lệnh SQL để lấy dữ liệu tương ứng và trả lại cho Customer Object ở tầng nghiệp vụ. Khi Customer Object nhận được dữ liệu, nó sẽ tổng hợp dữ liệu và trả lại các thông tin đó cho Customer Delegate, sau đó Customer Delegate trả lại các dữ liệu đó cho Customer Screen để hiển thị cho người dùng. Từ khía cạnh công nghệ, có hàng tá cách để cài đặt các mô đun này. Ví dụ với nên tàng Java, Customer Screen có thể là một màn hình JSF(Java Server Faces) cùng với Customer Delete là thành phần bean được quản lí. Customer Object ở tầng nghiệp vụ có thể là một Spring Bean cục bộ hoặc EJB3 bean từ xa. Các đối tượng truy cập cơ sở dữ liệu được minh hoạ trong ví dụ trước có thể được triển khai dưới dạng POJO (Plain Old Java Objects) đơn giản, MyBatis XML Mapper, hoặc ngay cả cá đối tượng đóng gọi lời gọi JDBC thuần hoặc các truy vấn Hibernate. Trên nền tảng Microsoft, Customer Screen có thể là một mô đun ASP(Active Server Pages) sử dụng framework .NET để truy cập các mô đun C# ở tầng nghiệp vụ với các mô đun truy cập dữ liệu khác hàng và đơn hàng được triển khai dưới dàng ADO(ActiveX Data Objects).

    Những điểm cần cân nhắc khi sử dụng kiến trúc phân tầng

    Kiến trúc phân tầng là một mẫu kiến trúc về cơ bản là vững chắc, hấu hết các ứng dụng có thể bắt đầu bằng kiến trúc này, đặc biệt khi chúng ta không chắc chắn kiến trúc nào là phù hợp nhất cho ứng dụng của chúng ta. Tuy nhiên về mặt kiến trúc thì có một vài điều cần cân nhắc trước khi chọn mẫu kiến trúc này.

    Điểm đầu tiên cần chú ý là các anti-pattern(phản mẫu – các mẫu thiết kế cần tránh sử dụng). Một trong số đó là kiến trúc hố sụt. Kiến trúc này mô tả tình huống luồng yêu cầu đi qua nhiều tầng của kiến trúc mà đơn giản xử lí chuyển tiếp với rất ít hoặc không có logic được thực hiện bên trong mỗi tầng. Ví dụ, tầng trình diễn phản hồi một yêu cầu từ người dùng muốn lấy dữ liệu khách hàng. Tầng trình diễn chuyển yêu cầu tới tầng nghiệp vụ, nó đơn giản chuyển tiếp yêu cầu tới tầng lưu trữ, sau đó tạo một lời gọi SQL đơn giản tới tầng cơ sở dữ liệu để lấy dữ liệu khách hàng. Dữ liệu sau đó được truyền theo đường ngược lại mà không có xử lí thêm hay logic tổng hợp, tính toán hoặc biến đổi dữ liệu.

    Mỗi kiến trúc phân tầng sẽ có ít nhất một số tình huống rơi vào phản mẫu kiến trúc hố sụt. Tuy nhiên điều quan trọng là phân tích tỉ lệ phần trăm yêu cầu thuộc loại này. Quy tắc 80-20 thường là một phương pháp hay, nên tuân theo để xác định liệu có hay không chúng ta đang rơi vào phản mẫu kiến trúc hố sụt. Thông thường có khoảng 20% yêu cầu được xử lí đơn giản và 80% yêu cầu có một số logic nghiệp vụ liên quan đến yêu cầu đó. Tuy nhiên nếu chúng ta thấy rằng tỉ lệ này bị đảo ngược và phần lớn các yêu cầu của chúng ta là quá trình xử lí đơn giản, chúng ta có thể cân nhắc một số tầng trong kiến trúc là mở.

    Một điểm khác cần cân nhắc với mẫu kiến trúc phân lớp là nó có xu hướng thích ứng với các ứng dụng nguyên khối, ngay cả khi chúng ta tách tầng trình diễn và tầng nghiệp vụ thành các đơn có thể triển khai riêng biệt. Mặc dù điều này có thể không phải là vấn đề đáng lo ngại đối với một số ứng dụng nhưng nó đặt ra một số vấn đề tiềm ẩn về triển khai, độ bền và độ tin cậy chung, hiệu suất và khả năng mở rộng.

    Phân tích kiến trúc phân tầng

    Dưới đây là bảng đánh giá và phân tích về các đặc điểm kiến trúc phổ biến của kiến trúc phân tầng. Các đánh giá cho từng đặc điểm dựa trên xu hướng tự nhiên của các đặc điểm đó, điển hình như là khả năng triển khai cũng như là mức độ phổ biến của mẫu kiến trúc này.

    Đặc điểm Đánh giá
    Tính linh hoạt tổng thể
    Dễ triển khai
    Khả năng kiểm thử
    Hiệu năng
    Khả năng mở rộng
    Dễ phát triển

    Tính linh hoạt tổng thể

    Tính linh hoạt tổng thể là khả năng đáp ứng nhanh chóng với một môi trường thay đổi liên tục. Trong khi thay đổi được cô lập thông qua tầng cô lập, nó vẫn cồng kềnh và tốn thời gian để thực hiện các thay đổi trong kiến trúc này bởi vì bản chất nguyên khối của hầu hết các triển khai cũng như sự liên kết chặt chẽ của các thành phần thường được tìm thấy với mẫu kiến trúc này.

    Dễ triển khai

    Tùy thuộc vào cách chúng ta triển khai mẫu này, khả năng triển khai có thể trở thành một vấn đề, đặc biệt đối với các ứng dụng lớn. Một thay đổi nhỏ đối với một thành phần có thể yêu cầu triển khai lại toàn bộ ứng dụng (hoặc một phần lớn của ứng dụng), dẫn đến việc triển khai cần được lập kế hoạch, được lên lịch và thực hiện ngoài giờ hoặc vào cuối tuần. Như vậy, mô hình này không dễ thích ứng với việc triển khai liên tục, tiếp tục giảm xếp hạng tổng thể cho triển khai.

    Khả năng kiểm thử

    Trong kiến thúc này, các thành phần trong thuộc vào một tầng cụ thể, các tầng khác có thể được mô phỏng hoặc khai thác, giúp cho kiến trúc này tương đối dễ kiểm thử. Một nhà phát triển có thể giả lập một thành phần trình bày hoặc màn hình để cô lập thử nghiệm trong một thành phần nghiệp vụ, cũng như mô phỏng tầng nghiệp vụ để kiểm tra chức năng màn hình nhất định.

    Hiệu năng

    Mặc dù đúng là một số kiến trúc phân tầng có thể hoạt động tốt, nhưng kiến trúc này không phù hợp với các ứng dụng hiệu năng cao do tính không hiệu quả của việc phải đi qua nhiều tầng của kiến trúc để đáp ứng yêu cầu nghiệp vụ.

    Khả năng mở rộng

    Do xu hướng triển khai nguyên khối và liên kết chặt chẽ của kiến trúc này, các ứng dụng được xây dựng bằng cách sử dụng mẫu kiến trúc này thường khó mở rộng quy mô. Chúng ta có thể mở rộng quy mô kiến trúc phân tầng bằng cách tách các tầng thành các triển khai vật lý riêng biệt hoặc sao chép toàn bộ ứng dụng thành nhiều nút, nhưng nhìn chung mức độ chi tiết quá rộng, khiến việc mở rộng quy mô trở nên tốn kém.

    Dễ phát triển

    Tính dễ phát triển nhận được điểm tương đối cao, chủ yếu là do mô hình này quá nổi tiếng và không quá phức tạp để thực hiện. Bởi vì hầu hết các công ty phát triển ứng dụng bằng cách tách các bộ kỹ năng theo tầng (trình diễn, nghiệp vụ, cơ sở dữ liệu), kiến trúc này trở thành lựa chọn tự nhiên cho hầu hết việc phát triển ứng dụng kinh doanh. Mối liên hệ giữa cơ cấu tổ chức và truyền thông của công ty với cách thức phát triển phần mềm được vạch ra là cái được gọi là định luật Conway. Bạn có thể Google "Conway’s law" để có thêm thông tin về mối tương quan hấp dẫn này.

    Tài liệu tham khảo

    • Software Architecture Patterns

  • Kiến trúc phân tầng (Layered Architecture) (Phần 1)

    Kiến trúc phân tầng (Layered Architecture) (Phần 1)

    Kiến trúc phân tầng(hay còn được gọi là kiến trúc n-tier) là kiến trúc phổ biến nhất. Kiến trúc này được xem là chuẩn không chính thức cho các ứng dụng Java EE và được biết đến rộng rãi bởi hầu hết kiến trúc sư, nhà thiết kế và nhà phát triển. Kiến trúc này quen thuộc với các cơ cấu tổ chức và truyền thông CNTT truyền thống, được tìm thấy ở hầu hết các công ty khiến nó trở thành một lựa chọn tự nhiên cho các công ty phát triển ứng dụng doanh nghiệp.

    Giới thiệu kiến trúc phân tầng

    Các thành phần bên trong kiến trúc phân tầng được tổ chức thành các tầng nằm ngang, mỗi tầng thực hiện một vai trò cụ thể trong ứng dụng, ví dụ như tầng trình diễn(presentation) hay tầng nghiệp vụ (business). Mặc dù kiến trúc phân tầng không qui định số lượng hay các loại tầng phải tồn tại, hầu hết các kiến trúc phân tầng bao gồm 4 tầng: tầng trình diễn, tầng nghiệp vụ, tầng lưu trữ(persistence), tầng cơ sở dữ liệu(database).

    4-tier

    Trong một vài trường hợp tầng nghiệp vụ và tầng lưu trữ có thể được kết hợp thành một tầng nghiệp vụ, đặc biệt khi logic lưu trữ (SQL hay HSQL) được nhúng bên trong các thành phần của tầng nghiệp vụ. Vì vậy với các ứng dụng nhỏ có thể chỉ có ba tầng, ngược lại với các ứng dụng lớn hoặc các ứng dụng có nghiệp vụ phức tạp có thể chứa năm tầng hoặc nhiều hơn.

    Mỗi tầng trong kiến trúc phân tầng có một vai trò và trách nhiệm cụ thể trong ứng dụng. Ví dụ tầng trình diễn sẽ có trách nhiệm xử lí tất cả giao diện người dùng và logic giao tiếp trình duyệt, trái lại tầng nghiệp vụ sẽ chịu trách nhiệm thực thi các quy tắc nghiệp vụ cụ thể. Mỗi tầng trong kiến trúc phân tầng trừu tượng hoá các công việc cần phải hoàn thành để đáp ứng một yêu cầu nghiệp vụ cụ thể. Ví dụ tầng trình diễn không cần phải biết hay lo lắng về việc lấy dự liệu khách hàng như thế nào; nó chỉ cần hiển thị các thông tin đó lên màn hình theo định dạng cụ thể. Tương tự như vậy, tầng nghiệp vụ không cần bận tâm làm thế nào định dạng dữ liệu khách hàng để hiển khị lên màn hình hoặc ngay cả việc dự liệu khách hàng được lưu trữ ở đâu; nó chỉ cần lấy dữ liệu từ tầng lưu trữ, thực hiện logic nghiệp vụ dựa trên dữ liệu đó(tính toán các giá trị hay tổng hợp dữ liệu), và truyền các thông tin đó tới tầng trình diễn.

    Một trong những tính năng mạnh mẹ của kiến trúc phân tầng sự tách biệt các mỗi bận tâm giữa các thành phần. Các thành phần bên trong một tầng cụ thể chỉ giải quyết các logic liên quan đến tầng đó. Ví dụ các thành phần ở tầng trình diễn chỉ giải quyết các logic hiển thị, ngược lại các thành phần cư trú ở tầng nghiệp vụ chỉ giải quyết các logic nghiệp vụ. Việc phân loại các loại thành phần như thế này giúp cho việc xây dựng mô hình trách nhiệm và vai trò có hiệu quả trở nên dễ dàng hơn, đồng thời giúp bạn dễ dàng phát triển, kiểm thử, quản lí, và bảo trì các ứng dụng nhờ vào các giao diện thành phần được mô tả rõ ràng và phạm vi thành phần được giới hạn.

    Tầng đóng(closed layer)

    Chúng ta cùng xem hình dưới đây:

    closed_layer.png

    Mỗi tầng trong kiến trúc này được đánh dấu CLOSED. Đây là một khái niệm rất quan trọng trong kiến trúc phân tầng. Một tầng đóng có nghĩa là một yêu cầu(request) đi từ tầng này sang tầng khác phải đi qua tầng ngay bên dưới nó để đi tới tầng tiếp theo bên dưới tầng đó. Ví dụ một request bắt nguồn từ tầng trình diễn phải đi qua tầng trình diễn sau đó tới tầng lưu trữ trước cuối cùng chốt lại ở tầng cơ sở dữ liệu.

    Tại sao không cho phép tầng trình diễn truy cập trực tiếp đến tầng lưu trữ hay tầng cơ sở dữ liệu?

    Truy cập trực tiếp cơ sở dữ liệu từ tầng trình diễn thì nhanh hơn là thông qua một loạt các tầng không cần thiết chỉ để lấy ra hay lưu lại thông tin cơ sở dữ liệu. Câu trả lời cho câu hỏi này nằm ở một khái niệm tầng cô lập(layers of isolation).

    Tầng cô lập

    Khái niệm tầng cô lập có nghĩa là những thay đổi được tạo ra trong một tầng của kiến trúc nhìn chung không có tác động hay ảnh hướng đến các thành phần của tầng khác: thay đổi được cô lập trong các thành phần bên trong lớp đó và một lớp nào đó có có thể có liên quan(ví dụ như tầng lưu trữ có chứa SQL). Nếu chúng ta cho phép tầng trình diễn truy cập trực tiếp tới tầng lưu trữ thì những thay đổi được tạo ra với SQL trong tầng lưu trữ sẽ có tác động tới cả tầng nghiệp vụ và tầng trình diễn, do đó tạo ra một ứng dụng liên kết rất chặt chẽ với rất nhiều phụ thuộc chéo giữa các thành phần. Những ứng dụng như thế này quá cứng nhắc và tốn kém khi có thay đổi.

    Khái niệm tầng cô lập cũng có nghĩa là mỗi một tầng độc lập với các tầng khác, do đó chúng biết rất ít hoặc không biết về hoạt động bên trong của các tầng khác trong kiến trúc. Để hiểu được sức mạnh và tầm quan trọng của khái niệm này, chúng ta cùng xem xét nỗ lực tái cấu trúc để chuyển đổi presentation framework từ JSP (Java Server Pages) to JSF (Java Server Faces). Giả sử rằng hợp đồng(model) được sử dụng giữa tầng trình diễn và tầng nghiệp vụ không thay đổi, tầng nghiệp vụ không bị ảnh hưởng bởi việc tái cấu trúc và giữ độc lập hoàn toàn với các loại giao diện người dùng được sử dụng bởi tầng trình diễn.

    Tầng mở

    Tầng đóng tạo điều kiện thuận lợi cho tầng cô lập và do đó giúp cô lập thay đổi trong kiến trúc, đôi khi tầng mở có ý nghĩa đối với các tầng nhất định. Giả sử bạn muốn thêm vào tầng dịch vụ chia sẻ(shared services) chứa các thành phần dịch vụ dùng chung được truy cập bởi các thành phần trong tầng nghiệp vụ(các lớp tiện ích dữ liệu hay chuổi, các lớp kiểm tra và ghi nhật ký). Trong trường hợp này việc tạo ra tầng dịch vụ thường là ý tưởng tốt bởi lẽ về mặt kiến trúc thì nó hạn chế các truy cập vào các dịch vụ chia sẻ đổi với tầng nghiệp vụ(chứ không phải tầng trình diễn). Không có sự tách biệt về tầng, về mặt kiến trúc không có gì hạn chế tầng trình diễn truy cập vào các dịch vụ dùng chung này, gây khó khăn cho việc quản lý hạn chế truy cập.

    Trong ví dụ này, tầng dịch vụ mới sẽ có thể sẽ nằm bên dưới tầng nghiệp vụ để chỉ rằng các thành phần trong tầng dịch vụ này không thể truy cập từ tầng trình diễn. Tuy nhiên, điều này có nghĩa rằng tầng nghiệp vụ lúc này bắt buộc phải đi qua tầng dịch vụ để tới tầng lưu trữ, điều này không có ý nghĩa gì cả. Đây là vấn đề lâu đời của kiến trúc phân tầng. Để giải quyết vấn đề này chúng ta tạo ra tầng mở trong kiến trúc.

    open_layer.png

    Các tầng dịch vụ trong trường hợp này được đánh dấu là OPEN, có nghĩa là các yêu cầu được cho phép đi qua tầng mở và đi trực tiếp tới tầng bên dưới. Trong ví dụ trên vì tầng dịch vụ là tầng mở nên tầng nghiệp vụ bây giờ được phép bỏ qua nó và đi trực tiếp tới tầng lưu trữ, điều này hoàn toàn hợp lí.

    Chúng ta có thể tận dụng khái niệm về tầng mở và đóng để xác định mối quan hệ giữa các tầng trong kiến trúc và luồng yêu cầu cũng như cung cấp cho nhà thiết kế, nhà phát triển các thông tin cần thiết để hiểu các hạn chế truy cập tới các tầng khác nhau trong kiến trúc. Việc không thể xác định chính xác các tầng nào trong kiến trúc là mở hay đóng và tại sao thường dẫn tới các cấu trúc liên kết chặt chẽ và dễ vỡ, chúng rất khó để kiểm thử, bảo trì và triển khai.

    Tài liệu tham khảo

    • Software Architecture Patterns
  • [MyBatis] Sử dụng MyBatis với Spring Boot

    [MyBatis] Sử dụng MyBatis với Spring Boot

    MyBatis là gì?

    MyBatis là một framework nổi tiếng trong cộng đồng Java. Nó là triển khai của tầng lưu trữ trong kiến trúc phân tầng trên nền tảng Java tương tự như Hibernate hoặc ngay cả là JDBC thuần. Nó giúp việc triển khai tầng lưu trữ trở nên đơn giản hơn với nhà phát triển. Thông tin về MyBatis bạn có thể tham khảo tại đây. Nội dung bài viết này tập trung vào cách sử dụng MyBatis cùng với Spring Boot.

    MyBatis-Spring-Boot-Starter

    Để sử dụng MyBatis với Spring Boot chúng ta sử dụng thư viện MyBatis-Spring-Boot-Starter. Đây là thư viện hỗ trợ cấu hình nhanh chóng một ứng dụng Spring Boot có sử dụng MyBatis. Thư viện này được hỗ trợ chính thức từ nhóm phát triển MyBatis. Các bước cấu hình tại đây.

    Repository hay Mapper

    Mapper là một Java interface mà các phương thức được ánh xạ tới các truy vấn SQL tương ứng. Mặc định MyBatis sẽ ánh xạ các method được định nghĩa trong các interface được đánh dấu với annotation @Mapper tới các truy vấn SQL tương ứng.

    ProductRepository

    Dưới đây là một ví dụ về Mapper. ProductRepository được định nghĩa trong package app.demo.mybatis.repository

    
    @Mapper
    public interface ProductRepository {
    
      void create(Product product);
    }
    

    Để định nghĩa truy vấn SQL tương ứng chúng ta tạo ProductRepository.xml trong thư mục app.demo.mybatis.repository bên trong resources.

    
    
    
    
      
        
      
    
    

    Trong ví dụ trên chúng ta đang ánh xạ phương thức create với câu truy vấn INSERT. Khi phương thức create được gọi thì câu truy vấn sẽ được thực thi. Kết quả câu truy vấn sẽ được trả về phương thức create.

    DataSource

    Trong bài viết này chúng ta sẽ sử dụng MySQL để thực hành với MyBatis. Chúng ta có thể sử dụng docker để tạo container chạy MySQL với câu lệnh sau:

    docker run --name demo -e MYSQL_ROOT_PASSWORD=demo@123 -p 3306:3306 -d mysql --lower_case_table_names=1
    

    Với câu lệnh trên chúng ta đã tạo xong MySQL với schema demo cũng như mật khẩu cho tài khoản rootdemo@123.

    Để cấu hình data source với Spring Boot chúng ta thêm các cấu hình sau trong application.yml:

    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/demo?createDatabaseIfNotExist=true
        username: root
        password: demo@123
        driverClassName: com.mysql.cj.jdbc.Driver
    

    BindingException

    org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
    

    Lỗi này xuất hiện khi chưa cấu hình thuộc tính mybatis.mapper-locations. Cấu hình thuộc tính này trong application.yml hoặc application.properties như sau:

    mybatis:
      mapper-locations: "classpath:app.demo.mybatis.repository/*.xml"
    
    mybatis.mapper-locations=classpath:app.demo.mybatis.repository/*.xml
    

    Tổng kết

    Trong bài viết này tôi đã hướng dẫn các bạn các bước cơ bản để tạo một ứng dụng Spring Boot có sử dụng MyBatis. Hi vọng bài viết sẽ giúp ích cho các bạn mới bắt đầu tiếp cận với MyBatis cũng như là cung cấp một hướng dẫn khi bắt đầu xây dựng một dự án.

  • Spring Security: Tìm hiểu về internal flow

    Spring Security: Tìm hiểu về internal flow

    Spring Security là gì?

    Spring Security là một framework được cung cấp bởi Spring cung cấp khả năng xác thực, bảo vệ, kiểm soát truy cập và có khả năng tuỳ biến cao. Tập trung chủ yếu vào Authentication và Authorization cho một ứng dụng Java.

    Giống như hầu hết các Spring projects khác, sức mạnh thực sự của Spring Security đến từ việc nó có thể dễ dàng mở rộng khi cần thiết với những yêu cầu cụ thể trong một dự án.

    Benefits/features chính:

    • Hỗ trợ authentication và authorization một cách toàn diện.
    • Ngăn chặn các nguy cơ bảo mật đến từ Cross-site Forgery, CSRF Attacks, ClickJacking,…
    • Hỗ trợ tích hợp với Spring Web MVC
    • Hỗ trợ tích hợp với Servlet API

    Internal Workflow: Cách Spring Security hoạt động?

    Dưới đây là workflow cách mà Spring Security mặc định (User Credentials) hoạt động:

    User Credentials Authentication workflow

    Ta hãy cùng đến với những objects chính có trong flow và tìm hiểu định nghĩa của chúng nhé.

    Spring Security Filters:

    Spring Security Authentication Filters là những filter sẽ nằm giữa client request với server. Khi nhận được request, các filter sẽ tách lọc những thông tin từ request thành các authentication details (username, password, roles,…). Default Spring Security sẽ sử dụng class UsernamePasswordAuthenticationFilter.

    UsernamePasswordAuthenticationFilter extends từ Abstract class AbstractAuthenticationProcessingFilter.

    Authentication: là một base object làm nhiệm vụ validate user credentials nhận được từ phía client. Ở behavior mặc định, Authentication object sẽ là class UsernamePasswordAuthenticationToken.

    UsernamePasswordAuthenticationToken sẽ được sử dụng để chứa user credentials.

    AuthenticationManager:

    AuthenticationManager là một interface với method authenticate() làm nhiệm vụ xác định những Authentication providers phù hợp nhất để xử lý Authentication object nhận được từ filters. AuthenticationManager sẽ nhận kết quả authenticate từ Provider (Success hoặc Not success). Nếu không success, nó sẽ thử một provider phù hợp khác.

    Ở behavior mặc định của Spring security, class ProviderManager sẽ được chọn để xử lý các request.

    ProviderManager implements interface AuthenticationManager.

    AuthenticationProvider:

    AuthenticationProvider là những classes implement interface AuthenticationProvider với method authenticate() làm nhiệm vụ xử lý các logic liên quan đến authentication. DaoAuthenticationProvider sẽ là authentication provider mặc định cho behavior mặc định của Spring Security.

    DaoAuthenticationProvider.

    UserDetailsService: là interface chứa thông tin, schema của user details. Ở behavior mặc định, Spring Security sẽ sử dụng class InMemoryUserDetailsManager, với method loadUserByUsername() để lấy ra thông tin của user từ memory của hệ thống.

    PasswordEncoder: là interface có nhiệm vụ encode, encrypt và decrypt password của user, validate và trả về kết quả valid/invalid cho Authentication Provider xử lý.

    Security Context:

    Sau khi Spring Security đã validate đủ, user details sẽ được lưu vào Security context. Ở lần truy cập tới, thông tin user sẽ được filter retrieve ở đây thay vì thực hiện đầy đủ các bước flow như ở trên.

    Kết luận

    Trên đây là tổng hợp về Spring Security cũng như một flow mặc định của Spring Security sẽ diễn ra như thế nào. Tất nhiên, còn rất nhiều những vấn đề to lớn khác từ Spring Security mà phạm vi bài viết không thể mô tả đủ. Hi vọng, qua bài viết trên, bạn đã có thể có cái nhìn tổng quan nhất về Spring Security, rất cảm ơn bạn đã dành thời gian ra để đọc qua bài viết trên của mình.

    Tài liệu tham khảo

    https://spring.io/projects/spring-security

    https://blog.knoldus.com/spring-security-internal/

    https://www.linkedin.com/pulse/how-does-spring-security-works-internally-ayush-jain/

  • Dependency Injection

    Dependency Injection

    Dependency Injection là một mẫu thiết kế được sử dụng để triển khai Inversion of Control. Nó cho phép tạo các đối tượng phụ thuộc bên ngoài một lớp và cung cấp các đối tượng đó cho một lớp thông qua các cách khác nhau. Sử dụng DI, chúng ta di chuyển việc tạo và ràng buộc các đối tượng phụ thuộc ra bên ngoài lớp phụ thuộc vào chúng. Điều này mang lại mức độ linh hoạt cao hơn, phân tách và kiểm tra dễ dàng hơn.

    Khi mà class A sử dụng một số chức năng của class class B, thì có thể nói là class A có quan hệ phụ thuộc với class B.

    Trong java, trước khi ta có thể sử dụng method của class khác, ta phải khởi tạo một đối tượng của class đấy.

    Tại sao cần sử dụng Dependency Injection?

    Ví dụ chúng ta có một class Car, trong đó có chứa đối tượng khác như Wheel.

    Ở đây, class Car chịu trách nhiệm khởi tạo tất cả các dependency object. Nhưng chuyện gì sẽ xảy ra nếu chúng ta muốn bỏ Wheel và thay thế bằng SteelWheel hoặc PlasticWheel.

    Để giải quyết vấn đề trên thì chúng ta phải tạo một class Car mới với SteelWheel hoặc PlasticWheel. Tuy nhiên khi sử dụng dependency injection, chúng ta có thể đổi Wheel trong thời gian chương trình chạy (Runtime) vì dependency có thể được đẩy vào ở Runtime thay vì Compiletime.

    Có 3 loại Dependency Injection

    1. Constructor injection: Các dependency được cung cấp thông qua constructor của class.
    2. Setter Injection: Khách hàng tạo ra một setter method để các class khác có thể sử dụng chúng để cấp dependency.
    3. Interface injection: Dependency sẽ cung cấp một hàm injector để inject nó vào bất kỳ khách hàng nào được truyền vào. Các khách hàng phải implement một interface mà có một setter method dành cho việc nhận dependency.

    Trách nhiệm của dependency injection là:

    1. Tạo ra các object.
    2. Biết class nào cần object đấy.
    3. Cung cấp cho những class đó object chúng cần.

    Bằng cách này, nếu trong tương lai object đó có sự thay đổi thì dependency injection có nhiệm vụ cấp lại những object cần thiết cho class.

    Điểm mạnh

    1. Giúp unit test dễ hơn.
    2. Giảm thiểu được code mẫu (boilerplate code) vì việc khởi tạo dependency được làm bởi một component khác.
    3. Mở dụng dự án dễ dàng hơn.
    4. Giúp ích trong việc liên kết lỏng giữa các thành phần trong dự án

    Điểm yếu

    1. Khá phức tạp để học.
    2. Rất nhiều các lỗi ở compile time có thể đẩy sang runtime.
    3. Có thể làm ảnh hường tới chức năng auto-complete hay Find references của một số IDE.

    Nguồn tham khảo:

    1. https://viblo.asia/p/dependency-injection-la-gi-va-khi-nao-thi-nen-su-dung-no-LzD5d0d05jY
  • [Spring] ApplicationContext trong Spring Framework

    [Spring] ApplicationContext trong Spring Framework

    Trong bài viết này chúng ta sẽ tìm hiểu chi tiết về ApplicationContext interface.

    ApplicationContext?

    Ta hãy cùng nhớ lại 2 khái niệm DI(Dependency Injection) và IoC(Inversion of Control) gây thương nhớ cho những developer của Spring framework. IoC Container chính là lõi của Spring Framework. IoC Container có chức năng tạo ra các đối tượng, liên kết chúng lại với nhau, cấu hình chúng, và quản lí vòng đời của chúng từ khi tạo ra đến khi bị hủy. IoC container sẽ sử dụng DI để quản lí các thành phần tạo nên một ứng dụng.

    Trong Spring framwork IoC được mô tả qua BeanFactory và ApplicationContext interface. BeanFactory là root interface truy cập vào Spring IoC, cung cấp chức năng cơ bản để quản lí Bean. Còn ApplicationContext là sub-interface cúa BeanFactory interface. Do đó, nó cung cấp tất cả các chức năng cơ bản của BeanFactory cùng nhưng chức năng tiên tiến hơn cho các ứng dụng Spring và phù hợp hơn cho những ứng dụng J2EE.

    Spring Bean

    Trước khi đi chi tiết hơn về ApplicationContext interface ta cần xem qua khái niệm về Spring Bean.

    In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.
    

    Hay hiểu đơn giản với các ứng dụng Spring, Bean là object được tạo ra và quản lí bới Spring IoC container.

    Cấu hình Bean trong Container

    Để ApplicationContext có thể quản lí được các Bean, ứng dụng phải cũng cấp cấu hình bean cho ApplicationContext container. Ta sẽ có những cách khác nhau cấu hình để cấu hình bean:

    1. XML-Based Configuration
    2. Java-Based Configuration
    3. Annotation-Based Configuration

    XML-Based Configuration

    Cuối cùng với cách cấu hình dựa trên XML, ta sẽ khai báo tất cả các cấu hình của Bean trong một file XML.

    Ta sẽ khai báo Bean cho class UserConfiguration trong một file user-config.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
      <bean id="userService" class="com.cuongnm.applicationcontext.UserService">
        <constructor-arg name="userRepository" ref="userRepository" />
      </bean>
      
      <bean id="userRepository" class="com.cuongnm.applicationcontext.UserRepository" />
    </beans>
    

    Java-Based Configuration

    Cấu hình dựa trên Java bằng cách sử dụng @Bean annotation bên trong 1 lớp @Configuration. Với mỗi @Bean annotation được khai báo đánh dấu cho một method để tạo ra 1 Spring Bean. Và những phương thức này đc chứa trong 1 class được đánh dấu là @Configuration – một class chứa các cấu hình Spring bean.

    Ví dụ:

    @Configuration
    public class UserConfig {
    
      @Bean
      public UserService userService() {
        return new AccountService(userRepository());
      }
    
      @Bean
      public UserRepository userRepository() {
        return new UserRepository();
      }
    }
    

    Bằng cách đưa ra @Configuration ta sẽ xử lí class UserConfig như thẻ bean trong XML

    Annotation-Based Configuration

    Để sử dụng được phương pháp này, đầu tiên ta sẽ cấu hình trong XML để cho phép sử dụng Annotation-Based Configuration trong ứng dụng.

    <context:annotation-config/>
    <context:component-scan base-package="com.cuongnm.applicationcontext"/>
    

    Thẻ context:annotation-config để khai báo cho việc sử dụng annotation-based mappings và thẻ context:component-scan với tham số base-package cho Spring tìm được package chưa các annotated classes.

    Sau đó, ta sẽ sử dụng các annotation được cung cấp bởi Spring để đánh dấu cho các class, method, constructor, field trong Java để cấu hình cho Bean như @Component, @Controller, @Service, @Repository, @Autowired.

    Ví dụ với class UserService được khai báo là một Bean sử dụng @Component annotation:

    @Component
    public class UserService {
      
    }
    

    Ta có thể lấy ra Bean này bằng cách:

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationcontext/user-bean-config.xml");
    UserService userService = context.getBean(UserService.class);
    assertNotNull(userService);
    

    Cách sử dụng ApplicationContext

    Cũng như việc cấu hình cho Bean thì cũng có rất nhiều cách sử dụng ApplicationContext interface.

    1) ClassPathXmlApplicationContext

    Phương pháp này sẽ tải các Bean config từ các file XML nằm trong đường dẫn classpath.

    ApplicationContext context = new ClassPathXmlApplicationContext ("user-bean-config.xml");
    

    2) FileSystemXmlApplicationContext

    Sử dụng các Bean config từ các file XML trong FileSystem hay từ URL.

    ApplicationContext context = new FileSystemXmlApplicationContext (“c: /myconfig.xml”);
    

    3) AnnotationConfigApplicationContext

    AnnotationConfigApplicationContext được sử dụng với Java-Based Configuration cho các cấu hình Bean.

    Ví dụ:

    public static void main(String[]args){
    /* Creating Spring IoC Container Without XML configuration file*/
    ApplicationContext context= new AnnotationConfigApplicationContext(UserConfig.class);
    MyBean beanObj = context.getBean(UserService.class);
    }
    

    Với ví dụ này ApplicationContext được lấy từ UserConfig class. Chúng ta lấy các cấu hình Bean từ một class được chú thích @Configuratation và nó sẽ được khai báo:

    @Configuration
    public class UserConfig {
    
      @Bean
      public UserService userService() {
        return new AccountService(userRepository());
      }
    
      @Bean
      public UserRepository userRepository() {
        return new UserRepository();
      }
    }
    

    3) XmlWebApplicationContext và AnnotationConfigWebApplicationContext

    XmlWebApplicationContext được sử dụng để đại diện cho Spring container trong ứng dụng Web. Và nó tải các cấu hình bean từ những file XML với mặc định từ đường dẫn “/WEB-INF/applicationContext.xml”. Chúng ta cũng có thể chỉ định được dẫn của nó qua tham số contextConfigLocation của ContextLoaderListener hoặc DispatcherServlet trong web.xml.

    Giống như XmlWebApplicationContext là class tương ứng với ClassPathXmlApplicationContext và FileSystemXmlApplicationContext và được sử dụng để tạo ra ApplicationContext cho các ứng dụng web, tương tự, AnnotationConfigWebApplicationContext là class tương ứng với AnnotationConfigApplicationContext.

    Lời kết

    Tóm tắt lại những gì mình muốn nói ở bài viết này, mục đích giúp chúng ta hiểu về ApplicationContext trong Spring, hiểu cách sử dụng, triển khai ApplicationContext. Cũng như cách gọi Spring container bằng ApplicationContext mang đến nhiều chức năng hơn so với BeanFactory.

    Bài viết được tham khảo từ “https://www.baeldung.com/spring-application-context“.

    Cảm ơn các bạn đã đọc bài viết!

  • [Spring] Sử dụng Spring ResponseStatusException

    [Spring] Sử dụng Spring ResponseStatusException

    Sử dụng Spring ResponseStatusException

    Giới thiệu

    Một ứng dụng RESTful, bằng cách trả về các HTTP status code trong HTTP response nó có thể thông báo về sự thành công hay thất bại của một HTTP request. Ví dụ như nếu người dùng request lên một id không hề tồn tại, các HTTP status code có thể giúp xác định được các vấn đề có thể xảy ra khi xử lí request.

    Trong Spring chúng ta cũng có rất nhiều cách để đặt HTTP status code cho một HTTP response. Tuy nhiên trong bài viết này mình sẽ giới thiệu về một class mới được giới thiệu trong Spring 5 đó chính là ResponseStatusException sẽ hỗ trợ cho ta việc áp dụng HTTP status code.

    @ResponseStatus

    Trước khi tìm hiểu về ResponseStatusException , ta sẽ tìm hiểu qua về @ResponseStatus annotation. Annotation này được giới thiệu trong Spring 3 để giải quyết vấn đề áp dụng HTTP status code cho HTTP response.

    Với annotaion này chúng ta sẽ sử dụng để định nghĩa status code và reason cho HTTP response:

    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.ResponseStatus;
    
    @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Image not found")
    public class ImageNotFoundException extends Exception{
        //...
    }
    

    Ở trong ví dụ này, nếu Exception được đưa ra trong một xử lí HTTP request, thì trong response sẽ bao gồm HTTP status code được chỉ định sẽ là 404.

    Tuy nhiên với phương pháp sử dụng @ResponseStatus ta có một nhược điểm là nó sẽ tạo ra mối quan hệ phụ thuộc chặt chẽ vói Exception. Và như trường hợp ta xét ở trên thì tất cả các Exception có kiểu ImageNotFoundException khi được đưa ra thì đều cho ta một response với thông báo lỗi và status code là như nhau trong mọi trường hợp.

    ResponseStatusException

    ResponseStatusException được tạo ra nhằm thay thế cho @ResponseStatus và là một base class cho các exception được sử dụng đề apply các HTTP status cho response. Và đây cũng là một RuntimeException.

    Với ResponseStatusException class sẽ cung cấp cho ta 3 contructor:

    Với các đối số truyền vào cho contructor method:

    • status – HTTP status được set cho HTTP response
    • reason – message được hiển thị để giải thích cho exception
    • cause – là một java.lang.Throwable nguyên nhân của ResponseStatusException

    Những điểm mạnh cảu việc dùng ResponseStatusException class:

    • Thứ nhất, việc khai báo và sử dụng dễ dàng
    • Thứ hai, các exception cùng loại ta có thể xử lí riêng biệt và có thể linh hoạt các stutas code khác nhau có thể được set trong response, giảm sự phụ thuộc vào nhau.
    • Thứ ba, ta có thể tránh được việc phải tạo các class exception không cần thiết.
    • Và cuối cùng, class cấp cho ta nhiều quyền hơn trong việc xử lí exception, vì các exception được tạo theo chương trình nên được kiểm soát tốt hơn.

    Ví dụ

    Bây giờ, hãy xem một ví dụ về cách sử dụng ResponseStatusException trong thực tế:

    Ta có 1 class ToDoController khái báo “/todo” mapping truyền vào tham số id để lấy ra Todo object tương ứng

    @RestController
    public class ToDoController {
    
        @Autowired
        private ToDoService toDoService;
    
        @GetMapping(value = "/todo")
        public ToDo getTodo(@RequestParam(value = "id", required = false) Long id) {
            try {
                return toDoService.getTodo(id);
            } catch (TodoNotFoundException e) {
                throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Todo not found", e);
            }
        }
    
    }
    

    Với một exception được khai báo

    public class TodoNotFoundException extends Exception{
    
        public TodoNotFoundException(String errorMessage){
            super(errorMessage);
        }
    }
    

    Spring sẽ cung cấp cho ta một “/error” mapping sẽ trả về response dưới định dạng JSON với HTTP status. Trong ví dụ này khi truyền vào id không hề tồn tại chương trình sẽ trả về ResponseStatusException với response chứa status code tương ứng.

    Đây sẽ là response trong trường hợp có exception (ta sẽ dùng Postman để gửi request):

    Để xem được message về lỗi trong response ta sẽ thêm thuộc tính server.error.include-message=always. Khi đó response trả về sẽ có nội dung:

    {
        "timestamp": "2021-08-03T01:35:20.878+00:00",
        "status": 404,
        "error": "Not Found",
        "message": "Todo not found",
        "path": "/todo"
    }
    

    Ngoài ra với lợi ích là tính linh hoạt khi có thể gán các status code khác nhau cho cùng một exception ta có thể thứ với một ví dụ khác:

    @GetMapping(value = "/todo")
        public ToDo getTodo(@RequestParam(value = "id", required = false) Long id) {
            try {
                return toDoService.getTodo(id);
            } catch (TodoNotFoundException e) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Provide correct Todo Id", e);
            }
        }
    

    Và response trả về sẽ là:

    {
        "timestamp": "2021-08-03T01:57:41.502+00:00",
        "status": 400,
        "error": "Bad Request",
        "message": "Provide correct Todo Id",
        "path": "/todo"
    }
    

    Lời kết

    Như vậy trong bài viết này, chúng ta đã cùng tìm hiểu về ResponseStatusException – một cách tốt hơn để tạo một HTTP status code trong HTTP response so với annotation @ResponseStatus. Tuy nhiên với việc nên thận trọng với việc sử dụng vì nếu không có phương pháp xử lí exception thống nhất thì việc thực thi một số quy ước trên toàn ứng dụng sẽ khó khăn và có thể xảy ra code bị trùng lặp.

    Cảm ơn các bạn đã đọc bài viết!

    Tài liệu tham khảo: https://www.baeldung.com/spring-response-status-exception

  • [DesignPattern] Simple Factory Pattern

    [DesignPattern] Simple Factory Pattern

    Background

    Hầu hết anh em developer đều đã nghe qua về Design Pattern

    Tuy nhiên mình thấy còn nhiều người (gồm cả mình) đều không có kinh nghiêm áp dụng nó.

    Nên mình lập topic về design pattern để mọi người có thể chia sẻ kinh nghiệm và cách áp dụng nó trong các bài toán cụ thể.

    Vậy đầu tiên chúng ta phải hiểu design pattern là gì?

    • Theo mình hiểu design pattern đơn giản là các giải pháp mẫu tối ưu cho từng tình huống cụ thể trong lập trình OOP.

    Taị sao lại sử dụng design pattern?

    Một thực tế là mình nghe rất nhiều câu như refactor đi (dm code như shit, fucking coding …),đập hết đi xây lại.

    Tại sao chúng ta lại muốn như thế?

    Vì hầu hết các source code ban đầu đều rất khó maintain và mở rộng, nên khi có một yêu cầu mới hay một bug,

    anh em lại cặm cụi sửa code, sửa bug này lại sinh ra bug khác nên effort để giải quyết một vấn đề rất tốn kém.

    Do đó nếu mình không chỉ "code để chạy được" mà suy nghĩ, áp dụng các design pattern trươc khi viết ra

    sẽ rút ngắn phần lớn thời gian development và maintain.

    Sau đây mình xin trình bầy một số design pattern mà mình đã đọc qua.

    Đầu tiên mình xin tập trung vào một loại pattern mà chắc anh em ai cũng nghe qua, đó là Factory Pattern

    Factory Pattern có 3 loại: Simple factory, Factory method và Abstract Factory

    Bài lần này mình sẽ trình bày về Simple Factory Pattern

    Simple Factory Pattern

    Bài toán

    Nào chúng ta hãy làm quen với Simple Factory Pattern với một ví dụ như sau:

    Giả sử bạn đang viết chương trình đặt hàng và ship hàng cho một cửa hàng Pizza

    Quá trình đặt hàng một chiếc Pizza sẽ qua các công đoạn như chuẩn bị (prepare), nướng (bake) và đóng hộp (box)

    Xử lý để chạy được & vấn đề

    Chương trình có thể được viết như sau

    Class PizzaStore {
      public Pizza orderPizza() {
        Pizza pizza = new Pizza();
        pizza.prepare();
        pizza.bake();
        pizza.box();
        return pizza;
      }
      public void shipPizza() {
        Pizza pizza = new Pizza();
        pizza.ship();
       }
    }
    

    OK như vậy là có sample về chương trình orderPizza và shipPizza.

    Nhưng chủ cửa hàng lại muốn có nhiều món pizza cơ mà.

    Ví du: Pizza gà (ChickenPizza), Pizza phô mai(CheesePizza)

    Bạn nên làm thế nào ???

    Dễ mà tạo một lớp cha Pizza và create 2 lớp con là ChickenPizza và CheesePizza

    Sau đó add thêm parameter type cho orderPizza và shipPizza

    Class PizzaStore {
      public Pizza orderPizza(String type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle ("cheese".equals(type) {
          pizza = new ChickenPizza();
        }
        pizza.prepare();
        pizza.bake();
        pizza.box();
        return pizza;
      }
      public void shipPizza(String type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle if ("cheese".equals(type) {
          pizza = new ChickenPizza();
        }
        pizza.ship();
      }
    }
    

    OK các bạn thấy thế nào, chương trình chạy ngon không có lỗi gì luôn :D.

    Một tuần sau cửa hàng thấy cần thêm món Pizza hải sản (SeaFoodPizza) để tăng thêm khách hàng

    Bạn sẽ vào sửa method orderPizza???

    Class PizzaStore {
      public Pizza orderPizza(String type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle if ("cheese".equals(type) {
          pizza = new ChickenPizza();
        } else if ("seafood".equals(type) {
          pizza = new SeaFoodPizza();
        }
        pizza.prepare();
        pizza.bake();
        pizza.box();
      }
      public void shipPizza(String type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle if ("cheese".equals(type) {
          pizza = new ChickenPizza();
        }
        pizza.ship();
      }
    }
    

    Cơn ác mộng mới chỉ bắt đầu :)). Đấy là mình chỉ liệt kê 2 chức năng cơ bản, điều gì sẽ xảy ra nếu còn rất nhiều chức năng khác cần lấy thông tin pizza theo từng loại

    Ví du: Lấy thông tin giá, tên, … của từng loại Pizza

    Bạn sẽ phải hì hục sửa code tất cả cả các method đấy nếu cửa hàng tạo thêm một loại Pizza.

    Vâng bạn sẽ vẫn cố gắng sửa để nó có thể chạy được nhưng bạn sẽ tốn rất nhiều effort để làm việc này nếu có hàng chục method cần sửa.

    Bạn cũng lo lắng mình có thể quên xử lý ở một method nào đấy,…

    -> Giờ bạn đã sợ maintain chưa

    Áp dụng Simple Factory Pattern vào bài toán

    Quá nhiều điều để lo lắng chúng ta phải refactor thôi.

    Đầu tiên chắc các bạn cũng thấy luôn, chúng ta cần đóng gói việc khởi tạo object bên dưới

    if ("chicken".equals(type) {
      pizza = new ChickenPizza();
    } esle if ("cheese".equals(type) {
      pizza = new ChickenPizza();
    } else if ("seafood".equals(type) {
      pizza = new SeaFoodPizza();
    }
    

    Chuyển đoạn code trên sang một class factory

    Class SimplePizzaFactory {
      public Pizza createPizza(type) {
        Pizza pizza;
        if ("chicken".equals(type) {
          pizza = new ChickenPizza();
        } esle if ("cheese".equals(type) {
          pizza = new ChickenPizza();
        } else if ("seafood".equals(type) {
          pizza = new SeaFoodPizza();
        }
        return pizza;
     }
    }
    

    Tiếp theo chúng ta sẽ sử dụng SimplePizzaFactory để tạo trong PizzaStore để tạo các object

    Class PizzaStore {
      SimplePizzaFactory mFactory;
    
      public PizzaStore (SimplePizzaFactory factory) {
        mFactory = factory;
      }
    
      public Pizza orderPizza(String type) {
        Pizza pizza = mFactory.createPizza(type);
        pizza.prepare();
        pizza.bake();
        pizza.box();
      }
      public void shipPizza(String type) {
        Pizza pizza = mFactory.createPizza(type);
        pizza.ship();
      }
    }
    

    Bạn thấy công việc đơn giản hơn chưa, việc khởi tạo object cần thiết được xử lý trong Factory

    Như vậy chúng ta không cần phải lo lắng sửa code ở từng method như orderPizzahay shipPizza, … nữa 🙂

    Conlution

    Đây chỉ là một ví dụ rất cơ bản về Design Pattern giúp mọi người dễ hình dung và tiếp cận

    Chúng ta còn rất nhiều bài toán phức tạp khác cần Design Pattern để xử lý

    Quan trọng mọi người hiểu được việc "code để chạy" có thể nhanh trong thời điểm đấy

    Tuy nhiên việc mọi người phải rework để phát triển nó sẽ tốn gấp đôi gấp mười lần effort nếu mọi người có thể làm Design cẩn thận từ đầu

    Do đó việc hiểu các Design pattern hỗ trợ rất tốt cho mọi người trong việc triển khai các bài toán cụ thể

    Hy vọng bài này giúp mọi người hiểu được cơ bản Design Pattern là gì và tầm quan trọng của nó trong software

  • Hướng dẫn sử dụng Project Lombok

    Hướng dẫn sử dụng Project Lombok

    Hướng dẫn sử dụng Project Lombok

    Chinh chiến với Java nhiều năm, bạn có cảm thấy nhàm chán khi làm việc với những đoạn code theo khuôn mẫu của nó hay lười biếng phải khai báo các phương thức Getter, Setter cho các class Java? Nếu câu trả lời là có thì hãy sử dụng Project Lombok. Vậy Project Lombok là gì?

    • Lombok?
    • Cài đặt
    • Sử dụng Lombok
    • Lời kết

    Lombok?

    Lombok là một thư viện, một plugin, giúp chúng ta giảm thiểu các đoạn code thừa (boilerplate) bằng cách tự động sinh ra các hàm GetterSetterConstructor, v.v..

    Lombok giúp chúng ta generate code một cách tự động nhưng không giống như cách các IDE làm cho chúng ta. Các IDE generate các phương thức Getter, Setter và một số phương thức khác trong các tập tin .java. Lombok cũng generate các phương thức đó nhưng là trong các tập tin .class file. Không những làm cho code sáng sửa mà còn trông rất hợp lý, dễ quản lý hơn giúp developer tập trung vào tầng nghiệp vụ và logic thay vì mất thời gian làm những việc thừa thãi.

    Cài đặt

    Để sử dụng Lombok trong project ta cần:

    Bước 1

    Thêm lib vào project bằng Maven hoặc Gradle

    Maven

    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
        <scope>provided</scope>
    </dependency>
    

    Gradle

    
    // https://mvnrepository.com/artifact/org.projectlombok/lombok
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.20'
    

    Bước 2

    Đến đây có thể bạn sẽ thắc mắc tại sao đã thêm Lombok vào project rồi mà còn phải cài thêm Lombok Plugin vào IDE nữa???

    Ví dụ bạn muốn sử dụng Lombok để generate Get/Set thì nó sẽ tự động thêm code vào class đó trước khi thành file .jar. Nhưng các IDE thì chỉ nhìn thấy các dòng code hiện tại của bạn và tham chiếu tới nó, điều này sẽ dẫn đến những thông báo lỗi khi bạn sử dụng hàm Get/Set này.

    Nên để IDE hiểu rằng các class đã có các hàm Get/Set rồi, thì bạn phải cài thêm Lombok Plugin.

    Bây giờ ta sẽ cài đặt cho IntelliJ IDEA

    Với IntelliJ IDEA version 2020.3 trở lên thì IDE đã hỗ trợ Lombok mà ko cần cài plugin(phần hướng dẫn này cho version trước 2020.3 )

    Vào File -> Setting -> Plugin …

    Search “Lombok”, chọn Lombok Plugin và Install.

    Sử dụng Lombok

    Lombok dùng các Annotation để khai báo

    @Data

    Ví dụ này 2 đoạn code sẽ tương đương

    public class User {
    
        private String name;
    
        private int age;
    
        public User() {
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "User: " + name
                    + " - " + age;
        }
    }
    
    import lombok.Data;
    
    @Data
    public class User {
    
        private String name;
    
        private int age;
    }
    

    Khi bạn đánh dấu 1 class là @Data, thì nó sẽ generate ra Constructor rỗng hoặc có tham số theo yêu cầu, toàn bộ Get/Set, hàm equals, hashCode, toString().

    @NoArgsConstructor@RequiredArgsConstructor@AllArgsConstructor

    Các annotation được dùng trong trường hợp bạn muốn định nghĩa các Contructor theo yêu cầu khác nhau:

    • @NoArgsConstructor: Hàm khởi tạo rỗng, đã đề cập ở trên
    • @RequiredArgsConstructor: Hàm khởi tạo chứa tất cả thuộc tính, ví dụ Champion(String name, String type)
    • @AllArgsConstructor: Hàm khởi tạo theo yêu cầu. Bạn chỉ muốn hàm khởi tạo có vài thuộc tính do bạn chọn thôi, thì bạn thêm final trước thuộc tính trong class, nó sẽ tự sinh ra Constructor như thế.

    @Getter@Setter

    Được sử dụng trong trường hợp chỉ muốn generate Get/Set và không muốn dùng @Data vì có chức năng không cần thiết.

    import lombok.Getter;
    import lombok.Setter;
    
    @Getter
    @Setter
    public class User {
    
        private String name;
    
        private int age;
    }
    

    @Builder

    Thông thường, khi chúng ta cần khởi tạo một đối tượng với rất nhiều thông tin, chúng ta có thể sử dụng Builder Pattern để làm điều này.

    Ví dụ với class User như sau:

    public class Users {
    
        private String name;
    
        private int age;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "User: " + name
                    + " - " + age;
        }
    }
    

    Để tạo đối tượng User với tất cả các thông tin sử dụng Builder Pattern, chúng ta cần tạo một đối tượng UserBuilder như sau:

    public class UserBuilder {
    
        private Users user;
    
        public UserBuilder() {
            user = new User();
        }
    
        public UserBuilder name(String name){
            user.setName(name);
            return this;
        }
    
        public UserBuilder age(int age){
            user.setAge(age);
            return this;
        }
    
        public Users build(){
            return user;
        }
    }
    

    Và dùng UserBuilder này:

    Users users = new UserBuilder()
                    .name("CuongNM")
                    .age(20)
                    .build();
    

    Chắc hẳn ai cũng ngại khi viết 1 class Builder cổ điển như trên, tự dưng phải tạo thêm 1 class nữa, gấp đôi số lượng thuộc tính khai báo, gấp đôi số hàm cần viết.

    Với Lombok vấn đề này đc giải quyết rất nhanh gọn

    @Data
    @Builder
    public class Users {
    
        private String name;
    
        private int age;
    }
    
    Users users = Users.builder()
                    .name("CuongNM")
                    .age(20)
                    .build();
    

    Logging với Project Lombok(@Slf4j)

    Thông thường, khi chúng ta sử dụng các Logging frameworks như Log4J, Logback hay Simple Logging Facade for Java (SLF4J), ta sẽ khai báo như sau:

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
     
    public class Example {
     
        public static final Logger LOGGER = LoggerFactory.getLogger(Example.class);
     
        public void print(String message) {
            LOGGER.info(message);
        }
    }
    

    Nhưng đối với Lombok, ta chỉ cần dùng annotation dành cho Logging framework mà chúng ta muốn sử dụng mà thôi, sau đó thì có thể sử dụng như bình thường. Như ví dụ ở đây, mình đang sử dụng Simple Logging Facade for Java(SLF4J) để logging thì mình sẽ khai báo annotation @Slf4j của Lombok dành cho thư viện này như sau:

    import lombok.extern.slf4j.Slf4j;
     
    @Slf4j
    public class Example {
     
        public void print(String message) {
            log.info(message);
        }
    }
    

    Lời kết

    Lombok sinh ra để giúp cho code của chúng ta trở nên ngắn gọn dễ hiểu hơn và nó còn rất nhiều tính năng hay ho khác. Trong khuôn khổ bài viết này mình chỉ giới thiệu về các tính năng cơ bản của Lombok và được mình sử dụng nhiều nhất trong suốt quá trình làm việc.

    Cảm ơn các bạn đã đọc bài viết! Các bạn có thể tìm hiểu thêm tại trang chủ của Lombok.

    Tác giả

    [email protected]

  • [MacOS] Hướng dẫn cài đặt Oracle JDK

    [MacOS] Hướng dẫn cài đặt Oracle JDK

    Mặc định thì Oracle JDK sẽ được chọn cài đặt trên MacOS. Do đó nếu muốn sử dụng Oracle JDK thì bạn cần phải cài đặt lại. Trong bài viết này tôi sẽ hướng dẫn các bạn cài đặt Oracle JDK.

    Homebrew

    • Nếu bạn chưa cài đặt brew thì có thể sử dụng lệnh sau để tiến hành cài đặt
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
    
    • Nếu đã cài đặt rồi thì tiến hành update lastest brew như sau:
    hieunv@HieuNV ~ % brew update && brew upgrade
    Updated 1 tap (homebrew/core).
    ==> New Formulae
    swift-sh
    ==> Updated Formulae
    apache-spark               jetty                      xcodegen
    docker-slim                vim                        zsh-syntax-highlighting
    Updating Homebrew...
    

    Kiểm tra caskjava

    brew cask info java
    

    Nếu homebrew/cask chưa được cài đặt thì nó sẽ tự động cài đặt luôn

    hieunv@HieuNV ~ % brew cask info java
    ==> Tapping homebrew/cask
    Cloning into '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask'...
    remote: Enumerating objects: 3655, done.
    remote: Counting objects: 100% (3655/3655), done.
    remote: Compressing objects: 100% (3648/3648), done.
    remote: Total 3655 (delta 26), reused 510 (delta 5), pack-reused 0
    Receiving objects: 100% (3655/3655), 1.23 MiB | 215.00 KiB/s, done.
    Resolving deltas: 100% (26/26), done.
    Tapped 1 command and 3543 casks (3,660 files, 4.0MB).
    java: 13.0.2,8:d4173c853231432d94f001e99d882ca7
    https://openjdk.java.net/
    Not installed
    From: https://github.com/Homebrew/homebrew-cask/blob/master/Casks/java.rb
    ==> Name
    OpenJDK Java Development Kit
    ==> Artifacts
    jdk-13.0.2.jdk -> /Library/Java/JavaVirtualMachines/openjdk-13.0.2.jdk (Generic Artifact)
    
    • Nếu đã cài đặt rồi bạn sẽ nhận được thông tin về phiên bản java đã được cài đặt
    hieunv@HieuNV ~ % brew cask info java
    java: 13.0.2,8:d4173c853231432d94f001e99d882ca7
    https://openjdk.java.net/
    Not installed
    From: https://github.com/Homebrew/homebrew-cask/blob/master/Casks/java.rb
    ==> Name
    OpenJDK Java Development Kit
    ==> Artifacts
    jdk-13.0.2.jdk -> /Library/Java/JavaVirtualMachines/openjdk-13.0.2.jdk (Generic Artifact)
    

    Tiến hành cài đặt Oracle JDK sử dụng brew cask

    hieunv@HieuNV ~ % brew cask install oracle-jdk
    ==> Caveats
    Installing oracle-jdk means you have AGREED to the license at:
      https://www.oracle.com/technetwork/java/javase/terms/license/javase-license.html
    
    ==> Downloading https://download.oracle.com/otn-pub/java/jdk/13.0.2+8/d4173c8532
    ==> Downloading from https://download.oracle.com/otn-pub/java/jdk/13.0.2+8/d4173
    ######################################################################## 100.0%
    ==> Verifying SHA-256 checksum for Cask 'oracle-jdk'.
    ==> Installing Cask oracle-jdk
    ==> Running installer for oracle-jdk; your password may be necessary.
    ==> Package installers may write to any location; options such as --appdir are i
    Password:
    installer: Package name is JDK 13.0.2
    installer: Installing at base path /
    installer: The install was successful.
    ?  oracle-jdk was successfully installed!
    

    Kiểm tra phiên bản java sau khi cài đặt

    hieunv@HieuNV ~ % java --version
    java 13.0.2 2020-01-14
    Java(TM) SE Runtime Environment (build 13.0.2+8)
    Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)
    
    hieunv@HieuNV ~ % javac --version
    javac 13.0.2
    

    setting JAVA_HOME

    Thêm export JAVA_HOME=$(/usr/libexec/java_home) vào ~/.zshrc

    echo 'export JAVA_HOME=$(/usr/libexec/java_home)' >> ~/.zshrc
    

    Kiểm tra biến JAVA_HOME

    Đóng Termial sau đó bật lại và kiểm tra biến JAVA_HOME

    hieunv@HieuNV libexec % echo $JAVA_HOME
    /Library/Java/JavaVirtualMachines/jdk-13.0.2.jdk/Contents/Home
    

    Như vậy là bạn đã tiến hành cài đặt thành công Oracle Java rồi.
    Tài liệu tham khảo
    https://emcorrales.com/blog/install-oracle-jdk-macos-homebrew