Design Patterns in the Spring Framework – Spring框架中的设计模式

最后修改: 2020年 2月 16日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

Design patterns are an essential part of software development. These solutions not only solve recurring problems but also help developers understand the design of a framework by recognizing common patterns.

设计模式是软件开发的一个重要组成部分。这些方案不仅可以解决反复出现的问题,还可以帮助开发人员通过识别常见的模式来理解框架的设计。

In this tutorial, we’ll look at four of the most common design patterns used in the Spring Framework:

在本教程中,我们将看一下Spring框架中最常用的四种设计模式。

  1. Singleton pattern
  2. Factory Method pattern
  3. Proxy pattern
  4. Template pattern

We’ll also look at how Spring uses these patterns to reduce the burden on developers and help users quickly perform tedious tasks.

我们还将研究Spring如何利用这些模式来减轻开发者的负担,帮助用户快速完成繁琐的任务。

2. Singleton Pattern

2.单子模式

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

singleton 模式是一种确保每个应用程序只存在一个对象实例的机制。这种模式在管理共享资源或提供跨领域的服务(如日志)时可能很有用。

2.1. Singleton Beans

2.1.单子Bean

Generally, a singleton is globally unique for an application, but in Spring, this constraint is relaxed. Instead, Spring restricts a singleton to one object per Spring IoC container. In practice, this means Spring will only create one bean for each type per application context.

一般来说,单子对于一个应用程序来说是全局唯一的,但是在Spring中,这种约束被放松了。相反,Spring限制每个Spring IoC容器只有一个对象。在实践中,这意味着Spring将只为每个应用上下文的每种类型创建一个Bean。

Spring’s approach differs from the strict definition of a singleton since an application can have more than one Spring container. Therefore, multiple objects of the same class can exist in a single application if we have multiple containers.

Spring的方法与单体的严格定义不同,因为一个应用程序可以有一个以上的Spring容器。因此,如果我们有多个容器,同一个类的多个对象可以存在于一个应用程序中。

 

 

By default, Spring creates all beans as singletons.

默认情况下,Spring将所有Bean创建为singletons.

2.2. Autowired Singletons

2.2.自动连接的单体

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

例如,我们可以在一个应用上下文中创建两个控制器,并向每个控制器注入相同类型的Bean。

First, we create a BookRepository that manages our Book domain objects.

首先,我们创建一个BookRepository来管理我们的Book域对象。

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

接下来,我们创建LibraryController,它使用BookRepository来返回图书馆中的书籍数量。

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

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

Lastly, we create a BookController, which focuses on Book-specific actions, such as finding a book by its ID:

最后,我们创建一个BookController,它专注于Book特定的动作,例如通过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 this application and perform a GET on /count and /book/1:

然后我们启动这个应用程序,对/count/book/1:执行GET操作。

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

In the application output, we see that both BookRepository objects have the same object ID:

在应用程序的输出中,我们看到两个BookRepository对象有相同的对象ID。

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

The BookRepository object IDs in the LibraryController and BookController are the same, proving that Spring injected the same bean into both controllers.

BookRepository对象ID在LibraryControllerBookController中是相同的,证明Spring在两个控制器中注入了相同的bean。

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.

我们可以通过将bean scopesingleton改为prototype,使用@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)annotation来创建BookRepository Bean的独立实例。

Doing so instructs Spring to create separate objects for each of the BookRepository beans it creates. Therefore, if we inspect the object ID of the BookRepository in each of our controllers again, we see that they are no longer the same.

这样做指示Spring为它创建的每个BookRepositoryBean创建单独的对象。因此,如果我们再次检查每个控制器中BookRepository的对象ID,我们会发现它们不再是同一个。

3. Factory Method Pattern

3.工厂方法模式

The factory method pattern entails a factory class with an abstract method for creating the desired object.

工厂方法模式需要一个带有抽象方法的工厂类来创建所需对象。

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

通常情况下,我们希望根据特定的环境来创建不同的对象。

For example, our application may require a vehicle object. In a nautical environment, we want to create boats, but in an aerospace environment, we want to create airplanes:

