Quarkus框架实践-响应式编程与Mutiny

2,303 阅读5分钟

前言

在上篇文章我们已经实现了一个简单Quarkus应用,以及尝试了如何将Quarkus应用打包本地运行,本文旨在进一步讨论在Quarkus进行简单的响应式编程。

响应式编程与传统编程

image.png 大部分的程序员熟悉的程序代码都是这样的:

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}

在 Quarkus,你可以使用响应式流  Publisher 作为返回的类型

@GET
@Produces(MediaType.TEXT_PLAIN)
public Publisher<String> hello() {
return "hello";
}

在这篇文章,我们会使用Mutiny框架作为讨论响应式编程的工具,但实际在java上使用 ReactiveStreams APIs, Java 8 CompletableFuture, RxJava 2也都可以达到相同的效果。

Mutiny简介

Mutiny与其他反应式编程的库是非常不同的。它采用不同的方法来设计你的程序。在Mutiny中,一切都是由事件驱动的:你接收事件,然后你对此做出反应。这个事件驱动的方面包含了分布式系统的异步特性,并提供了一种优雅而精确的方式来表达连续性。

在Mutiny的首页我们可以看见一段这样的程序,完美的表达了Mutiny的特性

//请求的部分
Uni<String> request = (...)
//异步处理
request.ifNoItem().after(ofMillis(100))
//失败处理
    .failWith(new Exception("💥")) 
    .onFailure().recoverWithItem(fail -> "📦") 
    //订阅程序并且打印运行
    .subscribe().with(item -> log("👍 " + item));

Mutiny也是使用Quarkus编写反应式应用程序的主要模型。

Mutiny的使用

大部分的Quarkus的响应式编程已经不在使用reactive,而是转向依赖于Mutiny。
同时也可以显式的添加quarkus-mutiny 依赖

mvn quarkus:add-extension -Dextensions=mutiny

或者显式的使用pom文件引入依赖:

<dependency> 
  <groupId>io.quarkus</groupId> 
  <artifactId>quarkus-mutiny</artifactId>
</dependency>

引用以后简单跑一个样例:

public static void main(String[] args) {
    Uni.createFrom().item("hello")
            .onItem().transform(item -> item + " mutiny")
            .onItem().transform(String::toUpperCase)
            .subscribe().with(item -> System.out.println(">> " + item));
}

运行的结果: >> HELLO MUTINY 简单的讨论一下这个条信息是如果进行构建的,我们构建了一个处理管道,管道接收到一个项目,处理项目并最终消费它。

首先,我们创建了一个Uni,这是mutiny提供的两种类型中的一种。Uni会产生一个信息或者失败就发送0个信息。

我们创建一个发出“hello”内容的Uni。这是我们管道的输入信息。然后我们处理这个item:

然后我们加上“Mutiny”,并且转换成大写字符串。

这就是我们这个管道的处理部分,然后最后订阅了前面的管道,数出了数据:HELLO MUTINY

最后一部分非常重要。如果你没有订阅,一切都不会发生。mutiny类型是惰性的的,这意味着你需要作出一些触发性质的动作。如果你不这样做,计算就不会开始。

如果程序没有运行,注意自己有没有订阅!

Mutiny 与Quarkus

刚刚我们已经在pom中引入了Quarkus-Mutiny的包,可以直接运行我们的程序, 这次我们的目的是运用 quarkus和Mutiny简单的做一个天气预报的系统,通过restful的接口返回天气预报的值:

设置查询数据库返回城市代码

首先在pom文件里面加入依赖

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-jdbc-mysql</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-spring-data-jpa</artifactId>
        </dependency>

然后创建实体对象:

@Entity(name = "t_city")
public class TCity {

    @Id
    private Integer id;
    @Column
    private String name;
    @Column
    private String code;
}

在application.properties中配置数据库的连接:

quarkus.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC
quarkus.datasource.driver=com.mysql.cj.jdbc.Driver
quarkus.datasource.username=root
quarkus.datasource.password=123456
quarkus.datasource.min-size=8
quarkus.datasource.max-size=8

然后我们就可以使用jpa来操作数据库了:

public interface CityMapper extends CrudRepository<TCity, Integer> {

    Optional<TCity> findByName(String name);
}

@Inject
CityMapper cityMapper;


public Optional<TCity> findByName(String name ) {
    return cityMapper.findByName(name );
}

