前言
在上篇文章我们已经实现了一个简单Quarkus应用,以及尝试了如何将Quarkus应用打包本地运行,本文旨在进一步讨论在Quarkus进行简单的响应式编程。
响应式编程与传统编程
大部分的程序员熟悉的程序代码都是这样的:
@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/ 即可看到相应的文档
然后运行就可以看到程序的结果: