Spring Boot集成vavr快速入门demo

1.什么是vavr?

初闻vavr,感觉很奇怪,咋这个名字,后面看到它的官网我沉默了,怀疑初创团队付费资讯了UC震惊部如何取名字,好家伙,vavr就是java这四个字倒过来,真的是'颠覆'了java.....

官网截图

官网截图倒置处理后

接下来我会介绍vavr的一些简单特性,为了避免成为官方文档的翻译,我会提炼一下加一些demo,不会深入源码细节,重在使用。如果看到现在你还不知道vavr有啥用,这里先简单说一下这个库我觉得三个重要的'颠覆':

  1. vavr提供通过增强函数接口(提供比jdk自带更加强大便利的接口)。
  2. 提供众多依赖函数式接口的特性(方法)。
  3. 提供接近于scala的集合库(符合函数式编程特性的不可变集合)。

2.vavr知识点介绍

Function接口的增强

jdk自带的函数式接口上篇介绍了,其实无论是需求还是功能都稍稍有点弱,Function记得吧,Function的抽象方法是apply,它的函数作用是传入一个类型转换成另外一个类型。那如果我想要传入两个不同类型转成第三种类型呢,如果看过java.util.function包以后你肯定会说有BiFunction,那三个呢,四个呢,那是不是要自己扩展了。

Fcuntion(0....8)接口

vavr给我们提供了能扩展更多的函数,例如Function类,就提供Function0到 Function8,也就是最多可接受8个参数的函数。例如下面展示的一个拼接:

