🦄with Spring Boot

Singleton pattern, Factory Method pattern, Proxy pattern, Template pattern.

Singleton Pattern

Singleton pattern là cơ chế để đảm bảo chỉ có 1 instance của 1 Object được thể hiện trên mỗi ứng dụng.

Singleton Beans

Thông thường, Singleton thường là duy nhất trên toàn ứng dụng, nhưng với Spring, ràng buộc này được nới lỏng. Thay vào đó, Spring hạn chế chỉ có một đối tượng Singleton cho mỗi Container Spring IoC. Trên thực tế, điều này có nghĩa là Spring chỉ tạo một bean cho mỗi loại trong mỗi ApplicationContext.

Cách tiếp cận của Spring khác với định nghĩa chặt chẽ của Singleton, bởi vì một ứng dụng có thể có nhiều hơn một Container Spring. Do đó, nhiều đối tượng cùng lớp có thể tồn tại trong một ứng dụng nếu có nhiều Container.

Theo mặc định, Spring tạo ra tất cả các Bean dưới dạng Singleton.

Autowired Singletons

Ví dụ:

Đầu tiên, chúng ta tạo ra một BookRepository để quản lý các đối tượng của Book class.

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.model.Book;

@Repository
public interface BookRepository extends CrudRepository<Book, Long> {
}

Tiếp theo, chúng ta tạo ra LibraryController, sử dụng BookRepository để tính tổng số lượng sách trong thư viện:

@RestController
public class LibraryController {
    
    @Autowired
    private BookRepository repository;

    @GetMapping("/count")
    public Long findCount() {
        System.out.println(repository);
        return repository.count();
    }
}

Cuối cùng, chúng ta tạo BookController để thực hiện các hành động với đặc thù với Book như là tìm Book theo ID:

@RestController
public class BookController {
     
    @Autowired
    private BookRepository repository;
 
    @GetMapping("/book/{id}")
    public Book findById(@PathVariable long id) {
        System.out.println(repository);
        return repository.findById(id).get();
    }
}

Sau đó, chúng ta start application và thực hiện API GET trên /count và /book/1:

curl -X GET http://localhost:8080/count
curl -X GET http://localhost:8080/book/1

Kết quả trả về từ ứng dụng cho thấy cả hai đối tượng BookRepository đều có cùng object ID:

com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f
com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f

Các object ID của BookRepository trong LibraryController và BookController giống nhau, chứng tỏ Spring inject cùng một bean vào cả hai controller.

Chúng ta có thể tạo ra các thực thể riêng biệt của bean BookRepository bằng cách thay đổi phạm vi bean từ singleton (đối tượng duy nhất) sang prototype (mẫu) bằng cách sử dụng annotation @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE).

Khi thực hiện điều này, Spring sẽ tạo ra các đối tượng riêng biệt cho mỗi BookRepository bean mà nó tạo ra. Do đó, nếu chúng ta kiểm tra lại object ID của BookRepository trong mỗi controller, chúng ta sẽ thấy rằng chúng không còn giống nhau nữa.

Factory Method Pattern

Factory method pattern liên quan đến việc tạo ra một factory class với một phương thức trừu tượng để tạo ra đối tượng mong muốn.

Thường thì chúng ta muốn tạo ra các đối tượng khác nhau dựa trên context cụ thể.

Ví dụ, ứng dụng của chúng ta có thể yêu cầu một vehicle object. Trong môi trường thuỷ cơ (nautical environment), chúng ta muốn tạo ra những chiếc tàu (boats), nhưng trong môi trường không gian (aerospace environment), chúng ta muốn tạo ra những chiếc máy bay (airplanes):

Để thực hiện điều này, chúng ta có thể tạo ra một factory implementation cho mỗi đối tượng mong muốn và trả về đối tượng mong muốn từ phương thức nhà máy cụ thể (concrete factory method).

Application Context

