пятница, 22 августа 2014 г.

Живое общение: WebSocket (Часть вторая) - Передача объектов

В предыдущем своём посте я коротко рассказал про реализацию веб-сокетов в Java EE и в качестве примера привёл простое приложение, которое представляет собой эхо-сервис: клиент отправляет серверу текстовое сообщение и получает его обратно. Однако передача текстовых данных - лишь основа для тех возможностей, которые предоставляет реализация веб-сокетов в Java EE. Одной из таких возможностей является передача объектов в виде JSON-строки.

Отправка объектов

Для отправки объектов существует метод sendObject в субинтерфейсах Async и Basic интерфейса RemoteEndpoint. В качестве аргумента он принимает объект, для которого существут класс-энкодер, реализующий один из субинтерфейсов Encoder (Text, TextStream, Binary и BinaryStream). В качестве примеров в данном посте будет использован Encoder.Text. Для каждого класса, объекты которого будут передаваться через веб-сокеты, необходимо написать класс-энкодер. Для всех четырёх интерфейсов обязательны к реализации три метода: init и destroy для выполнения необходимых действий до и после обработки объекта, а так же encode, в котором и происходит преобразование объекта в JSON-строку. На стороне клиента теперь остаётся вызвать JSON.parse для преобразования полученного сообщения в объект.

public class ChatMessageEncoder implements Encoder.Text<ChatMessage> {

    public static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";

    public static final String MESSAGE_CLASS_NAME = "org.acruxsource.sandbox.websocket.domain.ChatMessage";

    @Override
    public void init(EndpointConfig config) {
    }

    @Override
    public void destroy() {
    }

    @Override
    public String encode(ChatMessage object) throws EncodeException {
        DateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN);

        return Json.createObjectBuilder()
                .add("_class", MESSAGE_CLASS_NAME)
                .add("message", Json.createObjectBuilder()
                        .add("sender", object.getSender())
                        .add("message", object.getMessage())
                        .add("sendDate", dateFormat.format(object.getSendDate())))
                .build()
                .toString();
    }

}
После того как класс-энкодер написан, его необходимо указать в свойстве encoders аннотации @ServerEndpoint у нашего эндпоинта. Если он не будет там указан, то эндпоинт не будет знать, каким образом нужно преобразовать объект в строку, и выкинет ошибку. Так как посредством одного эндпоинта можно отправлять объекты разных классов, нужно написать необходимое количество классов-энкодеров и указать их в эндпоинте.
@ServerEndpoint(
        value = "/chat",
        encoders = {
            ChatMessageEncoder.class
        }
)
public class EncodedSandboxEndpoint {
//
}

Получение объектов

В случае с получением объектов через веб-сокеты алгоритм аналогичен: пишем класс-декодер, реализующий один из четырёх субинтерфейсов Decoder, указываем его в свойстве decoders аннотации @ServerEndpoint, и вместо строки в методе @OnMessage принимаем объект нужного класса. Единственное серьёзное отличие - декодер используется только один, т.е. эндпоинт может получать сообщения только одного класса, не смотря на то, что в decoders можно указать любое количество классов-декодеров. Если нужна возможность принимать объекты разных классов всё же нужна, то решается данная проблема использованием абстрактного класса, либо интерфейса и написанием класса-декодера для них. В классе-декодере обязательны для реализации четыре метода: init, destroy, decode, преобразующий строку в объект нужного типа, и willDecode, определяющий, может ли данная строка быть преобразована при помощи данного декодера.

public class MessageTextDecoder implements Decoder.Text<Message> {

    public static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";

    @Override
    public Message decode(String s) throws DecodeException {
        System.out.println(s);
        Message message;
        try (JsonReader messageReader = Json.createReader(new StringReader(s))) {
            JsonObject messageObject = messageReader.readObject();
            switch (messageObject.getString("_class")) {
                case ChatMessageEncoder.MESSAGE_CLASS_NAME:
                    message = decodeChatMessage(messageObject.getJsonObject("message"));
                    break;
                case PrivateChatMessageEncoder.MESSAGE_CLASS_NAME:
                    message = decodeUserChatMessage(messageObject.getJsonObject("message"));
                    break;
                default:
                    throw new IllegalArgumentException("Unknown message type: " + messageReader.readObject().getString("class"));
            }
        }

        return message;
    }

