Перейти к основному содержимому

Шаблоны проектирования в Spring Framework

· 9 мин. чтения

1. Введение

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

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

  1. Одноэлементный шаблон
  2. Шаблон фабричного метода
  3. Шаблон прокси
  4. Шаблон шаблона

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

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

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

2.1. Синглтон Бобы

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

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

./f52dcb6d83d06f42468ac32dffcf667f.png

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

2.2. Автопроводные синглтоны

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

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

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

@RestController
public class LibraryController {

@Autowired
private BookRepository repository;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

./2206d5cf7a64e65460e0accf848145ae.png

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

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

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

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

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

public interface BeanFactory {

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

// ...
]

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

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

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

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

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

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

@Component
public class Foo {
}

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

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Bar {

private String name;

public Bar(String name) {
this.name = name;
}

// Getter ...
}

Наконец, мы создаем наши bean-компоненты через реализацию ApplicationContext AnnotationConfigApplicationContext : ``

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

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

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

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

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

./17e0b7ff11d19a50239051d9fcfcd56d.png

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

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() {

String expectedName = "Some name";
ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");

// Same test as before ...
}

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

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

./43003a763c51ed67b8aa782b3ef0a3ea.png

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

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

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

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

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

4.2. Прокси CGLib

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

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

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

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

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

./ab1bc224100831ddcbd1bc7532f6cb69.png

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

  1. CGLib Proxies — используется при проксировании классов
  2. Динамические прокси JDK — используются при проксировании интерфейсов.

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

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

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

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

  1. Установить соединение
  2. Выполнить запрос
  3. Выполнить очистку
  4. Закрыть соединение

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

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

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

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

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

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

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

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

./c8928e7a3131ead2f1c41e33c0cbc15c.png

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

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

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

Затем мы меняем наш класс 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...
}
}

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

5.2. JdbcШаблон

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

public class JdbcTemplate {

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

// Other methods...
}

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

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

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

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

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

Чтобы адаптировать интерфейс 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...
}

Вместо предоставления логики для преобразования всего объекта 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;
}
}

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

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

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

6. Заключение

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

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

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