例如,我们的应用可能需要一个车辆对象。在航海环境中,我们想创建船只,但在航空航天环境中,我们想创建飞机。

 

 

To accomplish 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

3.1.应用背景

Spring uses this technique at the root of its Dependency Injection (DI) framework.

Spring在其依赖注入(DI)框架的基础上使用了这种技术。

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

基本上,Spring将一个bean容器视为生产bean的工厂。

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

因此,Spring定义了BeanFactory接口作为Bean容器的抽象。

public interface BeanFactory {

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

    // ...
]

Each of the getBean methods is considered a factory method, which returns a bean matching the criteria supplied to the method, like the bean’s type and name.

每个getBean方法都被认为是一个工厂方法,它返回一个与提供给该方法的条件相匹配的Bean,如Bean的类型和名称。

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

然后,Spring用ApplicationContext接口扩展了BeanFactory,这引入了额外的应用程序配置。Spring使用这种配置,根据一些外部配置(如XML文件或Java注释)来启动一个Bean容器。

Using the ApplicationContext class implementations like AnnotationConfigApplicationContext, we can then create beans through the various factory methods inherited from the BeanFactory interface.

使用ApplicationContext类的实现,如AnnotationConfigApplicationContext,我们就可以通过从BeanFactory接口继承的各种工厂方法创建Bean。

First, we create a simple application configuration:

首先,我们创建一个简单的应用程序配置。

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

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

接下来,我们创建一个简单的类,Foo,它不接受构造函数参数。

@Component
public class Foo {
}

Then create another class, Bar, that accepts a single constructor argument:

然后创建另一个类,Bar,它接受一个构造函数参数。

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

Lastly, we create our beans through the AnnotationConfigApplicationContext implementation of ApplicationContext:

最后,我们通过AnnotationConfigApplicationContext实现ApplicationContext来创建我们的bean。

@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 configured beans using just the class type and — in the case of Bar — constructor parameters.

使用getBean工厂方法,我们可以只使用类的类型和–在Bar的情况下–构造函数参数来创建配置的Bean。

3.2. External Configuration

3.2.外部配置

This pattern is versatile because we can completely change the application’s behavior based on external configuration.

这种模式是通用的,因为我们可以根据外部配置完全改变应用程序的行为。

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

如果我们希望改变应用程序中自动连接对象的实现,我们可以调整我们使用的ApplicationContext实现。

 

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

