jax-rs jax-ws_迟来总比没有好:SSE或服务器发送的事件现在已在JAX-RS中

jax-rs jax-ws

服务器发送的事件 (或简称为SSE )是非常有用的协议,它允许服务器通过HTTP将数据推送到客户端。 这是我们的Web浏览器支持的年龄,但是令人惊讶的是, JAX-RS规范在很长一段时间内都忽略了这一点。 尽管Jersey提供了适用于SSE媒体类型的扩展名,但该API尚未正式化,因此不能移植到其他JAX-RS实现中。

幸运的是, JAX-RS 2.1 (也称为JSR-370)通过将SSE支持(客户端和服务器端)作为正式规范的一部分,已经改变了这一点。 在今天的帖子中,我们将研究如何使用最近发布的出色的Apache CXF框架3.2.0版将SSE支持集成到现有的Java REST(ful) Web服务中。 实际上,除了自举之外,实际上没有CXF特定的东西,所有示例都应在实现JAX-RS 2.1规范的任何其他框架中工作。

事不宜迟,让我们开始吧。 由于这些天大量的Java项目都建立在出色的Spring Framework之上,因此我们的示例应用程序将使用Spring BootApache CXF Spring Boot Integration使我们Swift起步。 老好伙伴Apache Maven也可以通过管理项目依赖项来帮助我们。

org.springframework.boot
    spring-boot-starter
    1.5.8.RELEASE



    org.apache.cxf
    cxf-rt-frontend-jaxrs
    3.2.0



    org.apache.cxf
    cxf-spring-boot-starter-jaxrs
    3.2.0



    org.apache.cxf
    cxf-rt-rs-client
    3.2.0



     org.apache.cxf
     cxf-rt-rs-sse
     3.2.0

在后台, Apache CXF正在使用Atmosphere框架来实现SSE传输,因此这是我们必须包含的另一个依赖项。

org.atmosphere
    atmosphere-runtime
    2.4.14

有关依赖于Atmosphere框架的详细信息导致需要提供其他配置设置,即transportId ,以便确保在运行时拾取支持SSE的传输。 相关详细信息可以添加到application.yml文件中:

cxf:
  servlet:
    init:
      transportId: http://cxf.apache.org/transports/http/sse

太好了,所以基础就在那里,继续前进。 我们将要构建的REST(ful) Web服务将在SSE流中公开虚构的CPU平均负载(为简单起见,随机生成)。 Stats类将构成我们的数据模型。

public class Stats {
    private long timestamp;
    private int load;

    public Stats() {
    }

    public Stats(long timestamp, int load) {
        this.timestamp = timestamp;
        this.load = load;
    }

    // Getters and setters are omitted
    ...
}

说到流, Reactive Streams规范已进入Java 9 ,希望我们能看到Java社区加速采用React式编程模型。 此外,在具有Reactive Streams支持的情况下,开发支持SSE的 REST(ful) Web服务将变得更加容易和直接。 为此,让我们将RxJava 2集成到示例应用程序中。

io.reactivex.rxjava2
    rxjava
    2.1.6

这是开始使用我们的StatsRestService类(典型的JAX-RS资源实现)的好时机。 JAX-RS 2.1中的关键SSE功能以Sse上下文对象为中心,可以像这样注入。