Spring sử dụng kỹ thuật này ở nền tảng của Dependency Injection (DI) framework của nó.

Về cơ bản, Spring coi 1 bean container như là 1 factory. Vì vậy, Spring xem BeanFactory interface như 1 abstraction của 1 bean container.

public interface BeanFactory {

    getBean(Class<T> requiredType);
    getBean(Class<T> requiredType, Object... args);
    getBean(String name);

    // ...
]

Mỗi phương thức getBean được xem là một factory method, trả về một bean phù hợp với các tiêu chí cung cấp cho phương thức, như type và name của bean.

Sau đó, Spring mở rộng BeanFactory với giao diện ApplicationContext, giới thiệu thêm cài đặt cấu hình ứng dụng. Spring sử dụng cấu hình này để khởi động một bean container dựa trên một số cấu hình bên ngoài, như tệp XML hoặc annotations Java.

Sử dụng các lớp triển khai ApplicationContext như AnnotationConfigApplicationContext, chúng ta có thể tạo ra các bean thông qua các phương thức nhà máy (factory methods) được kế thừa từ giao diện BeanFactory.

Đầu tiên, chúng ta tạo 1 simple application configuration

@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
public class ApplicationConfig {
}

Tiếp theo, chúng ta tạo 1 simple class Foo , không agrument trong constructor.

@Component
public class Foo {
}

Sau đó, chúng ta tạo lớp khác là Bar, có 1 agrument trong constructor:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Bar {
 
    private String name;
     
    public Bar(String name) {
        this.name = name;
    }
     
    // Getter ...
}

Cuối cùng, chúng ta tạo các "Bean" thông qua AnnotationConfigApplicationContext - một đối tượng triển khai từ ApplicationContext:

@Test
public void whenGetSimpleBean_thenReturnConstructedBean() {
    
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    
    Foo foo = context.getBean(Foo.class);
    
    assertNotNull(foo);
}

@Test
public void whenGetPrototypeBean_thenReturnConstructedBean() {
    
    String expectedName = "Some name";
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    
    Bar bar = context.getBean(Bar.class, expectedName);
    
    assertNotNull(bar);
    assertThat(bar.getName(), is(expectedName));
}

Khi sử dụng phương thức getBean, chúng ta có thể tạo các "Bean" được cấu hình chỉ bằng cách sử dụng kiểu lớp và - trong trường hợp của Bar - các tham số trong hàm khởi tạo.

External Configuration

Đây là một mẫu (pattern) linh hoạt vì chúng ta có thể hoàn toàn thay đổi hành vi của ứng dụng dựa trên cấu hình bên ngoài.

Nếu chúng ta muốn thay đổi cài đặt của các đối tượng được tiêm (autowired) trong ứng dụng, chúng ta có thể điều chỉnh đối tượng ApplicationContext mà chúng ta sử dụng.

Ví dụ, bạn có thể sử dụng các tệp cấu hình bên ngoài (như .xml hoặc .yml) để định nghĩa và quản lý Beans trong ứng dụng Spring đồng thời tùy chỉnh cài đặt của chúng thông qua các thuộc tính cấu hình.

Nhờ khả năng đọc các cấu hình này, ApplicationContext có thể thay đổi hành vi của các đối tượng được tiêm mà không cần thay đổi mã nguồn.

Bên cạnh đó, ApplicationContext còn cho phép chúng ta sử dụng cấu hình @Profile để áp dụng những cài đặt khác nhau cho các môi trường phát triển (development) và sản xuất (production), giúp tận dụng tối đa tính linh hoạt của ứng dụng.

Như vậy, cấu trúc linh hoạt này giúp chúng ta dễ dàng thay đổi hành vi của ứng dụng mà không ảnh hưởng đến mã nguồn, giúp tăng khả năng mở rộng và điều chỉnh ứng dụng trong thời gian chạy.

