пятница, 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;
    }

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

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

воскресенье, 27 июля 2014 г.

Живое общение: WebSocket (Часть первая)

После написания постов о связке WebSocket-JMS, я подумал, а почему бы не написать более подробный пост о WebSocket? Сам протокол вполне заслуживает отдельного внимания. В данном посте я опишу WebSocket, а так же работу с ним на серверной стороне при помощи Java EE.
WebSocket - это програмный протокол полнодуплексной связи, работающий поверх TCP, позволяющий обмениваться сообщениями между клиентом и сервером в реальном времени. Проще говоря, этот протокол предоставляет возможность отправки сообщений между клиентом и сервером в любом направлении, чего раньше очень не хватало (хотя был Comet, но это совсем другая история).
На данный момент стандарт протокола описан W3C и утверждён IETF в RFC 6455.
По информации из Википедии, следующие браузеры имеют поддержку WebSocket:
Google Chrome (начиная с версии 4.0.249.0);
Apple Safari (начиная с версии 5.0.7533.16);
Mozilla Firefox (начиная с версии 4);
Opera (начиная с версии 10.70 9067);
Internet Explorer (начиная с версии 10);

Клиентская сторона (JavaScript)

На сторое клиента всё достаточно просто: создаём объект класса WebSocket с указанием адреса для соединения (эндпоинт).
var ws = new WebSocket("ws://localhost:8080/sandbox-websocket/sandbox");

Обратите внимание, что протокол в адресе ws. Так же может быть wss в случае, если используется SSL-шифрование.
После того как объект создан, можно описать его поведение в методах onopen (при открытии соединения), onerror (при ошибке соединения), onclose (при закрытии соединения) и onmessage (при получении события).
ws.onopen = function(event) {
 console.log('Connection opened');
};

ws.onerror = function(event) {
 console.log('Error occcured');
};

ws.onclose = function(event) {
 console.log('Console closed');
};

ws.onmessage = function(event) {
 console.log('Message received: ' + event.data);
}; 
Проверить браузер на поддержку вебсокетов достаточно просто:
if ('WebSocket' in window) {
// do what you want with WebSocket
}

Во всех методах информация о событии передаётся в объекте event. При получении сообщения от сервера текст содержится в event.data. Если в сообщении передана JSON-строка, то её можно преобразовать в объект методом JSON.parse. Отправка сообщения серверу тоже не составит труда.
ws.send("Some text");// отправка текста

ws.send(JSON.stringify({foo:"bar"}));// отправка объекта

Серверная сторона (Java EE)

Подержка WebSocket была реализована в Java EE 7. Для создания серверного эндпоинта, который будет общаться с клиентом, достаточно создать класс с аннотацией @ServerEndpoint и методами, помеченными соответствующими аннотациями.
@ServerEndpoint(value = "/sandbox")
public class SandboxEndpoint {

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        System.out.println("New Session opened");
    }
    
    @OnMessage
    public void onMessage(String message, Session session) {
        session.getBasicRemote().sendText("Client wrote: " + message);
    }
    
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.out.println("Error occured: " + throwable.getMessage());
    }
    
    @OnClose
    public void onClose (Session session, CloseReason closeReason) {
        System.out.println("Session closed: " + closeReason.getReasonPhrase());
    }
    
}

Порядок агрументов в методах нестрогий. Так же путь, передаваемый в адресе эндпоинта, может содержать переменные пути:
@ServerEndpoint(value = "/sandbox/{chat}/{username}")

Данные переменные можно передавать в методы в качестве аргументов примитивных типов или объектов класса String. Особое внимание стоит обратить на метод, помеченный аннотацией @OnMessage. Набор его аргументов может разниться в зависимости от ситуации. Отправлять текстовые сообщения от сервера клиенту так же достаточно просто:
session.getBasicRemote().sendText("Some text message");

