Статьи

Использование Java API для WebSockets в JDeveloper 12.1.3

Вступление

В последней версии 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 внутри приложения и объявить все необходимые конечные точки в этом проекте:

Снимок экрана 2014-10-07 в 12.17.58 PM

Это также важно для настройки читаемого корня веб-контекста Java EE для проекта:

Снимок экрана 2014-10-07 в 12.26.35 PM

Следующим шагом является создание класса Java, который будет конечной точкой WebSocket. Итак, это обычный класс со специальной аннотацией в самом начале:

1
2
3
4
5
6
7
@ServerEndpoint(value = "/message")
public class MessageEndPoint {
 
    public MessageEndPoint() {
        super();
    }
}

Обратите внимание, что JDeveloper подчеркивает аннотацию красным цветом. Мы собираемся решить эту проблему, позволив JDeveloper настроить проект для Web Socket.

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

Снимок экрана 2014-10-07 в 1.32.56 PM
Кроме того, класс конечной точки становится работоспособным, и мы можем просто запустить его, чтобы проверить, как он на самом деле работает:

Снимок экрана 2014-10-07 в 1.34.55 вечера
В ответ JDeveloper генерирует следующий URL, по которому доступна конечная точка WebSocket. Обратите внимание, что URL содержит корневой каталог контекста проекта ( WebSocket ) и свойство value аннотации ( / message ). Если все в порядке, тогда, когда мы нажмем на URL, мы получим информационное окно «Подключено успешно»:

Снимок экрана 2014-10-07 в 13:37.44 PM

Кстати, в сообщении есть опечатка.

А теперь давайте добавим некоторую реализацию в класс конечных точек 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 click
public 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);      
       }
    }
}

Ставка на свою лодку

И наконец, результат нашей работы выглядит так:

Снимок экрана 2014-10-07 в 17:4.01 PM
Пример приложения для этого поста требует JDeveloper 12.1.3. Повеселись!

Это оно!

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