@Service
@Path("/api/stats")
public class StatsRestService {
    @Context 
    public void setSse(Sse sse) {
        // Access Sse context here
    }

Sse上下文之外,我们可以访问两个非常有用的抽象: SseBroadcasterOutboundSseEvent.Builder ,例如:

private SseBroadcaster broadcaster;
private Builder builder;
    
@Context 
public void setSse(Sse sse) {
    this.broadcaster = sse.newBroadcaster();
    this.builder = sse.newEventBuilder();
}

您可能已经猜到了, OutboundSseEvent.Builder构造了OutboundSseEvent类的实例,这些实例可以通过电线发送,而SseBroadcaster则向所有连接的客户端广播相同的SSE流。 话虽如此,我们可以生成OutboundSseEvent的流并将其分发给感兴趣的每个人:

private static void subscribe(final SseBroadcaster broadcaster, final Builder builder) {
    Flowable
        .interval(1, TimeUnit.SECONDS)
        .zipWith(eventsStream(builder), (id, bldr) -> createSseEvent(bldr, id))
        .subscribeOn(Schedulers.single())
        .subscribe(broadcaster::broadcast);
}

private static Flowable<OutboundSseEvent.Builder> eventsStream(final Builder builder) {
    return Flowable.generate(emitter -> emitter.onNext(builder.name("stats")));
}

如果您不熟悉RxJava 2 ,请不用担心,这就是这里发生的情况。 eventsStream方法为stats类型的SSE事件返回有效的无限OutboundSseEvent.Builder实例流。 订阅方法稍微复杂一点。 我们首先创建一个流,该流每秒发出序号,例如fe 0、1、2、3、4、5、6等。 稍后,我们将此流与eventsStream方法返回的流组合在一起,实质上将两个流合并为一个流,该流每秒发出一个元组(number, OutboundSseEvent.Builder 。 公平地说,该元组对我们不是很有用,因此我们将其转换为OutboundSseEvent类的实例,将数字视为SSE事件标识符:

private static final Random RANDOM = new Random();

private static OutboundSseEvent createSseEvent(OutboundSseEvent.Builder builder, long id) {
    return builder
        .id(Long.toString(id))
        .data(Stats.class, new Stats(new Date().getTime(), RANDOM.nextInt(100)))
        .mediaType(MediaType.APPLICATION_JSON_TYPE)
        .build();
}

OutboundSseEvent可以使用常规MessageBodyWriter解析策略在data属性中携带将相对于指定的mediaType进行序列化的任何有效负载。 一旦获得OutboundSseEvent实例,便使用SseBroadcaster :: broadcast方法将其发送出去。 请注意,我们使用subscribeOn运算符将控制流移交给了另一个线程,这通常是您一直在做的事情。

很好,希望现在清除了流部分,但是我们如何才能真正订阅SseBroadcaster发出的SSE事件? 这比您想像的要容易:

@GET
@Path("broadcast")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void broadcast(@Context SseEventSink sink) {
    broadcaster.register(sink);
}

我们都准备好了。 这里最重要的是正在生成的内容类型,应将其设置为MediaType.SERVER_SENT_EVENTS 。 在这种情况下, SseEventSink的上下文实例变为可用,并且可以向SseBroadcaster实例注册。

要查看我们的JAX-RS资源,我们需要使用例如JAXRSServerFactoryBean引导服务器实例,并在此过程中配置所有必需的提供程序。 请注意,我们也在显式指定要使用的传输,在这种情况下为SseHttpTransportFactory.TRANSPORT_ID

@Configuration
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter {
    @Bean
    public Server rsServer(Bus bus, StatsRestService service) {
        JAXRSServerFactoryBean endpoint = new JAXRSServerFactoryBean();
        endpoint.setBus(bus);
        endpoint.setAddress("/");
        endpoint.setServiceBean(service);
        endpoint.setTransportId(SseHttpTransportFactory.TRANSPORT_ID);
        endpoint.setProvider(new JacksonJsonProvider());
        return endpoint.create();
    }
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
          .addResourceHandler("/static/**")
          .addResourceLocations("classpath:/web-ui/"); 
    }
}

要结束循环,我们只需要为Spring Boot应用程序提供运行器即可:

@SpringBootApplication
public class SseServerStarter {    
    public static void main(String[] args) {
        SpringApplication.run(SseServerStarter.class, args);
    }
}

现在,如果我们运行该应用程序并使用多个Web浏览器或同一浏览器中的不同选项卡导航到http:// localhost:8080 / static / broadcast.html ,我们将观察到所有事件内部绘制的相同事件流:

不错,广播当然是一个有效的用例,但是在每次端点调用时返回一个独立的SSE流又如何呢? 简单,只需使用SseEventSink方法(例如sendclose )即可直接操作SSE流。

@GET
@Path("sse")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void stats(@Context SseEventSink sink) {
    Flowable
        .interval(1, TimeUnit.SECONDS)
        .zipWith(eventsStream(builder), (id, bldr) -> createSseEvent(bldr, id))
        .subscribeOn(Schedulers.single())
        .subscribe(sink::send, ex -> {}, sink::close);
}

这次,如果我们运行该应用程序并使用多个Web浏览器或同一浏览器中的不同选项卡导航到http:// localhost:8080 / static / index.html ,我们将观察到完全不同的图表:

出色的服务器端API确实非常简洁且易于使用。 但是客户端方面,我们可以使用Java应用程序中的SSE流吗? 答案是肯定的。 JAX-RS 2.1还概述了客户端API,其核心是SseEventSource

final WebTarget target = ClientBuilder
    .newClient()
    .register(JacksonJsonProvider.class)
    .target("http://localhost:8080/services/api/stats/sse");
        
try (final SseEventSource eventSource =
            SseEventSource
                .target(target)
                .reconnectingEvery(5, TimeUnit.SECONDS)
                .build()) {

    eventSource.register(event -> {
        final Stats stats = event.readData(Stats.class, MediaType.APPLICATION_JSON_TYPE);
        System.out.println("name: " + event.getName());
        System.out.println("id: " + event.getId());
        System.out.println("comment: " + event.getComment());
        System.out.println("data: " + stats.getLoad() + ", " + stats.getTimestamp());
        System.out.println("---------------");
    });
    eventSource.open();

    // Just consume SSE events for 10 seconds
    Thread.sleep(10000); 
}

如果运行此代码段(假设服务器也已启动并且正在运行),我们将在控制台中看到类似的内容(您可能还记得,数据是随机生成的)。

name: stats
id: 0
comment: null
data: 82, 1509376080027
---------------
name: stats
id: 1
comment: null
data: 68, 1509376081033
---------------
name: stats
id: 2
comment: null
data: 12, 1509376082028
---------------
name: stats
id: 3
comment: null
data: 5, 1509376083028
---------------

...

如我们所见,服务器端的OutboundSseEvent变为客户端的InboundSseEvent 。 客户端可以使用通常的MessageBodyReader解析策略,通过指定预期的媒体类型来消耗数据属性中可以反序列化的任何有效负载。

单篇文章中压缩了很多内容。 而且,关于SSEJAX-RS 2.1的其他事情我们在这里没有涉及,例如使用HttpHeaders.LAST_EVENT_ID_HEADER或配置重新连接延迟。 如果有兴趣学习的话,这些可能是即将发布的帖子的重要话题。

总而言之, JAX-RSSSE的支持是我们许多人期待已久的事情。 最后,它在那里,请尝试一下!

完整的项目资源可在Github上找到

翻译自: https://www.javacodegeeks.com/2017/10/better-late-never-sse-server-sent-events-now-jax-rs.html

jax-rs jax-ws

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以按照以下步骤将JAX-WS接口替换为JAX-RS接口: 1. 创建JAX-RS接口:创建一个新的Java接口来定义您的JAX-RS服务。在接口上使用`@Path`注解指定资源的URL路径。 ```java import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/your-resource") public interface YourResource { @GET @Produces(MediaType.APPLICATION_JSON) String getResource(); } ``` 2. 实现JAX-RS接口:创建一个类来实现您的JAX-RS接口,并实现接口定义的方法。 ```java public class YourResourceImpl implements YourResource { @Override public String getResource() { // 实现您的业务逻辑 return "Hello JAX-RS!"; } } ``` 3. 注册JAX-RS服务:将您的JAX-RS服务注册到应用程序。这可以通过创建一个`javax.ws.rs.core.Application`子类并在其注册资源类来完成。 ```java import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; import java.util.HashSet; import java.util.Set; @ApplicationPath("/api") public class YourApplication extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> classes = new HashSet<>(); classes.add(YourResourceImpl.class); return classes; } } ``` 4. 配置JAX-RS:根据您使用的应用程序服务器,将JAX-RS的实现(如Jersey或RestEasy)添加到您的应用程序的构建配置文件。您还需要确保在应用程序服务器上正确配置JAX-RS。 5. 测试JAX-RS接口:启动您的应用程序服务器,并使用JAX-RS客户端或浏览器等工具测试您的JAX-RS接口。 请注意,以上步骤是一般的指导,具体步骤可能因您使用的框架和工具而有所不同。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值