Java 17 中的模式匹配与和类型

从 Spring Security 获取用户谈起

使用 Spring Security做用户校验和权限控制时,常常使用和线程绑定的容器来获取当前登录用户。

// 使用前设置用户,重点的在下一条
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(anAuthentication);
SecurityContextHolder.setContext(context); // 在使用时,
public Resonse<Boolean> process(SomeRequest request) {
// code
Object principal = SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
// code
}

我们来梳理一下,容器可以存放验证过的Authentication,Authentication存放isAuthenticated表示是否验证通过,验证后的用户叫做Principal。security的定义如下:

The identity of the principal being authenticated. In the case of an authentication request with username and password, this would be the username. Callers are expected to populate the principal for an authentication request.

The AuthenticationManager implementation will often return an Authentication containing richer information as the principal for use by the application. Many of the authentication providers will create a UserDetails object as the principal.

这里最值得关注的点实际上是Principal的类型是Object,从语法上来说,可以是任意类型,这就是本文想要讨论的点:对于一个确定的系统,我们获取的Principal实际上只可能是几种类型中的一种,比如我们将用户分为注册用户,管理员,游客三种。在如上的process方法中,我们需要对principal的实际类型进行动态获取(运行时确定),然后执行相关的代码逻辑。

当然,对于用户的划分根据业务各有不同,一些简单的业务直接实现了Security提供的UserDetails接口,其中包含了一些权限的控制策略(账号是否过期、锁定等)。

在MVC模型的 service 层中,如果需要使用用户,最好是显式表示在方法签名中,这样的方法更易测试,方便排除副作用。但是我们的用户虽然限制在(注册用户 | 管理员 | 游客)三种类型下,我们只能使用 Object principal来进行匹配。我们需要在方法的前面进行参数校验,对于不同的方法,如果使用到了principal, 都需要进行这样的参数校验。这样做的缺点是没有了编译器的帮助,传入错误的类型,在运行时才发现。

// 举例:UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken::getName 只能做运行时检查
public String getName() {
if (this.getPrincipal() instanceof UserDetails) {
return ((UserDetails) this.getPrincipal()).getUsername();
}
if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
}
if (this.getPrincipal() instanceof Principal) {
return ((Principal) this.getPrincipal()).getName();
}
return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
} // 根据不同的用户,获取不同的信息
public Response<String> getInfo(Object principal) {
if (principal instanceof Admin admin) return adminGetInfo(admin);
else if (principal instanceof NormalUser normalUser) return normalGetInfo(normalUser);
else if (principal instanceof Guest guest) return guestGetInfo(guest);
else throw MyException("invalid param " + principal);
}

Java 17 借鉴了现代编程语言中的模式匹配思想,可以轻松地解决这个问题。

模式匹配

如果用户限制在固定的数目下,我们可以定义一个类型,其属于(注册用户 | 管理员 | 游客)中的一个,这种类型 Type 称为和类型 sum type,因为其只有三种情况,满足加法法则;如果注册用户将来分成了普通用户和星级用户,则变为 4 种情况。

与sum type对应的 product type,假设 UserInfo 类中拥有 age 和 height 两个字段,则 UserInfo 的实例有 n(age) * n(height) 种情况。

