Статьи

Going REST / NoXML: встраивание Tomcat в Spring и JAX-RS с использованием Apache CXF


Этот пост является логическим продолжением
предыдущего . Единственное отличие — это контейнер, который мы собираемся использовать: вместо
Jetty это будет наш старый приятель
Apache Tomcat . Удивительно, но было очень легко встроить последнюю
версию Apache Tomcat 7, поэтому позвольте мне показать это сейчас.

Я не буду повторять последний пост полностью, так как нет никаких изменений, кроме как в файле POM и классе Starter . Помимо этих двух, мы повторно используем все, что мы сделали раньше.

Для файла POM нам нужно удалить зависимости Jetty и заменить их на Apache Tomcat . Первое изменение будет в разделе свойств . Мы заменим org.eclipse.jetty.version на org.apache.tomcat .

Итак, эта строка:

<org.eclipse.jetty.version>8.1.8.v20121106</org.eclipse.jetty.version>

будет выглядеть так:

<org.apache.tomcat>7.0.34</org.apache.tomcat>

Вторым изменением будут сами зависимости, мы заменим эти строки:

<dependency>
    <groupid>org.eclipse.jetty</groupid>
    <artifactid>jetty-server</artifactid>
    <version>${org.eclipse.jetty.version}</version>
</dependency>
     
<dependency>
    <groupid>org.eclipse.jetty</groupid>
    <artifactid>jetty-webapp</artifactid>
    <version>${org.eclipse.jetty.version</version>
</dependency> 

с этими:

<dependency>
    <groupid>org.apache.tomcat.embed</groupid>
    <artifactid>tomcat-embed-core</artifactid>
    <version>${org.apache.tomcat}</version>
</dependency>
  
<dependency>
    <groupid>org.apache.tomcat.embed</groupid>
    <artifactid>tomcat-embed-logging-juli</artifactid>
    <version>${org.apache.tomcat}</version>
</dependency>

Отлично, эта часть сделана. Последняя часть посвящена изменениям в реализации нашего основного класса, где мы заменим Jetty на Apache Tomcat .

package com.example;

import java.io.File;
import java.io.IOException;

import org.apache.catalina.Context;
import org.apache.catalina.loader.WebappLoader;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import com.example.config.AppConfig;

public class Starter { 
    private final static Log log = LogFactory.getLog( Starter.class );
 
    public static void main(final String[] args) throws Exception {
        final File base = createBaseDirectory();
        log.info( "Using base folder: " + base.getAbsolutePath() );
  
        final Tomcat tomcat = new Tomcat();
        tomcat.setPort( 8080 );
        tomcat.setBaseDir( base.getAbsolutePath() ); 
  
        Context context = tomcat.addContext( "/", base.getAbsolutePath() );
        Tomcat.addServlet( context, "CXFServlet", new CXFServlet() );
  
        context.addServletMapping( "/rest/*", "CXFServlet" );
        context.addApplicationListener( ContextLoaderListener.class.getName() );
        context.setLoader( new WebappLoader( Thread.currentThread().getContextClassLoader() ) );
  
        context.addParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() );
        context.addParameter( "contextConfigLocation", AppConfig.class.getName() );
   
        tomcat.start();
        tomcat.getServer().await();
    }

    private static File createBaseDirectory() throws IOException {
        final File base = File.createTempFile( "tmp-", "" );
  
        if( !base.delete() ) {
            throw new IOException( "Cannot (re)create base folder: " + base.getAbsolutePath()  );
        }
  
        if( !base.mkdir() ) {
            throw new IOException( "Cannot create base folder: " + base.getAbsolutePath()  );         
        }
  
        return base;
    } 
}

