Modèles de conception dans Spring Framework

1. Introduction

Les modèles de conception sont une partie essentielle du développement logiciel. Ces solutions résolvent non seulement les problèmes récurrents, mais aident également les développeurs à comprendre la conception d'un framework en reconnaissant des modèles communs.

Dans ce didacticiel, nous examinerons quatre des modèles de conception les plus courants utilisés dans Spring Framework:

  1. Motif singleton
  2. Modèle de méthode d'usine
  3. Modèle proxy
  4. Modèle de modèle

Nous verrons également comment Spring utilise ces modèles pour réduire le fardeau des développeurs et aider les utilisateurs à effectuer rapidement des tâches fastidieuses.

2. Motif singleton

Le modèle de singleton est un mécanisme qui garantit qu'une seule instance d'un objet existe par application . Ce modèle peut être utile lors de la gestion de ressources partagées ou de la fourniture de services transversaux, tels que la journalisation.

2.1. Haricots Singleton

Généralement, un singleton est globalement unique pour une application, mais dans Spring, cette contrainte est assouplie. Au lieu de cela, Spring limite un singleton à un objet par conteneur Spring IoC . En pratique, cela signifie que Spring ne créera qu'un seul bean pour chaque type par contexte d'application.

L'approche de Spring diffère de la définition stricte d'un singleton car une application peut avoir plusieurs conteneurs Spring. Par conséquent, plusieurs objets de la même classe peuvent exister dans une seule application si nous avons plusieurs conteneurs.

Par défaut, Spring crée tous les beans sous forme de singletons.

2.2. Singletons à câblage automatique

Par exemple, nous pouvons créer deux contrôleurs dans un même contexte d'application et injecter un bean du même type dans chacun.

Tout d'abord, nous créons un BookRepository qui gère nos objets de domaine Book .

Ensuite, nous créons LibraryController , qui utilise le BookRepository pour renvoyer le nombre de livres dans la bibliothèque:

@RestController public class LibraryController { @Autowired private BookRepository repository; @GetMapping("/count") public Long findCount() { System.out.println(repository); return repository.count(); } }

Enfin, nous créons un BookController , qui se concentre sur les actions spécifiques aux livres , comme la recherche d'un livre par son 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(); } }

Nous démarrons ensuite cette application et effectuons un GET sur / count et / book / 1:

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

Dans la sortie de l'application, nous voyons que les deux objets BookRepository ont le même ID d'objet:

[email protected] [email protected]

Les BookRepository ID d'objet dans le contrôleur de bibliothèque et controlleur sont les mêmes, ce qui prouve que le printemps injecté le même grain dans les deux contrôleurs.

Nous pouvons créer des instances séparées du bean BookRepository en changeant la portée du bean de singleton en prototype à l'aide de l' annotation @ Scope (ConfigurableBeanFactory.SCOPE_PROTOTYPE) .

Cela indique à Spring de créer des objets séparés pour chacun des beans BookRepository qu'il crée. Par conséquent, si nous inspectons à nouveau l'ID objet du BookRepository dans chacun de nos contrôleurs, nous voyons qu'ils ne sont plus les mêmes.

3. Modèle de méthode d'usine

Le modèle de méthode de fabrique implique une classe de fabrique avec une méthode abstraite pour créer l'objet souhaité.

Souvent, nous voulons créer différents objets en fonction d'un contexte particulier.

Par exemple, notre application peut nécessiter un objet véhicule. Dans un environnement nautique, nous voulons créer des bateaux, mais dans un environnement aérospatial, nous voulons créer des avions:

Pour ce faire, nous pouvons créer une implémentation d'usine pour chaque objet souhaité et renvoyer l'objet souhaité à partir de la méthode de fabrique concrète.

3.1. Contexte de l'application

Spring utilise cette technique à la racine de son framework d'injection de dépendances (DI).

Fondamentalement, Spring traite un récipient à grains comme une usine qui produit des grains.

Ainsi, Spring définit l' interface BeanFactory comme une abstraction d'un conteneur de bean:

public interface BeanFactory { getBean(Class requiredType); getBean(Class requiredType, Object... args); getBean(String name); // ... ]

Chacune des méthodes getBean est considérée comme une méthode de fabrique , qui renvoie un bean correspondant aux critères fournis à la méthode, comme le type et le nom du bean.

Spring étend ensuite BeanFactory avec l' interface ApplicationContext , qui introduit une configuration d'application supplémentaire. Spring utilise cette configuration pour démarrer un conteneur de bean basé sur une configuration externe, telle qu'un fichier XML ou des annotations Java.

En utilisant les implémentations de classe ApplicationContext comme AnnotationConfigApplicationContext , nous pouvons ensuite créer des beans via les différentes méthodes de fabrique héritées de l' interface BeanFactory .

Tout d'abord, nous créons une configuration d'application simple:

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

Ensuite, nous créons une classe simple, Foo , qui n'accepte aucun argument de constructeur:

@Component public class Foo { }

Ensuite, créez une autre classe, Bar , qui accepte un seul argument de constructeur:

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

Enfin, nous créons nos beans via l' implémentation AnnotationConfigApplicationContext d' 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)); }

Using the getBean factory method, we can create configured beans using just the class type and — in the case of Bar — constructor parameters.

3.2. External Configuration

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.

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

@Test public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { String expectedName = "Some name"; ApplicationContext context = new ClassPathXmlApplicationContext("context.xml"); // Same test as before ... }

4. Proxy Pattern

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

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:

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

4.2. CGLib Proxies

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

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

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.

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

Generally, Spring uses two types of proxies:

  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.

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

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.

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

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

Then we change our DatabaseQuery class to utilize this callback:

public abstract DatabaseQuery { public  T 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... } }

This callback mechanism is precisely the approach that Spring uses with the JdbcTemplate class.

5.2. JdbcTemplate

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

public class JdbcTemplate { public  T query(final String sql, final ResultSetExtractor 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:

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

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

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

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

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

public class JdbcTemplate { public  List query(String sql, RowMapper 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:

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

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

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

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

  • Java Message Service (JMS)
  • Java Persistence API (JPA)
  • Hibernate (now deprecated)
  • Transactions

6. Conclusion

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

Nous avons également exploré comment Spring utilise ces modèles pour fournir des fonctionnalités riches tout en réduisant le fardeau des développeurs.

Le code de cet article se trouve à l'adresse over sur GitHub.