Код данного поста доступен на GitHub. В следующих постах я постараюсь описать все варианты использования метода @OnMessage на сервере, работу с энкодерами и декодерами, а так же использование веб-сокетов за пределами веб-приложений. Рекомендую ознакомиться со статьёй "Building a Smart Home Server by Using the Java EE 7 WebSocket API", где демонстрируется наглядное применение вебсокетов.

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

пятница, 11 июля 2014 г.

Передача сообщений между WebSocket и JMS (Spring Framework)

После вчерашнего поста о реализации передачи сообщений между JMS и WebSocket силами Java EE я решил сообразить аналогичную реализацию при помощи Spring Framework. Для этого потребуется сервер приложений, реализующий JSR 356 (Apache Tomcat версии 7.0.47 и выше, GlassFish 4, WildFly 8 или любой другой), JMS-брокер, в качестве которого я выбрал Apache ActiveMQ, и браузер, реализующий RFC 6455.

pom.xml

Для нашего проекта потребуются следующие зависимости:
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>4.0.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jms</artifactId>
            <version>4.0.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-client</artifactId>
            <version>5.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>4.0.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
Если используется Tomcat, то понадобится реализация Java API for JSON Processing:
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.0.4</version>
        </dependency> 

Реализация серверной стороны

Как я уже упомянул в предыдущем посте, реализовать передачу сообщений между JMS и WebSocket при помощи Spring очень просто. Для этого понадобится класс, обрабатывающий события WebSocket (хэндлер), а так же JMS-листнер, в который хэндлер будет внедрён. 

WebSocket-хэндлер (ChatHandler.java):
public class ChatHandler extends TextWebSocketHandler {

    private final static Logger logger = LoggerFactory.getLogger(ChatHandler.class);
    private final static Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<WebSocketSession>());

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        logger.info("User has left the chat");

        super.afterConnectionClosed(session, status);
        sessions.remove(session);
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        logger.info("New user has joined the chat");

        super.afterConnectionEstablished(session);
        sessions.add(session);
    }

    public void sendMessage(final String message) throws IOException {
        logger.info("Sending message to all participants");

        for (WebSocketSession session : sessions) {
            if (session.isOpen()) {
                session.sendMessage(new TextMessage(message));
            } else {
                sessions.remove(session);
            }
        }
    }
}

JMS-листнер (WebSocketOutMessageListener.java):
public class WebSocketOutMessageListener implements MessageListener {

    private static final Logger logger = LoggerFactory.getLogger(WebSocketOutMessageListener.class);

    private ChatHandler webSocketHandler;

    public void setWebSocketHandler(ChatHandler webSocketHandler) {
        this.webSocketHandler = webSocketHandler;
    }

    @Override
    public void onMessage(Message message) {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            try {
                webSocketHandler.sendMessage(textMessage.getText());
            } catch (JMSException | IOException ex) {
                logger.error(ex.getMessage(), ex);
            }
        }
    }
}

Spring-beans XML
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:websocket="http://www.springframework.org/schema/websocket"
       xmlns:jms="http://www.springframework.org/schema/jms"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/websocket
                           http://www.springframework.org/schema/websocket/spring-websocket.xsd
                           http://www.springframework.org/schema/jms
                           http://www.springframework.org/schema/jms/spring-jms.xsd">

    <bean class="org.apache.activemq.ActiveMQConnectionFactory"
          id="activeMQConnectionFactory"
          p:brokerURL="tcp://localhost:61616"/>
    
    <websocket:handlers>
        <websocket:mapping path="/chat" handler="chatHandler"/>
    </websocket:handlers>
    
    <bean class="org.acruxsource.sandbox.spring.jmstows.websocket.ChatHandler" 
          id="chatHandler"/>
    
    <bean class="org.acruxsource.sandbox.spring.jmstows.jms.WebSocketOutMessageListener"
          id="webSocketOutMessageListener"
          p:webSocketHandler-ref="chatHandler"
          lazy-init="false"/>
    
    <jms:listener-container connection-factory="activeMQConnectionFactory" container-type="default">
        <jms:listener destination="websocket.out" ref="webSocketOutMessageListener" method="onMessage"/>
    </jms:listener-container>

