Вступление
В последней версии JDeveloper 12c (12.1.3.0) вместе с WebLogic Server 12.1.3 появилось несколько новых функций Java EE 7. Одним из них является поддержка JSR 356 Java API для WebSockets. На самом деле протокол WebSocket (RFC 6455) поддерживается начиная с версии 12.1.2.0, но он был основан на конкретной реализации WebSocket API для WebLogic. Теперь этот проприетарный API WebSocket WebLogic устарел. Тем не менее, он все еще поддерживается для обратной совместимости.
В этом посте я собираюсь показать пример использования Java API JSR 356 для WebSockets в простом приложении ADF. Вариант использования — про парусную регату, которая проходит в Тасмановом море. В регате участвуют три лодки, и они собираются пересечь Тасмановое море, плывущее от Австралии до побережья Новой Зеландии. Целью примера приложения является наблюдение за регатой и информирование пользователей о том, как это происходит, с указанием местоположения лодок на карте.
Мы собираемся объявить конечную точку сервера WebSocket в приложении, и когда пользователь открывает страницу, функция скрипта Java открывает новое соединение WebSocket. Приложение использует запланированный сервис, который каждую секунду обновляет координаты лодок и отправляет сообщение с новыми позициями лодок всем активным клиентам WebSocket. На стороне клиента функция скрипта Java получает сообщение и добавляет маркеры на карту Google в соответствии с GPS-координатами. Итак, каждый пользователь, заинтересованный в регате, увидит одно и то же обновленное изображение, отображающее текущее состояние соревнования.
Конечная точка сервера WebSocket
Начнем с объявления конечной точки сервера WebSocket. В текущей реализации есть небольшая проблема, которая, вероятно, будет решена в будущих выпусках. Конечные точки WebSocket нельзя смешивать со страницами ADF, и их следует развертывать в отдельном файле WAR. Самый простой способ сделать это — создать отдельный проект WebSocket внутри приложения и объявить все необходимые конечные точки в этом проекте:
Это также важно для настройки читаемого корня веб-контекста Java EE для проекта:
Следующим шагом является создание класса Java, который будет конечной точкой WebSocket. Итак, это обычный класс со специальной аннотацией в самом начале:
|
1
2
3
4
5
6
7
|
@ServerEndpoint(value = "/message")public class MessageEndPoint { public MessageEndPoint() { super(); }} |
Обратите внимание, что JDeveloper подчеркивает аннотацию красным цветом. Мы собираемся решить эту проблему, позволив JDeveloper настроить проект для Web Socket.

Сделав это, JDeveloper собирается преобразовать проект в веб-проект, добавив файл Web.xml и добавив необходимую библиотеку:

Кроме того, класс конечной точки становится работоспособным, и мы можем просто запустить его, чтобы проверить, как он на самом деле работает:

