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. 接口测试 Mock 实战(二) | 结合 jq 完成批量化的手工 Mock

    因为本章的内容是使用jq工具配合完成,因此在开始部分会先花一定的篇幅介绍jq机器使用,如果读者已经熟悉jq,可以直接跳过这部分.先来看应用场景,App 经常会有一些信息展示的列表页,比如商家的菜品.股 ...

  2. debian 11 开启 samba 共享文件夹

    安装 apt-get install   samba 安装时,提示搜索不到 此包时 解决办法1:apt-get update  更新源 解决办法2:更改镜像源,可以采用阿里云,网易云等镜像站里提供的镜 ...

  3. PHP代码审计之SQL注入

    代码审计之SQL注入 SQL注入攻击(SQLInjection),是攻击者在表单中提交精心构造的sql语句,改变原来的sql语句,如果web程序没有对提交的数据经过检查,那么就会造成sql注入攻击. ...

  4. Next.js 在 Serverless 中从踩坑到破茧重生

    作者 杨苏博,偏后端的全栈开发,目前负责腾云扣钉的 Cloud Studio 产品.在团队中负责接技术架构设计与 Review.Cloud Studio 编辑器内核设计与开发.部分核心插件设计与开发: ...

  5. Hbase数据库安装部署

    Hbase单机版安装 hbase介绍 HBase – Hadoop Database是一个分布式的.面向列的开源数据库,该技术来源于Chang et al所撰写的Google论文"Bigta ...

  6. 【多线程】线程强制执行 join()

    线程强制执行 join() Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞 : 可以想象成插队. 代码示例: /** * @Description 测试join方法 * @Auth ...

  7. 重新认识 MSBuild - 1

    前言 很多人一谈到 MSBuild,脑子里就会出现 "XML"."只能用 VS 的属性框图形界面操作"."可定制性和扩展性差" 和 &quo ...

  8. 用Repo管理自己的本地仓库

    AOSP使用Repo工具管理项目源码.而Repo工具则依赖一个名叫manifest的git仓库来记录Android源码中都包含哪些子仓库. 进入Android源码根目录下的.repo目录,可以看到ma ...

  9. 从标准输入流中读取并执行shell指定函数

    巧妙的ohmytmux配置 看oh my tmux的配置,发现他们很巧妙的将配置和shell函数放到一个文件里 比如切换鼠标模式的相关配置和shell函数, # : << EOF # .. ...

  10. Web 前端实战(三):雷达图

    前言 在<Canvas 线性图形(五):多边形>实现了绘制多边形的函数.本篇文章将记录如何绘制雷达图.最终实现的效果是这样的: 绘制雷达图 雷达图里外层 如动图中所示,雷达图从里到外一共有 ...