Spring Boot集成vavr快速入门demo
1.什么是vavr?
初闻vavr,感觉很奇怪,咋这个名字,后面看到它的官网我沉默了,怀疑初创团队付费资讯了UC震惊部如何取名字,好家伙,vavr就是java这四个字倒过来,真的是’颠覆’了java…..
官网截图
官网截图倒置处理后
接下来我会介绍vavr的一些简单特性,为了避免成为官方文档的翻译,我会提炼一下加一些demo,不会深入源码细节,重在使用。如果看到现在你还不知道vavr有啥用,这里先简单说一下这个库我觉得三个重要的‘颠覆’:
- vavr提供通过增强函数接口(提供比jdk自带更加强大便利的接口)。
- 提供众多依赖函数式接口的特性(方法)。
- 提供接近于scala的集合库(符合函数式编程特性的不可变集合)。
2.vavr知识点介绍
Function接口的增强
jdk自带的函数式接口上篇介绍了,其实无论是需求还是功能都稍稍有点弱,Function记得吧,Function的抽象方法是apply,它的函数作用是传入一个类型转换成另外一个类型。那如果我想要传入两个不同类型转成第三种类型呢,如果看过java.util.function包以后你肯定会说有BiFunction,那三个呢,四个呢,那是不是要自己扩展了。
Fcuntion(0….8)接口
vavr给我们提供了能扩展更多的函数,例如Function类,就提供Function0到 Function8,也就是最多可接受8个参数的函数。例如下面展示的一个拼接:
@Test
public void multiFunctionTest() {
Function4 func =
(country, name, isMan, score) -> String.format("%s-%s-%s-%d", country, name, isMan ? "男" : "女", score);
System.out.println(func.apply("中国", "小明", true, 10));
}
// 中国-小明-男-10
更多函数式特性
vavr还对函数做了增强,除了jdk也有的andThen()和compose()。vavr的接口还有函数编程的真闭包特性,例如科里化、Lifting、Memoization等,下面一一介绍
Composition
这个jdk其实也自带,其实就是数学中的复合函数概念,f(x)的y可以是g(x)的xg(f(x))。 其中有两个方法都可以完成,一个是andThen(),一个是个compose(), demo一下就知道了
@Test
public void andThenTest() {
Function4 func1 =
(country, name, isMan, score) -> String.format("%s-%s-%s-%d", country, name, isMan ? "男" : "女", score);
// andThen就是把fun1的返回值然后在进行接下来的func2操作
Function4 func2 = func1
.andThen(str -> String.join(":", StrUtil.split(str, '-')));
System.out.println(func2.apply("中国", "小明", true, 10));
}
// 中国:小明:男:10
于此类似还有compose,但是这个方法只有Function1才有,本质其实就是把执行顺序换一下,其实都是做到类似符合函数
@Test
public void composeTest() {
Function1 func1 = num -> num + "%";
// 先执行分compose里面的apply, 然后把结果放入func1的apply中
Function1 func2 = func1.compose((Double num) -> Math.round(num));
System.out.println(func2.apply(12.25));
}
// 12%
PartialApply
部分应用是指假如的Fcuntion入参有5个,你apply()中传入了2个,那么编译器不会报错,但是apply也不会正常执行你的函数,而是再生成一个函数,这个函数的入参只有3个,是由原来五个参数其中两个被写固定值转换而来的。show code
@Test
public void partialApplyTest() {
Function4 func1 =
(country, name, isMan, score) -> String.format("%s-%s-%s-%d", country, name, isMan ? "男" : "女", score);
Function3 func2 = func1.apply("中国");
System.out.println(func2.apply("小明", true, 10));
Function2 func3 = func1.apply("中国", "小明");
System.out.println(func3.apply(true, 10));
Function1 func4 = func1.apply("中国", "小明", true);
System.out.println(func4.apply(10));
System.out.println(func1.apply("中国", "小明", true, 10));
}
// 中国-小明-男-10
// 中国-小明-男-10
// 中国-小明-男-10
// 中国-小明-男-10
科里化
科里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。只需要调用func的curried()方法就可以把科里化函数,接下来你每次apply()只能传入一个值,他的返回值还是一个科里化的函数。具体看下面代码。
@Test
public void curriedTest() {
Function4 func1 =
(country, name, isMan, score) -> String.format("%s-%s-%s-%d", country, name, isMan ? "男" : "女", score);
Function1 func2 = func1.curried().apply("中国");
Function1 func3 = func2.apply("小明");
Function1 func4 = func3.apply(true);
String result = func4.apply(10);
System.out.println(result);
}
// 中国-小明-男-10
这样上面任意一个函数都可以进行扩展,复用率大大提升,调用起来方便。
Memorization
见名知意,就是把一个函数的结果存起来,下次再次调用函数直接返回第一次计算的结果。使用方法是只需要调用接口的memoized()方法即可。 Emmm…实际作用不多,我如果要演示都只能找个随机数来操作,感觉项目事件中的场景不多。
@Test
public void memorizeTest() {
Function0 hashCache = Function0.of(Math::random).memoized();
double randomValue1 = hashCache.apply();
System.out.println(randomValue1);
double randomValue2 = hashCache.apply();
System.out.println(randomValue2);
}
// 0.6590067689384973
// 0.6590067689384973
利用函数式接口完成的新特性
模式匹配
好消息好消息,java14已经支持模式匹配了,什么?你们公司还没吃上14,哦,我们公司也没….但是使用vavr可以体验到scala般的模式匹配。Java的switch只能对常量起作用,而且限制非常多,虽然jdk7加入的string,但是居然底层用equals()去比较,这意味着你传入一个null,直接NPE抛出来而不是走到default。而模式匹配不仅仅可以规避种种问题,还可以对另外一个函数的返回值起作用代码函数也能节省不少。
- 语法基础演示,
Match(里面放入要匹配的变量).of后面就开始进行case匹配,其中$()里面放入匹配预期值,后面放入匹配后需要返回的值。注意模式匹配自动会break,如果$()啥也不写就是类似switch的default
@Test public void showTest() { int input = 2; String result = Match(input).of( Case($(1), "one"), Case($(2), "two"), Case($(3), "three"), Case($(), "?")); System.out.println(result); } // two- 语法高级匹配演示
$()其实还有一个重载方法,是传入一个predicate函数,vavr有自己的predicate函数式接口,里面有很多方法,例如下面代码块的isIn()就是predicate里面的方法,他的返回值就是一个predicate函数,其作用是可以匹配多个值。
- $(): 类似于 switch 语句中的 default case 的通配符模式。它处理找不到匹配项的情况。
- $(value): 这是等值模式,其中一个值只是简单地与输入进行比较。
- $(predicate): 这是条件模式,其中predicate函数应用于输入,结果布尔值用于做出决定。
@Test public void isInTest() { int input = 1; String result = Match(input).of( Case($(isIn(0, 1)), "zero or one"), Case($(2), "two"), Case($(3), "three"), Case($(), "?")); System.out.println(result); } // zero or one@Test public void anyOfTest() { Integer year = 1990; String result = Match(year).of( Case($(anyOf(isIn(1990, 1991, 1992), is(1986))), "Age match"), Case($(), "No age match")); System.out.println(result); } // Age match@Test public void customTest() { int i = 5; List container = Lists.newArrayList(1, 2, 3, 4); String result = Match(i).of( // 这里可以换成方法引用, 为了更加好理解,就使用lambda写了 Case($(e -> container.contains(e)), "Even Single Digit"), Case($(), "Out of range")); System.out.println(result); } // Out of range- 副作用展示
看到上面的例子,其实每一个case都返回了一个值,有时候我们匹配到,但是没有东西返回,仅仅通过副作用来dosomething。下面代码看起来比较绕,我稍稍解释一下,因为Case的第二个参数我们最开始是放返回值,现在如果要使用副作用必须放一个Supplier,别问我为什么这是人家要求的,所以必须使用() ->,那返回什么呢,这里放入run()的方法,run()方法的入参是一个Runnable,出参是一个Void,那么这个Void是可以忽略掉,注意这个Void是vavr提供的,不是jdk的关键字void。只需要把副作用的代码放入构建一个Runnable接口就可以啦。Runnable就是lang包下的Runnable,这个不用我多说吧。
@Test public void sideEffectsTest() { int i = 4; Match(i).of( Case($(isIn(2, 4, 6, 8)), () -> run(() -> System.out.println("这是第一类"))), Case($(isIn(1, 3, 5, 7, 9)), () -> run(() -> System.out.println("这是第二类"))), Case($(), o -> run(() -> System.out.println("没有找到")))); }Try
Try类似于jdk的try catch。在Try中执行的代码不会抛异常,异常和正常返回值都会被vavr接管,然后通过Try的在进行返回。具体用法就是Try.of()。 然后of里面传入一个supplier,入参固定是() ->,返回值就是你的函数产生的结果。
- 基本演示
@Test public void tryTest() { Try result = Try.of(() -> 1 / 0); // 返回是否失败 System.out.println(result.isSuccess()); // 返回异常原因, 如果没有异常进行获取则会UOE System.out.println(result.getCause()); // 获取返回值, 如果有异常则返回null System.out.println(result.getOrNull()); // 获取返回值, 如果有异常则返回设置的默认值 System.out.println(result.getOrElse(0)); } // false // java.lang.ArithmeticException: / by zero // null // 0里面其实还自带了很多方法,有点类似jdk的optional,也是类似于一个’‘容器’‘,只不过它容纳的是可能出错的行为,可以让你进行接下的处理或者兜底方案。一般简单处理我会使用Try,因为真的很方便。 例如原来在JSON.parseObject()的时候我一般都会用try catch包一下,希望能够健壮一点,鬼知道上游传过来的是什么串,但是try catch写的挺难看的,如果使用Try包一下就看起来舒服一些。
@Test public void trySeniorTest() { List list = Try.of(() -> JSON.parseArray("json", Integer.class)) .getOrElse(Collections.emptyList()); System.out.println(list); } // []不可变的集合类
Tuple
众所周知,java是没有元祖的,但是有时候元祖是真的好用,vavr通过泛型实现了元祖,可以使用Tuple的静态工厂创建元祖,并且使用idea的自动推断或者java10的var类型推断直接效率高到爆有没有。使用方法也和scala差不多,
元祖(Tuple)由不同元素组成,每个元素可以储存不同类型的数据。有点像多个泛型的List,例如List这个list就只能放Integer, 元祖是Tuple这就表示里面可以放Integer和String,但是往往是需要指定数量的,因为需要指定那个位置的元素是哪个类型。- 基础使用
通过Tuple.of可以初始化,你只需要在of里面放入元素,idea会自动帮你推断出Tuple几,然后你使用元素只需要_几号就可以了,例如1号元素就是_1
@Test public void tupleTest() { Tuple2 t2 = Tuple.of(1, "1"); System.out.println(t2._1); System.out.println(t2._2); }- 其他使用
Tuple是不可变的,你可以对它进行修改或者添加,但是进行更改操作它都会返回一个新的元祖。更改很简单调用update加位置()方法,增加也很简单调用append()方法
@Test public void tupleSeniorTest() { Tuple2 t2 = Tuple.of(1, "1"); System.out.println(t2); Tuple2 t2s = t2.update1(2); System.out.println(t2s); Tuple3 t3 = t2.append(1.0); System.out.println(t3); }我很喜欢元祖,因为有时候我很懒,不想干啥都创建一个创建pojo,更不想map到处乱飞,元祖用起来方便也很清晰明了,是两者的权衡,尤其是配合匹配模式使用优雅度直接起飞。但是!!!请注意,vavr的Tuple不支持jackson和json的序列化,这个坑我已经替大家踩过了,http返回值或者是rpc通信时请不要使用。
List/Set/Map
函数式编程很重要一个特性就是不变性,jdk的Collections可以让一个集合类成为不可变,但是….show code
@Test public void collectionsTest() { List list = Lists.newArrayList(1, 2, 3); System.out.println(list); List unmodifiableList = Collections.unmodifiableList(list); System.out.println(unmodifiableList); list.add(1); System.out.println(list); System.out.println(unmodifiableList); unmodifiableList.add(1); } // [1, 2, 3] // [1, 2, 3] // [1, 2, 3, 1] // [1, 2, 3, 1] // // java.lang.UnsupportedOperationException上面代码可以看到,Collections不可变的list是原来list的一个浅拷贝,原来的list进行元素的更改依然会改动这个所谓的’不可变’list。
- vavr的list
vavr的list使用List.of()来创建,创建后不可变,但是可以增加或者删除元素,聪明的你肯定知道了,每次改动以后都会生成一个新的不可变list。
@Test public void collectionsTest() { io.vavr.collection.List list = io.vavr.collection.List.of(1, 2); // 增加一个元素 io.vavr.collection.List appendList = list.append(3); // 丢掉一个元素 io.vavr.collection.List dropList = list.drop(1); // 变成java的可变list List javaList = list.asJava(); }另外vavr的list可以直接使用stream算子,不许用在通过stream()转换成Stream流,然后再使用算子,不能说和scala一模一样,只能说毫无区别。类似的也提供提供了更多 Functional 的 API,比如
- take(Integer) 取前 n 个值
- tail() 取除了头结点外的集合
- zipWithIndex() 使得便利时可以拿到索引(不用 fori)
- find(Predicate) 基于条件查询值,在 Java 标准库得使用 filter + findFirst 才能实现 …..
其他函数式编程特性
Option
不装了,我摊牌,这个option和jdk的optional是一样的,应该灵感都是来自于guava的Optional。不过vavr的Otion是一个接口,它有两个实现类,分别是Some和None。前者有值的状态,后者无值的状态。食用方法是Option.of()
@Test public void multiFunctionTest() { Integer num = null; Option opt = Option.of(num); // 这个和optional一样 Integer result = opt.getOrElse(0); System.out.println(result); // 如果是None则会返回ture boolean isEmpty = opt.isEmpty(); System.out.println(isEmpty); // 变成java的optional Optional optional = opt.toJavaOptional(); } // 0 // true因为很多方法所以都没放,大部分方法都是和optional一样的,还有一些是vavr通用的,并不是option独有。
Lazy
延迟计算也是函数式编程里面一个特性,尤其是在scala中用的很多,并且第一次计算后会把值进行缓存。对节省内存和提升性能都有很大的帮助。 scala中是通过关键字来做的,但是vavr在java中怎么做到呢。类似于option一样的把变量装载一个‘’容器‘’中,取值加载。
@Test public void lazyTest() { // 生成一个随机数给到lazy容器 Lazy lazy = Lazy.of(Math::random); // 判断是否已经获取过了 System.out.println(lazy.isEvaluated()); // 正式获取lazy的值 System.out.println(lazy.get()); // 看看现在是否计算了 System.out.println(lazy.isEvaluated()); // 再次获取lazy的值 System.out.println(lazy.get()); } // false // 0.896267693320266 // true // 0.896267693320266当然如果是真的感兴趣,推荐大家看一下resilience4j,这是用vavr写的限流熔断降级中间件,用来代替Hystrix。代码质量真的是非常高,用它来学习函数式编程我认为是目前最好的材料,唯独就是比较难啃,因为函数编程本身就是写起来很爽但是对viewer不是很友好。
3.代码工程
实验目标
使用vavr来编写查询github用户信息的接口
pom.xml
springboot-demo com.et 1.0-SNAPSHOT 4.0.0 vavr 8 8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-autoconfigure org.springframework.boot spring-boot-starter-test test io.vavr vavr 0.10.4 cn.hutool hutool-all 5.8.20 test org.projectlombok lombokcontroller
package com.et.vavr.controller; import com.et.vavr.domain.User; import com.et.vavr.service.GithubService; import io.vavr.control.Try; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * * @author armena */ @RestController @RequestMapping("/api/v1/github") public class GithubController { @Autowired private GithubService githubService; @GetMapping(path = "/{username}", produces = "application/json;charset=UTF-8") public ResponseEntity get(@Valid @PathVariable String username ) { Try githubUserProfile = githubService.findGithubUser(username); if (githubUserProfile.isFailure()) { return ResponseEntity.status(HttpStatus.FAILED_DEPENDENCY).body(githubUserProfile.getCause().getMessage()); } if (githubUserProfile.isEmpty()) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Response is empty"); } if (githubUserProfile.isSuccess()) { return ResponseEntity.status(HttpStatus.OK).body(githubUserProfile.get()); } return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body("username is not valid"); } @GetMapping(path = "/fail/{username}", produces = "application/json;charset=UTF-8") public ResponseEntity getFail(@Valid @PathVariable String username ) { Try githubUserProfile = githubService.findGithubUserAndFail(username); if (githubUserProfile.isFailure()) { System.out.println("Fail case"); return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(githubUserProfile.getCause().getMessage()); } if (githubUserProfile.isEmpty()) { System.out.println("Empty case"); return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Response is empty"); } if (githubUserProfile.isSuccess()) { System.out.println("Success case"); return ResponseEntity.status(HttpStatus.OK).body(githubUserProfile.get()); } return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body("username is not valid"); } }service
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.et.vavr.service; import com.et.vavr.domain.User; import io.vavr.control.Try; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; /** * * @author Admin */ @Service public class GithubService { @Autowired private RestTemplate restTemplate; public Try findGithubUser(String username) { return Try.of(() -> restTemplate.getForObject("https://api.github.com/users/{username}", User.class, username)); } public Try findGithubUserAndFail(String username) { return Try.of(() -> restTemplate.getForObject("https://api.twitter.com/users/fail/{username}", User.class, username)); } }entity
package com.et.vavr.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @ToString public class User { private String id; private String login; private String location; }config
package com.et.vavr.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class AppConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }以上只是一些关键代码,所有代码请参见下面代码仓库
代码仓库
- GitHub - Harries/springboot-demo: a simple springboot demo with some components for example: redis,solr,rockmq and so on.
4.测试
启动Spring Boot应用
测试查询接口
- 访问http://127.0.0.1:8088/api/v1/github/{username}
- 返回相应的用户信息
5.引用
- https://www.vavr.io/
- java8函数式编程-下 - Oreoft's blog
- GitHub - Harries/springboot-demo: a simple springboot demo with some components for example: redis,solr,rockmq and so on.
- vavr的list
- 其他使用
- 基础使用
- 基本演示
- 副作用展示
- 语法高级匹配演示


