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. Metalama简介4.使用Fabric操作项目或命名空间

    使用基于Roslyn的编译时AOP框架来解决.NET项目的代码复用问题 Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架 Metalama简介2.利用Aspect在编译时进行消除重 ...

  2. XCTF练习题---MISC---a_good_idea

    XCTF练习题---MISC---a_good_idea flag:NCTF{m1sc_1s_very_funny!!!} 解题步骤: 1.观察题目,下载附件 2.到手以后发现是一张图片,尝试修改文件 ...

  3. 架构师必备:Redis的几种集群方案

    结论 有以下几种Redis集群方案,先说结论: Redis cluster:应当优先考虑使用Redis cluster. codis:旧项目如果仍在使用codis,可继续使用,但也推荐迁移到Redis ...

  4. 一次IOS通知推送问题排查全过程

    原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 发现问题 在上周一个将要下班的夜晚,测试突然和我打招呼,说IOS推送的修复更新上线后存在问题,后台报错. 连忙跑到测试那 ...

  5. 深度长文:深入理解Ceph存储架构

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 本文是一篇Ceph存储架构技术文章,内容深入到每个存储特 ...

  6. ZooKeeper 到底解决了什么问题?

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 目标 ZooKeeper 很流行,有个基本的疑问: Zo ...

  7. 审计 Linux 系统的操作行为的 5 种方案对比

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 很多时候我们为了安全审计或者故障跟踪排错,可能会记录分析 ...

  8. 前端面试 -Vue2系列

    vue 1为啥用Vue? 1MVVM 数据的双向绑定 2指令系统 不需要操作DOM 3组件化 2v-show和v-if.v-for v-show 通过 display:none 隐藏元素,DOM还在. ...

  9. mybatis入门,CRUD,万能Map,模糊查询

    第一个Mybatis程序 核心配置文件mybatis-config.xml <?xml version="1.0" encoding="UTF-8" ?& ...

  10. 有了 Promise 和 then,为什么还要使用 async?

    有了 Promise 和 then,为什么还要使用 async? 本文写于 2020 年 5 月 13 日 最近代码写着写着,我突然意识到一个问题--我们既然已经有了 Promise 和 then,为 ...