RESTEasy除了能够运行于标准Web服务器(如Tomcat),或是与J2EE容器(如JBoss AS)整合之外,还可以嵌入式的方式运行,本文通过一些代码实例来对这些功能和RESTEasy中相关的classes进行说明。
TJWS
首先,我们需要理解,RESTEasy使用TJWS作为嵌入式RESTFul WebService服务的容器。TJWS是一个开源的Servlet容器,它相当轻便小巧,占用资源少,启动速度快,因此做为嵌入式容器非常适合。在RESTEasy中,已经自包含了一个tjws服务器,如果签出RESTEasy的代码则可以看到它位于代码中的tjws目录:
liweinan@smart:~/projs/resteasy/jaxrs$ ls -l | grep tjws
drwxr-xr-x 7 liweinan staff 238 Sep 5 22:37 tjws
基于TJWS这个容器,RESTEasy为我们提供了TJWSEmbeddedJaxrsServer,开发人员可以很方便地使用它来启动一个内嵌于代码之中的JAX-RS服务。下面我们动手实际做一个例子试试看:
HelloWorldService
首先我们来创建一个最简单的RESTFul WebService:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@Path("/helloworld")
public interface HelloWorldService {
@GET
@Produces("text/plain")
public String printHelloWorld();
}
以下是它的实现类:
public class HelloWorldServiceImpl implements HelloWorldService {
public static final String HELLO_WORLD = "Hello, world!";
public String printHelloWorld() {
return HELLO_WORLD;
}
}
很简单的一个WebService,不需做太多说明。接下来的重点是如何使用TJWSEmbeddedJaxrsServer来将这个WebService启动起来:
TJWSEmbeddedJaxrsServer
我们可以通过JUnit单元测试的形式来实现这个例子:
import org.jboss.resteasy.plugins.server.tjws.TJWSEmbeddedJaxrsServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import static org.junit.Assert.assertEquals;
public class TestTJWSEmbeddedJAXRSServer {
private static TJWSEmbeddedJaxrsServer tjws;
@Before
public void setUp() {
tjws = new TJWSEmbeddedJaxrsServer();
tjws.setPort(8081);
tjws.getDeployment().getActualResourceClasses().add(HelloWorldServiceImpl.class);
tjws.start();
}
@After
public void tearDown() {
tjws.stop();
}
@Test
public void testEmbeddedJAXRSServer() throws IOException {
URL url = new URL("http://127.0.0.1:8081/helloworld");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "text/plain");
connection.setDoOutput(true);
connection.setInstanceFollowRedirects(false);
connection.setConnectTimeout(1000);
BufferedReader ir = new BufferedReader(new InputStreamReader(connection.getInputStream()));
assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
assertEquals("Hello, world!", ir.readLine());
ir.close();
connection.disconnect();
}
}
从上面的代码中,我们可以看到,在setUp()方法中,我们将HelloWorldServiceImpl这个WebService添加进了TJWSEmbeddedJaxrsServer当中并启动了这个server,同时设定的服务端口为8081。值得说明的是这行代码:
tjws.getDeployment().getActualResourceClasses().add(HelloWorldServiceImpl.class);
我们是以perRequestResource的形式将HelloWorldServiceImpl服务进行注册,这一点从org.jboss.resteasy.spi.ResteasyDeployment的源代码中可以看出来:
...
registry.addPerRequestResource(actualResourceClass);
...
因此,我们的HelloWorldServiceImpl,将在每次客户端请求服务时被RESTEasy进行实例化,服务处理完成后销毁。如果我们想以singleton方式来添加我们的WebService,可以使用getResourceFactories()方法:
tjws.getDeployment().getResourceFactories().add(HelloWorldServiceImpl.class);
这种情况下我们必须自己来保证HelloWorldServiceImpl是线程安全的。如果你不了解我这里说的singleton模式,可参考蓝点上这篇 "RESTEasy的入门文章":http://bluedash.net/spaces/RESTEasy ,理解了RESTEasyr的基本使用方法后,再回来看本篇文章。
我们继续看这个单元测试的源代码,在setUp()方法中,我们进行了服务注册及参数的配置之后,便启动了这个内嵌的WebService服务:
tjws.start();
接下来,在主测试程序中,我们通过使用最基本的HTTP协议来调用WebService服务,并查看返回值与期望是否相附,代码很明白,没有需要特别说明的地方。
最后,在tearDown() 方法中,我们关闭了WebService服务:
tjws.stop();
这是测试完成后必要的清理工作。
InMemoryClientExecutor
如果我们不需要对外提供WebService服务,而只是会在自己的项目代码中,以编码的形式来调用自己的WebService,RESTEasy还提供了更为轻量级的InMemoryClientExecutor, 下面是一个例子:
import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.core.executors.InMemoryClientExecutor;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TestInMemoryClientExecutor {
@Test
public void testHelloWorld() throws Exception {
InMemoryClientExecutor executor = new InMemoryClientExecutor();
executor.getDispatcher().getRegistry().addPerRequestResource(HelloWorldServiceImpl.class);
ClientRequest request = new ClientRequest("/helloworld", executor);
assertEquals(HelloWorldServiceImpl.HELLO_WORLD, request.getTarget(String.class));
}
}
一般是在RESTEasy自身的测试代码中多见这个InMemoryClientExecutor的使用,在一般的应用编码情况下,可能InMemoryClientExecutor用到的场合并不会太多。
TJWSServletContainer
在RESTEasy的单元测试代码中,提供了一个很方便的TJWSServletContainer类,它对TJWSEmbeddedJaxrsServer进行了封装,我们也可以使用这个来实现我们的嵌入式WebService服务:
import org.jboss.resteasy.client.ProxyFactory;
import org.jboss.resteasy.plugins.providers.RegisterBuiltin;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.test.TJWSServletContainer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TestTJWSServletContainer {
@BeforeClass
public static void setUp() throws Exception {
ResteasyDeployment deployment = TJWSServletContainer.start();
deployment.getRegistry().addPerRequestResource(HelloWorldServiceImpl.class);
}
@AfterClass
public static void stop() throws Exception {
TJWSServletContainer.stop();
}
@Test
public void testIt() throws Exception {
RegisterBuiltin.register(ResteasyProviderFactory.getInstance());
HelloWorldService client = ProxyFactory.create(HelloWorldService.class, "http://127.0.0.1:8081");
assertEquals("Hello, world!", client.printHelloWorld());
}
}
这个例子在本质上和直接使用TJWSEmbeddedJaxrsServer没有任何区别(实际上是有一点区别,有兴趣的读者可以参考下一节深入讨论的内容)。另外,在这个单元测试的主测试功能testIt当中,我们并没有使用最直接的HTTP的调用方式,而是利用了RESTEasy客户端的方式来调用了WebService,可以看到,和第一个例子中那个单元测试相比,少写了多少代码,所以使用RESTEasy客户端来调用RESTEasy的WebService应该是最省事的。
深入讨论
如果对RESTEasy的源代码实现没兴趣,本节内容可以无视。我们要讨论第一个例子(针对TJWSEmbeddedJaxrsServer)和第三个例子(针对TJWSServletContainer)中的一处代码上的区别。在第一个例子中,我们注册服务的代码为:
tjws.getDeployment().getActualResourceClasses().add(HelloWorldServiceImpl.class);
而在第三个例子中,我们使用的方法为:
deployment.getRegistry().addPerRequestResource(HelloWorldServiceImpl.class);
实际上我们同为使用ResteasyDeploy这个类来注册我们的Service,为什么两个代码中使用的方法不同呢?
仔细看下我们会发现,两个方法所处的上下文环境不同。再看下第一个:
...
tjws.getDeployment().getActualResourceClasses().add(HelloWorldServiceImpl.class);
tjws.start();
...
我们是在start()之前来注册服务。而第三个例子中:
...
ResteasyDeployment deployment = TJWSServletContainer.start();
deployment.getRegistry().addPerRequestResource(HelloWorldServiceImpl.class);
...
我们是在start()之后来注册服务。这就是两者使用不同方法的重要区别,看下ResteasyDeployment这个RESTEasy中的核心类,我们会发现,它是在start()中进行registry的加载工作,并调用registration()方法将我们在getActualResourceClasses中添加的服务注册进Registry的:
package org.jboss.resteasy.spi;
...
public class ResteasyDeployment {
public void start() {
registry = dispatcher.getRegistry();
...
registration();
...
}
...
public void registration() {
...
if (resourceClasses != null)
{
for (String resource : resourceClasses)
{
Class clazz = null;
try
{
clazz = Thread.currentThread().getContextClassLoader().loadClass(resource.trim());
}
catch (ClassNotFoundException e)
{
throw new RuntimeException(e);
}
registry.addPerRequestResource(clazz);
}
}
}
...
}
了解了细节之后,我们便知道第一个例子和第三个例子当中这一差异的原因了。在这里要特别说明一下,我们在使用RESTEasy的时候,应该使用第一种方式,因为这是写在RESTEasy官方文档中的标准使用及调用方法。第三个例子中介绍的先start(),再添加服务的调用方法,并没有写在RESTEasy的文档里,是一种"非标准"的使用,了解即可。
有关样例代码
我将本文中使用的代码放在了github上面,有兴趣的读者可以自己下载代码玩玩看:
https://github.com/liweinan/try-resteasy
使用这个git命令签出代码:
git clone git://github.com/liweinan/try-resteasy.git
代码签出后可以用maven命令来执行本文中的代码:
mvn test
所有的本文源代码都以JUnit单元测试的形式存于项目当中。