例如,我们可以将AnnotationConfigApplicationContext改为基于XML的配置类,例如ClassPathXmlApplicationContext

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { 

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

4. Proxy Pattern

4.代理模式

Proxies are a handy tool in our digital world, and we use them very often outside of software (such as network proxies). In code, the proxy pattern is a technique that allows one object — the proxy — to control access to another object — the subject or service.

在我们的数字世界中,代理是一种方便的工具,我们在软件之外也经常使用它们(如网络代理)。在代码中,代理模式是一种技术,允许一个对象–代理–控制对另一个对象–主体或服务的访问

 

 

4.1. Transactions

4.1.事务

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

为了创建一个代理,我们创建一个与我们的主体实现相同接口的对象,并包含对主体的引用。

We can then use the proxy in place of the subject.

然后我们就可以用代理来代替主体了。

In Spring, beans are proxied to control access to the underlying bean. We see this approach when using transactions:

在Spring中,Bean被代理以控制对底层Bean的访问。我们在使用事务时看到这种方法。

@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 atomically execute our create method. Without a proxy, Spring wouldn’t be able to control access to our BookRepository bean and ensure its transactional consistency.

在我们的BookManager类中,我们用@Transactional注解来注释create方法。这个注解指示Spring以原子方式执行我们的create方法。如果没有代理,Spring将无法控制对BookRepository Bean的访问并确保其事务性的一致性。

4.2. CGLib Proxies

4.2 CGLib 代理人

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

相反,Spring创建了一个代理,包裹了我们的BookRepository bean,并利用我们的bean来原子化地执行我们的create方法。

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

当我们调用我们的BookManager#create方法,我们可以看到输出。

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

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

通常,我们希望看到一个标准的BookRepository对象ID;相反,我们看到一个EnhancerBySpringCGLIB对象ID。

Behind the scenes, Spring has wrapped our BookRepository object inside as EnhancerBySpringCGLIB object. Spring thus controls access to our BookRepository object (ensuring transactional consistency).

在幕后,Spring已经将我们的BookRepository对象包装成EnhancerBySpringCGLIB对象。因此,Spring控制了对我们的BookRepository对象的访问(确保事务性的一致性)。

Generally, Spring uses two types of proxies:

一般来说,Spring使用两种类型的代理机构

  1. CGLib Proxies – Used when proxying classes
  2. JDK Dynamic Proxies – Used when proxying interfaces

While we used transactions to expose the underlying proxies, Spring will use proxies for any scenario in which it must control access to a bean.

虽然我们使用事务来暴露底层代理,Spring将在任何必须控制对Bean的访问的情况下使用代理

5. Template Method Pattern

模板方法模式

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

在许多框架中,相当一部分的代码是模板代码。

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

例如,在数据库上执行查询时,必须完成同样的一系列步骤。

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

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

这些步骤是模板方法模式的一个理想场景。

5.1. Templates & Callbacks

5.1.模板和回调

The template method pattern is a technique that defines the steps required for some action, implementing the boilerplate steps, and leaving the customizable steps as abstract. Subclasses can then implement this abstract class and provide a concrete implementation for the missing steps.

模板方法模式是一种技术,它定义了一些动作所需的步骤,实现了模板步骤,而将可定制的步骤留作抽象的。然后,子类可以实现这个抽象类,并为缺少的步骤提供具体的实现。

We can create a template in the case of our 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 supplying a callback method.

另外,我们可以通过提供一个回调方法来提供缺少的步骤。

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

回调方法是一种允许主体向客户发出信号的方法,表明某种期望的行动已经完成

In some cases, the subject can use this callback to perform actions — such as mapping results.

在某些情况下,主体可以使用这个回调来执行行动–比如映射结果。

 

 

For example, instead of having an executeQuery method, we can supply the execute method a query string and a callback method to handle the results.

例如,我们可以为executeQuery方法提供一个查询字符串和一个处理结果的回调方法,而不是有一个execute方法。

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

首先,我们创建回调方法,该方法接收一个Results对象并将其映射到一个T类型的对象。

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

Then we change our DatabaseQuery class to utilize this callback:

然后我们改变我们的DatabaseQuery类来利用这个回调。

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 precisely the approach that Spring uses with the JdbcTemplate class.

这种回调机制正是Spring在JdbcTemplate类中使用的方法。

5.2. JdbcTemplate

5.2.JdbcTemplate

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

JdbcTemplate类提供了query方法,它接受一个查询StringResultSetExtractor对象。

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:

ResultSetExtractorResultSet对象(代表查询结果)转换为T类型的域对象。

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

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

Spring通过创建更具体的回调接口进一步减少了模板代码。

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

例如,RowMapper接口被用来将单行的SQL数据转换为T类型的域对象。

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

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

为了使RowMapper接口适应预期的ResultSetExtractor,Spring创建了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...
}

Instead of providing logic for converting an entire ResultSet object, including iteration over the rows, we can provide logic for how to convert a single row:

我们不需要提供转换整个ResultSet对象的逻辑,包括对行的迭代,而是可以提供如何转换单个行的逻辑。

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

With this converter, we can then query a database using the JdbcTemplate and map each resulting row:

有了这个转换器,我们就可以使用JdbcTemplate查询数据库,并映射每一条结果行。

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

Apart from JDBC database management, Spring also uses templates for:

除了JDBC数据库管理,Spring还使用模板来进行。

6. Conclusion

6.结论

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

在本教程中,我们看了Spring框架中应用的四个最常见的设计模式。

We also explored how Spring utilizes these patterns to provide rich features while reducing the burden on developers.

我们还探讨了Spring如何利用这些模式来提供丰富的功能,同时减轻开发者的负担。

The code from this article can be found over on GitHub.

本文的代码可以在GitHub上找到over