typescript 复制代码
  @Test
  public void multiFunctionTest() {
    Function4<String, String, Boolean, Integer, String> 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一下就知道了

typescript 复制代码
@Test
public void andThenTest() {
  Function4<String, String, Boolean, Integer, String> func1 =
      (country, name, isMan, score) -> String.format("%s-%s-%s-%d", country, name, isMan ? "男" : "女", score);
  // andThen就是把fun1的返回值然后在进行接下来的func2操作
  Function4<String, String, Boolean, Integer, String> func2 = func1
      .andThen(str -> String.join(":", StrUtil.split(str, '-')));
  System.out.println(func2.apply("中国", "小明", true, 10));
}
// 中国:小明:男:10

于此类似还有compose,但是这个方法只有Function1才有,本质其实就是把执行顺序换一下,其实都是做到类似符合函数

typescript 复制代码
@Test
public void composeTest() {
  Function1<Long, String> func1 = num -> num + "%";
  // 先执行分compose里面的apply, 然后把结果放入func1的apply中
  Function1<Double, String> 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

arduino 复制代码
@Test
public void partialApplyTest() {
  Function4<String, String, Boolean, Integer, String> func1 =
      (country, name, isMan, score) -> String.format("%s-%s-%s-%d", country, name, isMan ? "男" : "女", score);

  Function3<String, Boolean, Integer, String> func2 = func1.apply("中国");
  System.out.println(func2.apply("小明", true, 10));

  Function2<Boolean, Integer, String> func3 = func1.apply("中国", "小明");
  System.out.println(func3.apply(true, 10));

  Function1<Integer, String> func4 = func1.apply("中国", "小明", true);
  System.out.println(func4.apply(10));

  System.out.println(func1.apply("中国", "小明", true, 10));
}
// 中国-小明-男-10
// 中国-小明-男-10
// 中国-小明-男-10
// 中国-小明-男-10

科里化

科里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。只需要调用func的curried()方法就可以把科里化函数,接下来你每次apply()只能传入一个值,他的返回值还是一个科里化的函数。具体看下面代码。

typescript 复制代码
@Test
public void curriedTest() {
  Function4<String, String, Boolean, Integer, String> func1 =
      (country, name, isMan, score) -> String.format("%s-%s-%s-%d", country, name, isMan ? "男" : "女", score);

  Function1<String, Function1<Boolean, Function1<Integer, String>>> func2 = func1.curried().apply("中国");
  Function1<Boolean, Function1<Integer, String>> func3 = func2.apply("小明");
  Function1<Integer, String> func4 = func3.apply(true);
  String result = func4.apply(10);

  System.out.println(result);
}
// 中国-小明-男-10

这样上面任意一个函数都可以进行扩展,复用率大大提升,调用起来方便。

Memorization

见名知意,就是把一个函数的结果存起来,下次再次调用函数直接返回第一次计算的结果。使用方法是只需要调用接口的**memoized()**方法即可。 Emmm...实际作用不多,我如果要演示都只能找个随机数来操作,感觉项目事件中的场景不多。

scss 复制代码
@Test
public void memorizeTest() {
  Function0<Double> 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匹配,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( ) 里面放入匹配预期值,后面放入匹配后需要返回的值。注意模式匹配自动会 b r e a k ,如果 ()里面放入匹配预期值,后面放入匹配后需要返回的值。注意模式匹配自动会break,如果 </math>()里面放入匹配预期值,后面放入匹配后需要返回的值。注意模式匹配自动会break,如果()啥也不写就是类似switch的default

typescript 复制代码
@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函数,其作用是可以匹配多个值。


javascript 复制代码
$()

**: 类似于 switch 语句中的

default

case 的通配符模式。它处理找不到匹配项的情况。

makefile 复制代码
$(value)

**: 这是等值模式,其中一个值只是简单地与输入进行比较。

typescript 复制代码
$(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<Integer> 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,这个不用我多说吧。

scss 复制代码
@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包一下就看起来舒服一些。

csharp 复制代码
  @Test
  public void trySeniorTest() {
    List<Integer> 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>这就表示里面可以放Integer和String,但是往往是需要指定数量的,因为需要指定那个位置的元素是哪个类型。

  • 基础使用

通过Tuple.of可以初始化,你只需要在of里面放入元素,idea会自动帮你推断出Tuple几,然后你使用元素只需要_几号就可以了,例如1号元素就是_1

csharp 复制代码
@Test
public void tupleTest() {
  Tuple2<Integer, String> t2 = Tuple.of(1, "1");
  System.out.println(t2._1);
  System.out.println(t2._2);
}
  • 其他使用

Tuple是不可变的,你可以对它进行修改或者添加,但是进行更改操作它都会返回一个新的元祖。更改很简单调用update加位置()方法,增加也很简单调用append()方法

ini 复制代码
@Test
public void tupleSeniorTest() {
  Tuple2<Integer, String> t2 = Tuple.of(1, "1");
  System.out.println(t2);

  Tuple2<Integer, String> t2s = t2.update1(2);
  System.out.println(t2s);
  
  Tuple3<Integer, String, Double> t3 = t2.append(1.0);
  System.out.println(t3);
}

我很喜欢元祖,因为有时候我很懒,不想干啥都创建一个创建pojo,更不想map到处乱飞,元祖用起来方便也很清晰明了,是两者的权衡,尤其是配合匹配模式使用优雅度直接起飞。但是!!!请注意,vavr的Tuple不支持jackson和json的序列化,这个坑我已经替大家踩过了,http返回值或者是rpc通信时请不要使用。

List/Set/Map

函数式编程很重要一个特性就是不变性,jdk的Collections可以让一个集合类成为不可变,但是....show code

csharp 复制代码
@Test
public void collectionsTest() {
  List<Integer> list = Lists.newArrayList(1, 2, 3);
  System.out.println(list);
  List<Integer> 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。

ini 复制代码
@Test
public void collectionsTest() {
  io.vavr.collection.List<Integer> list = io.vavr.collection.List.of(1, 2);
  // 增加一个元素
  io.vavr.collection.List<Integer> appendList = list.append(3);
  // 丢掉一个元素
  io.vavr.collection.List<Integer> dropList = list.drop(1);
  
  // 变成java的可变list
  List<Integer> 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()

ini 复制代码
@Test
public void multiFunctionTest() {
  Integer num = null;
  Option<Integer> 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<Integer> optional = opt.toJavaOptional();
}
// 0
// true

因为很多方法所以都没放,大部分方法都是和optional一样的,还有一些是vavr通用的,并不是option独有。

Lazy

延迟计算也是函数式编程里面一个特性,尤其是在scala中用的很多,并且第一次计算后会把值进行缓存。对节省内存和提升性能都有很大的帮助。 scala中是通过关键字来做的,但是vavr在java中怎么做到呢。类似于option一样的把变量装载一个''容器''中,取值加载。

csharp 复制代码
@Test
public void lazyTest() {
  // 生成一个随机数给到lazy容器
  Lazy<Double> 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

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springboot-demo</artifactId>
        <groupId>com.et</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>vavr</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--vavr-->
        <dependency>
            <groupId>io.vavr</groupId>
            <artifactId>vavr</artifactId>
            <version>0.10.4</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.20</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>
</project>

controller

less 复制代码
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<User> 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<User> 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

kotlin 复制代码
/*
 * 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<User> findGithubUser(String username) {
        return Try.of(() -> restTemplate.getForObject("https://api.github.com/users/{username}", User.class, username));

    }

    public Try<User> findGithubUserAndFail(String username) {
        return Try.of(() -> restTemplate.getForObject("https://api.twitter.com/users/fail/{username}", User.class, username));
    }

}

entity

typescript 复制代码
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

kotlin 复制代码
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();
    }
}

以上只是一些关键代码,所有代码请参见下面代码仓库

代码仓库

4.测试

启动Spring Boot应用

测试查询接口

5.引用

相关推荐
蓝澈11218 分钟前
迪杰斯特拉算法之解决单源最短路径问题
java·数据结构
Kali_0715 分钟前
使用 Mathematical_Expression 从零开始实现数学题目的作答小游戏【可复制代码】
java·人工智能·免费
rzl0227 分钟前
java web5(黑马)
java·开发语言·前端
君爱学习32 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl1 小时前
深度解读jdk8 HashMap设计与源码
java
Falling421 小时前
使用 CNB 构建并部署maven项目
后端
guojl1 小时前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假1 小时前
我们来讲一讲 ConcurrentHashMap
后端
爱上语文1 小时前
Redis基础(5):Redis的Java客户端
java·开发语言·数据库·redis·后端