«1. Введение

Шаблоны проектирования являются неотъемлемой частью разработки программного обеспечения. Эти решения не только решают повторяющиеся проблемы, но и помогают разработчикам понять структуру фреймворка, распознавая общие шаблоны.

В этом руководстве мы рассмотрим четыре наиболее распространенных шаблона проектирования, используемых в Spring Framework:

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

Мы также рассмотрим, как Spring использует эти шаблоны, чтобы уменьшить нагрузку на разработчиков и помочь пользователям быстро выполнять утомительные задачи.

2. Одноэлементный шаблон

Одноэлементный шаблон — это механизм, обеспечивающий существование только одного экземпляра объекта для каждого приложения. Этот шаблон может быть полезен при управлении общими ресурсами или предоставлении сквозных услуг, таких как ведение журнала.

2.1. Singleton Beans

Как правило, синглтон глобально уникален для приложения, но в Spring это ограничение ослаблено. Вместо этого Spring ограничивает синглтон одним объектом на контейнер Spring IoC. На практике это означает, что Spring будет создавать только один bean-компонент для каждого типа для каждого контекста приложения.

Подход Spring отличается от строгого определения синглтона, поскольку приложение может иметь более одного контейнера Spring. Следовательно, несколько объектов одного класса могут существовать в одном приложении, если у нас есть несколько контейнеров.

По умолчанию Spring создает все bean-компоненты как синглтоны.

2.2. Autowired Singletons

Например, мы можем создать два контроллера в одном контексте приложения и внедрить в каждый bean-компонент одного типа.

Сначала мы создаем BookRepository, который управляет нашими объектами домена Book.

Затем мы создаем LibraryController, который использует BookRepository для возврата количества книг в библиотеке:

Наконец, мы создаем BookController, который фокусируется на действиях, связанных с книгой, таких как поиск книги по его идентификатор:

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

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

Затем мы запускаем это приложение и выполняем GET для /count и /book/1:

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

В выводе приложения мы видим, что оба объекта BookRepository имеют одинаковый идентификатор объекта:

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

Идентификаторы объектов BookRepository в LibraryController и BookController совпадают, что доказывает, что Spring внедрил один и тот же bean-компонент в оба контроллера.

[email protected]
[email protected]

Мы можем создать отдельные экземпляры компонента BookRepository, изменив область действия компонента с singleton на прототип с помощью аннотации @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE).

Это указывает Spring создавать отдельные объекты для каждого из создаваемых bean-компонентов BookRepository. Поэтому, если мы снова проверим идентификатор объекта BookRepository в каждом из наших контроллеров, мы увидим, что они уже не совпадают.

3. Шаблон фабричного метода

Шаблон фабричного метода влечет за собой класс фабрики с абстрактным методом для создания желаемого объекта.

Часто нам нужно создавать разные объекты на основе определенного контекста.

Например, нашему приложению может потребоваться объект транспортного средства. В морской среде мы хотим создавать лодки, но в аэрокосмической среде мы хотим создавать самолеты:

Для этого мы можем создать фабричную реализацию для каждого желаемого объекта и вернуть желаемое объект из метода конкретной фабрики.

3.1. Контекст приложения


Spring использует этот метод в основе своей инфраструктуры внедрения зависимостей (DI).

По сути, Spring рассматривает контейнер для бобов как фабрику по производству бобов.

Таким образом, Spring определяет интерфейс BeanFactory как абстракцию контейнера компонентов:

Каждый из методов getBean считается фабричным методом, который возвращает компонент, соответствующий критериям, предоставленным методу, например тип и название.

Затем Spring расширяет BeanFactory интерфейсом ApplicationContext, который вводит дополнительную конфигурацию приложения. Spring использует эту конфигурацию для запуска контейнера компонентов на основе некоторой внешней конфигурации, такой как файл XML или аннотации Java.

public interface BeanFactory {

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

    // ...
]

«Используя реализации класса ApplicationContext, такие как AnnotationConfigApplicationContext, мы можем затем создавать bean-компоненты с помощью различных фабричных методов, унаследованных от интерфейса BeanFactory.

Сначала мы создаем простую конфигурацию приложения:

Затем мы создаем простой класс Foo, не принимающий аргументов конструктора:

Затем создаем еще один класс Bar, который принимает единственный аргумент конструктора:

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

Наконец, мы создаем наши компоненты с помощью реализации ApplicationContext в AnnotationConfigApplicationContext:

@Component
public class Foo {
}

Используя фабричный метод getBean, мы можем создавать сконфигурированные компоненты, используя только тип класса и — в случай Bar — параметры конструктора.

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

3.2. Внешняя конфигурация

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

Этот шаблон универсален, поскольку мы можем полностью изменить поведение приложения на основе внешней конфигурации.

Если мы хотим изменить реализацию автосвязанных объектов в приложении, мы можем настроить используемую нами реализацию ApplicationContext.

