Статьи

Собираюсь отдохнуть: встраивать Jetty в Spring и JAX-RS (Apache CXF)

Для хардкорного серверного Java-разработчика единственным способом «высказаться» миру является использование API. Сегодняшняя статья посвящена JAX-RS : написанию и представлению сервисов RESTful с использованием Java.

Но мы не будем этого делать, используя традиционный, тяжеловесный подход, включающий сервер приложений, упаковку WAR и еще много чего. Вместо этого мы будем использовать потрясающую платформу Apache CXF и, как всегда, полагаемся на Spring для соединения всех частей вместе. И наверняка мы не остановимся на этом, так как нам нужен веб-сервер для запуска наших сервисов. Используя концепцию fat или one jar, мы встроим сервер Jetty в наше приложение и сделаем наш окончательный JAR-файл распространяемым (включая все зависимости) и работоспособным.

Это много работы, так что давайте начнем. Как мы уже говорили выше, мы будем использовать Apache CXF , Spring и Jetty в качестве строительных блоков, поэтому давайте опишем их в файле POM. Стоит упомянуть еще одну зависимость — превосходную библиотеку Джексона для обработки JSON.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
    <modelversion>4.0.0</modelversion>
 
    <groupid>com.example</groupid>
    <artifactid>spring-one-jar</artifactid>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <properties>
        <project.build.sourceencoding>UTF-8</project.build.sourceencoding>
        <org.apache.cxf.version>2.7.2</org.apache.cxf.version>
        <org.springframework.version>3.2.0.RELEASE</org.springframework.version>
        <org.eclipse.jetty.version>8.1.8.v20121106</org.eclipse.jetty.version>
    </properties>
 
    <dependencies>  
        <dependency>
            <groupid>org.apache.cxf</groupid>
            <artifactid>cxf-rt-frontend-jaxrs</artifactid>
            <version>${org.apache.cxf.version}</version>
        </dependency>
 
        <dependency>
            <groupid>javax.inject</groupid>
            <artifactid>javax.inject</artifactid>
            <version>1</version>
        </dependency>
 
        <dependency>
            <groupid>org.codehaus.jackson</groupid>
            <artifactid>jackson-jaxrs</artifactid>
            <version>1.9.11</version>
        </dependency>
      
        <dependency>
            <groupid>org.codehaus.jackson</groupid>
            <artifactid>jackson-mapper-asl</artifactid>
            <version>1.9.11</version>
        </dependency>
      
        <dependency>
            <groupid>cglib</groupid>
            <artifactid>cglib-nodep</artifactid>
            <version>2.2</version>
        </dependency>
 
        <dependency>
            <groupid>org.springframework</groupid>
            <artifactid>spring-core</artifactid>
            <version>${org.springframework.version}</version>
        </dependency>
 
        <dependency>
            <groupid>org.springframework</groupid>
            <artifactid>spring-context</artifactid>
            <version>${org.springframework.version}</version>
        </dependency>
 
        <dependency>
            <groupid>org.springframework</groupid>
            <artifactid>spring-web</artifactid>
            <version>${org.springframework.version}</version>
        </dependency>
       
        <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
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupid>org.apache.maven.plugins</groupid>
                <artifactid>maven-compiler-plugin</artifactid>
                <version>3.0</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin
            <plugin>
                <groupid>org.apache.maven.plugins</groupid>
                <artifactid>maven-jar-plugin</artifactid>
                <configuration>
                    <archive>
                        <manifest>
                            <mainclass>com.example.Starter</mainclass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupid>org.dstovall</groupid>
                <artifactid>onejar-maven-plugin</artifactid>
                <version>1.4.4</version>
                <executions>
                    <execution>
                        <configuration>
                            <onejarversion>0.97</onejarversion>
                            <classifier>onejar</classifier>
                        </configuration>
                        <goals>
                            <goal>one-jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
     
    <pluginrepositories>
        <pluginrepository>
            <id>onejar-maven-plugin.googlecode.com</id>
        </pluginrepository>
    </pluginrepositories>
  
    <repositories>
        <repository>
            <id>maven2-repository.dev.java.net</id>
            <url>http://download.java.net/maven/2/</url>
        </repository>
    </repositories>
</project>

Это много вещей, но должно быть довольно ясно. Теперь мы готовы разработать наши первые сервисы JAX-RS , начав с простого приложения JAX-RS .

1
2
3
4
5
6
7
8
package com.example.rs;
 
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
 
@ApplicationPath( 'api' )
public class JaxRsApiApplication extends Application {
}

Как бы просто это не выглядело, наше приложение определяет / api как путь входа для сервисов JAX-RS . Образец сервиса будет управлять людьми, представленными классом Person .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.model;
 