跟据城市名称查询code,可以改写到redis缓存,如果缓存内不存在则查询数据库。

@ApplicationScoped
public class CityService {

    private static final Logger LOGGER = Logger.getLogger(ReactiveGreetingService.class.getName());

    private final Map<String,String> map=new HashMap<>();

    @Inject
    CityMapper cityMapper;


    public String  getCode(String  name) {
        if (map.containsKey(name)){
            return map.get(name);
        }
        else{
            Optional<City> optional= cityMapper.findByName(name);
            if (!optional.isPresent()) {
                throw new WebApplicationException("org.reactive.model.City with name of " + name + " does not exist.", 404);
            }
            String code =optional.get().getCode();
            map.put(name,code);
            return code;
        }
    }
}

做好转换code和名称的程序,接下来就是调用第三方api的部分: 首先我们引入pom文件

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client</artifactId>
</dependency>

然后编写根据网址访问并且返回Stream的方法

@ApplicationScoped
public class RestClient {
    
    private static final Logger LOGGER = Logger.getLogger(RestClient.class.getName());

    private static final String weather_url = "http://t.weather.sojson.com/api/weather/city/";

    private final Client httpClient;

    public RestClient() {
        this.httpClient = ResteasyClientBuilder.newBuilder().build();
    }

    public InputStream getStream(String id) {

        return   httpClient.target(weather_url+id).request(
                ).get(InputStream.class);
    }
}

天气网站都是根据城市code返回天气预报信息的具体信息,所以上面先编写了根据名称查询code的方法

根据上文的部分编写处理类,处理以后输出给前端的接口

@ApplicationScoped
public class ReactiveGreetingService {


    private static final Logger LOGGER = Logger.getLogger(ReactiveGreetingService.class.getName());


    @Inject
    RestClient restClient;

    @Inject
    CityService cityService;

    private static Executor executor = new ForkJoinPool();


    public Uni<Response> getOne() {

        return Uni.createFrom().item(restClient.get());
    }

    public Uni<Response> getWeather(String id) {
        LOGGER.info("id "+id);

        return Uni.createFrom().item(restClient.getById(id)); // 1
    }


    public Uni<Weather> getByName(String name) {
        LOGGER.info("获取数据 "+name);
        String code= cityService.getCode(name);
        LOGGER.info("下面请求第三方数据接口 "+code);
        return Uni.createFrom().item(restClient.getStream(code))
                .onItem().transformToUni(this::invokeRemoteGreetingService)
                .onFailure().recoverWithItem(new Weather()); // 1
    }

    public Uni<Weather> getByCode(String code) {

        return   Uni.createFrom().item(restClient.getStream(code))
                .onItem().transformToUni(this::invokeRemoteGreetingService)
                .onFailure().recoverWithItem(new Weather());
    }

    Uni<Weather> invokeRemoteGreetingService(InputStream inputStream) {
        return Uni.createFrom().item(inputStream)
                .emitOn(executor)
                .onItem().delayIt().by(Duration.ofSeconds(1))
                .onItem().transform(s -> {
                    try {
                        return JSONObject.parseObject(s, Weather.class);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    return new Weather();
                });
    }

}

再在pom里引入swagger的相关依赖

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>

在代码中加入相关的注释:

@Path("weather")
@Tag(name = "WeatherResource",description = "获取天气预报")
public class ReactiveGreetingResource {
    @Inject
    ReactiveGreetingService service;


    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Operation(summary = "获取天气预报", description = "这是一个获取默认城市天气预报的接口")
    public Uni<Response> getOne() {
        return service.getOne();
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/getByName/{name}")
    @Operation(summary = "城市名称天气预报", description = "这是一个根据城市名称获取天气预报的接口")
    public Uni<Weather> getByName(@PathParam("name") String name) {
        return service.getByName(name);
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/getByCode/{code}")
    @Operation(summary = "城市代码天气预报", description = "这是一个根据城市code获取天气预报的接口")
    public Uni<Weather> getByCode(@PathParam("code") String code) {
        return service.getByCode(code);
    }

}

访问http://localhost:8080/q/swagger-ui/ 即可看到相应的文档

屏幕快照 2022-03-06 下午9.58.42.png

然后运行就可以看到程序的结果:

屏幕快照 2022-03-06 下午10.59.38.png