对于底层服务端Java开发人员来说,与外借沟通的唯一方法就是通过API。今天的文章都是关于JAX-RS的:使用Java来编写和发布RESTful服务。
但我们不会用那些涉及到应用服务器,WAR包和其他说不清的传统的、重量级的方式。作为代替,我们使用了不起的 Apache CXF框架和经常依赖的Spring来把所有的部件连接起来。当然我们不会继续停留在需要一个web服务器来运行我们的服务。通过使用fat or one jar(译者注:所有依赖的第三方库放在一个jar包里)的概念,我们会嵌入Jetty 服务器到我们的应用程序中,使得我们的程序最终的Java包可再发行和可运行的。
这里有很多工作要做,所以我们先现在就开始吧。 和我们上面描述的一样,我们会使用Apache CXF,Spring和Jetty作为一个构建的部分,所以先在一个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为它作出工作。