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. MKL与VS2019配置方法

    VS2019配置oneAPI并调用MKL库 oneAPI oneAPI是一个跨架构的编程工具,旨在简化跨GPU.CPU.FPGA和AI加速器之间的编程,可以与英特尔自身设备,或其他厂商的芯片配合使用, ...

  2. YOLO系列梳理(三)YOLOv5

    ​  前言 YOLOv5 是在 YOLOv4 出来之后没多久就横空出世了.今天笔者介绍一下 YOLOv5 的相关知识.目前 YOLOv5 发布了新的版本,6.0版本.在这里,YOLOv5 也在5.0基 ...

  3. 从0开始用webpack开发antd,react组件库npm包并发布

    一.初始化一个npm包 1.新建一个文件夹(名称随意,建议和报名一致),输入命令 :npm init -y 会自动生成一个包的说明文件 package.json如下(本文以scroll-antd-ta ...

  4. .NET性能优化-你应该为集合类型设置初始大小

    前言 计划开一个新的系列,来讲一讲在工作中经常用到的性能优化手段.思路和如何发现性能瓶颈,后续有时间的话应该会整理一系列的博文出来. 今天要谈的一个性能优化的Tips是一个老生常谈的点,但是也是很多人 ...

  5. 建设Kubernetes生产环境的16条建议

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! Kubernetes是用于构建高度可扩展系统的强大工具. ...

  6. typeScript类型总结

    1.对象类型 {} 用来指定对象中可以包含哪些属性,在属性名后加?表示属性是可选的 语法:{属性名:属性值类型,属性名:属性值类型} 示例: let a:{name:string,age?:numbe ...

  7. 用c++语言socket库函数实现服务端客户端聊天室

    客户端 /* * 程序名:client.cpp,此程序用于演示socket的客户端 * 作者:C语言技术网(www.freecplus.net) 日期:20190525 */ #include < ...

  8. zabbix 线路质量监控自定义python模块(Mysql版),多线程(后来发现使用协程更好)降低系统消耗

    之前零零碎碎写了一些zabbix 线路监控的脚本,工作中agnet较多,每条线路监控需求不一致,比较杂乱,现在整理成一个py模块,集合之前的所有功能 环境 python3.6以上版本,pip3(pip ...

  9. mybatis plus 使用 SQL 保留字(关键字)

    MybatisPlus:使用SQL保留字(关键字)的操作 必须要手动在 entity 的字段上加注解,否则最终会报错,因为 mybatis 或者 mybatis plus 不会自动处理

  10. RapidIO 逻辑层IO操作与Message操作的原理和区别

    接上一篇 SRIO RapidIO (SRIO)协议介绍(一) 1     说明 查看协议手册时会发现,逻辑层的操作分成了IO和Message 2类动作,那么为什么要分成2类操作?从原理和应用角度来看 ...