1. 简介
设计模式是软件开发的重要组成部分。这些解决方案不仅解决了反复出现的问题,还通过识别常见模式帮助开发人员理解框架的设计。
在本教程中,我们将了解 Spring 框架中使用的四种最常见的设计模式:
- 单例模式
- 工厂方法模式
- 代理模式
- 模板模式
我们还将了解 Spring 如何使用这些模式来减轻开发人员的负担并帮助用户快速执行繁琐的任务。
2. 单例模式
单例模式是一种确保每个应用程序只存在一个对象实例的机制。这种模式在管理共享资源或提供横切服务(例如日志记录)时很有用。
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,它专注于 Book特定的操作,例如通过 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();
}
}
然后我们启动这个应用程序并在/count和/book/1上执行 GET :
curl -X GET http://localhost:8080/count
curl -X GET http://localhost:8080/book/1
在应用程序输出中,我们看到两个BookRepository对象具有相同的对象 ID:
com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f
com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f
LibraryController和BookController中的BookRepository对象 ID相同,证明 Spring 将相同的 bean 注入到两个控制器中。
我们可以通过使用@Scope (ConfigurableBeanFactory.SCOPE_PROTOTYPE) 注释将bean 范围从单例更改 为原型来创建BookRepository bean 的单独实例。
这样做会指示 Spring 为其创建的每个BookRepository bean 创建单独的对象。因此,如果我们再次检查每个控制器中BookRepository的对象 ID ,我们会发现它们不再相同。
3.工厂方法模式
工厂方法模式需要一个带有抽象方法的工厂类来创建所需的对象。
通常,我们希望根据特定的上下文创建不同的对象。
例如,我们的应用程序可能需要一个车辆对象。在航海环境中,我们想要制造船只,但在航空环境中,我们想要制造飞机:
为此,我们可以为每个所需对象创建一个工厂实现,并从具体工厂方法返回所需对象。
3.1。应用程序上下文
Spring 在其依赖注入 (DI) 框架的根部使用这种技术。
从根本上说,Spring 将 bean 容器视为生产 bean 的工厂。
因此,Spring 将BeanFactory接口定义为 bean 容器的抽象:
public interface BeanFactory {
getBean(Class<T> requiredType);
getBean(Class<T> requiredType, Object... args);
getBean(String name);
// ...
]
每个 getBean方法都被认为是一个工厂方法,它返回一个与提供给该方法的条件匹配的 bean,例如 bean 的类型和名称。
然后 Spring使用ApplicationContext接口扩展 BeanFactory ,该接口引入了额外的应用程序配置。Spring 使用此配置基于一些外部配置(例如 XML 文件或 Java 注释)启动 bean 容器。
使用 像AnnotationConfigApplicationContext这样的ApplicationContext类实现,我们可以通过从BeanFactory接口继承的各种工厂方法创建 bean。
首先,我们创建一个简单的应用程序配置:
@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 ...
}
最后,我们通过ApplicationContext的AnnotationConfigApplicationContext实现创建我们的 bean :
@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工厂方法,我们可以只使用类类型和(在Bar的情况下)构造函数参数来创建配置的 bean。
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类中,我们使用@Transactional注释来注释create方法 。这个注解指示 Spring 以原子方式执行我们的create方法。如果没有代理,Spring 将无法控制对BookRepository bean 的访问并确保其事务一致性。
4.2. CGLib 代理
相反,Spring 创建了一个代理来包装我们的 BookRepository bean并检测我们的 bean 以原子地执行我们的create方法。
当我们调用BookManager#create方法时,我们可以看到输出:
com.baeldung.patterns.proxy.BookRepository$$EnhancerBySpringCGLIB$$3dc2b55c
通常,我们希望看到一个标准的BookRepository对象 ID;相反,我们看到的是EnhancerBySpringCGLIB对象 ID。
在幕后,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 框架中应用的四种最常见的设计模式。
我们还探讨了 Spring 如何利用这些模式来提供丰富的功能,同时减轻开发人员的负担。
本文中的代码可以在 GitHub 上找到。