Ví dụ, chúng ta có thể thay đổi AnnotationConfigApplicationContext sang một XML-based configuration class, chẳng hạn như ClassPathXmlApplicationContext:

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { 

    String expectedName = "Some name";
    ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
 
    // Same test as before ...
}

Proxy Pattern

Proxy là một công cụ tiện ích trong thế giới số của chúng ta, và chúng ta thường sử dụng chúng ngoài phần mềm (như network proxy). Trong mã code, mẫu proxy là một kỹ thuật cho phép một Object - proxy - kiểm soát việc truy cập đến Object khác - Subject hoặc service.

Đây là một class diagram minh họa cho mẫu thiết kế proxy. Trong diagram này, chúng ta có 4 thành phần chính:

  1. Subject: Đây là giao diện đại diện cho các real object và proxy. Nó định nghĩa các method mà cả RealSubject và proxy phải thực hiện.

  2. RealSubject: Đây là Object thực sự cần được truy cập. Nó cài đặt Subject Interface và thực hiện các Method thực sự cần thiết.

  3. Proxy: Đây là đối tượng kiểm soát quyền truy cập đến đối tượng RealSubject. Nó cũng cài đặt Subject Interface và chuyển tiếp các yêu cầu đến đối tượng RealSubject khi cần thiết.

  4. Client: Đây là Object tương tác với Proxy Object để truy cập RealSubject Object.

Trong ví dụ này, Client sẽ tương tác với Proxy thay vì tương tác trực tiếp với RealSubject. Đối tượng Proxy sẽ kiểm soát việc truy cập đối tượng RealSubject và đảm nhận nhiệm vụ chuyển tiếp các yêu cầu từ Client đến RealSubject khi thỏa mãn các điều kiện (ví dụ, kiểm tra quyền truy cập, tạo đối tượng, kích hoạt caching, v.v.).

Sử dụng mẫu thiết kế proxy, chúng ta có thể kiểm soát quyền truy cập, thêm chức năng mới, cải thiện hiệu suất và tận dụng tài nguyên một cách hiệu quả hơn trong quá trình phát triển ứng dụng.

Transactions

Để tạo một proxy, chúng ta tạo một Object thực hiện cùng một interface với subject và chứa một reference đến RealSubject đó.

Sau đó, chúng ta có thể sử dụng proxy thay cho Subject.

Trong Spring, các bean được tạo proxy để kiểm soát việc truy cập đến bean đích. Chúng ta thấy method này khi sử dụng transactions:

@Service
public class BookManager {
    
    @Autowired
    private BookRepository repository;

    @Transactional
    public Book create(String author) {
        System.out.println(repository.getClass().getName());
        return repository.create(author);
    }
}

Trong lớp BookManager, chúng ta chú thích phương thức create với annotation @Transactional. Annotation này hướng dẫn Spring thực thi phương thức create một cách nguyên tử (atomic). Nếu không có proxy, Spring sẽ không thể kiểm soát việc truy cập vào bean BookRepository và đảm bảo tính nhất quán của giao dịch.

Khi chúng ta sử dụng @Transactional trong ứng dụng Spring, nó sẽ tạo một proxy cho bean BookRepository, sau đó Spring sử dụng đối tượng proxy này để cung cấp quyền truy cập vào bean gốc và đảm bảo tính nhất quán giao dịch. Proxy sẽ quản lý việc bắt đầu, kết thúc, và xử lý các giao dịch dựa trên các điều kiện, và khi cần thiết, chuyển tiếp yêu cầu đến bean gốc.

Mẫu thiết kế proxy trong Spring giúp cung cấp khả năng kiểm soát ngữ cảnh giao dịch trong ứng dụng, tối ưu hóa mã nguồn và cải thiện hiệu suất.

CGLib Proxies

Thay vào đó, Spring tạo một proxy bao gồm bean BookRepository của chúng ta và cài đặt bean này để thực thi phương thức create một cách nguyên tử.

