优雅!Chain 结合 Spring Boot 轻松实现强大的责任链模式

文摘   2025-01-06 08:01   新疆  

Spring Boot 3实战案例合集》现已囊括超过60篇精选实战文章,并且此合集承诺将永久持续更新,为您带来最前沿的技术资讯与实践经验。欢迎积极订阅,享受不断升级的知识盛宴!订阅用户将特别获赠合集内所有文章的最终版MD文档(详尽学习笔记),以及完整的项目源码,助您在学习道路上畅通无阻。

【重磅发布】《Spring Boot 3实战案例锦集》PDF电子书现已出炉!

🎉🎉我们精心打造的《Spring Boot 3实战案例锦集》PDF电子书现已正式完成,目前已经有70个案例,后续还将继续更新。文末有电子书目录。

📚📚订阅获取
只需订阅我们的合集点我订阅,即可立即私信我们获取这本珍贵的电子书。轻松拥有Spring Boot 3的实战宝典!

💪💪永久更新承诺
我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务。这意味着,随着技术的不断发展和Spring Boot 3的深入应用,我们的电子书也将持续更新,确保您始终掌握最前沿、最实用的技术知识。

🔥🔥精彩内容不容错过
《Spring Boot 3实战案例锦集》汇聚了众多精心挑选的实战案例,旨在帮助您快速掌握Spring Boot 3的核心技术和实战技巧。无论您是初学者还是有一定经验的开发者,都能从中受益匪浅。

💌💌如何获取
请立即订阅我们的合集点我订阅,并通过私信联系我们,我们将第一时间将电子书发送给您。

现在就订阅合集


环境:SpringBoot3.2.5



1. 简介

Chain是Apache Commons的一个子项目,它实现了责任链模式。该项目提供了一种灵活和可扩展的方式来组织和执行一系列处理逻辑。在Commons Chain中,处理逻辑被封装为命令(Command),这些命令可以组合成链(Chain),并按照顺序执行。此外,Commons Chain还提供了上下文(Context)来共享命令间的信息,以及过滤器(Filter)来处理异常和资源清理。通过Catalog接口,可以方便地存储和查找命令及链。Commons Chain在Web应用框架、业务逻辑层和持久层中都有广泛应用,有助于实现模块化、可重用和可测试的代码。

Chain组件包含5个核心接口:

Context:Context表示应用程序的状态。在Chain包中,Context是java.util.Map的标记接口。

Command:Command表示一个工作单元。Command有一个单一入口方法:public boolean execute(Context context)。Command通过上下文对象传递的状态执行操作,但自己不保留任何状态。可以将命令组合成链,以便从离散的工作单元创建复杂的事务。如果Command返回true,则链中的其他Command不应执行。如果Command返回false,则链中的其他Command(如果有)可能会执行。

Chain:Chain实现了Command接口,因此Chain可以与Command互换使用。应用程序不需要知道它正在调用的是Chain还是Command,因此可以在它们之间进行重构。Chain可以根据需要嵌套其他Chain。这个属性被称为里氏替换原则。

Filter:理想情况下,每个命令都是一个独立的部分。但在现实中,我们有时需要分配资源,并确保无论发生什么情况,资源都会被释放。Filter是一种特殊的Command,它添加了postProcess方法。Chain在返回之前应该调用链中任何Filter的postProcess方法。实现Filter的Command可以通过postProcess方法安全地释放其分配的资源,即使这些资源与其他Command共享也是如此。

Catalog:许多应用程序使用“外观”、“工厂”和其他技术来避免将各层绑定得太紧密。层需要相互交互,但通常我们不希望它们在类名级别上进行交互。Catalog是一个逻辑命名的命令(或Chain)集合,客户端可以执行这些命令,而无需知道命令的类名。简单说:它管理了所有可用的Command,如果你需要用谁,你可以直接通过名称获取Command。

接下来,我们将详细介绍Chain组件的使用,最后会结合Spring Boot综合应用。

