Несколько часов назад вышел релиз Spring-Boot-1.3.0 Появилась возможность создавать war архивы через утилиту spring следующей командой

spring war example.war script.groovy

Рабочее веб приложение может уместиться в 1 твит. Никакие конфигурационные файлы не требуются. Можно запустить(или собрать в jar,war) следующий код:

// script.groovy. Да, кроме него ничего не надо
package ru.d10xa.springwar;

@RestController
@RequestMapping('/')
class Ctrl{
    @RequestMapping
    def map(){
        return ['a':'b']
    }
}

Не смотря на то, что это WAR, он всё так же является запускаемым.

Spring Boot умеет создавать запускаемые jar и war файлы благодаря проекту spring-boot-loader. По умолчанию, в java нет возможности загружать вложенные jar файлы. Этим занимаются загрузчики, которых spring подкидывает в проект при сборке. В манифест добавляется строка Main-Class: org.springframework.boot.loader.WarLauncher (или JarLauncher). В WarLauncher есть public static void main который занимается запуском нашего приложения.

Можно запускать как обычный jar:

java -jar example.war

Или на jetty:

wget http://central.maven.org/maven2/org/eclipse/jetty/jetty-runner/9.3.3.v20150827/jetty-runner-9.3.3.v20150827.jar
java -jar jetty-runner-9.3.3.v20150827.jar example.war

Или на wildfly

# Dockerfile
FROM jboss/wildfly
ADD build/app.war /opt/jboss/wildfly/standalone/deployments/
docker build --tag=wildfly-app-war . 
docker run -it --rm -p 8080:8080 wildfly-app-war

Структура Jar

example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-com
 |  +-mycompany
 |     + project
 |        +-YouClasses.class
 +-lib
    +-dependency1.jar
    +-dependency2.jar

Структура War

example.war
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-WEB-INF
    +-classes
    |  +-com
    |     +-mycompany
    |        +-project
    |           +-YouClasses.class
    +-lib
    |  +-dependency1.jar
    |  +-dependency2.jar
    +-lib-provided
       +-servlet-api.jar
       +-dependency3.jar

Основные отличия WAR от JAR, собранных spring-boot-cli

  • Строка указывающая на главный класс в MANIFEST.MF (JarLauncher, WarLauncher)
  • Для War нужен класс наследник SpringBootServletInitializer
  • Структура (Layout) архивов отличается
  • В случае с war, зависимость tomcat помещается отдельно от основных зависимостей (lib-provided)

Перепаковка из jar в war (bash)

исходники примера

Создадим класс, наследующийся от SpringBootServletInitializer (в той же директории, где script.groovy)

package ru.d10xa.springwar;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

   @Override
   protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
      return application.sources(Ctrl.class);
   }

}

Выполним скрипт:

# Удаляем директорию build
rm -rf build

# Создаем структуру war архива в build/tmp
mkdir -p build/tmp build/tmp/WEB-INF/ build/tmp/WEB-INF/classes/templates build/tmp/WEB-INF/lib build/tmp/WEB-INF/lib-provided

# Утилитой spring-boot-cli создаем jar
spring jar build/app.jar App.groovy ServletInitializer.groovy

# Распаковываем jar
unzip build/app.jar -d build/extracted_jar

# Библиотеки для встроенного сервера закидываем в папку lib-provided, остальные в lib
cp -p $(find build/extracted_jar/lib -name '*tomcat*') build/tmp/WEB-INF/lib-provided
cp -p $(find build/extracted_jar/lib -not -name '*tomcat*') build/tmp/WEB-INF/lib

# Копируем META-INF и спринговые классы в корень будующего war
cp -r build/extracted_jar/META-INF/ build/extracted_jar/org/ build/tmp/

# В манифесте меняем Main-Class JarLauncher на WarLauncher
sed -i -- 's/JarLauncher/WarLauncher/g' build/tmp/META-INF/MANIFEST.MF

# Копируем классы из пакета ru в WEB-INF/classes
cp -r build/extracted_jar/ru/ build/tmp/WEB-INF/classes

# Архивируем без сжатия, и размещаем war рядом с jar
cd build/tmp
zip -r --compression-method=store app.war *
mv app.war ../

Для сравнения, соберем war утилитой spring-boot-cli

spring war build/spring-cli-app.war App.groovy

Сравним

groovy scripts/zipdiff.groovy build/app.war build/spring-cli-app.war

Добавление загрузчиков осуществляется в классе ArchiveCommand.java , где в зависимости от Layout , классы попадают либо в корень архива (jar), либо в WEB-INF/classes/ (war). Поэтому классы PackagedSpringApplicationLauncher и SpringApplicationLauncher при сравнении, находятся в разных директориях. Но в classpath они всёравно попадают.

Отличается только SpringApplicationWebApplicationInitializer . Вместо него мы добавили ru/d10xa/springwar/ServletInitializer


---unique in build/app.war
WEB-INF/classes/ru/d10xa/springwar/ServletInitializer.class
org/springframework/boot/cli/app/SpringApplicationLauncher.class
org/springframework/boot/cli/archive/PackagedSpringApplicationLauncher.class
---unique in build/spring-cli-app.war
WEB-INF/classes/org/springframework/boot/cli/app/SpringApplicationLauncher.class
WEB-INF/classes/org/springframework/boot/cli/app/SpringApplicationWebApplicationInitializer.class
WEB-INF/classes/org/springframework/boot/cli/archive/PackagedSpringApplicationLauncher.class

spring war VS gradle build

Сборка с утилитой spring отличается от gradle наличием groovy в classpath + несколько вспомогательных классов (например, для тестов).

gradle clean build war
spring war example.war script.groovy

Классы и jar’ы которые были добавлены spring-boot-cli при сборке архива:

WEB-INF/classes/org/springframework/boot/cli/app/SpringApplicationLauncher.class
WEB-INF/classes/org/springframework/boot/cli/app/SpringApplicationWebApplicationInitializer.class
WEB-INF/classes/org/springframework/boot/cli/archive/PackagedSpringApplicationLauncher.class
WEB-INF/lib/groovy-2.4.4.jar
WEB-INF/lib/groovy-templates-2.4.4.jar
WEB-INF/lib/groovy-xml-2.4.4.jar
org/springframework/boot/groovy/DelegateTestRunner.class
org/springframework/boot/groovy/DependencyManagementBom.class
org/springframework/boot/groovy/EnableDeviceResolver.class
org/springframework/boot/groovy/EnableGroovyTemplates.class
org/springframework/boot/groovy/GroovyTemplate.class

А зачем вообще нужно собирать war? Issue на гитхабе, в котором просили добавить сборку war был открыт больше года и пул реквесты ни кто не присылал, хотя пофиксить это можно довольно просто. Я собирал war потому, что на OpenShift PaaS можно было создать себе контейнер с “каким нибудь” application-сервером в несколько кликов и потом через scp кидать war в папку автодеплой. Но, как оказалось, от application-сервера я получил только проблемы.
Локально с встроенным в jar томкатом всё работало хорошо, но application-сервер на openshift некорректно преобразовывал русские буквы в url. Теперь spring boot приложения я собираю только в jar.

Почитать как запускать jar на openshift можно в спринговой документации