</beans>

Этого достаточно, что бы отправлять сообщения из JMS клиентам WebSocket.

Реализация клиентской стороны

Для клиентской стороны достаточно небольшой странички:
index.jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>WebSocket test</title>
    </head>
    <body>
        <script type="text/javascript">
            var ws;

            if ('WebSocket' in window) {
                console.log('This browser supports websocket');
                ws = new WebSocket('ws://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/sandbox/chat');
                ws.onmessage = function(message) {
                    console.log(message);
                };
            }
        </script>
    </body>
</html> 

Сообщения из очереди websocket.out будут отображаться в консоли веб-браузера.

Для отправки сообщений в обратном направлении, из WebSocket в JMS понадобится ещё один класс.

JmsMessageSender.java
public class JmsMessageSender {

    private JmsTemplate jmsTemplate;

    public void setJmsTemplate(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public void sendMessage(String destination, final String message) {
        jmsTemplate.send(destination, new MessageCreator() {

            @Override
            public Message createMessage(Session session) throws JMSException {
                TextMessage textMessage = session.createTextMessage();
                textMessage.setText(message);
                return textMessage;
            }
        });
    }
}
Объект этого класса нужно внедрить в WebSocket-хэндлер и использовать его для отправки сообщений в очередь

ChatHandler.java
    private JmsMessageSender jmsMessageSender;

    public void setJmsMessageSender(JmsMessageSender jmsMessageSender) {
        this.jmsMessageSender = jmsMessageSender;
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        logger.info("New incoming message: " + message.getPayload());
        jmsMessageSender.sendMessage("websocket.in", message.getPayload());
    }

Остаётся изменить spring-beans:
    <bean class="org.springframework.jms.connection.SingleConnectionFactory"
          id="jmsProducerConnectionFactory"
          p:targetConnectionFactory-ref="activeMQConnectionFactory"/>

    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"
          p:connectionFactory-ref="jmsProducerConnectionFactory"/>
    
    <bean class="org.acruxsource.sandbox.spring.jmstows.websocket.ChatHandler" 
          id="chatHandler"
          p:jmsMessageSender-ref="jmsMessageSender"/>
    
    <bean class="org.acruxsource.sandbox.spring.jmstows.jms.JmsMessageSender"
          id="jmsMessageSender"
          p:jmsTemplate-ref="jmsTemplate"/>

Теперь для теста из консоли браузера можно отправить сообщение: ws.send("Some test message").
В следующих постах я более основательно опишу работу с WebSocket.

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

четверг, 10 июля 2014 г.

Передача сообщений между WebSocket и JMS (Java EE)

Появилась в одном проекте необходимость реализовать трансфер сообщений из JMS в веб-приложение конечному пользователю. Так как проект написан на Java EE 7, наиболее простым способом я счёт использование Websocket.

Передача сообщений из JMS в WebSocket

Первый способ передачи сообщений между JMS-листнером и эндпоинтом, который появился у меня в голове: добавляем к классу-эндпоинту аннотации @Named и @ApplicationScoped и внедряем его в JMS-листнер (Лишний код в примерах опущен).

@MessageDriven(mappedName = "websocket.out")
public class SimpleMessageListener implements MessageListener {
    @Inject
    private SimpleEndpoint endpoint;