2. 实战案例

2.1 基本应用

将我们要实现的逻辑通过Command来实现

public class Command1 implements Command {  public boolean execute(Context context) throws Exception {    System.err.println("command1...") ;    context.put("c1", "我是C1计算结果") ;    return false ;  }}public class Command2 implements Command {  public boolean execute(Context context) throws Exception {    System.err.println("command2...") ;    System.out.println(context.get("c1")) ;    context.put("c2", "我是C2计算结果") ;    return false ;  }}public class Command3 implements Command {  public boolean execute(Context context) throws Exception {    System.err.println("command3...") ;    System.out.println(context.get("c2")) ;    return false ;  }}

我们将具体逻辑分别在3个Command里实现,并通过Context来传递数据,分别在下一个Command中获取。

运行上面的命令

Context context = new ContextBase() ;
List<Command> commands = List.of(new Command1(), new Command2(), new Command3()) ;Chain chain = new ChainBase(commands) ;chain.execute(context) ;

执行结果

command1...command2...我是C1计算结果command3...我是C2计算结果

如果上面任何一个Command#execute返回了true,则后续的Command将不会执行。

2.2 资源释放

在每一个Command中,如果你有收尾工作或是释放的资源,那么你可以将具体的Command实现Filter接口,那么在execute执行完成后会执行Filter#postprocess方法进行资源的释放。修改上面的3个Command实现如下:

public class Command1 implements CommandFilter {  public boolean postprocess(Context context, Exception exception) {    System.err.println("C1释放资源") ;    return exception == null ;  }}public class Command2 implements CommandFilter {  public boolean postprocess(Context context, Exception exception) {    System.err.println("C2释放资源") ;    return exception == null ;  }}public class Command3 implements CommandFilter {  public boolean postprocess(Context context, Exception exception) {    System.err.println("C3释放资源") ;    return exception == null ;  }}

最终执行结果

command1...command2...我是C1计算结果command3...我是C2计算结果C3释放资源C2释放资源C1释放资源

资源的释放,是倒序执行的。

注意:postprocess方法返回值是通过异常对象来决定返回true还是false,当存在异常对象时返回false,否则返回true,这样我们能保证在最后能抛出异常。

2.3 Catalog管理Command

我们可以通过Catalog进行管理所有的Command,在需要的时候,可以通过名称name获取指定的Command,如下示例:

CatalogBase catalog = new CatalogBase() ;catalog.addCommand("c1", new Command1()) ;catalog.addCommand("c2", new Command2()) ;catalog.addCommand("c3", new Command3()) ;
Map<String, Object> data = new HashMap<>() ;Context context = new ContextBase(data) ;
Iterator<String> names = catalog.getNames() ;while (names.hasNext()) { catalog.getCommand(names.next()).execute(context) ;}

将所有的Command添加到CatalogBase中,这是基于内存的实现;你也可以自定义实现Catalog接口,进行持久化存储。

2.4 异常处理

任何一个Command执行发生异常后,都会终止当前执行的流程,但是不会影响Filter#postprocess的执行,当Filter执行完成以后会检查当前是否发生了错误,进而进行异常的抛出,我们只需要在Chain#execute执行时进行捕获即可,如下示例:

Map<String, Object> data = new HashMap<>() ;Context context = new ContextBase(data) ;
List<Command> commands = List.of(new Command1(), new Command2(), new Command3()) ;Chain chain = new ChainBase(commands) ;try { chain.execute(context) ;} catch (Exception e) { System.err.printf("发生错误: %s%n", e.getMessage()) ;}

当我们在Command2中抛出异常后,执行结果如下:

command1...我是C1计算结果command2...C2释放资源C1释放资源发生错误: / by zero

Command3没有执行,那么对应的Filter#postprocess也不会执行。最终我们在自己的代码中进行catch处理。

2.4 结合Spring使用案例

