Skip to content

REST колы и web сокеты на Jetty сервере (часть 3)

Синопсис

В этом посте продолжается серия постов про Jetty сервер. В первом посте данной серии мы создали простой Jetty сервер и запустили его. Во втором посте мы подвесили на наш jetty сервер веб сокеты, тем самым мы превратили его из просто jetty сервера в jetty сервер с веб сокетами. И наконец в этом посте мы еще довашаем на jetty сервер REST колы. Таким образом у нас должен получиться Jetty сервер с веб сокетами и REST колами. В принципе мы уже поднимали REST колы на Jetty сервере, это было в посте Как поднять CXF Servlet, но в этот раз мы сделаем немного по другому. Если тогда, мы создавали обычное веб приложение в виде стандартного варника, который мы затем деплоили в каталог webapp контейнера сервлетов и за тем запускали контейнер сервлетов, то в этот раз, мы обойдемся без контейнера сервлетов. Мы создадим некий гибрид который будет одновременно и веб и исполняемым приложением. То есть для того чтобы заработали наши REST колы, нужно чтобы варник поднялся Jetyy сервером. Для этого можно пойти двумя путями, либо закинуть варник в webapp каталог контейнера как мы это уже делали, либо запустить собственный jetty сервер из под этого варника, в этом случае он становится еще и java приложением. Мы запускаем Jetty сервер который поднимает REST колы находящиеся в нем же.

Готовый проект можно взять с gitHub.

Структура проекта

Нам предстоит собрать war архив с нестандартной структурой, то есть помимо каталога WEB-INF в корень варника нам нужно закинуть обычные классы, как если бы это был обычный джарник, а в файле манифеста, который находится в каталоге META-INF нужно указать класс с которого надо стартовать, то есть тот класс где находится метод main. По этой причине декларативный сборщик мавен уже не годится, так как он предназначен для сборки проектов с заранее занесенной в базу мавена структурой, а так как у нас структура проекта мавену как бы не известна, то собрать проект обычным способом не получится. Конечно можно и мавеном собирать не стандартные проекты, для этого нужно мавен об этой нестандартной структуре известить, но как будет при этом выглядеть pom.xml файл страшно представить, поэтому все преимущества мавена, при нестандартной структуре проекта, теряются. Можно взять ant скрипт где руками подробно расписать, что куда и как или вообще шелл скрипт, но в таком случае придется самостоятельно решать проблемы с зависимостями. Короче ни мавен ни ант ни шелл не годятся, однако нам необходимо изменять структуру варника, что называется, «на лету» и при этом не парится с зависимостями. В таком случае, для того, чтобы можно было и структурой проекта манипулировать и с зависимостями не париться существуют такие сборщики, как ivy и gradle. Мне больше нравится gradle. Gradle это отдельная большая тема, поэтому если в двух словах, это тот же мавен, но который позволяет управлять структурой проекта более гибко, за подробностями сюда.

JettyWebSocketAndRestCalls
    ├──src
    │   ├─main
    │   │  ├─java
    │   │  │  └─our
    │   │  │     └─task
    │   │  │         ├─JettyServer
    │   │  │         │   └─JettyStarter.java
    │   │  │         ├─JettyWebSocket
    │   │  │         │   ├─MySocket.java
    │   │  │         │   └─SocketHandler.java
    │   │  │         └─service
    │   │  │             ├─impl
    │   │  │             │   └─TestServiceImpl.java
    │   │  │             └─TestService.java
    │   │  └─webapp
    │   │     └─WEB-INF
    │   │        ├─spring
    │   │        │   └─spring-context.xml
    │   │        └─web.xml
    │   └─test
    │      └─java
    │         └─our
    │            └─task
    │                ├─JettyServer
    │                ├─JettyWebSocket
    │                └─service
    │                    └─impl
    └──build.gradle

Java код

Класс который будет запускаться JettyStater находится в пакете our.task.JettyServer. Это класс который изменяется от версии к версии. Подсвеченными строками выделено что было добавлено или изменено.

JettyStarter.java

package our.task.JettyServer;
 
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;

import java.net.URL;
import org.eclipse.jetty.webapp.WebAppContext;
import java.security.ProtectionDomain;
 
import our.task.JettyWebSocket.SocketHandler;
 
public class JettyStarter {
    public static void main(String [] args) {
        Server server = new Server();
        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8080);
        server.addConnector(connector);
 
        // add first handler
        ResourceHandler resource_handler = new ResourceHandler();
        resource_handler.setDirectoriesListed(true);
        resource_handler.setWelcomeFiles(new String[] { "index.html" });
        resource_handler.setResourceBase(".");
		