public class Person {
    private String email;
    private String firstName;
    private String lastName;
 
    public Person() {
    }
 
    public Person( final String email ) {
        this.email = email;
    }
 
    public String getEmail() {
        return email;
    }
 
    public void setEmail( final String email ) {
        this.email = email;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setFirstName( final String firstName ) {
        this.firstName = firstName;
    }
 
    public void setLastName( final String lastName ) {
        this.lastName = lastName;
    }
}

И следит за бизнес-сервисом (для простоты, база данных или другое хранилище не включены).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.services;
 
import java.util.ArrayList;
import java.util.Collection;
 
import org.springframework.stereotype.Service;
 
import com.example.model.Person;
 
@Service
public class PeopleService {
    public Collection< Person > getPeople( int page, int pageSize ) {
        Collection< Person > persons = new ArrayList< Person >( pageSize );
 
        for( int index = 0; index < pageSize; ++index ) {
            persons.add( new Person( String.format( 'person+%d@at.com', ( pageSize * ( page - 1 ) + index + 1 ) ) ) );
        }
 
        return persons;
    }
 
    public Person addPerson( String email ) {
        return new Person( email );
    }
}

Как видите, мы создадим список людей на лету в зависимости от запрашиваемой страницы. Стандартная аннотация Spring @Service помечает этот класс как служебный компонент. Наш сервис JAX-RS PeopleRestService будет использовать его для поиска людей, как показано в следующем коде.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.rs;
 
import java.util.Collection;
 
import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
 
import com.example.model.Person;
import com.example.services.PeopleService;
 
@Path( '/people' )
public class PeopleRestService {
    @Inject private PeopleService peopleService;
 
    @Produces( { 'application/json' } )
    @GET
    public Collection< Person > getPeople( @QueryParam( 'page') @DefaultValue( '1' ) final int page ) {
        return peopleService.getPeople( page, 5 );
    }
 
    @Produces( { 'application/json' } )
    @PUT
    public Person addPerson( @FormParam( 'email' ) final String email ) {
        return peopleService.addPerson( email );
    }
}

Несмотря на простоту, этот класс нуждается в дополнительных объяснениях. Прежде всего, мы хотим представить нашу службу RESTful конечной точке / people . Объединяя его с / api (где находится наше приложение JAX-RS ), он дает в качестве / api / people квалифицированный путь.

Теперь, когда кто-то выдает HTTP GET по этому пути, должен вызываться метод getPeople . Этот метод принимает необязательный параметр page (со значением по умолчанию 1) и возвращает список лиц в формате JSON . В свою очередь, если кто-то выдает HTTP PUT по тому же пути, следует вызвать метод addPerson (с обязательным параметром email ) и вернуть нового человека в виде JSON .

Теперь давайте посмотрим на конфигурацию Spring , ядро ​​нашего приложения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.example.config;
 
import java.util.Arrays;
 
import javax.ws.rs.ext.RuntimeDelegate;
 
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.codehaus.jackson.jaxrs.JacksonJsonProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import com.example.rs.JaxRsApiApplication;
import com.example.rs.PeopleRestService;
import com.example.services.PeopleService;
 
@Configuration
public class AppConfig {
    @Bean( destroyMethod = 'shutdown' )
    public SpringBus cxf() {
        return new SpringBus();
    }
 
    @Bean
    public Server jaxRsServer() {
        JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint( jaxRsApiApplication(), JAXRSServerFactoryBean.class );
        factory.setServiceBeans( Arrays.< Object >asList( peopleRestService() ) );
        factory.setAddress( '/' + factory.getAddress() );
        factory.setProviders( Arrays.< Object >asList( jsonProvider() ) );
        return factory.create();
    }
 
    @Bean
    public JaxRsApiApplication jaxRsApiApplication() {
        return new JaxRsApiApplication();
    }
 
    @Bean
    public PeopleRestService peopleRestService() {
        return new PeopleRestService();
    }
 
    @Bean
    public PeopleService peopleService() {
        return new PeopleService();
    }
 
