Design Patterns in Spring Framework 6

More Java / AI / Big Data Good Articles

In actual development work, we rely on other people's code in our work every day. Including the programming language you are using, the framework you are building, or some cutting-edge open source products.

They are all well done and really cool to use, but have you ever thought about implementing it yourself? Haha, there is a high probability that you have never thought about it, right?

If you have not tried to implement similar functions by yourself, or have dug deep into these excellent three-party frameworks, for technical people, there is actually a considerable risk implied.

If you're unlucky, when things fall apart in production, you have a production accident, and you have to debug the implementation of a third-party library you're not familiar with, it can be tricky to say the least. I have no clue, maybe I really want to resign on the spot...

Recommend a Lightrun, it is a new type of debugger.

It is specifically aimed at real-life production environments. With Lightrun, you can drill down to running applications, including third-party dependencies, as well as real-time logs, snapshots, and metrics.

This is not the point, the point is that we use Spring to talk about the design patterns of such a popular framework, so as to open your way to study the tripartite framework.

1 Introduction

Design patterns are an important part of software development. These solutions not only solve recurring problems, but also help developers understand the framework's design by identifying common patterns.

Next, we'll introduce the four most common design patterns used in the Spring framework:

  1. Singleton pattern Singleton pattern
  2. Factory Method pattern Factory method pattern
  3. Proxy pattern proxy mode
  4. Template pattern Template pattern

We'll also examine how Spring uses these patterns to ease the burden on developers and help users perform tedious tasks quickly.

2. Singleton Pattern

The singleton pattern is a mechanism that ensures that only one instance of an object exists per application. This mode is useful when managing shared resources or providing cross-cutting services such as logging.

2.1 Singleton Beans (Singleton Beans)

Typically, a singleton is globally unique to an application, but in Spring this constraint is relaxed. Instead, Spring limits a single instance to one object per Spring IoC container. In practice, this means that Spring will only create one bean of each type per application context.

Spring's approach differs from the strict definition of a singleton, since an application can have multiple Spring containers. So if we have multiple containers, multiple objects of the same class can exist in a single application.

Singleton

By default, Spring creates all beans as singletons.

2.2 Autowired Singletons

For example, we can create two controllers in a single application context and inject the same type of bean into each controller.

First, we create a BookRepository to manage our Book domain objects.

Next, we create the LibraryController, which uses the BookRepository to return the number of books in the library:

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

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

Finally, we create a BookController that focuses on book-specific operations, such as finding a book by 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();
    }
}

We then start the application and perform GET requests to /count and /book/1:

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

In the application output, we see two BookRepository objects with the same object ID:

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

The BookRepository object ID is the same in LibraryController and BookController, which proves that Spring injects the same bean into both controllers.

We can create separate instances of the BookRepository bean by changing the bean scope from singleton to prototype using the @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) annotation.

Doing so instructs Spring to create separate objects for each BookRepository bean it creates. So, if we check again the object id of BookRepository in each controller, we will find that they are no longer the same.

3. Factory Method Pattern

The factory method pattern requires a factory class that contains abstract methods for creating the required objects.

Often, we want to create different objects depending on a particular context.

For example, our application might require a vehicle object. In a nautical setting we want to make ships, but in an aerospace setting we want to make airplanes:

Factory pattern

To do this, we can create a factory implementation for each desired object and return the desired object from the concrete factory method.

3.1 Application Context

Spring uses this technique at the root of its dependency injection (DI) framework.

Fundamentally, Spring treats the bean container as a factory that produces beans.

Therefore, Spring defines the BeanFactory interface as an abstraction of the bean container:

public interface BeanFactory {
    
    

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

    // ...
}

Each getBean method is considered a factory method that returns a bean that matches the criteria supplied to the method, such as the bean's type and name.

Spring then extends BeanFactory with the ApplicationContext interface, which introduces additional application configuration. Spring uses this configuration to start the bean container based on some external configuration, such as an XML file or Java annotations.

Using the ApplicationContext class implementation (such as AnnotationConfigApplicationContext), we can create beans through various factory methods inherited from the BeanFactory interface.

First, we create a simple application configuration:

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

Next, we create a simple class Foo that takes no constructor arguments:

@Component
public class Foo {
    
    
}

Then create another class Bar that accepts a single constructor parameter:

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

Finally, we create our bean via the ApplicationContext's AnnotationConfigApplicationContext implementation:

@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));
}