Khi chúng ta gọi phương thức BookManager#create, chúng ta có thể thấy đầu ra:

com.baeldung.patterns.proxy.BookRepository$$EnhancerBySpringCGLIB$$3dc2b55c

Thông thường, chúng ta sẽ mong đợi thấy ID đối tượng BookRepository chuẩn; thay vào đó, chúng ta thấy ID đối tượng EnhancerBySpringCGLIB.

Ở bên dưới, Spring đã đóng gói đối tượng BookRepository của chúng ta bên trong đối tượng EnhancerBySpringCGLIB. Do đó, Spring kiểm soát việc truy cập vào đối tượng BookRepository của chúng ta (đảm bảo tính nhất quán giao dịch).

Việc đóng gói đối tượng BookRepository vào trong đối tượng proxy cho phép Spring điều khiển và quản lý việc truy cập sau này. Bằng cách này, Spring có thể giám sát và thực thi chính sách giao dịch trên bean BookRepository và đảm bảo sự nhất quán, an toàn và hiệu quả cho ứng dụng.

Thông thường, Spring sử dụng hai loại proxy:

  1. CGLib Proxies - Được sử dụng khi tạo proxy cho các class

  2. JDK Dynamic Proxies - Được sử dụng khi tạo proxy cho các interfaces

Trong khi chúng ta sử dụng transactions để giới thiệu các proxy bên dưới, Spring sẽ sử dụng proxies cho bất kỳ tình huống nào mà nó cần kiểm soát việc truy cập vào một bean.

Template Method Pattern

Trong nhiều framework, một phần đáng kể của mã nguồn là boilerplate code (mã lặp lại nhiều lần).

Chẳng hạn, khi thực hiện một truy vấn trên cơ sở dữ liệu, cùng một chuỗi các bước phải được hoàn thành:

  1. Thiết lập kết nối

  2. Thực thi truy vấn

  3. Thực hiện dọn dẹp

  4. Đóng kết nối

Những bước này tạo ra tình huống lý tưởng để áp dụng mẫu Template Method Pattern. Mẫu thiết kế này bao gồm một lớp trừu tượng (abstract class) định nghĩa một phương thức "template" bao gồm tất cả các bước để thực hiện một hoạt động. Các lớp con (subclass) có thể ghi đè một số phần của phương thức "template" để thay đổi cách thức hoạt động, nhưng vẫn giữ nguyên các bước đã đề cập ở trên.

Việc sử dụng Template Method Pattern giúp giảm bớt việc lặp lại mã nguồn và đảm bảo việc tuân thủ quy trình thực hiện, đồng thời cho phép mỗi lớp con tùy chỉnh cách thức hoạt động của phương thức "template" mà không làm ảnh hưởng đến quy trình cơ bản.

Templates & Callbacks

Mẫu template method pattern là một kỹ thuật định nghĩa các bước cần thiết cho một hành động nào đó, thực hiện các bước boilerplate và để các bước có thể tùy chỉnh thành abstract (trừu tượng). Sau đó, các lớp con (subclass) có thể thực thi lớp abstract này và cung cấp một cách triển khai cụ thể cho các bước còn thiếu. Chúng ta có thể tạo một template trong trường hợp truy vấn cơ sở dữ liệu của chúng ta:

public abstract DatabaseQuery {

    public void execute() {
        Connection connection = createConnection();
        executeQuery(connection);
        closeConnection(connection);
    } 

    protected Connection createConnection() {
        // Connect to database...
    }

    protected void closeConnection(Connection connection) {
        // Close connection...
    }

    protected abstract void executeQuery(Connection connection);
}

Một cách khác, chúng ta có thể cung cấp bước thiếu bằng cách sử dụng một phương thức callback.

Phương thức callback là một phương thức cho phép subject thông báo cho client rằng một hành động mong muốn nào đó đã hoàn thành.

Trong một số trường hợp, đối tượng có thể sử dụng callback này để thực hiện các hành động, chẳng hạn như ánh xạ kết quả.

