异常向北,流转向南(IF-ELSE
清除计划)
接上篇
上一篇,IF-ELSE
清除计划之管道风云,诸位老哥看了我一顿乱扯,一锅乱炖,就像我上篇总结的一样,我其实是描述了IF-ELSE
清除计划的前置条件,具有管道的意识,然后我们继续执行清除计划,这篇我们着重谈谈怎么通过Optional和Function实现管道的短路和流转
切题
异常向北(上),流转向南(下)!如上篇所述,管道模式是有Valve(阀门
)和Status(状态
)的,这两个组件主要是控制代码的流转,也就是真实用来清理IF-ELSE
的实现,但是我们CRUD-BOYS
使用的是极简模式的管道,并没有加入这些组件,那我们处理IF-ELSE
其实是通过以下几种方式
- 阀门管道(执行断路逻辑)以及通路管道(空值逻辑传递)
- 异常管道抛出异常使得执行向北,其实就是向上层抛出异常(抛给外部异常捕获框架)
- 正常逻辑走其他分支管道,流转向南,继续往下执行
来源
Optional
Java8
的新特性终于让我们可以使用函数式编程了,同时也提供了很多非常优秀的工具,比如我们今天的主角Optional
,他是用于进行控制判断的工具,很多文章都是这么说的。。。但是如果你仔细看看它的api,你会发现它其实一点也不简单,让我们看看今天会使用的几个方法
//用于过滤,可以有效清理`IF-ELSE`
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
//用于装载极简管道,注意其实可以使用andThen多级连接
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
//这里返回的还是Optional
return Optional.ofNullable(mapper.apply(value));
}
}
复制代码
Function
//嘿嘿,炒一份冷饭,就是极简管道的核心方法
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
复制代码
搞起
为了方便大家理解,我们使用一个稍微复杂场景来进行举例,同时也是为了方便大家能够在实际的业务中使用,减少知行合一
的成本,更好的方便实践,这个场景就是使用JWT
进行权限校验,Token
存放在请求头中,然后校验Token
的有效性,获取用户的信息,接着获取用户的权限,校验当前Url
是否在用户的权限中,我们试试校验Token有效性
public static void main(String[] args) {
HttpServletRequest request = new HttpServletRequestWrapper(null);
//如果不拼接极简管道,会有大量的`IF-ELSE`出现
//而且这边的逻辑本质就是校验Token是否有效
String headerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
//流转逻辑一直->向南
String userId = Optional.ofNullable(headerToken)
//分支管道,干掉IF-ELSE
.filter(StringUtils::isNoneBlank)
//极简管道拼接
.map(parseUserIdFromToken().andThen(gainCacheFromRedis()))
//这个比较其实是防止Token泄露等异常情况
.filter(cacheToken -> StringUtils.equals(headerToken, cacheToken))
//校验完成后,再获取一次UserId
//其实可以在上一个Map直接封装一层,这边只做演示,不做复杂拓展
.map(parseUserIdFromToken())
//所有的逻辑都由该异常->向北 这是异常管道
.orElseThrow(() -> new RuntimeException("Token异常!"));
//todo 继续通过UserId获取用户权限
}
/**
* 模拟JWT解析
*
* @return 从Token中解析UserId
*/
private static Function<String, String> parseUserIdFromToken() {
return token -> "JWT解析UserId";
}
/**
* 模拟Redis获取缓存(分布式缓存简单实现)
*
* @return 从Redis中获取UserId对应的Token
*/
private static Function<String, String> gainCacheFromRedis() {
return userId -> "通过Redis获取到的Token";
}
复制代码
真心小结
当我把上篇文章发到某个神秘的技术群时,有大佬一针见血的指出了这种方式其实也是IF-ELSE
,本质上也是条件判断,在重新审视代码和逻辑后发现确实如此,但是我依然要指出这样处理IF-ELSE
的好处,即管道之间的IF-ELSE
逻辑是隔离的,校验Token有效性的管道部分和通过UserId获取用户权限的管道之间的条件判断是隔离的,而不是堆在一起形成一段又一段的IF-ELSE
代码块,当然,这只是一家之言,也只是初步思考以后的实现,肯定会有疏漏,初衷也是期望对大家有所帮助,也希望有大佬可以提出更优美的解法,造福我等CRUD-BOY
,最后谢谢您的阅读,祝端午安康,阖家幸福
咸鸭蛋(彩蛋的一种)
最近其实有机会接触并使用了Spring Webflux
实现的Spring Cloud Gateway
,这是Spring基于Reactor的响应式编程实现,在Spring Webflux
的场景中,所有的处理结果在subscribe
之前都是不可直接读取,因为subscribe
是会block
当前线程的,这在响应式编程之中是不允许的,所以在Reactor中数据流转就是流式的,IF-ELSE
可以控制到出现得很少,而所有的数据必须被Mono和Flux包裹,而Mono和Flux的api是管道模式的大成者,可以做到一段到底,绝不分割,下面贴出Spring Webflux
一个小例子,大家可以尝试借鉴一哈
String hello = Mono
.just("HelloWorld")
//可以直接进行判断
.switchIfEmpty(Mono.just("HelloReactor"))
//可以设置默认值
.defaultIfEmpty("MyMan")
//可以转换其他值
.flatMap(word -> Mono.defer(() -> Mono.just("HelloPipeline")))
//延迟操作
.doOnSuccess(System.out::println)
.doOnError(System.out::println)
.block();
System.out.println(hello);
复制代码