Автор оригинала: Justin Albano.
1. введение
Шаблоны проектирования являются неотъемлемой частью разработки программного обеспечения. Эти решения не только решают повторяющиеся проблемы, но и помогают разработчикам понять дизайн фреймворка, распознав общие шаблоны.
В этом уроке мы рассмотрим четыре наиболее распространенных шаблона проектирования, используемых в Spring Framework:
- Одноэлементный паттерн
- Шаблон заводского метода
- Шаблон прокси-сервера
- Шаблон шаблона
Мы также рассмотрим, как Spring использует эти шаблоны, чтобы снизить нагрузку на разработчиков и помочь пользователям быстро выполнять утомительные задачи.
2. Одноэлементный паттерн
Шаблон singleton – это механизм, который гарантирует, что для каждого приложения существует только один экземпляр объекта . Этот шаблон может быть полезен при управлении общими ресурсами или предоставлении сквозных услуг, таких как ведение журнала.
2.1. Одноэлементные бобы
Как правило, синглтон глобально уникален для приложения, но весной это ограничение ослабляется. Вместо этого Spring ограничивает синглтон одним объектом на контейнер Spring IoC|/. На практике это означает, что Spring создаст только один компонент для каждого типа в контексте приложения.
Подход Spring отличается от строгого определения синглтона, поскольку приложение может иметь более одного контейнера Spring. Таким образом, несколько объектов одного класса могут существовать в одном приложении, если у нас есть несколько контейнеров.
По умолчанию Spring создает все бобы как синглеты.
2.2. Автономные синглеты
Например, мы можем создать два контроллера в одном контексте приложения и ввести в каждый из них компонент одного и того же типа.
Во-первых, мы создаем репозиторий Book , который управляет нашими объектами Book домена.
Далее мы создаем Библиотечный контроллер , который использует Хранилище книг для возврата количества книг в библиотеке:
@RestController public class LibraryController { @Autowired private BookRepository repository; @GetMapping("/count") public Long findCount() { System.out.println(repository); return repository.count(); } }
Наконец, мы создаем BookController , который фокусируется на Book -конкретных действиях, таких как поиск книги по ее идентификатору:
@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 on /count и /book/1:
curl -X GET http://localhost:8080/count curl -X GET http://localhost:8080/book/1
В выходных данных приложения мы видим, что оба объекта Book Repository имеют один и тот же идентификатор объекта:
Идентификаторы объектов Хранилища книг в контроллере библиотеки и BookController одинаковы, что доказывает, что Spring ввел один и тот же компонент в оба контроллера.
Мы можем создать отдельные экземпляры репозитория Book bean, изменив область bean с singleton на prototype с помощью @ Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) |/аннотация .
Это позволяет Spring создавать отдельные объекты для каждого из создаваемых им компонентов Хранилища книг|/. Поэтому, если мы снова проверим идентификатор объекта Хранилища книг в каждом из наших контроллеров, мы увидим, что они больше не совпадают.
3. Шаблон Заводского метода
Шаблон метода фабрики влечет за собой класс фабрики с абстрактным методом для создания желаемого объекта.
Часто мы хотим создавать различные объекты на основе определенного контекста.
Например, нашему приложению может потребоваться объект транспортного средства. В морской среде мы хотим создавать лодки, но в аэрокосмической среде мы хотим создавать самолеты:
Для этого мы можем создать фабричную реализацию для каждого желаемого объекта и вернуть желаемый объект из метода конкретной фабрики.
3.1. Контекст приложения
Spring использует этот метод в корне своей структуры внедрения зависимостей (DI).
По сути, Spring рассматривает контейнер для бобов как фабрику, производящую бобы.
Таким образом, Spring определяет интерфейс BeanFactory как абстракцию контейнера бобов:
public interface BeanFactory { getBean(ClassrequiredType); getBean(Class requiredType, Object... args); getBean(String name); // ... ]
Каждый из методов getBean считается заводским методом , который возвращает компонент, соответствующий критериям, предоставленным методу, таким как тип и имя компонента.
Затем Spring расширяет BeanFactory с помощью интерфейса ApplicationContext , который вводит дополнительную конфигурацию приложения. Spring использует эту конфигурацию для запуска контейнера компонентов на основе некоторой внешней конфигурации, такой как XML-файл или аннотации Java.
Используя реализации класса ApplicationContext , такие как AnnotationConfigApplicationContext , мы можем затем создавать бобы с помощью различных заводских методов, унаследованных от интерфейса 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 ... }
Наконец, мы создаем наши бобы с помощью AnnotationConfigApplicationContext реализации ApplicationContext :
@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 factory, мы можем создавать настроенные компоненты, используя только тип класса и — в случае Bar — параметры конструктора.
3.2. Внешняя конфигурация
Этот шаблон универсален, потому что мы можем полностью изменить поведение приложения на основе внешней конфигурации.
Если мы хотим изменить реализацию объектов autowired в приложении, мы можем настроить реализацию 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. Сделки
Чтобы создать прокси-сервер, мы создаем объект, который реализует тот же интерфейс, что и наш субъект, и содержит ссылку на субъект.
Затем мы можем использовать прокси вместо субъекта.
Весной бобы проксируются для управления доступом к базовому бобу. Мы видим такой подход при использовании транзакций:
@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 атомарно выполнить наш метод create . Без прокси Spring не смог бы контролировать доступ к нашему хранилищу книг bean и обеспечивать его транзакционную согласованность.
4.2. Прокси CGLib
Вместо этого Spring создает прокси-сервер, который обертывает наш репозиторий книг bean и использует наш bean для выполнения вашего create метода атомарно.
Когда мы вызываем наш метод BookManager#create , мы видим результат:
com.baeldung.patterns.proxy.BookRepository$$EnhancerBySpringCGLIB$$3dc2b55c
Как правило, мы ожидаем увидеть стандартный Репозиторий книг идентификатор объекта; вместо этого мы видим EnhancerBySpringCGLIB идентификатор объекта.
За кулисами Spring завернул наш Книжный репозиторий объект внутрь как EnhancerBySpringCGLIB объект . Таким образом, Spring контролирует доступ к нашему Хранилищу книг объекту (обеспечивая согласованность транзакций).
Как правило, Spring использует два типа прокси :
- Прокси CGLib – Используется при проксировании классов
- Динамические прокси JDK – Используются при проксировании интерфейсов
В то время как мы использовали транзакции для раскрытия базовых прокси-серверов, Spring будет использовать прокси-серверы для любого сценария, в котором он должен контролировать доступ к компоненту .
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{ public T map(Results results); }
Затем мы меняем наш Запрос к базе данных класс, чтобы использовать этот обратный вызов:
public abstract DatabaseQuery { publicT execute(String query, ResultsMapper 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. JdbcTemplate
Класс JdbcTemplate предоставляет метод query , который принимает запрос String и ResultSetExtractor object:
public class JdbcTemplate { publicT query(final String sql, final ResultSetExtractor rse) throws DataAccessException { // Execute query... } // Other methods... }
ResultSetExtractor преобразует объект ResultSet , представляющий результат запроса, в объект домена типа T :
@FunctionalInterface public interface ResultSetExtractor{ T extractData(ResultSet rs) throws SQLException, DataAccessException; }
Spring еще больше сокращает шаблонный код, создавая более конкретные интерфейсы обратного вызова.
Например, интерфейс RowMapper используется для преобразования одной строки данных SQL в объект домена типа T .
@FunctionalInterface public interface RowMapper{ T mapRow(ResultSet rs, int rowNum) throws SQLException; }
Чтобы адаптировать интерфейс RowMapper к ожидаемому ResultSetExtractor , Spring создает класс RowMapperResultSetExtractor :
public class JdbcTemplate { publicList query(String sql, RowMapper rowMapper) throws DataAccessException { return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper))); } // Other methods... }
Вместо того, чтобы предоставлять логику для преобразования всего объекта ResultSet , включая итерацию по строкам, мы можем предоставить логику для преобразования одной строки:
public class BookRowMapper implements RowMapper{ @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 также использует шаблоны для:
- Служба сообщений Java (JMS)
- Java Persistence API (JPA)
- Hibernate (теперь устарел)
- Операции
6. Заключение
В этом уроке мы рассмотрели четыре наиболее распространенных шаблона проектирования, применяемых в Spring Framework.
Мы также изучили, как Spring использует эти шаблоны для предоставления богатых функций при одновременном снижении нагрузки на разработчиков.
Код из этой статьи можно найти на GitHub .