Cách tiếp cận này giúp giảm đi lượng mã lặp lại, tối ưu hóa quá trình thực hiện và cho phép tùy chỉnh một số bước mà không làm thay đổi cấu trúc cơ bản của mẫu thiết kế. Tùy theo yêu cầu của ứng dụng, chúng ta có thể lựa chọn cách thức triển khai tương ứng để đạt hiệu quả tốt nhất.

Ví dụ, thay vì có một executeQuery method, chúng ta có thể cung cấp cho execute method một query string và một callback method để xử lý kết quả.

Đầu tiên, chúng ta tạo callback method nhận Results Object và Maps nó thành Object of type T:

public interface ResultsMapper<T> {
    public T map(Results results);
}

Tiếp theo, chúng ta thay đổi lớp DatabaseQuery của chúng ta để sử dụng callback này:

public abstract DatabaseQuery {

    public <T> T execute(String query, ResultsMapper<T> mapper) {
        Connection connection = createConnection();
        Results results = executeQuery(connection, query);
        closeConnection(connection);
        return mapper.map(results);
    ]

    protected Results executeQuery(Connection connection, String query) {
        // Perform query...
    }
}

Cơ chế callback này chính xác là cách tiếp cận mà Spring sử dụng với lớp JdbcTemplate. JdbcTemplate là một lớp tiện ích cung cấp một khung chức năng để truy vấn cơ sở dữ liệu và ánh xạ kết quả truy vấn thành các đối tượng Java. JdbcTemplate giúp giảm bớt boilerplate code và làm cho việc xử lý truy vấn cơ sở dữ liệu trở nên dễ dàng hơn, mang lại hiệu suất cao hơn và rút ngắn thời gian phát triển.

JdbcTemplate

The JdbcTemplate class provides the query method, which accepts a query String and ResultSetExtractor object:

public class JdbcTemplate {

    public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
        // Execute query...
    }

    // Other methods...
}

The ResultSetExtractor converts the ResultSet object — representing the result of the query — into a domain object of type T:

@FunctionalInterface
public interface ResultSetExtractor<T> {
    T extractData(ResultSet rs) throws SQLException, DataAccessException;
}

Spring tiếp tục giảm bớt mã boilerplate bằng cách tạo ra các callback inteface cụ thể hơn.

Ví dụ, giao diện RowMapper được sử dụng để chuyển đổi một hàng dữ liệu SQL thành một domain object of type T.

@FunctionalInterface
public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

Để điều chỉnh giao diện RowMapper với ResultSetExtractor mong đợi, Spring tạo ra lớp RowMapperResultSetExtractor:

public class JdbcTemplate {

    public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
    }

    // Other methods...
}

Thay vì cung cấp logic để chuyển đổi toàn bộ đối tượng ResultSet, bao gồm cả lặp qua các hàng, chúng ta có thể cung cấp logic để chuyển đổi một hàng riêng lẻ:

public class BookRowMapper implements RowMapper<Book> {

    @Override
    public Book mapRow(ResultSet rs, int rowNum) throws SQLException {

        Book book = new Book();
        
        book.setId(rs.getLong("id"));
        book.setTitle(rs.getString("title"));
        book.setAuthor(rs.getString("author"));
        
        return book;
    }
}

Với bộ chuyển đổi này, chúng ta sau đó có thể truy vấn cơ sở dữ liệu bằng cách sử dụng JdbcTemplate và ánh xạ mỗi hàng kết quả:

JdbcTemplate template = // create template...
template.query("SELECT * FROM books", new BookRowMapper());

Ngoài việc quản lý cơ sở dữ liệu JDBC, Spring cũng sử dụng các template cho:

  • Java Message Service (JMS)

  • Java Persistence API (JPA)

  • Hibernate (hiện đã bị khai tử)

  • Giao dịch (Transactions)

Last updated