Spring Framework 6 中的设计模式

简介: Spring Framework 6 中的设计模式

更多 Java / AI / 大数据 好文章

在实际开发工作中,我们每天都在自己的工作中依赖了别人的代码。包括了你正在用的编程语言、你正在构建的框架,或者一些很前沿的开源产品。

它们都做得很好,用起来真的很爽,但你自己有没有想过自己也要去实现它?哈哈,可能大概率是没想过的,是吧?

如果你没有尝试过自己去实现类似的功能,或者是深挖过这些优秀三方框架的话,对技术人来说,其实是隐含相当大一个风险的。

如果运气很不好,当你在生产中碰到了分崩离析的事情,出现了生产事故,又不得不去调试您不熟悉的第三方库的实现时,对你来说至少可以说是相当棘手的,毫无头绪的,搞不好真的就是想原地提离职……

推荐一个 Lightrun,它是一种新型的调试器。

它是专门针对现实生活中的生产环境。使用 Lightrun,您可以向下钻取到正在运行的应用程序,包括第三方依赖项,以及实时日志、快照和指标。

这不是重点,重点是我们借助 Spring 来聊聊这个如此受欢迎的框架的设计模式,以此打开你研究三方框架的道路。

1、简介

设计模式是软件开发的重要组成部分。这些解决方案不仅可以解决反复出现的问题,还可以通过识别常见模式来帮助开发人员了解框架的设计。

接下来呢,我们将介绍 Spring 框架中使用的四种最常见的设计模式:

  1. Singleton pattern 单例模式
  2. Factory Method pattern 工厂方法模式
  3. Proxy pattern 代理模式
  4. Template pattern 模板模式

我们还将研究 Spring 如何使用这些模式来减轻开发人员的负担并帮助用户快速执行繁琐的任务。

2、单例模式(Singleton Pattern)

单一实例模式是一种机制,可确保每个应用程序仅存在对象的一个实例。在管理共享资源或提供横切服务(如日志记录)时,此模式非常有用。

2.1 单例 Beans(Singleton Beans)

通常,单例对于应用程序是全局唯一的,但在 Spring 中,此约束是宽松的。相反,Spring 将单个实例限制为每个 Spring IoC 容器一个对象。在实践中,这意味着 Spring 只会为每个应用程序上下文的每种类型创建一个 bean。

Spring 的方法与单例的严格定义不同,因为一个应用程序可以有多个 Spring 容器。因此,如果我们有多个容器,则同一类的多个对象可以存在于单个应用程序中。

默认情况下,Spring 将所有 bean 创建为单例。

2.2 自动注入单例(Autowired Singletons)

例如,我们可以在单个应用程序上下文中创建两个控制器,并将相同类型的 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,它专注于特定于书籍的操作,例如按 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、工厂方法模式(Factory Method Pattern)

工厂方法模式需要一个工厂类,其中包含用于创建所需对象的抽象方法。

通常,我们希望根据特定的上下文创建不同的对象。

例如,我们的应用程序可能需要车辆对象。在航海环境中,我们想制造船只,但在航空航天环境中,我们想制造飞机:

为此,我们可以为每个所需对象创建一个工厂实现,并从具体的工厂方法返回所需的对象。

3.1 应用上下文(Application Context)

Spring 在其依赖注入(DI)框架的根中使用这种技术。

从根本上说,Spring 将 Bean 容器视为生产 Beans 的工厂。

因此,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 容器。

使用 ApplicationContext 类实现(如 AnnotationConfigApplicationContext),我们可以通过从 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 外部配置(External Configuration)

此模式是通用的,因为我们可以根据外部配置完全更改应用程序的行为。

如果我们希望更改应用程序中自动连线对象的实现,我们可以调整我们使用的 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、代理模式(Proxy Pattern)