    @Bean
    public JacksonJsonProvider jsonProvider() {
        return new JacksonJsonProvider();
    }
}

Это не выглядит сложно, но многое происходит под капотом. Давайте разберем его на кусочки. Здесь два ключевых компонента — это фабрика JAXRSServerFactoryBean, которая выполняет всю тяжелую работу по настройке нашего экземпляра сервера JAX-RS , и экземпляр SpringBus, который без проблем склеивает Spring и Apache CXF . Все остальные компоненты представляют собой обычные Spring bean.

На картинке пока нет встраивания экземпляра веб-сервера Jetty . Наш основной класс приложений Starter делает именно это.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example;
 
import org.apache.cxf.transport.servlet.CXFServlet;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
 
import com.example.config.AppConfig;
 
public class Starter {
    public static void main( final String[] args ) throws Exception {
        Server server = new Server( 8080 );
 
        // Register and map the dispatcher servlet
        final ServletHolder servletHolder = new ServletHolder( new CXFServlet() );
        final ServletContextHandler context = new ServletContextHandler();  
        context.setContextPath( '/' );
        context.addServlet( servletHolder, '/rest/*' ); 
        context.addEventListener( new ContextLoaderListener() );
 
        context.setInitParameter( 'contextClass', AnnotationConfigWebApplicationContext.class.getName() );
        context.setInitParameter( 'contextConfigLocation', AppConfig.class.getName() );
 
        server.setHandler( context );
        server.start();
        server.join();
    }
}

Просматривая этот код, вы обнаруживаете, что мы запускаем экземпляр сервера Jetty через порт 8080 , и настраиваем сервлет Apache CXF для обработки всех запросов по пути / rest / * (который вместе с нашим приложением и службой JAX-RS дает нам / rest / api / people ), мы добавляем прослушиватель контекста Spring, параметризованный конфигурацией, которую мы определили выше, и наконец мы запускаем сервер. На данный момент у нас есть полноценный веб-сервер, на котором размещены наши сервисы JAX-RS . Давайте посмотрим на это в действии. Во-первых, давайте запакуем его как один, работающий и распространяемый жир или одну банку :

1
mvn clean package

Давайте возьмем биты из целевой папки и запустим их:

1
java -jar target/spring-one-jar-0.0.1-SNAPSHOT.one-jar.jar

И мы должны увидеть результат так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2013-01-19 11:43:08.636:INFO:oejs.Server:jetty-8.1.8.v20121106
2013-01-19 11:43:08.698:INFO:/:Initializing Spring root WebApplicationContext
Jan 19, 2013 11:43:08 AM org.springframework.web.context.ContextLoader initWebApplicationContext
INFO: Root WebApplicationContext: initialization started
Jan 19, 2013 11:43:08 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing Root WebApplicationContext: startup date [Sat Jan 19 11:43:08 EST 2013]; root of context hierarchy
Jan 19, 2013 11:43:08 AM org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider registerDefaultFilters
INFO: JSR-330 'javax.inject.Named' annotation found and supported for component scanning
Jan 19, 2013 11:43:08 AM org.springframework.web.context.support.AnnotationConfigWebApplicationContext loadBeanDefinitions
INFO: Successfully resolved class for [com.example.config.AppConfig]
Jan 19, 2013 11:43:09 AM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
Jan 19, 2013 11:43:09 AM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1f8166e5:
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 19, 2013 11:43:10 AM org.apache.cxf.endpoint.ServerImpl initDestination
INFO: Setting the server's publish address to be /api
Jan 19, 2013 11:43:10 AM org.springframework.web.context.ContextLoader initWebApplicationContext
INFO: Root WebApplicationContext: initialization completed in 2227 ms
2013-01-19 11:43:10.957:INFO:oejsh.ContextHandler:started o.e.j.s.ServletContextHandler{/,null}
2013-01-19 11:43:11.019:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080

Запустив наш сервер, давайте отправим ему несколько HTTP-запросов, чтобы убедиться, что все работает так, как мы ожидали:

01
02
03
04
05
06
07
08
09
10
11
> curl http://localhost:8080/rest/api/people?page=2
[
  {'email':'person+6@at.com','firstName':null,'lastName':null},
  {'email':'person+7@at.com','firstName':null,'lastName':null},
  {'email':'person+8@at.com','firstName':null,'lastName':null},
  {'email':'person+9@at.com','firstName':null,'lastName':null},
  {'email':'person+10@at.com','firstName':null,'lastName':null}
]
 
> curl http://localhost:8080/rest/api/people -X PUT -d 'email=a@b.com'
{'email':'a@b.com','firstName':null,'lastName':null}

Потрясающие! И, пожалуйста, обратите внимание, мы полностью свободны от XML ! Исходный код: https://github.com/reta/spring-one-jar/tree/jetty-embedded

Прежде чем закончить пост, я хотел бы упомянуть один отличный проект, Dropwizard , который использует довольно похожие концепции, но доводит его до уровня превосходной, хорошо разработанной среды, спасибо ребятам из Yammer за это.

Ссылка: Going REST: встраивание Jetty в Spring и JAX-RS (Apache CXF) от нашего партнера по JCG Андрея Редько в блоге Андрея Редько {devmind} .