Статьи

Использование клиентов JacpFX с JSR 356 WebSockets

JSR 356 WebSockets — одна из новых и интересных функций в предстоящем выпуске JEE 7, включающая в себя как серверные, так и клиентские API-интерфейсы в своей эталонной реализации. Это делает его подходящим для интеграции с JavaFX на стороне клиента. JacpFX — это инфраструктура RCP поверх JavaFX, которая использует основанный на сообщениях подход для взаимодействия с компонентами. Этот основанный на сообщениях подход упрощает интеграцию WebSocket-ClientEndpoints и передачу входящих сообщений в ваше приложение JavaFX / JacpFX.

Следующая статья покажет вам, как создать простую Websocket-SeverEndpoint на GlassFish 4 и как связаться с этой конечной точкой из клиента JacpFX. Пример сценария очень прост: конечная точка сервера может создать соединение с Twitter, она принимает запрос-сообщение от клиента и передает результат Twitter всем подключенным

ClientEndpoints. Пример будет основан на GlassFish b85 и JacpFX 1.2. (Maven) пример проекта можно скачать здесь

Часть 1: создание конечной точки сервера

Пример проекта — это простой проект JEE 7 maven. Веб-проект содержит один POJO, который действует как точка WebSocket-ServerEndpoint. Он получает запрос и передает результат всем подключенным клиентам. Второй POJO — это компонент без сохранения состояния, который принимает сообщение-запрос, выполняет поиск в Твиттере и возвращает результат в конечную точку сервера. Чтобы избежать обработки сообщений в текстовом или двоичном формате, мы создаем два POJO, которые действуют как кодировщик / декодер для сообщений WebSocket. Итак, давайте начнем с TwitterRepositoryBean, это простой компонент без сохранения состояния:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Stateless(mappedName = "TwitterRepositoryBean")
 
public class TwitterRepositoryBean {
 
   public TwitterResult getTwitterDataByQuery(Query query) {
 
       return parser.fromJson(getFeedData(query.getQuery()), TwitterResult.class);
 
   }
 
   private String getFeedData(String input) {
 
       final String searchURL = "http://search.twitter.com/search.json?q=" + input + "&rpp=5&include_entities=true" +
 
               "&with_twitter_user_id=true&result_type=mixed";
 
       final URL twitter = new URL(searchURL);
 
 
       return "";
 
   }
 
}

Затем мы создаем WebSocket-ServerEndpoint. Самый простой способ создать конечную точку — написать POJO и аннотировать его с помощью @ServerEndpoint («путь»). Конечная точка может иметь следующие аннотации жизненного цикла: @OnOpen, @OnClose, @OnError и один или несколько @OnMessage.

Имейте в виду, что допускается только одно @OnMessage для собственного типа сообщения; это текстовые, бинарные и понг. Что это означает? Вы можете использовать два метода: @OnMessage login(Login lg); и @OnMessage message(Message m) . НО один должен передаваться как текст, а другой как двоичный, иначе вы получите исключение при развертывании.

ServerEndpoint будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@ServerEndpoint(value = "/twitter")
 
public class TwitterEndpoint {
 
   @Inject
 
   private TwitterRepositoryBean twitterRepo;
 
   @OnMessage
 
   public void handleChatMessage(Query query, Session session) {
 
TwitterResult result =    twitterRepo.getTwitterDataByQuery(query);
 
broadcastMessage(result,session);
 
   }
 
   private void broadcastMessage(TwitterResult result, Session session) {
 
       for (Session s : session.getOpenSessions()) {
 
           s.getBasicRemote().sendObject(result);
 
       }
 
   }
 
}

Теперь, когда мы создали ServerEndpoint, вопрос заключается в том, как работать с такими типами, как Query и TwitterResult , поскольку собственный формат сообщения — двоичный и текстовый. Решением для этого является сообщение «кодер / декодер». Поэтому нам нужен один декодер, который преобразует двоичное сообщение в Query и один кодер, который кодирует TwitterResult в двоичный файл.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class QueryDecoder implements Decoder.Binary<Query> {
 
   public Query decode(ByteBuffer byteBuffer) throws DecodeException {
 
       return (Query) SerializationUtils.deserialize(byteBuffer.array());
 
   }
 
   public boolean willDecode(ByteBuffer byteBuffer) {
 
       Object message = SerializationUtils.deserialize(byteBuffer.array());
 
       if (message == null) return false;
 
       return message instanceof Query;
 
   }
 
}