Например, мы можем изменить AnnotationConfigApplicationContext на класс конфигурации на основе XML, такой как ClassPathXmlApplicationContext:

4. Шаблон прокси

Прокси — удобный инструмент в нашем цифровом мире, и мы используем их очень часто вне программного обеспечения (например, сетевых прокси). В коде шаблон прокси — это метод, который позволяет одному объекту — прокси — управлять доступом к другому объекту — субъекту или службе.

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { 

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

4.1. Транзакции

Чтобы создать прокси, мы создаем объект, который реализует тот же интерфейс, что и наш субъект, и содержит ссылку на субъект.

Затем мы можем использовать прокси вместо субъекта.

В Spring bean-компоненты проксируются для управления доступом к базовому bean-компоненту. Мы видим этот подход при использовании транзакций:

В нашем классе BookManager мы аннотируем метод create аннотацией @Transactional. Эта аннотация указывает Spring атомарно выполнить наш метод создания. Без прокси Spring не смог бы контролировать доступ к нашему bean-компоненту BookRepository и обеспечивать согласованность его транзакций.

4.2. CGLib Proxies

Вместо этого Spring создает прокси, который обертывает наш bean-компонент BookRepository и настраивает наш bean-компонент для атомарного выполнения нашего метода create.

Когда мы вызываем наш метод BookManager#create, мы видим результат:

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

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

Обычно мы ожидаем увидеть стандартный идентификатор объекта BookRepository; вместо этого мы видим идентификатор объекта EnhancerBySpringCGLIB.

За кулисами Spring обернул наш объект BookRepository внутри как объект EnhancerBySpringCGLIB. Таким образом, Spring контролирует доступ к нашему объекту BookRepository (обеспечивая согласованность транзакций).

Как правило, Spring использует два типа прокси:

Пока мы использовали транзакции для предоставления базовых прокси, Spring будет использовать прокси для любого сценария, в котором он должен контролировать доступ к bean-компоненту.

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

5. Шаблон шаблонного метода

Во многих фреймворках значительная часть кода представляет собой шаблонный код.

Например, при выполнении запроса к базе данных необходимо выполнить ту же последовательность шагов:

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

Эти шаги являются идеальным сценарием для шаблона метода шаблона.

5.1. Шаблоны и обратные вызовы

Шаблон метода шаблона — это метод, который определяет шаги, необходимые для некоторого действия, реализует стандартные шаги и оставляет настраиваемые шаги абстрактными. Затем подклассы могут реализовать этот абстрактный класс и предоставить конкретную реализацию недостающих шагов.

Мы можем создать шаблон для нашего запроса к базе данных:

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

В качестве альтернативы мы можем добавить недостающий шаг, указав метод обратного вызова.

Метод обратного вызова — это метод, который позволяет субъекту сигнализировать клиенту о завершении некоторого желаемого действия.

В некоторых случаях субъект может использовать этот обратный вызов для выполнения действий, например отображения результатов.

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

«Например, вместо метода executeQuery мы можем предоставить методу execute строку запроса и метод обратного вызова для обработки результатов.

Сначала мы создаем метод обратного вызова, который принимает объект Results и сопоставляет его с объектом типа T:

Затем мы меняем наш класс DatabaseQuery, чтобы использовать этот обратный вызов:

Этот обратный вызов Механизм — это именно тот подход, который Spring использует с классом JdbcTemplate.

5.2. JdbcTemplate

Класс JdbcTemplate предоставляет метод запроса, который принимает строку запроса и объект ResultSetExtractor:

ResultSetExtractor преобразует объект ResultSet, представляющий результат запроса, в объект домена type T:

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

Spring еще больше сокращает шаблонный код, создавая более конкретные интерфейсы обратного вызова.

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

Например, интерфейс RowMapper используется для преобразования одной строки данных SQL в объект домена типа T.

Чтобы адаптировать интерфейс RowMapper к ожидаемому ResultSetExtractor, Spring создает класс RowMapperResultSetExtractor: ~ ~~

Вместо предоставления логики для преобразования всего объекта ResultSet, включая итерацию по строкам, мы можем предоставить логику для преобразования одной строки:

public class JdbcTemplate {

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

    // Other methods...
}

С помощью этого преобразователя мы можем затем запрашивать базу данных с помощью JdbcTemplate и сопоставьте каждую результирующую строку:

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

Помимо управления базой данных JDBC, Spring также использует шаблоны для:

Java Message Service (JMS) Java Persistence API (JPA) Hibernate (сейчас устарел) Transactions ~~ ~ 6. Заключение

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

В этом руководстве мы рассмотрели четыре наиболее распространенных шаблона проектирования, применяемых в Spring Framework.

public class JdbcTemplate {

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

    // Other methods...
}

Мы также изучили, как Spring использует эти шаблоны для предоставления расширенных функций при одновременном снижении нагрузки на разработчиков.

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

Код из этой статьи можно найти на GitHub.

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

«

6. Conclusion

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

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

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