代理在我们的数字世界中是一种方便的工具,我们经常在软件(例如网络代理)之外使用它们。在代码中,代理模式是一种技术,它允许一个对象(代理)控制对另一个对象(主体或服务)的访问。

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 注释注释创建方法。这个注释指示 Spring 原子地执行我们的创建方法。如果没有代理,Spring 将无法控制对 BookRepository bean 的访问并确保其事务一致性。

4.2 CGLib 代理(CGLib Proxies)

相反,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 代理 – 在代理类时使用;
  • JDK 动态代理 – 在代理接口时使用

虽然我们使用事务来公开底层代理,但 Spring 将在必须控制对 bean 的访问的任何场景中使用代理。

5、模版方法模式(Template Method Pattern)

在许多框架中,代码的很大一部分是样板代码。

例如,在数据库上执行查询时,必须完成相同的一系列步骤:

  1. Establish a connection 建立连接
  2. Execute query 执行查询
  3. Perform cleanup 执行清理
  4. Close the connection 关闭连接

这些步骤是模板方法模式的理想方案。

5.1 模版和回调(Templates & Callbacks)

模板方法模式是一种技术,用于定义某些操作所需的步骤,实现样板步骤,并将可自定义的步骤保留为抽象。然后,子类可以实现此抽象类,并为缺少的步骤提供具体的实现。

我们可以在数据库查询的情况下创建一个模板:

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 方法。

首先,我们创建一个回调方法,该方法采用 Result 对象并将其映射到 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 模版(JdbcTemplates)

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 还使用模板:

  • Java Message Service (JMS)
  • Java Persistence API (JPA)
  • Hibernate(现已弃用)
  • Transactions 事务

6、结论

上面,我们研究了 Spring 框架中应用的四种最常见的设计模式。

我们还探讨了Spring如何利用这些模式来提供丰富的功能,同时减轻开发人员的负担。

目录
相关文章
|
4天前
|
Java 开发者 Spring
Spring Framework 中的 @Autowired 注解:概念与使用方法
【4月更文挑战第20天】在Spring Framework中,@Autowired 注解是实现依赖注入(Dependency Injection, DI)的一种非常强大的工具。通过使用 @Autowired,开发者可以减少代码中的引用绑定,提高模块间的解耦能力
37 6
|
4天前
|
设计模式 SQL Java
Spring中的设计模式
Spring中的设计模式
|
4天前
|
设计模式 安全 Java
【初学者慎入】Spring源码中的16种设计模式实现
以上是威哥给大家整理了16种常见的设计模式在 Spring 源码中的运用,学习 Spring 源码成为了 Java 程序员的标配,你还知道Spring 中哪些源码中运用了设计模式,欢迎留言与威哥交流。
|
4天前
|
前端开发 Java Spring
Spring Framework五大功能模块
Spring Framework五大功能模块
|
4天前
|
设计模式 Java 数据库连接
Spring 中经典的 9 种设计模式
Spring 中经典的 9 种设计模式
55 2
|
4天前
|
设计模式 Java 数据库连接
9种设计模式在Spring中的运用
9种设计模式在Spring中的运用
81 0
|
4天前
|
设计模式 Java 数据库连接
Spring设计模式(一)
Spring设计模式(一)
|
4天前
|
设计模式 Java Spring
Spring5深入浅出篇:Spring工厂设计模式拓展应用
Spring5深入浅出篇:Spring工厂设计模式拓展应用
|
4天前
|
设计模式 Java Spring
Spring5深入浅出篇:Spring与工厂设计模式简介
Spring5深入浅出篇:Spring与工厂设计模式简介
|
4天前
|
Java 应用服务中间件 数据库连接
太厉害!Redis+Nginx+设计模式+Spring全家桶+Dubbo技术精选合集
最近花了很长的时间去搜罗Java核心技术好文,我把每个Java核心技术的优选文章都整理成了一个又一个的文档。昨天也是终于全部整理好了,今天就把这些东西分享给老铁们,也能为老铁们省去不少麻烦,想学什么技能了,遇到哪方面的问题了 直接打开文档学一学就好了。不多bb了,直接上干货!