И кодировщик:

1
2
3
4
5
6
7
8
9
public class TwitterResultEncoder implements Encoder.Binary<TwitterResult> {
 
   public ByteBuffer encode(TwitterResult message) throws EncodeException {
 
       return ByteBuffer.wrap(SerializationUtils.serialize(message));
 
   }
 
}

Чтобы сделать кодер / декодер пригодным для использования в качестве конечной точки, мы расширяем аннотацию @ServerEndpoint следующим образом:

1
@ServerEndpoint(value = "/twitter", decoders = {QueryDecoder.class},encoders = {TwitterResultEncoder.class})

Итак, теперь у нас есть полноценный пример ServerEndpoint, который вы можете развернуть на любом совместимом с JEE 7 сервере приложений.

Часть 2: создание клиента JacpFX и ClientEndpoint

Самый простой способ начать с JacpFX — использовать предоставленный архетип maven. Он генерирует пример клиента, включающего все интересные аспекты JacpFX. Таким образом, вы получаете готовую рабочую среду, две перспективы (fxml и JavaFX), два компонента пользовательского интерфейса и два компонента без пользовательского интерфейса. Поэтому мы просто используем это и расширяем его с помощью конечных точек WebSocket.

Если у вас> Java7u6 и maven, вы можете просто набрать:

1
mvn archetype:generate -DarchetypeGroupId=org.jacp -DarchetypeArtifactId=JacpFX-quickstart-archetype -DarchetypeVersion=1.2 -DarchetypeRepository=http://developer.ahcp.de/nexus/content/repositories/jacp

создать проект JacpFX.

Чтобы получить зависимости для API WebSocket-Client, вам нужно добавить следующую зависимость в файл pom.xml:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<dependency>
 
           <groupId>org.glassfish.tyrus</groupId>
 
           <artifactId>tyrus-client</artifactId>
 
           <version>1.0-rc1</version>
 
           <scope>compile</scope>
 
       </dependency>
 
 
 
 
       <dependency>
 
           <groupId>org.glassfish.tyrus</groupId>
 
           <artifactId>tyrus-container-grizzly</artifactId>
 
           <version>1.0-rc1</version>
 
           <scope>compile</scope>
 
       </dependency>

И добавьте следующий репозиторий:

1
2
3
4
5
6
7
<repository>
 
           <id>java.net-promoted</id>
 
           <url>https://maven.java.net/content/groups/promoted/</url>
 
       </repository>

Основная идея заключается в том, что мы используем созданный компонент с состоянием и повторно используем его как WebSocket-ClientEndpoint. Каждый раз, когда мы получаем новый TwitterReult , мы передаем его в шину сообщений JacpFX и делегируем его компоненту пользовательского интерфейса, который отображает результат в таблице. С другой стороны, мы делегируем ввод TextField компоненту с сохранением состояния, который отправляет запрос Query в ServerEndpoint. Поэтому измените компонент с состоянием следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@ClientEndpoint
 
@CallbackComponent(id = "<code>id003</code>", name = "twitterEndpointComponent", active = false)
 
public class StatefulCallback extends AStatefulCallbackComponent {
 
   private Session session;
 
   private CountDownLatch messageLatch = new CountDownLatch(1);
 
   @Override
 
   public Object handleAction(final IAction<Event, Object> arg0) {
 
       if (arg0.getLastMessage() instanceof Query){
 
           sendQuery((Query) arg0.getLastMessage());
 
       }
 
       return null;
 
   }
 
   @OnStart
 
   /**
 
    * JacpFX lifecycle
 
    */
 
   public void init() {
 
       ClientManager client = ClientManager.createClient();
 
      session = client.connectToServer(this, ClientEndpointConfig.Builder.create().build(), getURI());
 
      messageLatch.await(5, TimeUnit.SECONDS);
 
   }
 
 
   @OnTearDown
 
   /**
 
    * JacpFX lifecycle
 
    */
 
   public void cleanup() {
 
if(session!=null)session.close();
 
   }
 
   @OnOpen
 
   /**
 
    * WebSocket lifecycle
 
    */
 
   public void onOpen(Session session) {
 
       this.session = session;
 
       messageLatch.countDown();
 
   }
 
   @OnMessage
 
   /**
 
    * WebSocket lifecycle
 
    */
 