Using the getBean factory method, we can create a configured bean using only the class type and (in Bar's case) the constructor parameters.

3.2 External Configuration

This pattern is generic because we can completely change the behavior of the application based on external configuration.

If we wish to change the implementation of the autowiring objects in our application, we can adjust the ApplicationContext implementation we use.

Factory 1

For example, we can change AnnotationConfigApplicationContext to an XML-based configuration class such as ClassPathXmlApplicationContext:

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() {
    
     

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

4. Proxy Pattern

Proxies are such a handy tool in our digital world that we often use them in addition to software such as web proxies. In code, the proxy pattern is a technique that allows one object (proxy) to control access to another object (principal or service).

Proxy class diagram

4.1 Transaction

To create a proxy, we create an object that implements the same interface as our subject and contains a reference to the subject.

Then, we can use the proxy instead of the subject.

In Spring, beans are proxied to control access to underlying beans. We see this approach when using 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);
    }
}

In our BookManager class, we annotate the create method with the @Transactional annotation. This annotation instructs Spring to execute our create method atomically. Without the proxy, Spring would have no way of controlling access to the BookRepository bean and ensuring its transactional consistency.

4.2 CGLib Proxies (CGLib Proxies)

Instead, Spring creates a proxy that wraps our BookRepository bean and detects our bean to execute our create method atomically.

When we call the BookManager#create method, we can see the output:

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

Normally, we expect to see a standard BookRepository object ID; instead, we see an EnhancerBySpringCGLIB object ID.

Under the hood, Spring wraps our BookRepository object inside as an EnhancerBySpringCGLIB object. Therefore, Spring controls access to the BookRepository object (ensuring transactional consistency).

Proxy

Generally, Spring uses two types of proxies:

  • CGLib proxy – used when proxying classes;
  • JDK dynamic proxy – used when proxying interfaces

Although we use transactions to expose the underlying proxy, Spring will use proxies in any scenario where access to beans must be controlled.

5. Template Method Pattern

In many frameworks, a large portion of the code is boilerplate.

For example, when executing a query on a database, the same series of steps must be completed:

  1. Establish a connection Establish a connection
  2. Execute query Execute query
  3. Perform cleanup to perform cleanup
  4. Close the connection Close the connection

These steps are an ideal scenario for the template method pattern.

5.1 Templates & Callbacks

The Template Method pattern is a technique for defining the steps required for some operation, implementing boilerplate steps, and leaving the customizable steps as abstractions. Subclasses can then implement this abstract class and provide concrete implementations for missing steps.

We can create a template in case of a database query:

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);
}

Alternatively, we can provide the missing step by providing a callback method.

A callback method is a method that allows the principal to signal to the client that some desired operation has completed.

In some cases, principals can use this callback to perform operations, such as mapping results.

Template

For example, instead of an executeQuery method, we could provide the execute method with a query string and a callback method to process the results.

First, we create a callback method that takes a Result object and maps it to an object of type T:

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

Then we change our DatabaseQuery class to take advantage of this callback:

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...
    }
}

This callback mechanism is exactly what Spring uses in the JdbcTemplate class.

5.2 JDBC Templates (JdbcTemplates)

The JdbcTemplate class provides a query method that accepts a query string and a ResultSetExtractor object:

public class JdbcTemplate {
    
    

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

    // Other methods...
}

ResultSetExtractor converts a ResultSet object (representing query results) into a domain object of type T:

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

Spring further reduces boilerplate code by creating more specific callback interfaces.

For example, the RowMapper interface is used to convert a single row of SQL data into a domain object of type T.

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

To adapt the RowMapper interface to the expected ResultSetExtractor, Spring created the RowMapperResultSetExtractor class:

public class JdbcTemplate {
    
    

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

    // Other methods...
}

Instead of providing logic for transforming the entire ResultSet object, we can provide logic how to transform individual rows, including iteration over rows:

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;
    }
}

Using this converter, we can query the database using the JdbcTemplate and map each result row:

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

In addition to JDBC database management, Spring also uses templates:

  • Java Message Service (JMS)
  • Java Persistence API (JPA)
  • Hibernate (now deprecated)
  • Transactions

6 Conclusion

Above, we looked at four of the most common design patterns applied in the Spring Framework.

We also explore how Spring leverages these patterns to provide rich functionality while reducing the burden on developers.

Guess you like

Origin blog.csdn.net/qq_41340258/article/details/131362049