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. 一文读懂原子操作、内存屏障、锁(偏向锁、轻量级锁、重量级锁、自旋锁)、Disruptor、Go Context之上半部分

    我不想卷,我是被逼的 在做了几年前端之后,发现互联网行情比想象的差,不如赶紧学点后端知识,被裁之后也可接个私活不至于饿死.学习两周Go,如盲人摸象般不知重点,那么重点谁知道呢?肯定是使用Go的后端工程 ...

  2. SSM框架中返回的是字符串还是页面跳转的问题

    如果你在控制器前的注解是@RestController的话,将返回controller方法指定的String值,@RestController注解相当于@ResponseBody和@Controlle ...

  3. [cf]Codeforces Round #784(Div 4)

    由于一次比赛被虐得太惨,,生发开始写blog的想法,于是便有了这篇随笔(找了个近期的cf比赛练练手(bushi))第一次写blog,多多包涵. 第二场cf比赛,第一场打的Div2,被虐太惨,所以第二场 ...

  4. 【算法】归并排序(Merge Sort)(五)

    归并排序(Merge Sort) 归并排序是建立在归并操作上的一种有效的排序算法.该算法是采用分治法(Divide and Conquer)的一个非常典型的应用.将已有序的子序列合并,得到完全有序的序 ...

  5. Hbase——API操作

    1.判断表是否存在 public static boolean isTableExit(String tableName) throws IOException { // //获取配置文件信息 // ...

  6. python之三元表达式与生成式与匿名与内置函数(部分)

    目录 三元表达式 各种生成式 列表生成式(可同样作用于集合) 字典生成式 匿名函数 重要内置函数 map() zip() filter() reduce() 常见内置函数(部分) 三元表达式 三元表达 ...

  7. Python 生成图片验证码

    验证码图片生成 #!/usr/bin/env python # -*- coding: utf-8 -*- # refer to `https://bitbucket.org/akorn/wheezy ...

  8. Spring IOC 常用注解与使用

    @Component 注解@component代表spring ioc 会把这个类扫描生成Bean实例 @Component public class Role{ @Value("1&quo ...

  9. 《HALCON数字图像处理》第六章笔记

    目录 第六章 图像增强 图像增强的概念和分类 灰度变换 直方图处理 图像的平滑 图像的锐化 图像的彩色增强 我在Gitee上建了个仓库,会将学习书本的时候打的一些代码上传上去,笔记中所有代码都在仓库里 ...

  10. Linux系统sed命令常用参数实战

    Linux系统sed命令常用参数实战 常用参数 -n 输出某行的文本内容,通常与p联合使用, -e 命令行模式下进行sed的动作编辑,输出编辑后的内容,源文件不会发生变化 -f 以命令中指定的scri ...