Servlet的限制
在2003年末,Jetty Web容器的作者、Servlet规范的贡献者:Greg Wilkins在其博客上对Servlet的问题进行了如下总计:
* 没有对协议与应用之间的关系进行清洗的划分。
* 由于在设计Servlet时存在对阻塞IO的假设,因此不能充分利用非阻塞NIO机制。
* 所有的Servlet Web容器对于某些应用来讲是过度设计的。
他提出构思新的API规范,使其能够真实地脱离协议,并定义能够暴露内容和元数据的contentlets。这些想法就是Restlet项目创建的灵感源泉。在之后的文章中,Greg Wilkins解释了为什么当前Servlet API限制非阻塞NIO API得到高效使用的详细理由:这种传统的用法针对每个HTTP请求都创建独立的线程进行处理。并提出了他对下一代Servlet技术的设想。
另一个主要问题就是Servlet API鼓励应用开发者在应用或者用户会话级别直接将session状态保存于内存中,尽管这看上去不错,但它造成了Servlet容器扩展性和高可用性的主要问题。为了克服这些问题,就必须实现复杂的负载均衡、session复制、持久化机制。这导致了可扩展性必然成为灾难。
Restlet简介
当复杂核心化模式日趋强大之时,面向对象设计范例已经不总是Web开发中的最佳选择,Java开发者需要认识到这一点,并且在开发新的Web服务端或是AJAX Web客户端时开始思考更加RESTfully的设计。Restlet这个开源项目为那些要采用REST结构体系来构建应用程序的Java开发者提供了一个具体的解决方案。它的非常简单易用的功能和RESTfully的Web框架,这使其成为了Web2.0开发中的又一利器。好吧,朋友们,下面就让我们开始Restlet探索之旅吧!
1. 注册一个Restlet实现
Restlet框架由两部分构成。第一部分是"Restlet API", 这个中立的API完美地实现了REST概念并简化了客户端和服务端应用的调用处理。在使用它之前,我们还需要一个支持此API的Restlet实现。Restlet的诸多实现可以通过开源项目或者商业产品获得。
API与实现的分离和Servlet API与web容器的分离(就像Jetty或Tomcat)、JDBC API与相应JDBC驱动的分离非常类似。目前,"Noelios Restlet Engine" (缩写为NRE)是Restle tAPI的参考实现之一。当下载Restlet发布版本时,API和NRE就绑定在一起,以备随时使用。如果你需要使用不同的实现,那么只需要添加JAR 文件到classpath,并删除com.noelios.restlet.jar这个NRE的JAR文件即可。
API实现的注册过程是完全自动的,如果你对此存在疑问,那么请参考JAR规范。当完成实现装载工作后,它将自动回调org.restlet.util.Engine.setInstance()方法,来进行自注册。
2. 接收Web页面的内容
正如我们在Restlet介绍中所提到的,Restlet框架即是一个客户端,又是一个服务端框架。例如,NRE能够简单地通过它的HTTP客户端 connector(连接器)访问远程资源。在REST中,connector是一种软件元素,它使两个component(组件)之间能够进行通讯,其典型的实现方式是通过某种网络协议完成通讯。NRE提供了多种客户端connector实现,这些实现都基于现存的开源项目。在connector一节中,列举出了所有可用的客户端、服务端connector,并解释了如何使用和配置它们。
下面,我们将获取一个现存资源的表示法(representation )并将其输出在JVM控制台:
// Outputting the content of a Web page
Client client = new Client(Protocol.HTTP);
client.get("http://www.restlet.org").getEntity().write(System.out);
请注意上面的示例使用了最简单的方式:通过通用的客户端类(generic Client class)调用。更加灵活的方式是创建一个新的Request对象,然后请求客户端去处理它。下面的示例展示了如何在调用时设置首选项(例如 referrer URI)。当然,也可以是接收回应时的首选语言或者媒体类型:
// Prepare the request
Request request = new Request(Method.GET, "http://www.restlet.org");
request.setReferrerRef("http://www.mysite.org");
// Handle it using an HTTP client connector
Client client = new Client(Protocol.HTTP);
Response response = client.handle(request);
// Write the response entity on the console
Representation output = response.getEntity();
output.write(System.out);
3. 侦听浏览器
现在,我们将了解一下Restlet框架是如何侦听客户端请求并作出回应的。这里,我们选用了NRE HTTP服务端connector(例如基于Jetty的HTTP服务端connector),返回简单的字符串表达式“Hello World!”。请注意在更加实际的应用中,我们可以创建一个独立的类,此类继承自Restlet类,而不依靠这里的匿名内部类。
Restlet类与Servlet非常相似,并且在RESTful应用中处理调用时提供了有限的帮助。我们后面将看到一个提供了一些特定子类的框架,它能够更抽象、简单地进行处理。下面让我们先看一个简单的示例:
// Creating a minimal Restlet returning "Hello World"
Restlet restlet = new Restlet() {
@Override
public void handle(Request request, Response response) {
response.setEntity("Hello World!", MediaType.TEXT_PLAIN);
}
};
// Create the HTTP server and listen on port 8182
new Server(Protocol.HTTP, 8182, restlet).start();
如果你运行并启动服务端,那么你可以打开浏览器输入http://localhost:8182。实际上,输入任何的URI都可以工作,你也可以尝试一下http://localhost:8182/test/tutorial。值得注意的是,如果你从另一台服务器上测试服务端,那么就需要将localhost替换为服务器的IP地址或者它的域名。
4. REST架构概述
让我们先从REST的视角审视一下典型的web架构。在下面的图表中,端口代表了connector,而后者负责component之间的通讯(组件在图中被表示为大盒子)。链接代表了用于实际通讯的特定协议(HTTP,SMTP等)。
请注意,同一个component能够具有任何数量的客户端/服务端connector。例如,Web服务器B就具有一个用于回应用户代理组件(User Agent component)的服务端connector,和多个发送请求到其它服务端的客户端connector。
5. Component、virtual hosts和applications
另外,为了支持前面所表述的标准REST软件架构元素,Restlet框架也提供了一套类:它们极大地简化了在单一JVM中部署多个应用的工作。其目的在于提供一种RESTful、可移植的、比现存的Servlet API更加灵活的框架。在下面的图表中,我们将看到三种Restlet,它们用于管理上述复杂情况:Components能够管理多个Virtual Hosts和Applications。Virtual Hosts支持灵活的配置,例如同一个IP地址能够分享多个域名、使用同一个域名实现跨越多个IP地址的负载均衡。最后,我们使用应用去管理一套相关的 Restlet、Resource、Representations。另外,应用确保了在不同Restlet实现、不同Virtual Hosts之上的可移植性和可配置性。这三种Restlet的协助为我们提供了众多的功能:譬如访问日志、请求自动解码、配置状态页设置等。
为了展示这些类,让我们尝试一个简单的示例。首先,我们创建一个component,然后在其上添加一个HTTP服务端connector,并侦听 8182端口。接着创建一个简单的、具有追踪功能的Restlet,将它放置到组件默认的Virtual Hosts上。这个默认的主机将捕捉那些没有路由到指定Virtual Hosts的请求(详见Component.hosts属性)。在后面的一个示例中,我们还将介绍应用类的使用方法。请注意,目前你并不能在控制台输出中看到任何的访问日志。
// Create a new Restlet component and add a HTTP server connector to it
Component component = new Component();
component.getServers().add(Protocol.HTTP, 8182);
// Create a new tracing Restlet
Restlet restlet = new Restlet() {
@Override
public void handle(Request request, Response response) {
// Print the requested URI path
String message = "Resource URI : " + request.getResourceRef()
+ '
' + "Root URI : " + request.getRootRef()
+ '
' + "Routed part : "
+ request.getResourceRef().getBaseRef() + '
'
+ "Remaining part: "
+ request.getResourceRef().getRemainingPart();
response.setEntity(message, MediaType.TEXT_PLAIN);
}
};
// Then attach it to the local host
component.getDefaultHost().attach("/trace", restlet);
// Now, let's start the component!
// Note that the HTTP server connector is also automatically started.
component.start();
让我们通过在浏览器中输入http://localhost:8182/trace/abc/def?param=123来进行测试,得到测试结果如下:
Resource URI : http://localhost:8182/trace/abc/def?param=123
Root URI : http://localhost:8182/trace
Routed part : http://localhost:8182/trace
Remaining part: /abc/def?param=123
6. 为静态文件提供服务
你遇到过提供静态页面(类似Javadocs)服务的web应用?如果正在使用,那么可以直接编写一个Directory类,而无需为它建立Apache服务。请见下面如何使用:
// Create a component
Component component = new Component();
component.getServers().add(Protocol.HTTP, 8182);
component.getClients().add(Protocol.FILE);
// Create an application
Application application = new Application(component.getContext()) {
@Override
public Restlet createRoot() {
return new Directory(getContext(), ROOT_URI);
}
};
// Attach the application to the component and start it
component.getDefaultHost().attach("", application);
component.start();
正如你所注意到的,我们通过传递应用的父组件上下文(context)的方式来实例化应用,而不是在第5章中提到的代码那样简单。而其主要原因是应用在分配客户端请求时,请求的分配工作需要客户端connector来完成,而后者被component所控制,并在所有被包含其中的应用之间贡献。
为了运行此示例,你需要为ROOT_URI提供一个有效值,该值依赖于你的Restlet安装路径。默认情况下,它被设置为"file:///D: /Restlet/www/docs/api/"。请注意,这里不需要任何附加的配置。如果你希望自定义在文件扩展名和元数据(metadata,包括媒体类型、语言、编码等)之间的映射,或是提供一个与众不同的索引名,你可以使用应用的“metadataService”属性。
7. 访问日志
有目的地记录web应用的活动是一种常见的需求。Restlet组件能够在默认的情况下生成类似Apache风格的日志、甚至自定义日志。通过使用 JDK内置的日志功能,logger能够配置为像任何标准JDK日志那样过滤信息、对它们进行重新格式化或者发送它们到指定位置。并且支持日志的循环(rotation);细节请查看java.util.logging包。
值得注意的是,你能够通过修改component的"logService"属性来为java.util.logging框架自定义logger名。如果希望完全掌控日志的配置,你需要通过设置系统属性来声明一个配置文件:
System.setProperty("java.util.logging.config.file", "/your/path/logging.config");
关于配置文件格式的细节,请查看JDK的LogManager类。
8. 显示错误页
另外一个常见的需求是:在调用处理过程中某些期望结果没有出现时,能够自定义返回的状态页面。也许它是某个资源没有找到或者一个可接受的表示是无效的。在这种情况下,或者遇到任何无法处理的异常时,Application或者Component将自动提供一个默认的状态页面。此服务与 org.restlet.util.StatusService类相关联,并可以作为被称为“statusService”的Application或者 Component的属性而被访问。
为了自定义默认的信息,你只需要简单地创建StatusService类的子类,并覆盖其getRepresentation(Status, Request, Response)方法。然后设置这个类的实例为指定的“statusService”属性即可。
9. 对敏感资源的访问保护
当你需要保护对某些Restlet的访问时,可以使用下面的方法:一种通用的方法是依靠cookie来识别客户端(或者客户端session),并根据你的应用状态检查给定的用户ID或者session ID,从而判断次访问是否被允许。Restlet通过访问Request或者Response中的Cookie和CookieSetting对象支持cookie。
另一种方法是基于标准HTTP认证机制。Neolios Restlet引擎目前允许基于简单HTTP方案的证书发送、接收和基于Amazon Web服务方案的证书发送。
当接收到调用时,开发者能够通过Request.challengeResponse.identifier/secret类中的Guard filter(保护过滤器)使用已经解析好的证书。过滤器是一种特殊的Restlet,它能够在调用相应Restlet之前进行预处理,或者在相应 Restlet调用返回后进行后期处理。如果你熟知Servlet API,这里的过滤器概念和Servlet API中的Filter接口非常接近。看一下我们如何修改从前的代码来对目录访问进行访问保护:
// Create a Guard
Guard guard = new Guard(getContext(),
ChallengeScheme.HTTP_BASIC, "Tutorial");
guard.getSecrets().put("scott", "tiger".toCharArray());
// Create a Directory able to return a deep hierarchy of files
Directory directory = new Directory(getContext(), ROOT_URI);
guard.setNext(directory);
return guard;
请注意:认证和授权的最终结果是完全可定制的,这只需要通过authenticate()和authorize()方法便可完成。任何自定义的机制都能够被用来检查给定的证书是否有效、通过认证的用户是否被授权继续访问相应Restlet。下面是我们简单地硬编码了用户、密码对。为了测试,我们使用了客户端Restlet API:
// Prepare the request
Request request = new Request(Method.GET, "http://localhost:8182/");
// Add the client authentication to the call
ChallengeScheme scheme = ChallengeScheme.HTTP_BASIC;
ChallengeResponse authentication = new ChallengeResponse(scheme,
"scott", "tiger");
request.setChallengeResponse(authentication);
// Ask to the HTTP client connector to handle the call
Client client = new Client(Protocol.HTTP);
Response response = client.handle(request);
if (response.getStatus().isSuccess()) {
// Output the response entity on the JVM console
response.getEntity().write(System.out);
} else if (response.getStatus()
.equals(Status.CLIENT_ERROR_UNAUTHORIZED)) {
// Unauthorized access
System.out
.println("Access authorized by the server, " +
"check your credentials");
} else {
// Unexpected status
System.out.println("An unexpected status was returned: "
+ response.getStatus());
}
你可以修改这里的user ID或者password,来检查服务端返回的response。请别忘记了在启动客户端之前,先执行Restlet服务端程序。请注意,如果你从另一台机器上测试服务端,那么在浏览器中输入URI时需要将"localhost"替换为服务器的IP地址或者域名。由于使用了默认接收任何类型URI的 VirtualHost,因此服务端无需任何修改。
10. URI重写和重定向
Restlet框架的另一个优点是对cool URI的内建支持。Jacob Nielsen在他的AlertBox中给出了对URI设计的重要性的绝佳描述。
首先介绍的工具是Redirector,它能够将cool URI重写为另一个URI,并接着进行相应的自动重定向。这里支持一些重定向类型:通过客户端/浏览器的外部重定向、类似代理行为的connector重定向。在下面的例子中,我们将基于Google为名为"mysite.org"的站点定义一个检索服务。与URI相关的"/search"就是检索服务,它通过"kwd"参数接收一些检索关键字:
// Create an application
Application application = new Application(component.getContext()) {
@Override
public Restlet createRoot() {
// Create a Redirector to Google search service
String target =
"http://www.google.com/search?q=site:mysite.org+{keywords}";
return new Redirector(getContext(), target,
Redirector.MODE_CLIENT_TEMPORARY);
}
};
// Attach the application to the component's default host
Route route = component.getDefaultHost().attach("/search", application);
// While routing requests to the application, extract a query parameter
// For instance :
// http://localhost:8182/search?kwd=myKeyword1+myKeyword2
// will be routed to
// http://www.google.com/search?q=site:mysite.org+myKeyword1%20myKeyword2
route.extractQuery("keywords", "kwd", true);
请注意,Redirector只需要三个参数。第一个参数是父级上下文,第二个参数定义了如何基于URI模板重写URI。这里的URI模板将被Template类处理。第三个参数定义了重定向类型:出于简化的目的,我们选择了客户端重定向。
同时,当调用被传递给application时,我们使用了Route类从request中提取查询参数“kwd”。如果发现参数,参数将被复制到request的“keywords”属性中,以便Redirector在格式化目标URI时使用。
11. 路由器和分层URI
作为Redirector的补充,我们还具有另一个管理cool URI的工具:Router(路由器)。它们是一种特殊的Restlet,能够使其它Restlet(例如Finder和Filter)依附于它们,并基于URI模板进行自动委派调用(delegate call)。通常,你可以将Router设置为Application的根。
这里,我们将解释一下如何处理下面的URI模板:
1. /docs/ 用于显示静态文件
2. /users/{user} 用于显示用户帐号
3. /users/{user}/orders 用于显示特定用户的所有订单
4. /users/{user}/orders/{order} 用于显示特定的订单
实际上,这些URI包含了可变的部分(在大括号中)并且没有文件扩展名,这在传统的web容器中很难处理。而现在,你只需要做的只是使用URI模板将目标Restlet附着到Router上。在Restlet框架运行时,与request的URI最为匹配的Route将接收调用,并调用它所附着的 Restlet。同时,request的属性表也将自动更新为URI模板变量。
请看下面的具体实现代码。在真实的应用中,你可能希望创建单独的子类来代替我们这里使用的匿名类:
// Create a component
Component component = new Component();
component.getServers().add(Protocol.HTTP, 8182);
component.getClients().add(Protocol.FILE);
// Create an application
Application application = new Application(component.getContext()) {
@Override
public Restlet createRoot() {
// Create a root router
Router router = new Router(getContext());
// Attach a guard to secure access to the directory
Guard guard = new Guard(getContext(),
ChallengeScheme.HTTP_BASIC, "Restlet tutorial");
guard.getSecrets().put("scott", "tiger".toCharArray());
router.attach("/docs/", guard);
// Create a directory able to expose a hierarchy of files
Directory directory = new Directory(getContext(), ROOT_URI);
guard.setNext(directory);
// Create the account handler
Restlet account = new Restlet() {
@Override
public void handle(Request request, Response response) {
// Print the requested URI path
String message = "Account of user \""
+ request.getAttributes().get("user") + "\"";
response.setEntity(message, MediaType.TEXT_PLAIN);
}
};
// Create the orders handler
Restlet orders = new Restlet(getContext()) {
@Override
public void handle(Request request, Response response) {
// Print the user name of the requested orders
String message = "Orders of user \""
+ request.getAttributes().get("user") + "\"";
response.setEntity(message, MediaType.TEXT_PLAIN);
}
};
// Create the order handler
Restlet order = new Restlet(getContext()) {
@Override
public void handle(Request request, Response response) {
// Print the user name of the requested orders
String message = "Order \""
+ request.getAttributes().get("order")
+ "\" for user \""
+ request.getAttributes().get("user") + "\"";
response.setEntity(message, MediaType.TEXT_PLAIN);
}
};
// Attach the handlers to the root router
router.attach("/users/{user}", account);
router.attach("/users/{user}/orders", orders);
router.attach("/users/{user}/orders/{order}", order);
// Return the root router
return router;
}
};
// Attach the application to the component and start it
component.getDefaultHost().attach(application);
component.start();
请注意,变量的值是直接从URI中提取的,因此这是没有精确解码的。为了实现这样的工作,请查看手册中的decode(String)方法。
12. 抵达目标资源
在前面的示例中,在从目标URI中提取那些有趣部分时,我们利用了Restlet框架非常灵活的路由特性对request进行路由。但是,我们没有注意request方法和客户端对于它所期望的response的偏好。于是,我们如何才能将Restlet处理器和后台系统、域对象联系在一起呢?
到目前为止,我们已经介绍了一些在Restlet中超越传统Servlet API的特性。但我们并没有在"Restlet"这个框架名称中使用"REST"。如果你还没有做的话,我推荐你学习一些关于REST架构风格和将其应用于Web应用的最佳实践。这里提供了相关的FAQ记录,希望能给你一些启示,同时我们也运营着很有用的REST搜索引擎(基于Google)。如果你对传统MVC框架有一定了解,那么你可以阅读一下另一个FAQ记录,它提供了对MVC与Restlet关系的详细说明。
总结一下,request中含有标识目标资源的URI,而目标资源就是调用的主旨。这种资源信息被保存在Request.resourceRef属性中,并能够像我们之前所见那样服务于路由机制。因此在处理request时的首要目标就是发现目标资源。。。Resource类的实例或者其子类中的某个。为了帮助我们完成此项任务,我们可以使用专用的Finder,一个Restlet子类,它将Resource类引用作为参数并在request到来时自动实例化它。然后Finder将动态将调用分配给最新创建的实例,实际上就是根据request方法调用它的handle*()方法中的某一个(handleGet,handleDelete等)。当然,我们可以自定义这种行为,甚至使用Router的attach()方法,将URI模板和 Resource类作为其参数透明地创建Finder!现在,让我们看一下展示了示例中主框架类之间关系的全景图表:
回到代码中,我们在这里重构了Application.createRoot()方法。出于简化目的,我们没有提供具有静态文件的目录。你可以发现将Resource类直接指派给Router的方法。
// Create a router
Router router = new Router(getContext());
// Attach the resources to the router
router.attach("/users/{user}", UserResource.class);
router.attach("/users/{user}/orders", OrdersResource.class);
router.attach("/users/{user}/orders/{order}",
OrderResource.class);
// Return the root router
return router;
我们最后将重审一下UserResource类。这个类继承自org.restlet.resource.Resource类,因此它覆盖了具有三个参数的构造方法。此方法初始化了"context"、"request"和"response"属性。接着,我们使用从"/users/{user} "URI模板中提取出的"user"属性,将它的值保存在一个方便使用的成员变量中。然后,我们便可以在整个application中查找与"user" 相关的域对象了。最终,我们声明了用于暴露给用户的表示变量(representation variants),在这个简单的例子中只是文字而已。它将用于在运行时透明地完成一些内容导航,以便为每个request选择适合的变量,所有这些工作都是透明的。
public class UserResource extends Resource {
String userName;
Object user;
public UserResource(Context context, Request request,
Response response) {
super(context, request, response);
this.userName = (String) request.getAttributes().get("user");
this.user = null; // Could be a lookup to a domain object.
// Here we add the representation variants exposed
getVariants().add(new Variant(MediaType.TEXT_PLAIN));
}
@Override
public Representation getRepresentation(Variant variant) {
Representation result = null;
if (variant.getMediaType().equals(MediaType.TEXT_PLAIN)) {
result = new StringRepresentation("Account of user \""
+ this.userName + "\"");
}
return result;
}
}
你可以查看本指南中提供的代码包并对应用进行测试,并能够以仅接受Get请求的方式获得在第十一章中的相同行为。如果你希望使用PUT方法,那么就需要在UserResource中创建一个"allowPut()"方法并简单地返回"true",并且添加一个"put (Representation)"方法来处理调用。关于详细内容请查阅Restlet的Javadocs。
结论
我们已经涵盖了Restlet框架的许多方面。在你打算行动之前,让我们先回顾一下展示了本指南的主要概念和它们之间关系的两个层次图表: