1. Введение
Шаблоны проектирования являются неотъемлемой частью разработки программного обеспечения. Эти решения не только решают повторяющиеся проблемы, но и помогают разработчикам понять структуру фреймворка, распознавая общие шаблоны.
В этом руководстве мы рассмотрим четыре наиболее распространенных шаблона проектирования, используемых в Spring Framework:
- Одноэлементный шаблон
- Шаблон фабричного метода
- Шаблон прокси
- Шаблон шаблона
Мы также рассмотрим, как Spring использует эти шаблоны, чтобы уменьшить нагрузку на разработчиков и помочь пользователям быстрее выполнять утомительные задачи.
2. Одноэлементный шаблон
Шаблон singleton — это механизм, обеспечивающий существование только одного экземпляра объекта для каждого приложения . Этот шаблон может быть полезен при управлении общими ресурсами или предоставлении сквозных услуг, таких как ведение журнала.
2.1. Синглтон Бобы
Как правило, синглтон глобально уникален для приложения, но в Spring это ограничение ослаблено. Вместо этого Spring ограничивает синглтон одним объектом на контейнер Spring IoC . На практике это означает, что Spring будет создавать только один bean-компонент для каждого типа для каждого контекста приложения.
Подход Spring отличается от строгого определения синглтона, поскольку приложение может иметь более одного контейнера Spring. Следовательно, несколько объектов одного класса могут существовать в одном приложении, если у нас есть несколько контейнеров.
По умолчанию 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. Шаблон фабричного метода
Шаблон фабричного метода влечет за собой фабричный класс с абстрактным методом для создания желаемого объекта.
Часто мы хотим создавать разные объекты на основе определенного контекста.
Например, нашему приложению может потребоваться объект транспортного средства. В морской среде мы хотим создавать лодки, а в аэрокосмической среде мы хотим создавать самолеты:
Для этого мы можем создать фабричную реализацию для каждого желаемого объекта и вернуть желаемый объект из конкретного фабричного метода.
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 .
Например, мы можем изменить AnnotationConfigApplicationContext
на класс конфигурации на основе XML, такой как ClassPathXmlApplicationContext
:
@Test
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() {
String expectedName = "Some name";
ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
// Same test as before ...
}
4. Шаблон прокси
Прокси — удобный инструмент в нашем цифровом мире, и мы очень часто используем их вне программного обеспечения (например, сетевых прокси). В коде паттерн прокси — это техника, которая позволяет одному объекту — прокси — управлять доступом к другому объекту — субъекту или сервису .
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
(обеспечивая согласованность транзакций).
Как правило, Spring использует два типа прокси :
- CGLib Proxies — используется при проксировании классов
- Динамические прокси JDK — используются при проксировании интерфейсов.
В то время как мы использовали транзакции для предоставления базовых прокси-серверов, Spring будет использовать прокси-серверы для любого сценария, в котором он должен контролировать доступ к bean-компоненту .
5. Шаблон метода шаблона
Во многих фреймворках значительная часть кода представляет собой шаблонный код.
Например, при выполнении запроса к базе данных необходимо выполнить тот же ряд шагов:
- Установить соединение
- Выполнить запрос
- Выполнить очистку
- Закрыть соединение
Эти шаги являются идеальным сценарием для паттерна шаблонного метода .
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);
}
В качестве альтернативы мы можем предоставить недостающий шаг, предоставив метод обратного вызова.
Метод обратного вызова — это метод, который позволяет субъекту сигнализировать клиенту о том, что какое-то желаемое действие выполнено .
В некоторых случаях субъект может использовать этот обратный вызов для выполнения действий, таких как сопоставление результатов.
Например, вместо метода 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 .