Entwurfsmuster im Spring Framework

1. Einleitung

Entwurfsmuster sind ein wesentlicher Bestandteil der Softwareentwicklung. Diese Lösungen lösen nicht nur wiederkehrende Probleme, sondern helfen Entwicklern auch, das Design eines Frameworks zu verstehen, indem sie gemeinsame Muster erkennen.

In diesem Tutorial werden vier der im Spring Framework am häufigsten verwendeten Entwurfsmuster vorgestellt:

  1. Singleton-Muster
  2. Factory Method Muster
  3. Proxy-Muster
  4. Vorlagenmuster

Wir werden uns auch ansehen, wie Spring diese Muster verwendet, um die Belastung der Entwickler zu verringern und den Benutzern zu helfen, schnell mühsame Aufgaben auszuführen.

2. Singleton-Muster

Das Singleton-Muster ist ein Mechanismus, der sicherstellt, dass nur eine Instanz eines Objekts pro Anwendung vorhanden ist . Dieses Muster kann nützlich sein, wenn Sie gemeinsam genutzte Ressourcen verwalten oder Querschnittsdienste wie die Protokollierung bereitstellen.

2.1. Singleton Bohnen

Im Allgemeinen ist ein Singleton für eine Anwendung global eindeutig, aber im Frühjahr wird diese Einschränkung gelockert. Stattdessen beschränkt Spring einen Singleton auf ein Objekt pro Spring IoC-Container . In der Praxis bedeutet dies, dass Spring pro Anwendungskontext nur eine Bean für jeden Typ erstellt.

Der Ansatz von Spring unterscheidet sich von der strengen Definition eines Singletons, da eine Anwendung mehr als einen Spring-Container haben kann. Daher können in einer Anwendung mehrere Objekte derselben Klasse vorhanden sein, wenn mehrere Container vorhanden sind.

Standardmäßig erstellt Spring alle Beans als Singletons.

2.2. Autowired Singletons

Beispielsweise können wir zwei Controller in einem einzigen Anwendungskontext erstellen und jeweils eine Bean desselben Typs einfügen.

Zuerst erstellen wir eine BookRepository , die unsere verwaltetes Buch Domain - Objekte.

Als Nächstes erstellen wir LibraryController , der das BookRepository verwendet , um die Anzahl der Bücher in der Bibliothek zurückzugeben:

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

Schließlich schaffen wir eine BookController , die auf konzentriert Buch -spezifische Aktionen, wie zum Beispiel ein Buch durch seine ID zu finden:

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

Wir starten dann diese Anwendung und führen ein GET für / count und / book / 1 durch:

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

In der Anwendungsausgabe sehen wir, dass beide BookRepository- Objekte dieselbe Objekt-ID haben:

[email protected] [email protected]

Die BookRepository- Objekt-IDs im LibraryController und im BookController sind identisch, was beweist, dass Spring beiden Controllern dieselbe Bean injiziert hat.

Wir können separate Instanzen der BookRepository- Bean erstellen , indem wir den Bean-Bereich mithilfe der Annotation @ Scope (ConfigurableBeanFactory.SCOPE_PROTOTYPE) von Singleton auf Prototyp ändern .

Dadurch wird Spring angewiesen, separate Objekte für jede der von ihm erstellten BookRepository- Beans zu erstellen . Wenn wir daher die Objekt-ID des BookRepository in jedem unserer Controller erneut überprüfen , stellen wir fest, dass sie nicht mehr identisch sind.

3. Factory Method Pattern

Das Factory-Methodenmuster enthält eine Factory-Klasse mit einer abstrakten Methode zum Erstellen des gewünschten Objekts.

Oft möchten wir verschiedene Objekte basierend auf einem bestimmten Kontext erstellen.

Beispielsweise kann unsere Anwendung ein Fahrzeugobjekt erfordern. In einer nautischen Umgebung möchten wir Boote erstellen, aber in einer Luft- und Raumfahrtumgebung möchten wir Flugzeuge erstellen:

Um dies zu erreichen, können wir für jedes gewünschte Objekt eine Factory-Implementierung erstellen und das gewünschte Objekt von der konkreten Factory-Methode zurückgeben.

3.1. Anwendungskontext

Spring verwendet diese Technik an der Wurzel seines DI-Frameworks (Dependency Injection).

Grundsätzlich behandelt Spring einen Bohnenbehälter als eine Fabrik, in der Bohnen hergestellt werden.

Daher definiert Spring die BeanFactory- Schnittstelle als Abstraktion eines Bean-Containers:

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

Jede der getBean- Methoden wird als Factory-Methode betrachtet , die eine Bean zurückgibt, die den für die Methode angegebenen Kriterien wie Typ und Name der Bean entspricht.

Spring erweitert BeanFactory dann um die ApplicationContext- Schnittstelle, die eine zusätzliche Anwendungskonfiguration einführt. Spring verwendet diese Konfiguration, um einen Bean-Container basierend auf einer externen Konfiguration zu starten, z. B. einer XML-Datei oder Java-Anmerkungen.

Mithilfe der Implementierungen der ApplicationContext- Klasse wie AnnotationConfigApplicationContext können wir dann Beans über die verschiedenen Factory-Methoden erstellen, die von der BeanFactory- Schnittstelle geerbt wurden .

Zunächst erstellen wir eine einfache Anwendungskonfiguration:

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

Als nächstes erstellen wir eine einfache Klasse, Foo , die keine Konstruktorargumente akzeptiert:

@Component public class Foo { }

Erstellen Sie dann eine andere Klasse, Bar , die ein einzelnes Konstruktorargument akzeptiert:

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

Zuletzt erstellen wir unsere Beans über die AnnotationConfigApplicationContext- Implementierung von 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.

Wir haben auch untersucht, wie Spring diese Muster verwendet, um umfangreiche Funktionen bereitzustellen und gleichzeitig die Belastung für Entwickler zu verringern.

Der Code aus diesem Artikel ist auf GitHub zu finden.