    @Override
    public void onMessage(Message message) {
        try {
            endpoint.sendMessage(message);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}


@Named
@ApplicationScoped
@ServerEndpoint(value = "/wse")
public class SimpleEndpoint {

    public void sendMessage(Message message) throws IOException, JMSException {
        for (Session session : sessions) {
            if (session.isOpen()) {
                session.getBasicRemote().sendText(((TextMessage) message).getText());
            } else {
                sessions.remove(session);
            }
        }
    }
}


Тесты показали, что всё отлично работает.

Есть второй вариант, описанный Bruno Borges, который заключается в использовании CDI-событий. В данном случае JMS-листнер будет публиковать события, а эндпоинт будет их ловить. Изменения кода минимальны.

Изменения в JMS-листнере:
    @Inject
    @JmsToWsMessage
    private Event<Message> event;

    @Override
    public void onMessage(Message message) {
        try {
            event.fire(message);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
Изменения в эндпоинте:
    public void sendMessage(@Observes @JmsToWsMessage Message message) throws IOException, JMSException {
        for (Session session : sessions) {
            if (session.isOpen()) {
                session.getBasicRemote().sendText(((TextMessage) message).getText());
            } else {
                sessions.remove(session);
            }
        }
    }
    
Как видно, нам понадобится аннтоация, которой будут маркироваться события:

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface JmsToWsMessage {
}


Ещё один способ пришёл в голову буквально за утренним кофе. Для этого нужно:
1. Реализовать возможность доступа к websocket-сессиям из статических методов класса-эндпоинта
2. Сделать метод sendMessage статическим
И в JMS-листнере можно будет отправлять сообщения следующим образом:

SimpleEndpoint.sendMessage(message);

Передача сообщений из WebSocket в JMS

А что если потребуется трансфер в обратном направлении, из вебсокетов в JMS? Тут всё оказалось немного сложнее. Первый вариант, появившийся у меня в голове заключался во внедрении и использовании JMS-контекста в классе-эндпоинте.
Способ оказался нерабочим. Как выяснилось позднее из статьи Bruno Borges, проблема заключается в баге, который не позволяет внедрять JMS-контекст в эндпоинты.
Второй способ заключался во внедрении ConnectionFactory, но и он у меня не сработал - любые JMS-ресурсы, внедрённые не в EJB, оказались null. Вполне возможно, что это из-за особенностей используемого мной сервера приложений (WildFly 8 с внедрённым ActiveMQ).
Но, раз осталась возможность внедрять JMS-ресурсы в EJB, появился третий вариант. Для его реализации создадим сессионный компонент, который будет отправлять сообщения:

@Stateless
public class MessageSender {

    @Resource(mappedName = "java:/activemq/ConnectionFactory")
    private ConnectionFactory connectionFactory;

    @Resource(mappedName = "java:/jms/queue/websocket.in")
    private Queue queue;

    public void sendMessage(String message) {
        try {
            System.out.println("Sending to " + queue.getQueueName());
            try (Connection connection = connectionFactory.createConnection()) {
                connection.start();
                javax.jms.Session session = connection.createSession(true, javax.jms.Session.AUTO_ACKNOWLEDGE);
                MessageProducer producer = session.createProducer(queue);
                TextMessage textMessage = session.createTextMessage();
                textMessage.setText(message);
                producer.send(textMessage);
                connection.stop();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
Дело остаётся за малым - внедрить наш сессионный компонент в класс-эндпоинт. Но и тут оказался сюрприз - сработало только внедрение через конструктор, в остальных случаях я получал NullPointerException.

Изменения в WebSocket-эндпоинте
    
    private MessageSender messageSender;

    public SimpleEndpoint() {}

    @Inject
    public SimpleEndpoint(MessageSender messageSender) {
        this.messageSender = messageSender;
    }
В следующем посте я опишу решение данной задачи силами Springframework. По опыту работу с ним, могу предположить, что там всё будет просто.

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

понедельник, 30 июня 2014 г.

Oracle Certified Professional Java SE7 Programmer

Хотел написать о том, как я сертифицировался на "Oracle Certified Professional, Java SE7 Programmer", но что-то из головы вылетело. Да и вообще забросил свой блог. Хотя идей, мыслей и знаний всё больше и больше. Буду навёрстывать упущенное.
Сертификационный экзамен я проходил в конце октября, всё там же, в Тюменском филиале учебного центра "Микротест". К этому экзамену я готовился основательнее, чем к OCAJP, так как заранее знал, что вопросы будут сложнее и коварнее, да и охват тем, которые затронуты в этом экзамене, значительно больше. В качестве литературы для подготовки я использовал, в основном, всё ту же книгу "Java. Полное руководство" Герберта Шилдта (8 издание, исчерпывающее описание самого нужного в Java SE7)", так как в ней даётся вся нужная для сертификации информация. Кроме этого поглядывал в книгу "Oracle Certified Professional Java SE 7 Programmer Exams 1Z0-804 and 1Z0-805", в основном, для тестов. Двух этих книг предостаточно для подготовки к экзамену. "Философия Java" Брюса Эккеля в данном случае оказалась абсолютно бесполезной книгой, так как русская её версия не содержит описания основных библиотек, на которых акцентировано большое количество вопросов в экзамене. Интересен тот факт, что в оригинальной книге "Thinking in Java" вся эта информация есть. Спасибо русским издателям.
Экзамен был сдан успешно, но возникли некоторые вопросы. Первый (и, наверно, главный): "Почему отсутствуют вопросы по аннотациям?". То есть они отсутствуют вообще в принципе. Данная технология появилась в Java SE 5 и за прошедшее время нашла применение в большом количестве библиотек, фреймворков и технологий. Стандартные библиотеки, EJB, Hibernate, Spring, продолжать можно до бесконечности. Более того, в Java EE аннотации фактически стали стандартом де-факто. Конечно, можно ещё придраться к повторяющимся (с OCAJP) вопросам и к вопросам "на внимательность", смысл которых я никогда не понимал, но это уже незначительно.
Особое внимание при подготовке к этому экзамену стоит обратить на коллекции, JDBC, дженерики (особенно на разницу между super и extends) и наследование. А в целом - ничего сложного, хоть и ходят слухи о высоком проценте несдачи этого экзамена с первого раза. Дальше я думаю двигаться в направлении OCEWCD 6 (Oracle Certified Expert, Java EE 6 Web Component Developer) и OCEEJBD 6 (Oracle Certified Expert, Java EE 6 Enterprise JavaBeans Developer), так как много в последнее время работаю с технологиями, охватываемыми этими экзаменами.

воскресенье, 17 ноября 2013 г.

Интегрированные среды разработки для Java

На сегодняшний день существуют три основные IDE для разработки на Java: Eclipse, NetBeans и IntelliJ IDEA. Выбрать для себя, как же именно среда разработки наиболее удобна - задача, отнюдь, не из простых. И выбор этот со временем может изменяться.

NetBeans

Именно с NetBeans когда-то я начал познавать замечательный мир IDE, где открытые проекты всегда под рукой, есть подсветка синтаксиса кода, выделение ошибок и прочие вкусности. Полный дитрибутив NetBeans из коробки поддерживает Java (SE, ME, EE, Card, FX), C/C++, Groovy, PHP, HTML5. Возможности этой IDE внушительны: сразу есть поддержка JavaEE, Spring Framework, Hibernate, OSGI, Maven, юнит-тестов, возможность разработки настольных приложений на платформе NetBeans и использования облачных сервисов. Отдельным плюсом стоит отметить то, что поддержка новых версий спецификаций, входящих в JavaEE, изначально появляется именно в NetBeans.

Однако есть и свои недостатки - NetBeans 5 и 6 версий была очень тяжеловесной IDE и нередко лагала при достаточно хорошем железе. В 7 версии эта проблема уже не заметна. Для меня большим недостатком было отсутствие официальной поддержки JBoss Application Server 7 (причины, думаю, можно не указывать), что решалось установкой стороннего плагина. Но в версии NetBeans 7.3 поддержку всё таки добавили, когда в состоянии альфы уже была WildFly 8.

В целом - хорошая, мощная опенсорсная IDE, с большой функциональностью, расширяемой плагинами, но сильно зависящая от Oracle, что в плюсы сложно записать.

Eclipse

С данной IDE я сейчас работаю большую часть своего времени. Главные достоинства Eclipse - её расширяемость, огромное сообщество, разрабатывающее огромное же количество плагинов и расширений, поддержка практически всех технологий, связанных с Java. По своей природе Eclipse очень тяжеловесная IDE, и комфортно работать с ней стало возможно начиная с версии Juno (по крайней мере в линуксах).

Расширения в Eclipse есть как маленькие, добавляющие небольшие функциональные нововведения, так и большие, комплексно изменяющие IDE, например Spring Tool Suite и JBoss Developer Studio. Лично я уже привык использовать JBoss Developer Studio с расширением Spring, что даёт максимальное покрытие используемых технологий.

На мой взгляд данная IDE наиболее удобна при повседневной разработке на Java EE, в силу своей расширяемости и списка поддерживаемых технологий. Из минусов можно отметить только ресурсоёмкость.

IntelliJ IDEA

Самая известная коммерческая IDE для разработки на Java. В комплекте с IDE идёт огромное количество расширений, добавляющих в неё поддержку практически всех распространённых Java-технологий и не только. При этом стоит отметить её легковесность - поддерживая большее количество технологий, чем NetBeans, IDEA нисколько не лагает и использует гораздо меньшее количество ресурсов. Однако она и стоит денег - $199 для персонального использования, но она того стоит.

Кроме коммерческой Ultimate-версии, есть и Community-версия, которая обладает поддержкой базовых технологий, список которых можно расширить, установив нужные расширения. В Community-версии плюсом можно считать только её быстроту.

Если IntelliJ IDEA и использовать, то только коммерческую версию. В целом её можно назвать лучшей IDE для Java, отметив одно "но" - в одном окне можно работать только с одним проектом, что далеко не всегда удобно.

Spring Remoting (Часть 1): RMI

Иногда появляется необходимость наделить модульностью Spring-приложения по аналогии с JavaEE-приложениями. Типичный пример - нужно разработать приложение, которое будет использоваться другими приложениями для доступа к БД. Следовательно, между приложениями каким-то образом должно быть реализовано взаимодействие. Ответ на вопрос "А зачем?" достаточно прост: при таком подходе приложения-клиенты ничего не знают о конечной реализации, которую можно будет изменять, не внося изменения в клиенты (например, вместо Hibernate использовать MyBatis или вместо MySQL - MongoDB).

Spring Framework предоставляет большое количество возможных реализаций взаимодействия между приложениями: RMI, Hessian/Burlap, веб-сервисы, JMS, JMX, JCA и даже собственный сервис вызова компонентов через HTTP и интеграцию с EJB. Самым простым, с точки зрения разработчика, будет вариант с использованием RMI.

Разберём небольшой пример.

Допустим, у нас есть интерфейс GreetingsService:
package org.acruxsource.sandbox.remoting.services;

public interface GreetingsService {
    String DEFAULT_NAME = "Mate";
    String greetings(String name);
}

На стороне приложения, предоставляющего сервисы для общего использования, пишем небольшую реализацию этого интерфейса:
package org.acruxsource.sandbox.remoting.services;

public class SimpleGreetingsService implements GreetingsService {

    @Override
    public String greetings(String name) {
        if(name == null || name.isEmpty())
            name = DEFAULT_NAME;
       
        return new StringBuilder("G'day, ").append(name).append("!").toString();
    }
}
Теперь можно заняться описанием компонентонентов Spring. Наш сервис:

И, собственно, его экспорт:
    
        
        
        
        
    
Вместо 1199 можно использовать любой другой не занятный порт. По умолчанию используется 1099.
Осталось указать сервис на стороне клиента:

        
        

Теперь данный сервис на стороне клиента можно использовать как обычные компоненты - использовать для поиска и внедрения зависимостей. Все вызовы к методам GreetingsService будут проксироваться классом RmiProxyFactoryBean.

В результате мы имеем приложение-сервер, предоставляющее сервисы и приложения-клиенты, которые могут использовать эти сервисы. Однако в данном случае есть некоторые нюансы: у RMI отсутствует поддержка использования сервисов через протокол HTTP (а надо ли?), RMI как протокол очень тяжеловесный (но надёжный) и используется только в схеме Java-to-Java, то есть и сервер и клиенты должны быть написаны на Java, или исполняться в JVM (не забываем про Groovy и другие JVM-языки).