sum type 和 product type 两种类型,称为代数数据类型ADT,ADT在函数式编程中常见。(https://en.wikipedia.org/wiki/Algebraic_data_type)

在一些函数式语言中,没有类和继承,数据 = ADT,对象的行为 = function, 继承 = part of pattern matching。

Java 中借用接口解决和类型的问题:

public Response<String> getInfo(User principal) {
return switch (principal) {
case Admin admin -> adminGetInfo();
case NormalUser user && isSpecialDay() -> getInfoWithEasterEgg(user);
case NormalUser user -> getInfo();
case Guset guest -> guestGetInfo();
}
}
// user 使用sealed class(指定子类), switch 可以不用default.

模式匹配可以实现继承中的动态分派,并且可读性更好。

远古时期的 switch 语句,表达能力有限,只能使用在 int, Enum等类型上,即使如此,使用Enum也可以实现动态分派,如果使用的是 Java 8,可以使用Enum实现方法动态分派,可以参考 On Java 8。

什么时候使用?

面对复杂的 if-else 时

  1. 可以提取出分派方法的类,使用继承实现和类型 + sealed class,使用模式匹配 + 卫模式guarded pattern(predicate)。
  2. 不使用sealed class, 使用default 指定默认方法或者抛异常。
  3. 直接使用继承实现动态分派,这样的话可以直接调用 user.getInfo()。但是这样没法使用卫模式。
  4. 可以使用状态模式,如上面对用户权限的控制,抽取AuthType接口,根据行为不同,包含AdminAuth, NormalAuth, GuestAuth三种权限,User持有AuthType,权限相关方法委派给AuthType实现,调用authType.getInfo().
  5. 无法使用Java17 模式匹配,可以使用Enum这样的折中方法,好处是使用了编译器帮助检查Enum的完备性,可以没有default。
  6. 模式匹配可以完美替换 visitor pattern。比如:保存图片为jpg, cad有多种类型,使用visitor pattern 需要定义saver接口,问题是这个接口需要知道所有的cad类型,新增一个cad类型会很复杂。使用模式匹配改造后,cad.save(new JpgSaver())简化为saveJpg(cad)。原来的Cad.save(Visitor save) 方法实际上通过继承实现了两次分派,一次在Cad,另一次在Visitor。改造之后在saveJpg方法中只发生一次。

Java 17 中的模式匹配与和类型的更多相关文章

  1. 详解 Java 17 中新推出的密封类

    Java 17推出的新特性Sealed Classes经历了2个Preview版本(JDK 15中的JEP 360.JDK 16中的JEP 397),最终定稿于JDK 17中的JEP 409.Seal ...

  2. Java 17中对switch的模式匹配增强

    还记得Java 16中的instanceof增强 吗? 通过下面这个例子再回忆一下: Map<String, Object> data = new HashMap<>(); d ...

  3. Java泛型中的类型参数和通配符类型

    类型参数 泛型有三种实现方式,分别是泛型接口.泛型类.泛型方法,下面通过泛型方法来介绍什么是类型参数. 泛型方法声明方式:访问修饰符 <T,K,S...> 返回类型 方法名(方法参数){方 ...

  4. Java 17 新功能介绍(LTS)

    点赞再看,动力无限.Hello world : ) 微信搜「程序猿阿朗 」. 本文 Github.com/niumoo/JavaNotes 和 未读代码博客 已经收录,有很多知识点和系列文章. Jav ...

  5. 深入理解Java虚拟机--中

    深入理解Java虚拟机--中 第6章 类文件结构 6.2 无关性的基石 无关性的基石:有许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(ByteCode),从而 ...

  6. 我所使用的生产 Java 17 启动参数

    JVM 参数升级提示工具:jacoline.dev/inspect JVM 参数词典:chriswhocodes.com Revolut(英国支付巨头)升级 Java 17 实战:https://ww ...

  7. Java 17 新特性:switch的模式匹配(Preview)

    还记得Java 16中的instanceof增强吗? 通过下面这个例子再回忆一下: Map<String, Object> data = new HashMap<>(); da ...

  8. 详解Java 8中Stream类型的“懒”加载

    在进入正题之前,我们需要先引入Java 8中Stream类型的两个很重要的操作: 中间和终结操作(Intermediate and Terminal Operation) Stream类型有两种类型的 ...

  9. 关于Java运算中类型自动提升的问题

    1.表达式中的自动类型提升: 表达式求值时,Java自动的隐含的将每个byte.short或char操作数提升为int类型,这些类型的包装类型也是可以的. 例如:short s1 = 1; s1 =  ...

随机推荐

  1. SpringBoot扩展点EnvironmentPostProcessor

    一.背景 之前项目中用到了Apollo配置中心,对接Apollo配置中心后,配置中心的属性就可以在程序中使用了,那么这个是怎么实现的呢?配置中心的属性又是何时加载到程序中的呢?那么我们如果找到了这个是 ...

  2. 忘带U盘了??别急!一行python代码即可搞定文件传输

    近日发现了python一个很有趣的功能,今天在这里给大伙儿做一下分享 需求前提 1.想要拷贝电脑的文件到另一台电脑但是又没有U盘2.手机上想获取到存储在电脑的文件3.忘带U盘- 您也太丢三落四了吧,但 ...

  3. 手把手教你在Linux中快速检测端口的 3 个小技巧

    一个执着于技术的公众号 前言 无论是要解决网络连接问题还是配置防火墙,第一件事是要检查系统实际打开了哪些端口. 本文介绍了几种快速查找 Linux 系统上哪些端口向外部开放的方法. 什么是开放端口 监 ...

  4. JuiceFS v1.0 beta3 发布,支持 etcd、Amazon MemoryDB、Redis Cluster

    JuiceFS v1.0 beta3 在元数据引擎方面继续增强,新增 etcd 支持小于 200 万文件的使用场景,相比 Redis 可以提供更好的可用性和安全性.同时支持了 Amazon Memor ...

  5. C Primer Plus 学习笔记 -- 前六章

    记录自己学习C Primer Plus的学习笔记 第一章 C语言高效在于C语言通常是汇编语言才具有的微调控能力设计的一系列内部指令 C不是面向对象编程 编译器把源代码转化成中间代码,链接器把中间代码和 ...

  6. Spring事务源码解读

    一.Spring事务使用 1.通过maven方式引入jar包 <dependency> <groupId>com.alibaba</groupId> <art ...

  7. PHP_SESSION学习小结

    PHP Session PHP session 变量用于存储关于用户会话(session)的信息,或者更改用户会话(session)的设置.Session 变量存储单一用户的信息,并且对于应用程序中的 ...

  8. ML第5周学习小结

    本周收获 总结一下本周学习内容: 1.学习了<深入浅出Pandas>的第五章:Pandas高级操作的两个内容 数据迭代 函数应用 我的博客链接: pandas:数据迭代.函数应用 2.&l ...

  9. C/C++ 单元自动化测试解决方案实践

    vivo 互联网服务器团队 - Li Qingxin C/C++ 开发效率一直被业内开发人员诟病,单元测试开发效率也是如此,以至于开发人员不愿花时间来写单元测试.那么我们是不是可以通过改善编写单元测试 ...

  10. bintree

    Python实现二叉树的建立与遍历 创建(二叉)树节点类 class Node: def __init__(self,data,l=None,r=None): self.val = data self ...