The code looks pretty simple but verbose because of the fact that it seems impossible to run Apache Tomcat in embedded mode without specifying some working directory. The small createBaseDirectory() function creates a temporary folder which we are feeding to Apache Tomcat as a baseDir. Implementation reveals that we are running Apache Tomcat server instance on port 8080, we are configuring Apache CXF servlet to handle all request at /rest/* path, we are adding Spring context listener and finally we are starting server up.

After building the project as a fat or one jar, we have a full-blown server hosting our JAR-RS application:

mvn clean package
java -jar target/spring-one-jar-0.0.1-SNAPSHOT.one-jar.jar

And we should see the output like that:

Jan 28, 2013 5:54:56 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-bio-8080"]
Jan 28, 2013 5:54:56 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service Tomcat
Jan 28, 2013 5:54:56 PM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet Engine: Apache Tomcat/7.0.34
Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register
WARNING: Could not get url for /javax/servlet/jsp/resources/jsp_2_0.xsd
Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register
WARNING: Could not get url for /javax/servlet/jsp/resources/jsp_2_1.xsd
Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register
WARNING: Could not get url for /javax/servlet/jsp/resources/jsp_2_2.xsd
Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register
WARNING: Could not get url for /javax/servlet/jsp/resources/web-jsptaglibrary_1_1.dtd
Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register
WARNING: Could not get url for /javax/servlet/jsp/resources/web-jsptaglibrary_1_2.dtd
Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register
WARNING: Could not get url for /javax/servlet/jsp/resources/web-jsptaglibrary_2_0.xsd
Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register
WARNING: Could not get url for /javax/servlet/jsp/resources/web-jsptaglibrary_2_1.xsd
Jan 28, 2013 5:54:57 PM org.apache.catalina.loader.WebappLoader setClassPath
INFO: Unknown loader com.simontuffs.onejar.JarClassLoader@187a84e4 class com.simontuffs.onejar.JarClassLoader
Jan 28, 2013 5:54:57 PM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring root WebApplicationContext
Jan 28, 2013 5:54:57 PM org.springframework.web.context.ContextLoader initWebApplicationContext
INFO: Root WebApplicationContext: initialization started
Jan 28, 2013 5:54:58 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing Root WebApplicationContext: startup date [Mon Jan 28 17:54:58 EST 2013]; root of context hierarchy
Jan 28, 2013 5:54:58 PM org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider registerDefaultFilters
INFO: JSR-330 'javax.inject.Named' annotation found and supported for component scanning
Jan 28, 2013 5:54:58 PM org.springframework.web.context.support.AnnotationConfigWebApplicationContext loadBeanDefinitions
INFO: Successfully resolved class for [com.example.config.AppConfig]
Jan 28, 2013 5:54:58 PM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor 
INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
Jan 28, 2013 5:54:58 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@62770d2e: defining beans [org.springframework.context.annotation.internal
ConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProces
sor,org.springframework.context.annotation.internalCommonAnnotationProcessor,appConfig,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,c
xf,jaxRsServer,jaxRsApiApplication,peopleRestService,peopleService,jsonProvider]; root of factory hierarchy
Jan 28, 2013 5:54:59 PM org.apache.cxf.endpoint.ServerImpl initDestination
INFO: Setting the server's publish address to be /api
Jan 28, 2013 5:54:59 PM org.springframework.web.context.ContextLoader initWebApplicationContext
INFO: Root WebApplicationContext: initialization completed in 1747 ms
Jan 28, 2013 5:54:59 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-bio-8080"]

Let’s issue some HTTP requests so to be sure everything works as we expected:

> curl http://localhost:8080/rest/api/people?page=2
[
  {"email":"[email protected]","firstName":null,"lastName":null},
  {"email":"[email protected]","firstName":null,"lastName":null},
  {"email":"[email protected]","firstName":null,"lastName":null}, 
  {"email":"[email protected]","firstName":null,"lastName":null}, 
  {"email":"[email protected]","firstName":null,"lastName":null}
]

> curl http://localhost:8080/rest/api/people -X PUT -d "[email protected]"
{"email":"[email protected]","firstName":null,"lastName":null}

And we are still 100% XML free! One important note though: we create a temporary folder every time but never delete it (calling deleteOnShutdown for base doesn’t work as expected for non-empty folders). Please keep it in mind (add your own shutdown hook, for example) as I decided to leave code clean.

Source code: https://github.com/reta/spring-one-jar/tree/tomcat-embedded