В ответ JDeveloper генерирует следующий URL, по которому доступна конечная точка WebSocket. Обратите внимание, что URL содержит корневой каталог контекста проекта ( WebSocket ) и свойство value аннотации ( / message ). Если все в порядке, тогда, когда мы нажмем на URL, мы получим информационное окно «Подключено успешно»:
Кстати, в сообщении есть опечатка.
А теперь давайте добавим некоторую реализацию в класс конечных точек WebSocket. Согласно спецификации, для каждого соединения WebSocket будет создан новый экземпляр класса MessageEndPoin t. Чтобы сохранить кучу всех активных сеансов WebSocket, мы будем использовать статическую очередь:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class MessageEndPoint { //A new instance of the MessageEndPoint class //is going to be created for each WebSocket connection //This queue contains all active WebSocket sessions final static Queue<Session> queue = new ConcurrentLinkedQueue<>(); @OnOpen public void open(Session session) { queue.add(session); } @OnClose public void closedConnection(Session session) { queue.remove(session); } @OnError public void error(Session session, Throwable t) { queue.remove(session); t.printStackTrace(); } |
Аннотированные методы open , closedConnection и error будут вызываться соответственно, когда будет установлено новое соединение, когда оно было закрыто и когда произошло что-то не так. Сделав это, мы можем использовать статический метод для широковещательной рассылки текстовых сообщений всем клиентам:
|
01
02
03
04
05
06
07
08
09
10
|
public static void broadCastTex(String message) { for (Session session : queue) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } } |
В нашем случае мы должны уведомлять пользователей новыми GPS-координатами лодок, поэтому мы должны иметь возможность отправлять через WebSockets нечто более сложное, чем просто текстовые сообщения.
Отправка объекта
По сути, бизнес-модель примера приложения представлена двумя простыми Java-классами Boat :
|
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
|
public class Boat { private final String country; private final double startLongitude; private final double startLatitude; private double longitude; private double latitude; public String getCountry() { return country; } public double getLongitude() { return longitude; } public double getLatitude() { return latitude; } public Boat(String country, double longitude, double latitude) { this.country = country; this.startLongitude = longitude; this.startLatitude = latitude; }... |
и регата :
|
01
02
03
04
05
06
07
08
09
10
11
|
public class Regatta { private final Boat[] participants = new Boat[] { new Boat("us", 151.644, -33.86), new Boat("ca", 151.344, -34.36), new Boat("nz", 151.044, -34.86) }; public Boat[] getParticipants() { return participants; }... |
Для нашего варианта использования мы собираемся отправить экземпляр класса Regatta клиентам WebSocket. Регата содержит всех участников регаты, представленных экземплярами класса « Лодка » с обновленными GPS-координатами ( долгота и широта ).
Это может быть сделано путем создания пользовательской реализации интерфейса Encoder.Text <Regatta> или, другими словами, мы собираемся создать кодировщик, который может преобразовать экземпляр Regatta в текст и указать этот кодировщик, который будет использоваться WebSocket. конечная точка при отправке экземпляра регаты .
|
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
|
public class RegattaTextEncoder implements Encoder.Text<Regatta> { @Override public void init(EndpointConfig ec) { } @Override public void destroy() { } private JsonObject encodeBoat(Boat boat) throws EncodeException { JsonObject jsonBoat = Json.createObjectBuilder() .add("country", boat.getCountry()) .add("longitude", boat.getLongitude()) .add("latitude" , boat.getLatitude()).build(); return jsonBoat; } @Override public String encode(Regatta regatta) throws EncodeException { JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); for (Boat boat : regatta.getParticipants()) { arrayBuilder.add(encodeBoat(boat)); } return arrayBuilder.build().toString(); } } |
|
1
2
3
|
@ServerEndpoint( value = "/message", encoders = {RegattaTextEncoder.class }) |
Сделав это, мы можем отправлять объекты нашим клиентам:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
public static void sendRegatta(Regatta regatta) { for (Session session : queue) { try { session.getBasicRemote().sendObject(regatta); } catch (EncodeException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } |
RegattaTextEncoder представляет объект Regatta в виде списка лодок с использованием нотации Json, поэтому он будет примерно таким:
[{"country":"us","longitude":151.67,"latitude":-33.84},{"country":"ca", ...},{"country":"nz", ...}]
Получение сообщения
На стороне клиента мы используем функцию сценария Java для открытия нового соединения WebSocket:
|
1
2
3
4
5
6
|
//Open a new WebSocket connection//Invoked on page load function connectSocket() { websocket = new WebSocket(getWSUri()); websocket.onmessage = onMessage; } |
И когда приходит сообщение, мы собираемся перебрать массив лодок и для каждой лодки добавить маркер на карте:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
function onMessage(evt) { var boats = JSON.parse(evt.data); for (i=0; i<boats.length; i++) { markBoat(boats[i]); } }function markBoat(boat) { var image = '../resources/images/'+boat.country+'.png'; var latLng = new google.maps.LatLng(boat.latitude,boat.longitude); mark = new google.maps.Marker({ position: latLng, map: map, title: boat.country, icon: image });} |
Здесь вы можете узнать, как интегрировать карты Google в ваши приложения.
Запустить регату
Чтобы эмулировать живое шоу, мы используем ScheduledExecutorService . Каждую секунду мы собираемся обновлять координаты GPS и транслировать обновление всем подписчикам:
|
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
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);private ScheduledFuture<?> runHandle;//Schedule a new regatta on Start button clickpublic void startRegatta(ActionEvent actionEvent) { //Cancel the previous regatta if (runHandle != null) { runHandle.cancel(false); } runHandle = scheduler.scheduleAtFixedRate(new RegattaRun(), 1, 1, TimeUnit.SECONDS); }public class RegattaRun implements Runnable { private final static double FINISH_LONGITUDE = 18; private final Regatta regatta = new Regatta(); //Every second update GPS coordinates and broadcast //new positions of the boats public void run() { regatta.move(); MessageEndPoint.sendRegatta(regatta); if (regatta.getLongitude() >= FINISH_LONGITUDE) { runHandle.cancel(true); } }} |
Ставка на свою лодку
И наконец, результат нашей работы выглядит так:

Пример приложения для этого поста требует JDeveloper 12.1.3. Повеселись!
Это оно!
| Ссылка: | Использование Java API для WebSockets в JDeveloper 12.1.3 от нашего партнера по JCG Евгения Федоренко из блога ADF Practice . |


