Встраиваем Jetty-сервер в свой проект

Встраиваем Jetty-сервер в свой проект


Date of publication 29.09.2016



В этой статье я расскажу от такой крутой вещи, как Jetty сервер! Почему Jetty крутой и чем он может быть полезен сферическому java-программисту в вакууме? Всё дело в том что Jetty является одновременно легковесным и хорошо оптимизированным решением, которое можно использовать как в небольших, так и в крупных проектах. Jetty хорошо масштабируется, экономично использует память, но самый жир это то что его можно встроить в своё приложение. Это дико удобно когда нужно отладить работу веб-приложения, так как отпадает необходимость постоянно его пересобирать и заливать на сервер приложений. Да и вообще встроенный сервер может решать кучу полезных задач, например недавно мне понадобилось сделать легковесное веб-приложение со встроенным сервером, которое можно было бы запускать одной командой на любой машине, и для решения этой задачи я использовал Jetty.

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

  1. Наш проект будет собираться в jar-файл и запускаться простой командой java -jar. Все зависимости будут собираться в отдельную папку lib, которая будет лежать в одном каталоге с jar-файлом.
  2. Наш проект будет иметь внешний (не обязательный) файл настроек app.properties

Посвящать этим двум фишкам отдельную статью смысла нет, тем более что в данном примере они будут смотреться очень уместно. Теперь перейдём к разработке:

Для начала создайте веб-приложение в своей любимой IDE (я предпочитаю IDEA IntelliJ) или с помощью команды:

mvn archetype:generate -DgroupId=ru.kutepov 
-DartifactId=jetty-example 
-DarchetypeArtifactId=maven-archetype-webapp 
-DinteractiveMode=false

В результате этих манипуляций должен сгенерироваться проект вот с такой структурой:

Затем надо добавить в файл pom.xml все зависимости, которые будут нужны для дальнейшей работы. Для наглядности я решил прикрутить к проекту Spring MVC:

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>

Ну и конечно же зависимости для Jetty:

    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-server</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-webapp</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-jsp</artifactId>
      <version>${jetty.version}</version>
    </dependency>

Версии зависимостей я вынес в отдельный блок для красоты:

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.version>4.3.1.RELEASE</spring.version>
    <jetty.version>9.2.19.v20160908</jetty.version>
  </properties>

Кроме этого, нам потребуются три Maven-плагина, которые так же нужно добавить в pom.xml:

    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <executions>
          <execution>
            <id>copy-dependencies</id>
            <phase>prepare-package</phase>
            <goals>
              <goal>copy-dependencies</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.directory}/lib</outputDirectory>
              <overWriteReleases>false</overWriteReleases>
              <overWriteSnapshots>false</overWriteSnapshots>
              <overWriteIfNewer>true</overWriteIfNewer>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <classpathPrefix>lib/</classpathPrefix>
              <mainClass>ru.kutepov.launcher.Launcher</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>

maven-compiler-plugin отвечает за версию Java, которая используется при сборке проекта.

maven-dependency-plugin собирает все зависимости в папку lib и помещает её в каталог рядом с jar-файлом.

maven-jar-plugin делает jar-файл запускаемым и указывает класс (в данном случае ru.kutepov.launcher.Launcher, его мы создадим чуть позже), который вызывается при запуске проекта. Так же данный плагин записывает в манифест ссылку на папку с библиотеками - lib, которая создана предыдущим плагином.

Ещё необходимо задать папку ресурсов и указать местоположение внешнего файла с настройками приложения:

    <resources>
      <resource>
        <directory>src/main/webapp</directory>
      </resource>
      <resource>
        <directory>${basedir}/src/main/webapp/resources/properties</directory>
        <includes>
          <include>app.properties</include>
        </includes>
        <targetPath>..</targetPath>
      </resource>
    </resources>

В итоге должен получиться вот такой pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>ru.akutepov</groupId>
  <artifactId>jetty-example</artifactId>
  <packaging>jar</packaging>
  <version>1.0</version>
  <name>jetty-example</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.version>4.3.1.RELEASE</spring.version>
    <jetty.version>9.2.19.v20160908</jetty.version>
  </properties>

  <dependencies>

    <!--Spring-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!--Jetty-->
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-server</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-webapp</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-jsp</artifactId>
      <version>${jetty.version}</version>
    </dependency>

  </dependencies>

  <build>
    <resources>
      <resource>
        <directory>src/main/webapp</directory>
      </resource>
      <resource>
        <directory>${basedir}/src/main/webapp/resources/properties</directory>
        <includes>
          <include>app.properties</include>
        </includes>
        <targetPath>..</targetPath>
      </resource>
    </resources>
    <finalName>jetty-example</finalName>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <executions>
          <execution>
            <id>copy-dependencies</id>
            <phase>prepare-package</phase>
            <goals>
              <goal>copy-dependencies</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.directory}/lib</outputDirectory>
              <overWriteReleases>false</overWriteReleases>
              <overWriteSnapshots>false</overWriteSnapshots>
              <overWriteIfNewer>true</overWriteIfNewer>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <classpathPrefix>lib/</classpathPrefix>
              <mainClass>ru.kutepov.launcher.Launcher</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

С зависимостями разобрались, продолжаем дальше конфигурировать наше приложение. В папке /src/main/webapp/WEB-INF должен лежать файл web.xml. Если он ещё там не лежит, то его нужно немедленно создать:

<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
	     http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

  <display-name>Jetty example</display-name>

  <servlet>
    <servlet-name>mvc-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/beans.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>mvc-dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