		ProtectionDomain domain = JettyStarter.class.getProtectionDomain();
        URL location = domain.getCodeSource().getLocation();
		
		// add context
		WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        webapp.setWar(location.toExternalForm());
 
        HandlerList handlers = new HandlerList();
        // first element  is webSocket handler
		// second element is first handler,
		// third element is webContext
        handlers.setHandlers(new Handler[] {new SocketHandler(), resource_handler, webapp});
         
        server.setHandler(handlers);
 
        try {
            server.start();
            server.join();
        } catch (Throwable t) {
            t.printStackTrace(System.err);
        }
    }
}

Класс SocketHandler находится в пакете our.task.JettyWebSocket. Остается без изменений.

SocketHandler.java

package our.task.JettyWebSocket;
 
import org.eclipse.jetty.websocket.server.WebSocketHandler;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
 
public class SocketHandler extends WebSocketHandler {
 
    @Override
    public void configure(WebSocketServletFactory factory) {
        factory.register(MySocket.class);
    }  
}

Класс MySocket находится в пакете our.task.JettyWebSocket. Без изменений.

MySocket.java

package our.task.JettyWebSocket;
 
import java.io.IOException;
 
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
 
@WebSocket
public class MySocket {
    private Session session;
     
    @OnWebSocketConnect
    public void onConnect(Session session) {
        System.out.println("Connect: " + session.getRemoteAddress().getAddress());
        try {
            this.session = session;
            session.getRemote().sendString("Got your connect message");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
     
    @OnWebSocketMessage
    public void onText(String message) {
        System.out.println("text: " + message);
        try {
            this.session.getRemote().sendString(message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
     
    @OnWebSocketClose
    public void onClose(int statusCode, String reason) {
        System.out.println("Close: statusCode=" + statusCode + ", reason=" + reason);      
    }
}

Класс TestService находится в пакете our.task.service. Это собственно REST интерфейс, который был взят из поста Как поднять CXF Servlet.

TestService.java

package our.task.service;
 
import javax.ws.rs.GET;
import javax.ws.rs.Path;
 
@Path("/test-service/")
public interface TestService {
 
    @GET
    @Path("/test")
    public String execute();
}

TestServiceImpl находится в пакете our.task.service.impl. Имплементация класса TestService, так же был взят из поста Как поднять CXF Servlet.

TestServiceImpl.java

package our.task.service.impl;
 
import our.task.service.TestService;
 
public class TestServiceImpl implements TestService {
 
    public String execute() {
        return "test Servlet";
    }
}

web.xml расположен по пути projectName/src/main/webapp/WEB-INF. информация для контейнера сервлетов где указаны классы в которых будут обрабатываться http запросы.

web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
 
<web-app>
 
    <display-name>RestDemo</display-name>
    <description>RestDemo</description>
 
    <!-- Spring -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/spring/spring-context.xml</param-value>
    </context-param>
 
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
 
    <!-- CXF -->
    <servlet>
        <servlet-name>CXFServlet</servlet-name>
        <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>CXFServlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>

spring-context.xml расположен по пути projectName/src/main/webapp/WEB-INF/spring. Контекст спринга, который считывается контейнером spring для того, чтобы поднять бины.

spring-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:cxf="http://cxf.apache.org/core"
        xmlns:jaxws="http://cxf.apache.org/jaxws"
        xmlns:jaxrs="http://cxf.apache.org/jaxrs"
        xsi:schemaLocation="
            http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
 
http://www.springframework.org/schema/beans
 
 
http://www.springframework.org/schema/beans/spring-beans.xsd
 
            http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd
            http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">
 
    <import resource="classpath:META-INF/cxf/cxf.xml"/>
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>
 
    <bean id="serviceBean" class="our.task.service.impl.TestServiceImpl">
    </bean>
 
    <jaxrs:server id="restController" address="/">
        <jaxrs:serviceBeans>
            <ref bean="serviceBean"/>
        </jaxrs:serviceBeans>
    </jaxrs:server>
 
</beans>

Build scipt

Последнее, что нам осталось сделать перед сборкой, это добавить билд скрипт. Собирать проект будетм градлом. Добавьте в корень проекта файл build.gradle с таким содержанием:

apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'jetty'

repositories {
	mavenCentral()
}

List jetty_libraries = ['org.eclipse.jetty:jetty-webapp:9.1.3.v20140225',
	'org.eclipse.jetty:jetty-server:9.1.3.v20140225',
	'org.eclipse.jetty.websocket:websocket-server:9.1.3.v20140225'
]

configurations {
    embeddedJetty
}

dependencies {
	embeddedJetty jetty_libraries
}

dependencies {
	compile jetty_libraries
	compile 'org.springframework:spring-web:3.2.7.RELEASE'
	compile 'org.apache.cxf:cxf-rt-frontend-jaxrs:3.0.0-milestone1'
	testCompile 'junit:junit:4.11'
}

war {
	from {
		configurations.embeddedJetty.collect {
			project.zipTree(it)
		}
    }
	exclude "META-INF/*.SF", "META-INF/*.RSA", "about.html", "about_files/**", "readme.txt", "plugin.properties", "jetty-dir.css", "META-INF/maven/org.eclipse.jetty/*/pom.*"
	from "$buildDir/classes/main"
	manifest {
		attributes (
			'Main-Class': 'our.task.JettyServer.JettyStarter',
			'Manifest-Version': '1.0',
			'Gradle-Version': 'Gradle 1.7'
		)
	}
}

Так как раньше для сборки проектов gradle я не применял, то расскажу чуть более подробно о том, что делается в этом билд скрипте.
Сам по себе градл не заточен конкретно под джаву, как, например мавен, по этому перед тем как начать работать с проектом надо градлу указать, с каким типом проекта он будет работать, который добавляется в виде плагина. Кроме собственно сборки джававского проекта, у градала появляются и другие возможности которые можно просмотреть командой:

gradle tasks
:tasks

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

Application tasks
-----------------
distTar - Bundles the project as a JVM application with libs and OS specific scripts.
distZip - Bundles the project as a JVM application with libs and OS specific scripts.
installApp - Installs the project as a JVM application along with libs and OS specific scripts.
run - Runs this project as a JVM application

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles classes 'main'.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles classes 'test'.
war - Generates a war archive with all the compiled classes, the web-app content and the libraries.

Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.

Help tasks
----------
components - Displays the components produced by root project 'websockets'.
dependencies - Displays all dependencies declared in root project 'websockets'.
dependencyInsight - Displays the insight into a specific dependency in root project 'websockets'.
help - Displays a help message
projects - Displays the sub-projects of root project 'websockets'.
properties - Displays the properties of root project 'websockets'.
tasks - Displays the tasks runnable from root project 'websockets'.

Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.

Web application tasks
---------------------
jettyRun - Uses your files as and where they are and deploys them to Jetty.
jettyRunWar - Assembles the webapp into a war and deploys it to Jetty.
jettyStop - Stops Jetty.

Rules
-----
Pattern: clean: Cleans the output files of a task.
Pattern: build: Assembles the artifacts of a configuration.
Pattern: upload: Assembles and uploads the artifacts belonging to a configuration.

To see all tasks and more detail, run with --all.

В нашем скрипте мы добавляем 3 плагина:

apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'jetty'

Это означает, что возможности градла расширяются на 3 области java, war и jetty. То есть плагин java дает градлу возможность скомпилировать java проект, запустить его. Плагин war позволяет создать варник, плагин jetty позволяет запустить этот варник на jetty сервере.

repositories {
	mavenCentral()
}

Таск repositories говорит градлу от куда брать зависимости. В данному случае таск repositories говорит градлу искать зависимости в стандартном мавенском репозитории mavenCentral (понятно, что после того как он не нашел их в своем локальном репозитории).


dependencies {
    compile jetty_libraries
    compile 'org.springframework:spring-web:3.2.7.RELEASE'
    compile 'org.apache.cxf:cxf-rt-frontend-jaxrs:3.0.0-milestone1'
    testCompile 'junit:junit:4.11'
}

Таск dependencies означает точно тоже самое, что и тэг dependency в мавенском pom.xml — какие зависимости нужны проекту. Но тут есть маленькое отличие в указании того, когда нужны эти зависимости. Если мы не указываем мавену явно, что зависимости нужны в момент компиляции, то в градле нужно указывать это явно словом compile, то же самое касается других скопов, например testCompile, runtime и пр.
Можно определить список зависимости в переменной-массиве и потом эту переменную массива вставлять туда, где нужны перечисляемые элементы, как, например, с массивом jetty_libraries:

List jetty_libraries = ['org.eclipse.jetty:jetty-webapp:9.1.3.v20140225',
    'org.eclipse.jetty:jetty-server:9.1.3.v20140225',
    'org.eclipse.jetty.websocket:websocket-server:9.1.3.v20140225'
]
dependencies {
    embeddedJetty jetty_libraries
}

Тут в таске dependencies указан скоп embeddedJetty, который пришел в билд скрипт вместе с плагином jetty. Он указывает, что эти зависимости встроены в jetty сервер, и что они будут подниматься, во время его работы. Однако как они в jetty сервере появятся это не забота embeddedJetty, об этом позаботимся мы сами.
И наконец таск war:

war {
    from {
        configurations.embeddedJetty.collect {
            project.zipTree(it)
        }
    }
    exclude "META-INF/*.SF", "META-INF/*.RSA", "about.html", "about_files/**", "readme.txt", "plugin.properties", "jetty-dir.css", "META-INF/maven/org.eclipse.jetty/*/pom.*"
    from "$buildDir/classes/main"
    manifest {
        attributes (
            'Main-Class': 'our.task.JettyServer.JettyStarter',
            'Manifest-Version': '1.0',
            'Gradle-Version': 'Gradle 1.7'
        )
    }
}

которым мы поднастроем немного создание варника.
Свойство from означает откуда брать файлы, которые надо поместить в корень war архива. Тут мы указываем таких мест два.
Первое это $buildDir/classes/main, откуда надо взять все скомпилированные классы и поместить их в корень варника, чтобы варник стал как джарник, так как по умолчанию в варнике классы помещаются в WEB-INF/classes/main где их может найти только контейнер сервлетов, но не джава машина, так как класспас туда не указан.
Второе это configurations.embeddedJetty.collect. В свойстве embeddedJetty указаны все зависимости для jetty сервера, поэтому если мы хотим чтобы они были доступны в jetty сервере во время запуска их так же нужно перенести в корень варника.
exclude этим свойством мы исключаем не нужные файлы из варника.
Свойство manifest. Тут мы настраиваем файл манифеста, в котором указываем путь к классу с main методом. Затем, когда этот варник передадим джава машине, она (джава машина) первым делом из файла манифеста разрезолвит класс с main-методом, то есть класс, с которого надо запускать приложение.

Сборка и запуск

Теперь соберем и запустим проект. Для этого заходим в корень проекта и выполняем команду build:

gradle build
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:war UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build UP-TO-DATE

BUILD SUCCESSFUL

В вашем случае не должно быть UP-TO-DATE после каждой таски, это у меня так, потому что я уже запускал команду gradle build не один раз, и так как в последний раз я ничего не менял, то таски просто скипаются.
Если билд сакесфул то в каталоге build появятся скомпилированные классы, джарники, варники и пр. Нас интересует варник JettyWebSocketAndRestCalls.war который находится в каталоге build/libs. Заходим в этот каталог и запускаем war архив JettyWebSocketAndRestCalls.war командой:

java -jar JettyWebSocketAndRestCalls.war

Теперь проверим работу нашего jetty сервера. Сначала убедимся, что он вообще поднялся. Откройте браузер и введите в строке адреса http://localhost:8080. В браузере должно появится название варника, который мы запустили:

1
Теперь проверим REST колы, для этого введем в строке адреса http://localhost:8080/rest/test-service/test:

2
И последнее, это веб сокеты. Для этого создадим клиента на javaScript:

<html>
    <head>
        <meta charset=UTF-8>
        <title>Tomcat WebSocket Chat</title>
        <script>
			var ws = new WebSocket("ws://localhost:8080");
            ws.onopen = function(){
            };
            ws.onmessage = function(message){
                document.getElementById("chatlog").textContent += message.data + "\n";
            };
            function postToServer(){
                ws.send(document.getElementById("msg").value);
                document.getElementById("msg").value = "";
            }
            function closeConnect(){
                ws.close();
            }
        </script>
    </head>
    <body>
        <textarea id="chatlog" readonly></textarea><br/>
        <input id="msg" type="text" />
        <button type="submit" id="sendButton" onClick="postToServer()">Send!</button>
        <button type="submit" id="endButton" onClick="closeConnect()">End</button>
    </body>
</html>

И откроем html файл в браузере:

3
Ели в текстовом поле появилось сообщение Got your connect message значит соединение с веб сокетом установилось успешно. На стороне jetty сервера, должно появится:

Connect: /0:0:0:0:0:0:0:1

Далее введем какое нибудь сообщение в поле ввода, нажимаем кнопчку send как сервер тут же вернет эхо, которое отобразится в текстовом поле:

4
На стороне jetty сервера должно появится сообщение, которое отправил клиент:

text: test

Заключение

Конечно может быть запускать jetty сервер непосредственно джава машиной приходится редко, для этого существуют на много лучшее решение контейнеры сервлетов, но если надо совместить приложение веб приложение с обычным, то в этом случае приходится запускать веб приложение как обычное приложение.

Поделиться в социальных сетях

Опубликовать в Google Plus
Опубликовать в LiveJournal
Опубликовать в Мой Мир
Опубликовать в Одноклассники
Опубликовать в Яндекс

2 thoughts on “REST колы и web сокеты на Jetty сервере (часть 3)

  1. Прохожий says:

    javaScript файл исправить название кнопки, а то вешается 2 слушателя на одну

    Ответить

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *