REST 导学:在 Jetty 中运行 Spring 和 JAX-RS(Apache CXF)

对于底层服务端Java开发人员来说,与外借沟通的唯一方法就是通过API。今天的文章都是关于JAX-RS的:使用Java来编写和发布RESTful服务。

但我们不会用那些涉及到应用服务器,WAR包和其他说不清的传统的、重量级的方式。作为代替,我们使用了不起的 Apache CXF框架和经常依赖的Spring来把所有的部件连接起来。当然我们不会继续停留在需要一个web服务器来运行我们的服务。通过使用fat or one jar(译者注:所有依赖的第三方库放在一个jar包里)的概念,我们会嵌入Jetty 服务器到我们的应用程序中,使得我们的程序最终的Java包可再发行和可运行的。

这里有很多工作要做,所以我们先现在就开始吧。 和我们上面描述的一样,我们会使用Apache CXFSpringJetty作为一个构建的部分,所以先在一个POM文件里描述它们。值得一提的是,加入了一个非常好的JSON处理的库,Jackson 。

<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/xsd/maven-4.0.0.xsd">
    <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>
            <url>http://onejar-maven-plugin.googlecode.com/svn/mavenrepo</url>
        </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服务。 

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类代表的人。

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;
    } 
}

下面是个空心的业务服务(为了简单起见,这里没用到数据库或其他存储方式)。

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 );
    }
}

你能看到,我们将根据请求的page来生成一列的人对象。标准的Spring注解@Service使这个类变成了一个服务bean。我们的 JAX-RS服务PeopleRestService会用像下面演示的代码那样使用它(PeopleService)来获取人的数据。

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的配置,我们应用程序的核心:

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();
    }
}

上面的代码看起来并不复杂,但实际使用时会发生很多情况。让我们切开来细细分析它吧。这里两个关键的主键是负责全部的繁重工作来配置我们的JAX-RS服务器实例的工厂类JAXRSServerFactoryBean,以及SpringBus实例,它把 Spring 和Apache CXF无缝地连接在一起。其他的组件由 Spring的Bean来代表。

上面还没说到的是嵌入Jetty服务器实例。我们的程序主类Starter就是要做这种事情的。

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(); 
    }
}

看完这段代码会发现我们在8080端口上运行 Jetty服务器实例,我们要配置 Apache CXF的servlet来处理对/rest/*路径上的所有请求(包括了我们的JAX-RS应用和在/rest/api/people上给我们的服务),我们要在上面定义的配置用参数化来加入 Spring的上下文监听器,最后我们启动服务器。这点上,我们得到的是完整的WEB服务器来主持我们的 JAX-RS服务。让我们看下它运行起来。首先,我们要把它打包为单一的、可运行的和可再分发的 fat or one jar

mvn clean package

让我们从target文件夹里找到生成的包然后执行它:

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

同时我们能看到如下的输出:

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请求到它那里来确认下所有东西都能和我们期待的那样工作:

> 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(completely XML-free)!源代码:https://github.com/reta/spring-one-jar/tree/jetty-embedded

在结束这篇文章前,我要提一下一个很好的项目,Dropwizard,它使用了非常类似的概念,但把它放到了一个杰出的、有良好设计的框架的层次上,感谢 Yammer为它作出工作。