   public void onTwitterMessage(TwitterResult result) {
 
       this.getActionListener("id001", result) .performAction(null);
 
   }
 
   private void sendQuery(Query query) throws IOException, EncodeException {
 
       session.getBasicRemote().sendObject(query);
 
   }
 
   private URI getURI() {return “ws://…” }
 
}

Аннотация на уровне класса @ClientEndpoint помечает этот компонент как WebSocket-Endpoint, тогда как @CallbackComponent содержит метаданные JacpFX для этого компонента без состояния. Метод @OnStart init(..) содержит код для подключения к WebSocket-ServerEndpoint и для передачи экземпляра компонента с состоянием в качестве ClientEndpoint. @OnStart — это аннотация жизненного цикла JacpFX, которая будет выполняться при активации компонента. Когда компонент получает сообщение Query от пользовательского интерфейса, будет выполнен метод “handleAction” . Здесь мы называем “sendQuery” и используем WebSocket-Session для отправки объекта Query на Server-Endpoint.

Когда Сервер выполняет Query и получает результат из Twitter, он передает TwitterResult всем подключенным Клиентам, поэтому метод @OnMessage onTwitterMessage(…) будет выполняться на стороне клиента. Здесь мы вызываем компонент actionListener и передаем результат компоненту с идентификатором «id001», который отображает результат. Как и в ServerEndpoint ранее, нам нужен «кодер / декодер» для обработки сообщений с типами « Query and TwitterResult ». Итак, создайте кодер / декодер таким же образом и зарегистрируйте их в ClientEndpoint следующим образом:

1
@ClientEndpoint(decoders = {TwitterResultDecoder.class}, encoders = {QueryEncoder.class})

Часть 3: измените пример клиента, чтобы показать Query- и TableView

Последний шаг — изменить компоненты пользовательского интерфейса из примера клиента JacpFX, чтобы получить Query- и Table-View. Вы можете создать один компонент с полем ввода и таблицей для отображения результатов. Лучший подход — создать отдельные компоненты для этого. Прежде чем мы начнем, вы можете удалить PerspectiveTwo.java , ComponentFXMLRight.java и ComponentFXMLBottom.java . Также удалите ссылки в resources/main.xml и добавьте ссылку componentTop в PerspectiveOne:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<bean id="perspectiveOne" class="org.jacp.client.perspectives.PerspectiveOne">
 
        <property name="subcomponents">
 
            <list>
 
                <ref bean="componentLeft" />
 
                <ref bean="componentTop" />
 
                <ref bean="statefulCallback" />
 
                <ref bean="statelessCallback" />
 
            </list>
 
        </property>
 
    </bean>

Первая перспектива JacpFX (спектива) объявляет свой пользовательский интерфейс FXML и действует как контейнер для двух компонентов. Это уже SplitPane и мы просто меняем код, чтобы разделить представления по вертикали. Так что откройте resources/fxml/perspectiveOne.fxml и добавьте атрибут resources/fxml/perspectiveOne.fxml orientation="VERTICAL" <SplitPane> элементе <SplitPane> и измените dividerPositions на 0.30. Теперь откройте PerspectiveOne.java и измените имена целей регистрации для наших компонентов на:

1
2
3
4
5
6
perspectiveLayout.registerTargetLayoutComponent("QueryView",
 
                    this.gridPaneLeft);
 