    private ChatMessage decodeChatMessage(JsonObject messageObject) {
        ChatMessage chatMessage;
        DateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN);
        chatMessage = new ChatMessage();
        chatMessage.setSender(messageObject.getString("sender"));
        chatMessage.setMessage(messageObject.getString("message"));
        try {
            chatMessage.setSendDate(dateFormat.parse(messageObject.getString("sendDate")));
        } catch (ParseException exception) {
            exception.printStackTrace();
        }

        return chatMessage;
    }

    private PrivateChatMessage decodeUserChatMessage(JsonObject messageObject) {
        PrivateChatMessage chatMessage;
        DateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN);
        chatMessage = new PrivateChatMessage();
        chatMessage.setSender(messageObject.getString("sender"));
        chatMessage.setReceiver(messageObject.getString("receiver"));
        chatMessage.setMessage(messageObject.getString("message"));
        try {
            chatMessage.setSendDate(dateFormat.parse(messageObject.getString("sendDate")));
        } catch (ParseException exception) {
            exception.printStackTrace();
        }

        return chatMessage;
    }

    @Override
    public boolean willDecode(String s) {
        if (s != null && !s.isEmpty()) {
            try (JsonReader messageReader = Json.createReader(new StringReader(s))) {
                String className = messageReader.readObject().getString("_class");
                if (ChatMessageEncoder.MESSAGE_CLASS_NAME.equals(className) || PrivateChatMessageEncoder.MESSAGE_CLASS_NAME.equals(className)) {
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    public void init(EndpointConfig config) {}

    @Override
    public void destroy() {}
}
В моих примерах корневой JSON-объект несёт информацию о классе передаваемого объекта в поле _class, а так же сам объект в поле message. Таким образом достаточно просто разбирать типы передаваемых объектов как на стороне сервера, так и на стороне клиента.

Использование TextStream вместо Text

При использовании TextStream вместо Text различия в коде будут минимальны. В классе-энкодере метод encode вторым аргументом принимает объект Writer и ничего не возвращает. Ответ отправляется при помощи методов объекта Writer.

public class PrivateChatMessageEncoder implements Encoder.TextStream<PrivateChatMessage> {

    public static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";

    public static final String MESSAGE_CLASS_NAME = "org.acruxsource.sandbox.websocket.domain.PrivateChatMessage";

    @Override
    public void init(EndpointConfig config) {}

    @Override
    public void destroy() {}

    @Override
    public void encode(PrivateChatMessage object, Writer writer) throws EncodeException, IOException {
        DateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN);

        String jsonString = Json.createObjectBuilder()
                .add("_class", MESSAGE_CLASS_NAME)
                .add("message", Json.createObjectBuilder()
                        .add("sender", object.getSender())
                        .add("receiver", object.getReceiver())
                        .add("message", object.getMessage())
                        .add("sendDate", dateFormat.format(object.getSendDate())))
                .build()
                .toString();
        
        writer.write(jsonString);
    }

}

В классе-декодере метод decode принимает в качестве единственного аргумента не JSON-строку, а объект Reader, из которого нужно вычитать передаваемую строку. Плюс к этому у Decoder.TextStream нет метода willDecode, так как в момент вызова метода OnMessage сообщение передано не полностью.
public class MessageTextStreamDecoder implements Decoder.TextStream<Message> {

    public static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";

    @Override
    public Message decode(Reader r) throws DecodeException {
        Message message;
        try (JsonReader messageReader = Json.createReader(r)) {
            JsonObject messageObject = messageReader.readObject();
            switch (messageObject.getString("_class")) {
                case ChatMessageEncoder.MESSAGE_CLASS_NAME:
                    message = decodeChatMessage(messageObject.getJsonObject("message"));
                    break;
                case PrivateChatMessageEncoder.MESSAGE_CLASS_NAME:
                    message = decodeUserChatMessage(messageObject.getJsonObject("message"));
                    break;
                default:
                    throw new IllegalArgumentException("Unknown message type: " + messageReader.readObject().getString("class"));
            }
        }

        return message;
    }

    // код опущен
}

Полезные ссылки

Комментариев нет:

Отправить комментарий