我们将实现一个校验处理用户输入数据的功能,包含3个Command,分别如下:

  • 数据有效性验证

@Component@Order(1)public class ValidatorCommand implements Command {
@Override public boolean execute(Context context) throws Exception { System.err.println("数据有效性检查...") ; return false ; }}

这里使用了@Order定义了组件的顺序。

  • 替换输入内容中包含的所有HTML标签

@Component@Order(2)public class HtmlCheckCommand implements Command {
@Override public boolean execute(Context context) throws Exception { System.err.println("处理HTML标签...") ; return false ; }}

其实如果你不知道你将来会有多少个Command,那么execute都返回false也是ok的。

  • 过滤所有的敏感词

@Component@Order(3)public class SensitiveWordCommand implements Command {
@Override public boolean execute(Context context) throws Exception { System.err.println("敏感词检查...") ; return true ; }}

 以上定义了3个Command后,我们再定义一个针对这3个Command统一入口Bean对象,如下:

@Componentpublic class CharCheckComponent {
private final Map<String, Command> commandMapping ; private final List<Command> commands ; private final Catalog catalog ;
public CharCheckComponent(List<Command> commands, Map<String, Command> commandMapping) { this.commands = commands ; this.commandMapping = commandMapping ; this.catalog = new CatalogBase(this.commandMapping) ; }
public void check(Context context) { Chain chain = new ChainBase(this.commands) ; try { chain.execute(context) ; } catch (Exception e) { throw new RuntimeException(e) ; } }
public Optional<Command> getCommand(String name) { return Optional.ofNullable(this.catalog.getCommand(name)) ; }
public boolean execute(String name, Context context) { try { return this.getCommand(name).orElseThrow(() -> new RuntimeException("不存在")).execute(context) ; } catch (Exception e) { throw new RuntimeException(e) ; } }}

该bean中,我们分别注入了List,Map类型的Command,注入List是因为我们希望Command有顺序;而Map类型是为了将所有的Command都加入到Catalog中进行统一的管理,这样我们就可以通过对应的beanName获取指定的Command了。

接下来,我们通过CommandLineRunner进行测试

@Componentpublic class CommandRunner implements CommandLineRunner {
private final CharCheckComponent charCheckComponent ; public CommandRunner(CharCheckComponent charCheckComponent) { this.charCheckComponent = charCheckComponent; }
@Override public void run(String... args) throws Exception { Context context = new ContextBase() ; this.charCheckComponent.check(context) ; System.err.println("----------------------------") ; this.charCheckComponent.execute("sensitiveWordCommand", context) ; }}

启动 Spring Boot 应用,输入如下:

数据有效性检查...处理HTML标签...敏感词检查...----------------------------敏感词检查...

Command链正确的执行;通过beanName获取指定的Command也正确的执行。

特别说明:当前commons-chain 处于休眠状态,也就是说没维护升级了。比如它内部提供的ServletWebContext上下文对象是基于JavaEE包相关的Servlet API依赖,不支持jakarta EE。

以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

推荐文章

这4个关于Spring AOP开发技巧你知道吗?

避坑!为了性能Spring挖了一个大坑

高级开发!Spring Boot 定制化请求参数和响应格式

高级开发!Spring Boot 乐观锁正确处理的3种方案,第三种方案最优雅

优雅!基于Spring Boot字段加密后的模糊查询,支持MyBatis, JPA

优雅!Spring Boot 这样记录操作日志非常灵活强大

Java中的七种函数式编程技巧

这6个Spring高级开发技巧掌握了吗?

高效开发!Lambda表达式和函数式接口最佳实践

Map你只会用put,get?试试这些高级方法

@Order注解,你理解错了!

自己动手实现Agent统计API接口调用耗时

SpringBoot整合Flink CDC,实时追踪数据变动,无缝同步至Redis

解锁Spring资源Resource的强大功能,提升开发效率

Spring全家桶实战案例源码
spring, springboot, springcloud 案例开发详解
 最新文章