            perspectiveLayout.registerTargetLayoutComponent("TableView",
                    this.gridPaneRight);

То, что мы сделали, — это установили новый целевой макет для наших компонентов. Компоненты теперь могут регистрироваться в одном из этих целевых макетов, а затем будут отображаться там. Этот механизм регистрации позволяет вам определять любую сложную структуру пользовательского интерфейса в вашей перспективе и определять точки рендеринга для ваших компонентов. Теперь мы изменим ComponentTop который будет отображаться в цели “QueryView” . Для этого мы просто изменим значение атрибута defaultExecutionTarget @ Component следующим образом:

1
@Component(defaultExecutionTarget = "QueryView", id = "id006", name = "componentTop", active = true, resourceBundleLocation = "bundles.languageBundle", localeID = "en_US")

Этот компонент уже содержит TextField и Button , поэтому нам просто нужно изменить EventHandler для Button, чтобы передать значение TextFiled .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private EventHandler<Event> getEventHandler() {
 
        return new EventHandler<Event>() {
 
            @Override
 
            public void handle(final Event arg0) {
 
                getActionListener("id01.id003",
 
                       textField.getText()).performAction(arg0);
 
            }
 
        };
 
    }

getActionListener(“id01.id003”… просто определяет компонент с «id003» (компонентом с состоянием) в перспективе «id01», как цель для этого сообщения. Затем мы изменим ComponentLeft.java чтобы он действовал как TableView . Опять же, мы просто изменим createUI() по умолчанию на “TableView” . Мы также изменим метод createUI() для отображения TableView и обновим postHandleAction для обработки TwitterResults и передачи их в таблицу. Окончательное решение будет выглядеть следующим образом:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@Component(defaultExecutionTarget = " TableView ", id = "id001", name = "componentLeft", active = true, resourceBundleLocation = "bundles.languageBundle", localeID = "en_US")
 
public class ComponentLeft extends AFXComponent {
 
   private AnchorPane pane;
 
   private ObservableList<Tweet> tweets = FXCollections.observableArrayList();
 
 
   @Override
 
   /**
 
    * The handleAction method always runs outside the main application thread. You can create new nodes, execute long running tasks but you are not allowed to manipulate existing nodes here.
 
    */
 
   public Node handleAction(final IAction<Event, Object> action) {
 
       // runs in worker thread
 
       if (action.getLastMessage().equals(MessageUtil.INIT)) {
 
           return this.createUI();
 
       }
 
       return null;
 
   }
 
   @Override
 
   /**
 
    * The postHandleAction method runs always in the main application thread.
 
    */
 
   public Node postHandleAction(final Node arg0,
 
                                final IAction<Event, Object> action) {
 
       // runs in FX application thread
 
       if (action.getLastMessage().equals(MessageUtil.INIT)) {
 
           this.pane = (AnchorPane) arg0;
 
       } else if (action.getLastMessage() instanceof TwitterResult) {
 
           tweets.clear();
 
           TwitterResult result = (TwitterResult) action.getLastMessage();
 
           if (!result.getResults().isEmpty()) {
 
               tweets.addAll(result.getResults());
 
               Collections.sort(tweets);
 
           }
 
       }
 
       return this.pane;
 
   }
 
 
   /**
 
    * create the UI on first call
 
    *
 
    * @return
 
    */
 
   private Node createUI() {
 
       final AnchorPane anchor = AnchorPaneBuilder.create()
 
               .styleClass("roundedAnchorPaneFX").build();
 
       TableView table = new TableView();
 
       table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
 
       table.getColumns().addAll(createColumns());
 
       table.setItems(tweets);
 
       AnchorPane.setTopAnchor(table, 25.0);
 
       AnchorPane.setRightAnchor(table, 25.0);
 
       AnchorPane.setLeftAnchor(table, 25.0);
 
       anchor.getChildren().addAll(table);
 
       GridPane.setHgrow(anchor, Priority.ALWAYS);
 
       GridPane.setVgrow(anchor, Priority.ALWAYS);
 
       return anchor;
 
   }
 
   private List<TableColumn> createColumns() {
 
 
       return Arrays.asList(imageView, nameView, messageView);
   }

Компонент JacpFX UI имеет определенный жизненный цикл: каждое сообщение сначала проходит через handleAction(..) , а затем метод postHandleAction(…) . handleAction(..) всегда выполняется в рабочем потоке, поэтому вы можете выполнять любые сложные и трудоемкие вычисления, не блокируя пользовательский интерфейс. Здесь вы можете создавать новые узлы пользовательского интерфейса и возвращать их. Но вы не можете изменить ни один из существующих узлов, потому что вы не находитесь в потоке приложений JavaFX. postHandleAction(…) узел затем будет передан в postHandleAction(…) который выполняется в postHandleAction(…) приложения. Полный жизненный цикл вы можете увидеть здесь:

В postHandleAction(…) мы проверяем сообщение TwitterResult и добавляем записи Twitter в Table . Теперь все готово, и вы можете развернуть ServerEndpoint на своем экземпляре GlassFish 4 и запустить приложение. Если вы запустите много экземпляров, TwitterResult будет передан всем подключенным клиентам. Полную документацию по JacpFX вы можете найти в проекте Wiki здесь . Дополнительную информацию о JSR 356 вы можете получить на странице проекта и в блоге Аруна Гупты.

Ресурсы

Справка: Использование клиентов JacpFX с JSR 356 WebSockets от нашего партнера по