Файл web.xml определяет соответствие между путями URL и сервлетами, которые эти URL будут обрабатывать. Веб-сервер использует эту конфигурацию, чтобы определить сервлет для обработки данного запроса и вызвать метод класса, который соответствует методу запроса.

Обратите внимание что в init-param мы указали местоположение конфига для Spring, поэтому создайте файл beans.xml в папке /src/main/webapp/WEB-INF/ со следующим содержимым:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd">

  <context:property-placeholder location="file:app.properties"
                                ignore-resource-not-found="true"/>

  <context:component-scan base-package="ru.kutepov"/>

  <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/pages/"/>
    <property name="suffix" value=".jsp"/>
  </bean>

</beans>

В property-placeholder я указал расположение внешнего файла конфига app.properties, затем с помощью component-scan инициализирую все классы из пакета ru.kutepov с аннотацией @Component. В конце инициализирую InternalResourceViewResolver, который отвечает за отображение .jsp страницы.

Теперь откройте файл index.jsp, который располагается в папке /src/main/webapp/WEB-INF/pages/ и вставьте туда такой код:

<html>
  <body>
    <h1>${message}</h1>
    <h3>${property_value}</h3>
  </body> 
</html> 

Как вы видите, у нас будет простая страница, которая должна отображать значения двух переменных, первую мы будем задавать в контроллере, а вторую возьмём из файла пропертей.

Создайте в папке /src/main/webapp/resources/properties/ файл app.properties и добавьте туда строку:

value=Have a nice day!

Затем создайте в папке /src/main/java пакет ru.kutepov.controller и добавьте в него класс MainController.java:

package ru.kutepov.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/")
public class MainController {

  @Value("${value:none}")
  public String value;


	@RequestMapping(method = RequestMethod.GET)
	public String printWelcome(ModelMap model) {
		model.addAttribute("message", "Hello world!");
    model.addAttribute("property_value", value);
		return "index";
	}
}

Тут тоже всё довольно просто: метод printWelcome обрабатывает все GET-запросы по адресу "/" и возвращает в ответ страницу html, которая генерируется на основе .jsp шаблона. В шаблон передаются 2 переменные message и property_value. В messageмы передаём произвольную строку а в property_value значение переменной value из файла app.properties.

Завершающим этапом будет создание загрузчика, который будет запускать jetty-сервер. добавьте в пакет ru.kutepov пакет launcher и создайте в нём класс Launcher.java:

package ru.kutepov.launcher;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

import java.net.URL;
import java.security.ProtectionDomain;

/**
 * Starts jetty-server on the specified port
 */
public class Launcher {

  public static void main(String[] args) throws Exception {
    int port = 12135;
    try {
      if (args.length > 0) {
        port = Integer.parseInt(args[0]);
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }

    Server server = new Server(port);

    ProtectionDomain domain = Launcher.class.getProtectionDomain();
    URL location = domain.getCodeSource().getLocation();

    WebAppContext webapp = new WebAppContext();
    webapp.setContextPath("/");
    webapp.setWar(location.toExternalForm());

    server.setHandler(webapp);
    server.start();
    server.join();
  }
}

Данный загрузчик запускает jetty-сервер на порту 12135 по умолчанию, либо на том порту, который был передан в качестве аргумента командной строки. Рассмотрим код подробнее:

В начале переменной port присваиваем номер порта по умолчанию - 12135, затем проверяем был ли передан номер порта в качестве аргумента командной строки. Если в args[0] находим номер порта, то записываем его в переменною port. Это позволит запускать приложение на любом порту, и у нас всегда будет значение по умолчанию - очень удобно!

Далее создаём класс сервера (Server) и передаём этого ему номер порта.

Затем нужно проинициализировать контекст веб-приложения - WebAppContext. Для этого объекту WebAppContext необходимо передать путь строки запроса, который будет обрабатывать веб-приложение, а так же указать расположение папки проекта с исходным кодом. Со строкой запроса всё понятно: я просто передаю путь "/" в метод setContextPath. А вот с исходным кодом немного сложнее:

В начале я получаю объект класса ProtectionDomain, который содержит в себе все характеристики домена. У ProtectionDomain есть замечательный метод getCodeSource(), который возвращает объект класса CodeSource. Класс CodeSource содержит в себе информацию о расположении ресурса (URL ресурса или ссылка на локальный ресурс), а так же информацию о цепочки сертификатов, которые использовались, чтобы проверить подписанный код, происходящий из того расположения. Чтобы получить информацию о расположении ресурсов, я вызываю метод getLocation() объекта класса CodeSource и получаю объект URL, который как раз содержит то что нужно. Так как в WebAppContext необходимо передать путь в виде строки, вызываем метод toExternalForm() объекта класса URL и передаём полученную строку в метод setWar() объекта класса WebAppContext.

В конце передаём готовый объект класса WebAppContext в метод setHandler()объекта класса Server и запускаем наш сервер.

Всё, приложение готово! Соберите проект командой:

mvn clean install

Перейдите в сгенерированный каталог /target и выполните команду:

java -jar jetty-example.jar

или если хотите запустить приложение ну другом порту, то команду:

java -jar jetty-example.jar 8800

Откройте в браузере ссылку http://localhost:12135/ или http://localhost:8800/, если ввели вторую команду. Вы должны увидеть такую страницу:

Есть и альтернативный способ запуска приложения, который особенно удобно использовать во время отладки - запуск из IDE. В IntelliJ IDEA достаточно открыть Launcher.java, нажать правую кнопку мыши и выбрать пункт Run 'Launcher.main()'. Как такой трюк повторить в других IDE разбирайтесь сами :)

Исходники проекта вы можете найти тут: https://github.com/AlexeyKutepov/jetty-example