Вступление
В последней версии 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")publicclassMessageEndPoint {    publicMessageEndPoint() {        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 | publicclassMessageEndPoint {    //A new instance of the MessageEndPoint class     //is going to be created for each WebSocket connection    //This queue contains all active WebSocket sessions    finalstaticQueue<Session> queue = newConcurrentLinkedQueue<>();     @OnOpen     publicvoidopen(Session session) {         queue.add(session);              }        @OnClose      publicvoidclosedConnection(Session session) {         queue.remove(session);      }           @OnError     publicvoiderror(Session session, Throwable t) {           queue.remove(session);           t.printStackTrace();       } | 
Аннотированные методы open , closedConnection и error будут вызываться соответственно, когда будет установлено новое соединение, когда оно было закрыто и когда произошло что-то не так. Сделав это, мы можем использовать статический метод для широковещательной рассылки текстовых сообщений всем клиентам:
| 01 02 03 04 05 06 07 08 09 10 |      publicstaticvoidbroadCastTex(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 | publicclassBoat {  privatefinalString country;  privatefinaldoublestartLongitude;  privatefinaldoublestartLatitude;  privatedoublelongitude;  privatedoublelatitude;    publicString getCountry() {      returncountry;  }  publicdoublegetLongitude() {      returnlongitude;  }  publicdoublegetLatitude() {      returnlatitude;  }    publicBoat(String country, doublelongitude, doublelatitude) {      this.country = country;      this.startLongitude = longitude;      this.startLatitude = latitude;  }... | 
и регата :
| 01 02 03 04 05 06 07 08 09 10 11 | publicclassRegatta {    privatefinalBoat[] participants = newBoat[] {        newBoat("us", 151.644, -33.86),        newBoat("ca", 151.344, -34.36),        newBoat("nz", 151.044, -34.86)    };        publicBoat[] getParticipants() {        returnparticipants;    }... | 
Для нашего варианта использования мы собираемся отправить экземпляр класса 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 | publicclassRegattaTextEncoder implementsEncoder.Text<Regatta> {  @Override  publicvoidinit(EndpointConfig ec) { }  @Override  publicvoiddestroy() { }  privateJsonObject encodeBoat(Boat boat) throwsEncodeException {      JsonObject jsonBoat = Json.createObjectBuilder()          .add("country", boat.getCountry())          .add("longitude", boat.getLongitude())          .add("latitude", boat.getLatitude()).build();           returnjsonBoat;   }    @Override   publicString encode(Regatta regatta) throwsEncodeException {      JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();                       for(Boat boat : regatta.getParticipants()) {          arrayBuilder.add(encodeBoat(boat));      }      returnarrayBuilder.build().toString();    }      } | 
| 1 2 3 | @ServerEndpoint(  value = "/message",  encoders = {RegattaTextEncoder.class}) | 
Сделав это, мы можем отправлять объекты нашим клиентам:
| 01 02 03 04 05 06 07 08 09 10 11 12 |     publicstaticvoidsendRegatta(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 = newWebSocket(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 = newgoogle.maps.LatLng(boat.latitude,boat.longitude);      mark = newgoogle.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 | privatefinalScheduledExecutorService scheduler =    Executors.newScheduledThreadPool(1);privateScheduledFuture<?> runHandle;//Schedule a new regatta on Start button clickpublicvoidstartRegatta(ActionEvent actionEvent) {    //Cancel the previous regatta    if(runHandle != null) {        runHandle.cancel(false);      }                runHandle = scheduler.scheduleAtFixedRate(newRegattaRun(), 1, 1,                                               TimeUnit.SECONDS); }publicclassRegattaRun implementsRunnable {    privatefinalstaticdoubleFINISH_LONGITUDE = 18;    privatefinalRegatta regatta = newRegatta();    //Every second update GPS coordinates and broadcast    //new positions of the boats    publicvoidrun() {                   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 . | 


