二十八、延续机制支持
28.1 延续简介
延续是一种机制用来实现类似于Servlet 3.0异步功能的异步Servlet,但提供了一个简单易操作的接口。
28.1.1 为什么使用异步Servlets
不使用异步IO:
异步servlet的概念往往与异步IO或NIO的使用产生混淆。但是异步Servlets 和异步IO还是有主要不同点:
-
- HTTP请求通常很小并且位于一个单独的包,Servlets 很少在请求时阻塞。
- 许多responses 通常很小并且大小适合server缓冲,所以servlets 通常不会再写入response时堵塞
- 即便我们能在servlet中使用异步IO,那也将时编程变得更加困难。例如当一个应用程序读到2到3个字节的UTF-8它会怎么做?它不得不缓冲等待更多的字节。这件事最好由容器来做而不是应用程序。
异步等待:
异步servlets 的主要用法是用来等待非IO的事件或资源。许多web应用程序需要等待处理HTTP请求的各种阶段,例如:
-
- 处理请求前等待资源可用(例如:thread、JDBC连接)
- 在AJAX Comet应用中等待一个应用程序的事件(例如:聊天消息、价格变动)
- 等待远程服务的一个响应(例如:RESTful 、SOAP )
servlet API(2.5之前)仅支持一种同步调用方式,所以servlet 的任何等待都是阻塞式的。不幸的是,这意味着分配给请求的线程将会在等待所有资源的时候被持有:内核线程、栈存储、缓冲池、字符转换器、EE认真context等等。保存对这些资源的等待会浪费大量的系统资源。如果等待是异步进行的,那么可以进行更好的扩展和提高服务质量。
28.1.2 异步Servlets 例子
AJAX服务端推送
Web 2.0可以使用comet技术(又叫做 AJAX推送、服务端推送、长轮询)动态更新一个页面,而不需要刷新整个页面。
考虑一个股票投资的web应用程序。每个浏览器都会发一个长轮询到服务器,要求服务器提供任何用户股票价格的变动。服务器将从它所有的客户端接收到长轮询请求,并且不会立即进行响应。服务器将等待股票价格的变动,在那时它将发送一个响应到所有等待着的客户端。客户端接收到长轮询响应后会立即再次发送一个长轮询来获得未来价格的改变。
这样的话,服务器将持有每一个用户连接的长轮询,所以如果servlet不是异步的话,那么至少需要1000个线程来持有同时1000各用户。1000个线程会消耗256M资源;这些资源最好被应用所使用而不是进行无意义的等待。
如果servlet是异步的话,那么需要的线程数量将由生成相应的时间和价格变化的频率所决定。如果每个用户每10秒接收一次请求,响应需要10ms来生成,那么1000个用户仅仅需要一个线程来提供服务,256M的栈资源将被释放用于其他用途。
想获得更多关于comet 的知识,可以阅读comet 项目的与Jetty异步工作章节。
异步RESTful Web Service
假设一个web应用访问一个远程web服务(例如,SOAP service或RESTful service),通常一个远程服务仅花几百毫秒便可产生一个响应,eBay的RESTful web服务通常需要350ms来匹配给定关键字的拍卖列表-虽然仅仅只有少数的10ms的CPU时间来处理本地请求和生成一个响应。
为了每秒处理1000个请求,每一个web服务调用需要200ms,那么一个web应用需要1000*(200+20)/1000 = 220 个线程和110MB内存。如果发生请求风暴时仍会导致线程不足和web服务变慢的情况。如果进行异步处理,那么web应用将不需要持有每一个线程在等待web service响应的时候。及时异步机制需要消耗10ms(通常不消耗),那么web应用将需要1000*(20+10)/1000 = 30 个线程和15MB内存。这将有86%的性能提升,和95MB的内存释放可用于其他地方。而且,如果多重web services请求需要,异步调用将允许并行调用,而不是顺序调用,不需要额外分配线程。
这有一个Jetty的异步解决方案,查看例子。
服务质量(例如,JDBC连接池)
假设一个web应用每秒处理400个应用请求,每个请求需要与数据库交互50ms。为了处理这些请求,平均每秒需要400*50/1000 = 20 个JDBC连接。然而请求经常爆发或者延迟。为了保护访问风暴下的数据库,通常使用数据库连接池来限制连接。所以对于这个应用程序,它将是合理应用JDBC 30连接池,提供50%的额外保证。
如果请求在某一时刻翻倍,那么30个连接将不能每秒处理600个请求,那么每秒200个请求将要等待连接池分配连接。如果一个servlet容器有一个200个线程的线程池,那么所有的线程将要等待连接池1秒钟。1秒过后,web应用将不能处理任何请求,因为所有的线程都不可用,即便请求不是用数据库。加倍线程池数量需要额外的100MB内存,而只会给应用程序另外1mc的负载恩典。
这个线程饥饿状况也会发生如果数据库运行缓慢或暂时不可用。线程饥饿是频发的问题,并导致整个web服务来锁定和变得反应迟钝。如果web容器能够不使用线程来暂停等待一个JDBC连接请求,那么线程饥饿就不会发生,因为只有30个线程被用来访问数据库,而其他470个线程用于处理请求,不访问数据库。
Jetty的解决方案的一个示例,请参阅Server过滤器的质量。
28.1.3 Servlet线程模型
Java servlet的可伸缩性能的主要原因是由服务线程模型引起:
每连接一个线程
传统的Java IO模型,每个TCP/IP 连接与一个线程关联。如果你有一些非常活跃的线程,那么这个模型可以扩展到非常高的每秒请求数。
然而,许多web应用程序的典型特性是许多持久的HTTP连接等待用户阅读完网页,或者搜索下一个点击的连接。这样的配置,thread-per-connection模型将会有成千的线程需要支持成千的用户的大规模部署问题。
每请求一个线程
Java NIO库支持异步IO,这样线程就不需要分配到每个连接上,当连接闲置时(两次请求中间),那么连接将会添加到NIO选择集合,它允许一个线程扫描许多活动连接。只有当IO被检测到输入输出的时候线程才会被分配给它。然而servlet 2.5 API模型仍然需要将一个线程分配给一个请求持用的所有时间内。
这种thread-per-request模型允许更大比例的连接(用户)由于更少的调度延时导致的每秒请求数的减少。
异步请求处理
Jetty的支持连接API(和 servlet 3.0 异步)在servlet API引入一种改变,就是允许将一个请求多次分配到一个servlet。如果这个servlet没有所需的资源,那么这个servlet将被挂起(或将它放入异步模型),那么这个servlet将会没有任何响应的被返回。当等待的资源可用时,请求将会重新分配到这个servlet,使用一个新的线程,一个响应就会产生。
28.2 使用延续
异步servlet最初是由Jetty的延续机制引进来的,这是Jetty的一个特殊的机制。Jetty7以后,延续的API已经被扩展成一个通用的API,将在任何servlet-3.0容器上进行异步工作,Jetty6,7,8同样支持。延续机制还可以在servlet 2.5 容器下以阻塞方式运行。
28.2.1 获得一个延续
ContinuationSupport工厂可以通过request来获得一个延续实例:
Continuation continuation = ContinuationSupport.getContinuation(request);
28.2.2 挂起一个请求
为了挂起一个请求,挂起方法可以在延续上被调用:
void doGet(HttpServletRequest request, HttpServletResponse response) { ... // 可选择的: // continuation.setTimeout(long); continuation.suspend(); ... }
请求的生命周期将会从Servlet.service(...) 和Filter.doFilter(...) 的调用延续到容器的返回。当这些调用方法返回时,挂起的请求将不会被提交响应也不会被发送到客户端。
一旦请求被挂起,延续将会注册一个异步服务,这样等待的事件发生时异步服务将会被调用。
请求将会被挂起,直到continuation.resume()或continuation.complete()方法被调用。如果两个都没有被调用那么延续将会被超时。超时应该设置在挂起之前,通过continuation.setTimeout(long)这个方法,如果没有被设置,那么将使用默认的时间。如果没有超时监听着挂起延续,或者完成这个延续,那么调用continuation.isExpired()返回true。
挂起类似于servlet 3.0 的 request.startAsync()方法。不像jetty 6的延续,异常不会被抛出并且方法会正常返回。这允许注册的延续在挂起后发生避免发生互斥。如果一个需要一个异常(不知道延续而绕过代码并试图提交响应),那么continuation.undispatch() 方法将会被调用退出当前线程,并抛出一个ContinuationThrowable异常。
28.2.3 恢复一个请求
一旦异步事件发生,那么延续将被恢复:
void myAsyncCallback(Object results) { continuation.setAttribute("results",results); continuation.resume(); }
当延续被恢复,请求会被重新分配到这个servlet 容器,就像请求重新来过一样。然而在重新分配过程中, continuation.isInitial()方法返回false,所有设置到异步处理器上的参数都是有效的。
延续的恢复类似于Servlet 3.0 的 AsyncContext.dispatch()方法。
28.2.4 完成一个请求
当重新恢复一个请求时,异步处理器可能会生成响应,在写入响应后,处理器必须显示的通过调用方法表明延续完成了。
void myAsyncCallback(Object results) { writeResults(continuation.getServletResponse(),results); continuation.complete(); }
当完成方法被调用,容器会安排响应提交并刷新。延续的完成类似于Servlet 3.0 的 AsyncContext.complete()。
28.2.5 延续的监听
一个应用有可能通过ContinuationListener监听延续的各个状态。
void doGet(HttpServletRequest request, HttpServletResponse response) { ... Continuation continuation = ContinuationSupport.getContinuation(request); continuation.addContinuationListener(new ContinuationListener() { public void onTimeout(Continuation continuation) { ... } public void onComplete(Continuation continuation) { ... } }); continuation.suspend(); ... }
延续的监听类似于Servlet 3.0 的 AsyncListeners。
28.3 常用延续模式
28.3.1 暂停恢复模式
暂停/恢复模式用在当一个servlet 或 一个filter用来产生一个响应,在被异步处理器中断并恢复后。一般请求的属性用来传递结果,用来表明请求已经被暂停。
void doGet(HttpServletRequest request, HttpServletResponse response) { // 如果我们需要获得异步的结果 Object results = request.getAttribute("results"); if (results==null) { final Continuation continuation = ContinuationSupport.getContinuation(request); // 如果没有超时 if (continuation.isExpired()) { sendMyTimeoutResponse(response); return; } // 恢复request continuation.suspend(); // 注册前一直被暂停 // 被异步服务注册,此处的代码将被服务调用 myAsyncHandler.register(new MyHandler() { public void onMyEvent(Object result) { continuation.setAttribute("results",results); continuation.resume(); } }); return; // 或者continuation.undispatch(); } // 发送结果 sendMyResultResponse(response,results); }
这是一个非常好的模式即当响应需要servlet容器的工具(如,使用了一个web框架)或者一个事件将恢复多个请求导致容器的线程池被多个处理器使用。
28.3.2 暂停继续模式
暂停/完成模式是用来当一个异步处理器被用来生成一个响应的情况:
void doGet(HttpServletRequest request, HttpServletResponse response) { final Continuation continuation = ContinuationSupport.getContinuation(request); // 如果没有超时 if (continuation.isExpired()) { sendMyTimeoutResponse(request,response); return; } // 将请求挂起 continuation.suspend(); // response 可能被包装 // r注册给异步服务,代码将被服务调用 myAsyncHandler.register(new MyHandler() { public void onMyEvent(Object result) { sendMyResultResponse(continuation.getServletResponse(),results); continuation.complete(); } }); }
这种模式在以下这种情况下是非常好的,即响应不需要容器的工具(如使用一种web框架)并且事件将会恢复一次延续。如果多个响应将被发送(例如,聊天室),那么写一次响应将会阻塞并在其他响应上导致一个DOS。
28.3.3 示例
// // ======================================================================== // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package com.acme; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.continuation.Continuation; import org.eclipse.jetty.continuation.ContinuationSupport; // 简单的异步聊天室 // 这不处理重复的用户名和同一个浏览器的标签情况 // 一些代码是重复的 public class ChatServlet extends HttpServlet { // 内部类用来保存每个成员的消息队列 class Member { String _name; Continuation _continuation; Queue<String> _queue = new LinkedList<String>(); } Map<String,Map<String,Member>> _rooms = new HashMap<String,Map<String, Member>>(); // 处理浏览器的AJAX调用 @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Ajax调用的编码形式 String action = request.getParameter("action"); String message = request.getParameter("message"); String username = request.getParameter("user"); if (action.equals("join")) join(request,response,username); else if (action.equals("poll")) poll(request,response,username); else if (action.equals("chat")) chat(request,response,username,message); } private synchronized void join(HttpServletRequest request,HttpServletResponse response,String username) throws IOException { Member member = new Member(); member._name=username; Map<String,Member> room=_rooms.get(request.getPathInfo()); if (room==null) { room=new HashMap<String,Member>(); _rooms.put(request.getPathInfo(),room); } room.put(username,member); response.setContentType("text/json;charset=utf-8"); PrintWriter out=response.getWriter(); out.print("{action:\"join\"}"); } private synchronized void poll(HttpServletRequest request,HttpServletResponse response,String username) throws IOException { Map<String,Member> room=_rooms.get(request.getPathInfo()); if (room==null) { response.sendError(503); return; } Member member = room.get(username); if (member==null) { response.sendError(503); return; } synchronized(member) { if (member._queue.size()>0) { // 发送一个聊天消息 response.setContentType("text/json;charset=utf-8"); StringBuilder buf=new StringBuilder(); buf.append("{\"action\":\"poll\","); buf.append("\"from\":\""); buf.append(member._queue.poll()); buf.append("\","); String message = member._queue.poll(); int quote=message.indexOf(‘"‘); while (quote>=0) { message=message.substring(0,quote)+‘\\‘+message.substring(quote); quote=message.indexOf(‘"‘,quote+2); } buf.append("\"chat\":\""); buf.append(message); buf.append("\"}"); byte[] bytes = buf.toString().getBytes("utf-8"); response.setContentLength(bytes.length); response.getOutputStream().write(bytes); } else { Continuation continuation = ContinuationSupport.getContinuation(request); if (continuation.isInitial()) { // 没有消息时,挂起等待聊天或超时 continuation.setTimeout(20000); continuation.suspend(); member._continuation=continuation; } else { // 超时后发送空的响应 response.setContentType("text/json;charset=utf-8"); PrintWriter out=response.getWriter(); out.print("{action:\"poll\"}"); } } } } private synchronized void chat(HttpServletRequest request,HttpServletResponse response,String username,String message) throws IOException { Map<String,Member> room=_rooms.get(request.getPathInfo()); if (room!=null) { // 推送消息到所有成员 for (Member m:room.values()) { synchronized (m) { m._queue.add(username); // from m._queue.add(message); // chat // 如果轮询到则唤醒 if (m._continuation!=null) { m._continuation.resume(); m._continuation=null; } } } } response.setContentType("text/json;charset=utf-8"); PrintWriter out=response.getWriter(); out.print("{action:\"chat\"}"); } // 提供嵌入css和js的html服务 // 这应该是静态内容和真正使用的js库 @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (request.getParameter("action")!=null) doPost(request,response); else getServletContext().getNamedDispatcher("default").forward(request,response); } }
这个ChatServlet 例子,展示了挂起/恢复模式被用来建立一个聊天室(使用了异步servlet)。相同的原则将应用于cometd这样的框架,为这样的应用提供一个基于延续的丰富的环境。
// // ======================================================================== // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.servlets; import java.io.IOException; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** * Quality of Service Filter. * <p> * This filter limits the number of active requests to the number set by the "maxRequests" init parameter (default 10). * If more requests are received, they are suspended and placed on priority queues. Priorities are determined by * the {@link #getPriority(ServletRequest)} method and are a value between 0 and the value given by the "maxPriority" * init parameter (default 10), with higher values having higher priority. * <p> * This filter is ideal to prevent wasting threads waiting for slow/limited * resources such as a JDBC connection pool. It avoids the situation where all of a * containers thread pool may be consumed blocking on such a slow resource. * By limiting the number of active threads, a smaller thread pool may be used as * the threads are not wasted waiting. Thus more memory may be available for use by * the active threads. * <p> * Furthermore, this filter uses a priority when resuming waiting requests. So that if * a container is under load, and there are many requests waiting for resources, * the {@link #getPriority(ServletRequest)} method is used, so that more important * requests are serviced first. For example, this filter could be deployed with a * maxRequest limit slightly smaller than the containers thread pool and a high priority * allocated to admin users. Thus regardless of load, admin users would always be * able to access the web application. * <p> * The maxRequest limit is policed by a {@link Semaphore} and the filter will wait a short while attempting to acquire * the semaphore. This wait is controlled by the "waitMs" init parameter and allows the expense of a suspend to be * avoided if the semaphore is shortly available. If the semaphore cannot be obtained, the request will be suspended * for the default suspend period of the container or the valued set as the "suspendMs" init parameter. * <p> * If the "managedAttr" init parameter is set to true, then this servlet is set as a {@link ServletContext} attribute with the * filter name as the attribute name. This allows context external mechanism (eg JMX via {@link ContextHandler#MANAGED_ATTRIBUTES}) to * manage the configuration of the filter. */ @ManagedObject("Quality of Service Filter") public class QoSFilter implements Filter { private static final Logger LOG = Log.getLogger(QoSFilter.class); static final int __DEFAULT_MAX_PRIORITY = 10; static final int __DEFAULT_PASSES = 10; static final int __DEFAULT_WAIT_MS = 50; static final long __DEFAULT_TIMEOUT_MS = -1; static final String MANAGED_ATTR_INIT_PARAM = "managedAttr"; static final String MAX_REQUESTS_INIT_PARAM = "maxRequests"; static final String MAX_PRIORITY_INIT_PARAM = "maxPriority"; static final String MAX_WAIT_INIT_PARAM = "waitMs"; static final String SUSPEND_INIT_PARAM = "suspendMs"; private final String _suspended = "[email protected]" + Integer.toHexString(hashCode()) + ".SUSPENDED"; private final String _resumed = "[email protected]" + Integer.toHexString(hashCode()) + ".RESUMED"; private long _waitMs; private long _suspendMs; private int _maxRequests; private Semaphore _passes; private Queue<AsyncContext>[] _queues; private AsyncListener[] _listeners; public void init(FilterConfig filterConfig) { int max_priority = __DEFAULT_MAX_PRIORITY; if (filterConfig.getInitParameter(MAX_PRIORITY_INIT_PARAM) != null) max_priority = Integer.parseInt(filterConfig.getInitParameter(MAX_PRIORITY_INIT_PARAM)); _queues = new Queue[max_priority + 1]; _listeners = new AsyncListener[_queues.length]; for (int p = 0; p < _queues.length; ++p) { _queues[p] = new ConcurrentLinkedQueue<>(); _listeners[p] = new QoSAsyncListener(p); } int maxRequests = __DEFAULT_PASSES; if (filterConfig.getInitParameter(MAX_REQUESTS_INIT_PARAM) != null) maxRequests = Integer.parseInt(filterConfig.getInitParameter(MAX_REQUESTS_INIT_PARAM)); _passes = new Semaphore(maxRequests, true); _maxRequests = maxRequests; long wait = __DEFAULT_WAIT_MS; if (filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM) != null) wait = Integer.parseInt(filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM)); _waitMs = wait; long suspend = __DEFAULT_TIMEOUT_MS; if (filterConfig.getInitParameter(SUSPEND_INIT_PARAM) != null) suspend = Integer.parseInt(filterConfig.getInitParameter(SUSPEND_INIT_PARAM)); _suspendMs = suspend; ServletContext context = filterConfig.getServletContext(); if (context != null && Boolean.parseBoolean(filterConfig.getInitParameter(MANAGED_ATTR_INIT_PARAM))) context.setAttribute(filterConfig.getFilterName(), this); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean accepted = false; try { Boolean suspended = (Boolean)request.getAttribute(_suspended); if (suspended == null) { accepted = _passes.tryAcquire(getWaitMs(), TimeUnit.MILLISECONDS); if (accepted) { request.setAttribute(_suspended, Boolean.FALSE); if (LOG.isDebugEnabled()) LOG.debug("Accepted {}", request); } else { request.setAttribute(_suspended, Boolean.TRUE); int priority = getPriority(request); AsyncContext asyncContext = request.startAsync(); long suspendMs = getSuspendMs(); if (suspendMs > 0) asyncContext.setTimeout(suspendMs); asyncContext.addListener(_listeners[priority]); _queues[priority].add(asyncContext); if (LOG.isDebugEnabled()) LOG.debug("Suspended {}", request); return; } } else { if (suspended) { request.setAttribute(_suspended, Boolean.FALSE); Boolean resumed = (Boolean)request.getAttribute(_resumed); if (resumed == Boolean.TRUE) { _passes.acquire(); accepted = true; if (LOG.isDebugEnabled()) LOG.debug("Resumed {}", request); } else { // Timeout! try 1 more time. accepted = _passes.tryAcquire(getWaitMs(), TimeUnit.MILLISECONDS); if (LOG.isDebugEnabled()) LOG.debug("Timeout {}", request); } } else { // Pass through resume of previously accepted request. _passes.acquire(); accepted = true; if (LOG.isDebugEnabled()) LOG.debug("Passthrough {}", request); } } if (accepted) { chain.doFilter(request, response); } else { if (LOG.isDebugEnabled()) LOG.debug("Rejected {}", request); ((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); } } catch (InterruptedException e) { ((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); } finally { if (accepted) { for (int p = _queues.length - 1; p >= 0; --p) { AsyncContext asyncContext = _queues[p].poll(); if (asyncContext != null) { ServletRequest candidate = asyncContext.getRequest(); Boolean suspended = (Boolean)candidate.getAttribute(_suspended); if (suspended == Boolean.TRUE) { candidate.setAttribute(_resumed, Boolean.TRUE); asyncContext.dispatch(); break; } } } _passes.release(); } } } /** * Computes the request priority. * <p> * The default implementation assigns the following priorities: * <ul> * <li> 2 - for an authenticated request * <li> 1 - for a request with valid / non new session * <li> 0 - for all other requests. * </ul> * This method may be overridden to provide application specific priorities. * * @param request the incoming request * @return the computed request priority */ protected int getPriority(ServletRequest request) { HttpServletRequest baseRequest = (HttpServletRequest)request; if (baseRequest.getUserPrincipal() != null) { return 2; } else { HttpSession session = baseRequest.getSession(false); if (session != null && !session.isNew()) return 1; else return 0; } } public void destroy() { } /** * Get the (short) amount of time (in milliseconds) that the filter would wait * for the semaphore to become available before suspending a request. * * @return wait time (in milliseconds) */ @ManagedAttribute("(short) amount of time filter will wait before suspending request (in ms)") public long getWaitMs() { return _waitMs; } /** * Set the (short) amount of time (in milliseconds) that the filter would wait * for the semaphore to become available before suspending a request. * * @param value wait time (in milliseconds) */ public void setWaitMs(long value) { _waitMs = value; } /** * Get the amount of time (in milliseconds) that the filter would suspend * a request for while waiting for the semaphore to become available. * * @return suspend time (in milliseconds) */ @ManagedAttribute("amount of time filter will suspend a request for while waiting for the semaphore to become available (in ms)") public long getSuspendMs() { return _suspendMs; } /** * Set the amount of time (in milliseconds) that the filter would suspend * a request for while waiting for the semaphore to become available. * * @param value suspend time (in milliseconds) */ public void setSuspendMs(long value) { _suspendMs = value; } /** * Get the maximum number of requests allowed to be processed * at the same time. * * @return maximum number of requests */ @ManagedAttribute("maximum number of requests to allow processing of at the same time") public int getMaxRequests() { return _maxRequests; } /** * Set the maximum number of requests allowed to be processed * at the same time. * * @param value the number of requests */ public void setMaxRequests(int value) { _passes = new Semaphore((value - getMaxRequests() + _passes.availablePermits()), true); _maxRequests = value; } private class QoSAsyncListener implements AsyncListener { private final int priority; public QoSAsyncListener(int priority) { this.priority = priority; } @Override public void onStartAsync(AsyncEvent event) throws IOException { } @Override public void onComplete(AsyncEvent event) throws IOException { } @Override public void onTimeout(AsyncEvent event) throws IOException { // Remove before it‘s redispatched, so it won‘t be // redispatched again at the end of the filtering. AsyncContext asyncContext = event.getAsyncContext(); _queues[priority].remove(asyncContext); asyncContext.dispatch(); } @Override public void onError(AsyncEvent event) throws IOException { } } }
这个QoSFilter(这是jetty-servlets包中的一个类的),在过滤器内限制请求数量并使用挂起/恢复风格。这个可以用来保护JDBC连接池,或者限制对其他有限资源的访问。
这个DosFilter (这是jetty-servlets包中的一个类的,源码不再贴出)例子和QoSFilter比较相似,但是以拒绝服务攻击的方式来保护web应用,尽可能从应用程序内进行保护。
如果检测到同一个源的大量请求,那么这些请求将会被挂起并生成一个警告。这假设攻击者使用简单的阻塞方式来攻击,所以暂停你想保护的他们想访问的资源。真正有效避免DOS攻击应该交于网络设备。
// // ======================================================================== // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.proxy; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; import javax.servlet.AsyncContext; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.util.InputStreamContentProvider; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.util.Callback; /** * <p>Servlet 3.0 asynchronous proxy servlet.</p> * <p>The request processing is asynchronous, but the I/O is blocking.</p> * * @see AsyncProxyServlet * @see AsyncMiddleManServlet * @see ConnectHandler */ public class ProxyServlet extends AbstractProxyServlet { @Override protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { final int requestId = getRequestId(request); String rewrittenTarget = rewriteTarget(request); if (_log.isDebugEnabled()) { StringBuffer uri = request.getRequestURL(); if (request.getQueryString() != null) uri.append("?").append(request.getQueryString()); if (_log.isDebugEnabled()) _log.debug("{} rewriting: {} -> {}", requestId, uri, rewrittenTarget); } if (rewrittenTarget == null) { onProxyRewriteFailed(request, response); return; } final Request proxyRequest = getHttpClient().newRequest(rewrittenTarget) .method(request.getMethod()) .version(HttpVersion.fromString(request.getProtocol())); copyRequestHeaders(request, proxyRequest); addProxyHeaders(request, proxyRequest); final AsyncContext asyncContext = request.startAsync(); // We do not timeout the continuation, but the proxy request asyncContext.setTimeout(0); proxyRequest.timeout(getTimeout(), TimeUnit.MILLISECONDS); if (hasContent(request)) proxyRequest.content(proxyRequestContent(request, response, proxyRequest)); sendProxyRequest(request, response, proxyRequest); } protected ContentProvider proxyRequestContent(HttpServletRequest request, HttpServletResponse response, Request proxyRequest) throws IOException { return new ProxyInputStreamContentProvider(request, response, proxyRequest, request.getInputStream()); } @Override protected Response.Listener newProxyResponseListener(HttpServletRequest request, HttpServletResponse response) { return new ProxyResponseListener(request, response); } protected void onResponseContent(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, byte[] buffer, int offset, int length, Callback callback) { try { if (_log.isDebugEnabled()) _log.debug("{} proxying content to downstream: {} bytes", getRequestId(request), length); response.getOutputStream().write(buffer, offset, length); callback.succeeded(); } catch (Throwable x) { callback.failed(x); } } /** * <p>Convenience extension of {@link ProxyServlet} that offers transparent proxy functionalities.</p> * * @see org.eclipse.jetty.proxy.AbstractProxyServlet.TransparentDelegate */ public static class Transparent extends ProxyServlet { private final TransparentDelegate delegate = new TransparentDelegate(this); @Override public void init(ServletConfig config) throws ServletException { super.init(config); delegate.init(config); } @Override protected String rewriteTarget(HttpServletRequest request) { return delegate.rewriteTarget(request); } } protected class ProxyResponseListener extends Response.Listener.Adapter { private final HttpServletRequest request; private final HttpServletResponse response; protected ProxyResponseListener(HttpServletRequest request, HttpServletResponse response) { this.request = request; this.response = response; } @Override public void onBegin(Response proxyResponse) { response.setStatus(proxyResponse.getStatus()); } @Override public void onHeaders(Response proxyResponse) { onServerResponseHeaders(request, response, proxyResponse); } @Override public void onContent(final Response proxyResponse, ByteBuffer content, final Callback callback) { byte[] buffer; int offset; int length = content.remaining(); if (content.hasArray()) { buffer = content.array(); offset = content.arrayOffset(); } else { buffer = new byte[length]; content.get(buffer); offset = 0; } onResponseContent(request, response, proxyResponse, buffer, offset, length, new Callback.Nested(callback) { @Override public void failed(Throwable x) { super.failed(x); proxyResponse.abort(x); } }); } @Override public void onComplete(Result result) { if (result.isSucceeded()) onProxyResponseSuccess(request, response, result.getResponse()); else onProxyResponseFailure(request, response, result.getResponse(), result.getFailure()); if (_log.isDebugEnabled()) _log.debug("{} proxying complete", getRequestId(request)); } } protected class ProxyInputStreamContentProvider extends InputStreamContentProvider { private final HttpServletResponse response; private final Request proxyRequest; private final HttpServletRequest request; protected ProxyInputStreamContentProvider(HttpServletRequest request, HttpServletResponse response, Request proxyRequest, InputStream input) { super(input); this.request = request; this.response = response; this.proxyRequest = proxyRequest; } @Override public long getLength() { return request.getContentLength(); } @Override protected ByteBuffer onRead(byte[] buffer, int offset, int length) { if (_log.isDebugEnabled()) _log.debug("{} proxying content to upstream: {} bytes", getRequestId(request), length); return onRequestContent(request, proxyRequest, buffer, offset, length); } protected ByteBuffer onRequestContent(HttpServletRequest request, Request proxyRequest, byte[] buffer, int offset, int length) { return super.onRead(buffer, offset, length); } @Override protected void onReadFailure(Throwable failure) { onClientRequestFailure(request, proxyRequest, response, failure); } } }
这个ProxyServlet(这是jetty-proxy包中的一个类的)例子使用挂起/完成模式,Jetty异步客户端实现一个可扩展的代理服务器。
二十九、框架
29.1 Spring设置
你可以在代码中组装和配置Jetty或者完全使用Spring的IOC框架。如果你想做的仅仅是将你的Jetty服务嵌入到Spring中,那么可以简单的看下面例子的xml片段。如果你想使用spring来替换jetty-xml 用于启动一个普通的Jetty应用,你可以这么做但是将不会依赖其他的系统框架。
29.1.1 Jetty-Spring模块
Jetty spring模块的功能是可以通过该模块启动Jetty。例如:
$ java -jar start.jar --add-to-startd=spring
这(或者使用 --add-to-start=spring命令)将创建一个${jetty.home}/lib/spring目录,并将jetty-spring的jar包放入其中。但是不提供spring的jar包及其依赖包。你需要自己下载它们并将它们放到Jetty的classpath下 - 你可以使用由spring.mod创建的 ${jetty.home}/lib/spring 目录存放这些jar。
29.1.2 使用Spring配置Jetty
通过spring配置Jetty是非常简单的通过调用spring APIs将其作为一个bean。下面是一个例子模仿默认jetty启动配置。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <!-- =============================================================== --> <!-- Configure the Jetty Server with Spring --> <!-- This file is the similar to jetty.xml, but written in spring --> <!-- XmlBeanFactory format. --> <!-- =============================================================== --> <beans> <bean id="contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/> <bean id="server" name="Main" class="org.eclipse.jetty.server.Server" init-method="start" destroy-method="stop"> <constructor-arg> <bean id="threadPool" class="org.eclipse.jetty.util.thread.QueuedThreadPool"> <property name="minThreads" value="10"/> <property name="maxThreads" value="50"/> </bean> </constructor-arg> <property name="connectors"> <list> <bean id="connector" class="org.eclipse.jetty.server.ServerConnector"> <constructor-arg ref="server"/> <property name="port" value="8080"/> </bean> </list> </property> <property name="handler"> <bean id="handlers" class="org.eclipse.jetty.server.handler.HandlerCollection"> <property name="handlers"> <list> <ref bean="contexts"/> <bean id="defaultHandler" class="org.eclipse.jetty.server.handler.DefaultHandler"/> </list> </property> </bean> </property> <property name="beans"> <list> <bean id="deploymentManager" class="org.eclipse.jetty.deploy.DeploymentManager"> <property name="contexts" ref="contexts"/> <property name="appProviders"> <list> <bean id="webAppProvider" class="org.eclipse.jetty.deploy.providers.WebAppProvider"> <property name="monitoredDirName" value="webapps"/> <property name="scanInterval" value="1"/> <property name="extractWars" value="true"/> </bean> </list> </property> </bean> </list> </property> </bean> </beans>
29.2 OSGI
29.2.1 简介
Jetty OSGi基础设施提供了一个内置有OSGi容器的Jetty容器内。传统的JavaEE web应用可以被部署,除此之外还有Jetty的与OSGi一起的ContextHandlers。此外,基础设施还支持OSGi HttpService接口。
29.2.2 常规设置
所有的Jetty jar包含清单条目确保他们可以部署为一个OSGi容器包。您将需要安装一些jetty jar到OSGi容器中。你总是可以在maven仓库中获得Jetty的jar包,或者你可以在下载的Jetty软件包中获取。这里有一个Jetty jar包的最小集合:
Jar | 包名称 |
---|---|
jetty-util |
org.eclipse.jetty.util |
jetty-http |
org.eclipse.jetty.http |
jetty-io |
org.eclipse.jetty.io |
jetty-security |
org.eclipse.jetty.security |
jetty-server |
org.eclipse.jetty.server |
jetty-servlet |
org.eclipse.jetty.servlet |
jetty-webapp |
org.eclipse.jetty.webapp |
jetty-deploy |
org.eclipse.jetty.deploy |
jetty-xml |
org.eclipse.jetty.xml |
jetty-osgi-servlet-api |
org.eclipse.jetty.toolchain |
+提示
我们建议在部署的时候同时添加annotation-related jar包,因为越来越多的Servlet使用注解来完成它们的功能。
你也同样会需要OSGi事件管理服务和OSGi配置管理服务。如果你的OSGi容器没有自动将这些服务启用,那么你需要以适当的方式将它们添加到容器中。
29.2.3 Jetty OSGi容器
29.2.3.1 jetty-osgi-boot jar
现在你已经安装好Jetty最基本的jar包,那么你需要继续安装jetty-osgi-boot.jar包,在Maven仓库中下载,点击我(http://central.maven.org/maven2/org/eclipse/jetty/osgi/jetty-osgi-boot/)。
当它启动时,这个包将会实例化并在Jetty的OSGi容器可用。如果这个包没有自动安装到OSGi容器中,那么你应该使用命令手动将其安装到容器的中。
29.2.3.2 定制Jetty容器
在安装之前,你可能希望定制一下Jetty容器。通常这应该由一些系统属性个组合和通常的jetty xml配置文件来完成。定义系统属性的方式依赖于您使用OSGi容器,所以确保你熟悉如何设置您的环境。在下面的例子中,我们将假定OSGi容器允许我们将系统属性设置为简单的名称=值对。
可用的系统属性有:
jetty.http.port
如果没有特别指定,默认8080。
jetty.home
这个属性或者 jetty.home.bundle必须被指定。这个属性应该指向包含配置xml文件的一个文件系统位置。例如:
jetty.home=/opt/custom/jetty
/opt/custom/jetty文件夹包含:
etc/jetty.xml
etc/jetty-selector.xml
etc/jetty-deployer.xml
etc/jetty-special.xml
jetty.home.bundle
这个属性或jetty.home属性必须被配置。这个属性应该指定一个包含 jettyhome/ 文件夹的包名。 jettyhome/ 文件夹必须有一个名为 etc/ (包含启动的xml配置文件)的子文件夹。 jetty-osgi-boot.jar里面包含 jettyhome/文件夹,并有默认的xml配置文件。下面展示如何指定它:
jetty.home.bundle=org.eclipse.jetty.osgi.boot
这个jar包,有如下配置文件:
META-INF/MANIFEST.MF
jettyhome/etc/jetty.xml
jettyhome/etc/jetty-deployer.xml
jettyhome/etc/jetty-http.xml
jetty.etc.config.urls
这个属性指定xml文件的路径。如果没有指定则默认为:
etc/jetty.xml,etc/jetty-http.xml,etc/jetty-deployer.xml
29.2.3.3 将Jetty容器做为OSGi一个服务
现在你可以将jetty-osgi-boot.jar部署到你的OSGi容器中了。一个Jetty的server实例将会被创建,xml配置文件的配置将会应用在上面,然后它将做为一个OSGi服务发布。通常,你不需要与服务的实例进行交互,然而你仍然可以通过使用OSGi API的引用来获得Jetty。
org.osgi.framework.BundleContext bc;
org.osgi.framework.ServiceReference ref = bc.getServiceReference("org.eclipse.jetty.server.Server");
Server服务有一些与之相关的属性,你可以通过org.osgi.framework.ServiceReference.getProperty(String) 方法来获得:
managedServerName
由 jetty-osgi-boot.jar创建的Jetty实例,被称为“默认Jetty服务”
jetty.etc.config.urls
在jetty.home 或 jetty.home.bundle/jettyhome下的xml配置url列表
29.2.3.4 新增更多的Jetty服务
正如我们在前面章节中所看到的,jetty-osgi-boot的代码将会通过jetty.etc.config.urls 指定的xml配置文件创建一个 org.eclipse.jetty.server.Server实例,然后将其注册为一个OSGi服务。默认实例的默认名称为“defaultJettyServer”
你也可以创建另外的应用实例,然后注册为OSGi服务,jetty-osgi-boot代码会发现它们,然后配置它们,这样它们就可以部署ContextHandlers 和webapp包。当你部署webapps 或者ContextHandlers 做为一个包或者一个服务(看下面的章节),你可以通过服务器的名称针对他们被部署到一个特定的服务器实例。
这里有一个例子说明如何创建一个新的server实例并且注册到OSGi这样jetty-osgi-boot代码会发现它、配置它,这样它就可以部署目标:
public class Activator implements BundleActivator { public void start(BundleContext context) throws Exception { Server server = new Server(); // 在这里对服务进行任何配置 String serverName = "fooServer"; Dictionary serverProps = new Hashtable(); // 为当前服务定义一个唯一的服务名 serverProps.put("managedServerName", serverName); // 为当前服务设置一个唯一的端口 serverProps.put("jetty.http.port", "9999"); // 让Jetty应用一些配置文件到这个实例上 serverProps.put("jetty.etc.config.urls", "file:/opt/jetty/etc/jetty.xml,file:/opt/jetty/etc/jetty-selector.xml,file:/opt/jetty/etc/jetty-deployer.xml"); // 做为一个OSGi服务,使之被发现 context.registerService(Server.class.getName(), server, serverProps); } }
现在我们可以创建一个名为"fooServer"的服务,我们可以部署这个webapps和ContextHandlers做为一个包或者服务。这里有一个例子关于部署一个webapp做为一个服务并且将其定位到上面创建的"fooServer"服务:
public class Activator implements BundleActivator { public void start(BundleContext context) throws Exception { // 创建一个webapp并指向"fooServer WebAppContext webapp = new WebAppContext(); Dictionary props = new Hashtable(); props.put("war", "."); props.put("contextPath", "/acme"); props.put("managedServerName", "fooServer"); context.registerService(ContextHandler.class.getName(), webapp, props); } }
29.2.4 作为Webapps部署Bundles
29.2.4.1 部署规则
OSGi 容器监听已经安装的Bundles ,并将其部署到已有的webapp上。
任何符合下面条件的将会做为webapp进行部署:
包含一个WEB-INF/web.xml文件的Bundle
如果一个bundle 包含一个web应用描述,那么它将会被自动部署。这是一个简单的方法来部署经典JavaEE webapps。Bundle 的 MANIFEST 包含Jetty-WarFolderPath(之前的版本为jetty-9.3)或Jetty-WarResourcePath,这是bundle 里面关于webapp资源的位置。通常这是有用的尤其当bundle 不是一个纯粹的webapp,而是bundle的一个组件的时候。这里有一个webapp资源的路径不是bundle根路径的例子,而是在web/MANIFEST文件中:
Bundle-Name: Web
Jetty-WarResourcePath: web
Import-Package: javax.servlet;version="3.1",
javax.servlet.resources;version="3.1"
Bundle-SymbolicName: com.acme.sample.web
Bundle 包含:
META-INF/MANIFEST.MF
web/index.html
web/foo.html
web/WEB-INF/web.xml
com/acme/sample/web/MyStuff.class
com/acme/sample/web/MyOtherStuff.class
Bundle MANIFEST文件包含Web-ContextPath属性
这个头是在OSGi中使用webapp的RFC-66 规范的一部分。这里有一个基于前面的例子,使用Web-ContextPath头来设置部署context path为 /sample。MANIFEST如下:
Bundle-Name: Web
Jetty-WarResourcePath: web
Web-ContextPath: /sample
Import-Package: javax.servlet;version="3.1",
javax.servlet.resources;version="3.1"
Bundle-SymbolicName: com.acme.sample.web
你也可以在你的bundle的MANIFEST中定义额外的头信息用来帮助应用程序部署:
Jetty-defaultWebXmlFilePath
用来部署webapp的webdefault.xml的路径。这个路径可以是绝对路径(绝对路径或file: url)或者相对路径(相对于bundle 根路径)。默认的webdefault.xml 文件将后安装到OSGi容器中。
Jetty-WebXmlFilePath
web.xml文件的地址。这个路径可以是绝对路径(绝对路径或file: url)或者相对路径(相对于bundle 根路径)。默认为WEB-INF/web.xml。
Jetty-extraClassPath
新增到webapp的类加载器中额外的类路径。
Jetty-bundleInstall
基础文件夹的路径用来覆盖已经安装的bundled - 主要用于那些默认方式解压的OSGi框架。
Require-TldBundle
webapp依赖的所有包含TLD的bundle,以逗号分割的列表。
managedServerName
部署webapp bundle的服务实例名。如果没有指定,那么默认为 "defaultJettyServer"。
Jetty-WarFragmentResourcePath
包含webapp静态资源的在web-bundle中的路径。这些路径将会追加到webapp的基础资源路径下。
Jetty-WarPrependFragmentResourcePath
包含webapp静态资源的在web-bundle中的路径。这些路径将会追加到webapp的基础资源路径下。
Jetty-ContextFilePath
将要部署到webapp中的bundle路径列表。或者可能包含一个单独的Jetty context文件叫做"jetty-webapp-context.xml",在webapp 的bundle的META-INF文件夹下,它将会自动部署到webapp中。
29.2.4.2 为webapp bundle定义上下文路径
正如我们所看到的在前面的小节中,如果一个bundled的MANIFEST 文件包含RFC-66规范的头属性Web-ContextPath,那么Jetty将会使用这个做为上下文路径。如果MANIFEST 文件中没有这个头信息,那么Jetty将会根据最后一个bundle元素的地址(通过Bundle.getLocation()方法,并去掉扩展名)编造一个上下文路径。
例如,我们有一个bundle,路径如下:
file://some/where/over/the/rainbow/oz.war
那么生成的上下文路径将是:
/oz
29.2.4.3 用于webapp bundles的额外属性
你可以通过Jetty的xml文件来进一步定制你的webapp。这些xml文件必须放置在bundle的META-INF下面,而且名称必须为jetty-webapp-context.xml。
这里有一个webapp bundle例子,包含如下文件:
META-INF/MANIFEST.MF
META-INF/jetty-webapp-context.xml
web/index.html
web/foo.html
web/WEB-INF/web.xml
com/acme/sample/web/MyStuff.class
com/acme/sample/web/MyOtherStuff.class
这里还有一个META-INF/jetty-webapp-context.xml 文件的例子:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd"> <Configure class="org.eclipse.jetty.webapp.WebAppContext"> <Set name="defaultsDescriptor"><Property name="bundle.root"/>META-INF/webdefault.xml</Set> </Configure>
正如你所看到的,这是一个普通的上下文xml文件,用来设置webapp。然而,有一些额外的有用的属性,可以引用:
Server
这是一个Jetty org.eclipse.jetty.server的引用。在这个context xml文件配置的Server实例将会被部署。
bundle.root
这是一个org.eclipse.jetty.util.resource.Resource的引用,用来代表Bundle路径。这个可以是一个文件系统的文件夹,或者是一个jar文件的地址。
29.2.5 部署Bundles做为JettyContextHandlers
29.2.5.1 基本部署
为了部署一个webapps,Jetty OSGi容器监听所有安装的bundle,不使用重量级的webapp而是使用灵活的ContextHandlers概念的Jetty-specific。
根据下面的标准来决定是否部署为一个ContextHandler:
Bundel的MANIFEST 包含Jetty-ContextFilePath属性
一系列context文件的名字 - 每一个代表一个ContextHandler都将会部署到Jetty。context文件可以在bundle里面,也可以在文件系统的任何位置,或者在jetty.home文件夹下。一个在bundle里面的context文件的配置:
Jetty-ContextFilePath: ./a/b/c/d/foo.xml
一个在文件系统的context文件配置:
Jetty-ContextFilePath: /opt/app/contexts/foo.xml
一个相对于jetty.home路径的context文件配置:
Jetty-ContextFilePath: contexts/foo.xml
多个不同的context文件配置:
Jetty-ContextFilePath: ./a/b/c/d/foo.xml,/opt/app/contexts/foo.xml,contexts/foo.xml
其它可用来配置部署ContextHandler的属性有:
managedServerName
用来部署webapp bundle的Server实例的名称。如果没有特别指定,默认的名称为"defaultJettyServer"。
29.2.5.2 为ContextHandler Bundle确定上下文路径
通常ContextHandler的上下文路径在context的xml文件中进行配置。然而你仍然可以通过使用MANIFEST文件中的Web-ContextPath属性进行覆盖。
29.2.5.3 额外的context xml文件可用属性
在Jetty OSGi容器将发现于MANIFEST文件头信息应用于context xml文件中之前,可以在文件中设置一些有用的属性:
Server
这是一个Jetty org.eclipse.jetty.server的引用。在这个context xml文件配置的Server实例将会被部署。
bundle.root
这是一个org.eclipse.jetty.util.resource.Resource的引用,用来代表Bundle路径。这个可以是一个文件系统的文件夹,或者是一个jar文件的地址。
这里有一个context xml文件的例子,应用了这些属性:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> <Configure class="org.eclipse.jetty.server.handler.ContextHandler"> <!-- Get root for static content, could be on file system or this bundle --> <Call id="res" class="org.eclipse.jetty.util.resource.Resource" name="newResource"> <Arg><Property name="bundle.root"/></Arg> </Call> <Ref refid="res"> <Call id="base" name="addPath"> <Arg>/static/</Arg> </Call> </Ref> <Set name="contextPath">/unset</Set> <!-- 设置相对于bundle的静态文件的基本资源 --> <Set name="baseResource"> <Ref refid="base"/> </Set> <Set name="handler"> <New class="org.eclipse.jetty.server.handler.ResourceHandler"> <Set name="welcomeFiles"> <Array type="String"> <Item>index.html</Item> </Array> </Set> <Set name="cacheControl">max-age=3600,public</Set> </New> </Set> </Configure>
29.2.6 部署服务做为webapp
为了部署而去监听MANIFEST 文件中定义的webapp 或ContextHandler ,Jetty OSGi容器也会监听已经注册的是org.eclipse.jetty.webapp.WebAppContext实例的OSGi服务。所以你可以编程创建一个WebAppContext,把它注册为一个服务,然后让Jetty发现并部署它。
这里有一个例子,部署了一个静态资源包,是一个org.osgi.framework.BundleActivator实例的WebAppContext:
包中的资源有:
META-INF/MANIFEST.MF
index.html
com/acme/osgi/Activator.class
MANIFEST.MF文件的内容为:
Bundle-Classpath: .
Bundle-Name: Jetty OSGi Test WebApp
DynamicImport-Package: org.eclipse.jetty.*;version="[9.0,10.0)"
Bundle-Activator: com.acme.osgi.Activator
Import-Package: org.eclipse.jetty.server.handler;version="[9.0,10)",
org.eclipse.jetty.webapp;version="[9.0,10)",
org.osgi.framework;version= "[1.5,2)",
org.osgi.service.cm;version="1.2.0",
org.osgi.service.packag eadmin;version="[1.2,2)",
org.osgi.service.startlevel;version="1.0.0",
org.osgi.service.url;version="1.0.0",
org.osgi.util.tracker;version= "1.3.0",
org.xml.sax,org.xml.sax.helpers
Bundle-SymbolicName: com.acme.testwebapp
主要的代码为:
public void start(BundleContext context) throws Exception { WebAppContext webapp = new WebAppContext(); Dictionary props = new Hashtable(); props.put("Jetty-WarResourcePath","."); props.put("contextPath","/acme"); context.registerService(ContextHandler.class.getName(),webapp,props); }
上面的安装信息足够Jetty来识别并部署一个WebAppContext 到/acme下。
就像上面的例子展示的那样,你可以使用OSGi服务属性来对Jetty进行额外的配置:
- Jetty-WarFolderPath (9.3及以后版本) 或者 Jetty-WarResourcePath::webapp静态资源root根目录。
- Web-ContextPath::部署webapp的路径。
- Jetty-defaultWebXmlFilePath::部署webapp包的webdefault.xml文件的路径。默认在OSGi容器内。
- Jetty-WebXmlFilePath::web.xml的路径,默认为WEB-INF/web.xml 。
- Jetty-extraClassPath::增加到webapp当前的类加载的类路径。
- Jetty-bundleInstall::用于覆盖安装包的基文件夹 - 常用于没有打包的OSGi框架。
- Require-TldBundle::额外的包含TLD的包。
- managedServerName::部署webapp bundle的服务实例名。如果没有指定,那么默认为 "defaultJettyServer"。
- Jetty-WarFragmentResourcePath::包含webapp静态资源的在web-bundle中的路径。这些路径将会追加到webapp的基础资源路径下。
- Jetty-WarPrependFragmentResourcePath::将要部署到webapp中的bundle路径列表。或者可能包含一个单独的Jetty context文件叫做"jetty-webapp-context.xml",在webapp 的bundle的META-INF文件夹下,它将会自动部署到webapp中。
29.2.7 部署服务做为JettyContextHandlers
同部署WebAppContexts相似,Jetty的OSGi容器可以侦测注册的ContextHandler的OSGi服务并确保它们被部署。ContextHandler可以在注册为一个OSGi服务前被完全配置好 - 在这种情况下,Jetty 的OSGi容器仅仅部署它 - 或者ContextHandler可以配特别的配置,这种情况下Jetty 的OSGi容器完全通过context的xml配置文件或者属性文件来配置。
这里有一个例子部署一个org.osgi.framework.BundleActivator实例的静态资源包,并注册为OSGi服务,传递定义在context的xml配置文件中的属性用于部署:
包内容如下:
META-INF/MANIFEST.MF
static/index.html
acme.xml
com/acme/osgi/Activator.class
com/acme/osgi/Activator$1.class
MANIFEST.MF文件内容如下:
Bundle-Classpath: .
Bundle-Name: Jetty OSGi Test Context
DynamicImport-Package: org.eclipse.jetty.*;version="[9.0,10.0)"
Bundle-Activator: com.acme.osgi.Activator
Import-Package: javax.servlet;version="2.6.0",
javax.servlet.resources;version="2.6.0",
org.eclipse.jetty.server.handler;version="[9.0,10)",
org.osgi.framework;version="[1.5,2)",
org.osgi.service.cm;version="1.2.0",
org.osgi.service.packageadmin;version="[1.2,2)",
org.osgi.service.startlevel;version="1.0.0.o",
org.osgi.service.url;version="1.0.0",
org.osgi.util.tracker;version="1.3.0",
org.xml.sax,org.xml.sax.helpers
Bundle-SymbolicName: com.acme.testcontext
主要的代码:
public void start(final BundleContext context) throws Exception { ContextHandler ch = new ContextHandler(); ch.addEventListener(new ServletContextListener () { @Override public void contextInitialized(ServletContextEvent sce) { System.err.println("Context is initialized"); } @Override public void contextDestroyed(ServletContextEvent sce) { System.err.println("Context is destroyed!"); } }); Dictionary props = new Hashtable(); props.put("Web-ContextPath","/acme"); props.put("Jetty-ContextFilePath", "acme.xml"); context.registerService(ContextHandler.class.getName(),ch,props); }
acme.xml配置文件的内容:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> <Configure class="org.eclipse.jetty.server.handler.ContextHandler"> <!-- 静态资源根路径,可以是文件系统或者安装包 --> <Call id="res" class="org.eclipse.jetty.util.resource.Resource" name="newResource"> <Arg><Property name="bundle.root"/></Arg> </Call> <Ref refid="res"> <Call id="base" name="addPath"> <Arg>/static/</Arg> </Call> </Ref> <Set name="contextPath">/unset</Set> <!-- 设置静态资源根路径到包内 --> <Set name="baseResource"> <Ref refid="base"/> </Set> <Set name="handler"> <New class="org.eclipse.jetty.server.handler.ResourceHandler"> <Set name="welcomeFiles"> <Array type="String"> <Item>index.html</Item> </Array> </Set> <Set name="cacheControl">max-age=3600,public</Set> </New> </Set> </Configure>
你也可以使用如下的OSGi服务的属性:
managedServerName
部署的webapp的Server实例的名称。如果没有指定,默认的Server实例名称为"defaultJettyServer"。
额外的可配置的在context 文件的属性
在Jetty的OSGi容器应用一个在Jetty-ContextFilePath属性下发现的context的xml文件时,它可以设置一些有用的属性在xml文件中:
Server
这个引用Jetty org.eclipse.jetty.server.Server实例,及即将应用context的xml文件部署的ContextHandler 。
bundle.root
这个引用org.eclipse.jetty.util.resource.Resource实例,代表着做为一个服务部署ContextHandler包的路径(通过Bundle.getLocation()来获得)。这个可以是一个在文件系统的文件夹如果OSGi容器可以自动解压包文件,或者是一个jar包路径,如果包文件不被解压。
在上面的例子中你可以看到这两个属性同时应用的场景在context的xml文件中。
29.2.8 对于OSGi服务平台的企业规范支持
Jetty OSGi容器对于 WebAppContexts 和 ContextHandlers 实现企业规范v4.2,在前面的章节已介绍通过包或者OSGi服务的形式进行部署。
Context属性
对于每一个WebAppContext或ContextHandler,下面的属性是必须的:
-
- osgi-bundleContext:此属性的值是代表与WebAppContext 或ContextHandler关联的代表包的BundleContext。
Service 属性
规范要求,每一个被Jetty OSGi容器部署的WebAppContext或ContextHandler,同时必须发布为OSGi服务(除非它已经是一个OSGi服务),下面的属性与服务有关:
-
- osgi.web.symbolicname:与WebAppContext 或ContextHandler关联的包的符号名称
- osgi.web.version:与WebAppContext 或ContextHandler关联的包的版本
- osgi.web.contextpath:WebAppContext 或ContextHandler的context路径
OSGi 事件
根据规范要求,下面的事件需要被公布:
-
- org/osgi/service/web/DEPLOYING:Jetty OSGi容器部署一个WebAppContext或者ContextHandler
- org/osgi/service/web/DEPLOYED:已经完成部署并成为一个service
- org/osgi/service/web/UNDEPLOYING:Jetty OSGi容器卸载一个WebAppContext或者ContextHandler
- org/osgi/service/web/UNDEPLOYED:Jetty OSGi容器卸载完成一个WebAppContext或者ContextHandler,已不再是一个service
- org/osgi/service/web/FAILED:Jetty OSGi容器部署一个WebAppContext或者ContextHandler失败
29.2.9 JSP的使用
29.2.9.1 安装
为了在你的webapp和应用包中使用JSP,你需要按照JSP和JSTL的jar包和它们的依赖包到你的OSGi容器中。一些你可以在Jetty程序包中发现,当然也有些需要从Maven仓库中下载。这里有一个需要的jar包的列表。
Table 29.2. JSP需要的jar包
Jar包 | 包名 | 路径 |
---|---|---|
The annotation jars |
||
org.mortbay.jasper:apache-el |
org.mortbay.jasper.apache-el |
Distribution lib/apache-jsp |
org.mortbay.jasper:apache-jsp |
org.mortbay.jasper.apache-jsp |
Distribution lib/apache-jsp |
org.eclipse.jetty:apache-jsp |
org.eclipse.jetty.apache-jsp |
Distribution lib/apache-jsp |
org.eclipse.jdt.core-3.8.2.v20130121.jar |
org.eclipse.jdt.core.compiler.batch |
Distribution lib/apache-jsp |
org.eclipse.jetty.osgi:jetty-osgi-boot-jsp |
org.eclipse.jetty.osgi.boot.jsp |
Maven central |
对于JSTL库,我们建议使用Glassfish的实现,它有一些简单的依赖如下:
Table 29.3. Glassfish JSTL需要的jar包
Jar包 | 包名 | 路径 |
---|---|---|
The jsp jars |
||
org.eclipse.jetty.orbit:javax.servlet.jsp.jstl-1.2.0.v201105211821.jar |
javax.servlet.jsp.jstl |
Distribution lib/jsp |
org.glassfish.web:javax.servlet.jsp.jstl-1.2.2.jar |
org.glassfish.web.javax.servlet.jsp.jstl |
Distribution lib/jsp |
当然,你也可以使用Apache的JSTL实现,当然也需要一些依赖:
Table 29.4. Apache JSTL需要的jar包
Jar包 | 包名 | 路径 |
---|---|---|
The jsp jars |
||
org.apache.taglibs:taglibs-standard-spec:jar:1.2.1 |
org.apache.taglibs.taglibs-standard-spec |
Distribution lib/apache-jstl |
org.apache.taglibs:taglibs-standard-spec:jar:1.2.1 |
org.apache.taglibs.standard-impl |
Distribution lib/apache-jstl |
org.apache.xalan 2.7.1 |
Try Eclipse Orbit |
|
org.apache.xml.serializer 2.7.1 |
Try Eclipse Orbit |
29.2.9.2 jetty-osgi-boot-jsp包
为了使用JSP你需要在OSGi容器中安装jetty-osgi-boot-jsp.jar。这个jar包可以在Maven仓库中心获取。它作为一个jetty-osgi-boot.jar延伸用于支持JSP。Jetty JSP OSGi容器可以支持所有webapp的JSTL tag库。如果你的web应用将不需要做任何修改即可使用。
然而,如果你想使用其他的taglibs,你要确保它们被安装到OSGi容器中,并且定义了系统属性或在webapp的MANIFEST头部声明。这是有必要的,因为OSGi容器的类加载模式与使用JSP容器非常不同,webapp的MANIFEST文件如果没有包含足够的信息的话,那么OSGi环境将不允许JSP容器来获取和解析.jsp文件中的TLDs引用。
首先我们看下面这个修改过的MANIFEST 例子,让你知道哪些是必须要的。这个例子使用了Spring servlet框架:
Bundle-SymbolicName: com.acme.sample
Bundle-Name: WebSample
Web-ContextPath: taglibs
Import-Bundle: org.springframework.web.servlet
Require-TldBundle: org.springframework.web.servlet
Bundle-Version: 1.0.0
Import-Package: org.eclipse.virgo.web.dm;version="[3.0.0,4.0.0)",org.s
pringframework.context.config;version="[2.5.6,4.0.0)",org.springframe
work.stereotype;version="[2.5.6,4.0.0)",org.springframework.web.bind.
annotation;version="[2.5.6,4.0.0)",org.springframework.web.context;ve
rsion="[2.5.6,4.0.0)",org.springframework.web.servlet;version="[2.5.6
,4.0.0)",org.springframework.web.servlet.view;version="[2.5.6,4.0.0)"
29.2.10 使用Annotations/ServletContainerInitializers
注解是Servlet 3.0和Servlet 3.1规范的重要组成部分。为了在OSGi Jetty容器中使用它们,你需要增加一些额外的依赖包到你的OSGi容器中:
Table 29.5. 注解的依赖包
Jar包 | 包名 | 路径 |
---|---|---|
org.ow2.asm:asm-5.0.1.jar |
org.objectweb.asm |
Maven central |
org.ow2.asm:asm-commons-5.0.1.jar |
org.objectweb.asm.commons |
Maven central |
org.ow2.asm:asm-tree-5.0.1.jar |
org.objectweb.asm.tree |
Maven central |
org.apache.aries:org.apache.aries.util-1.0.1.jar |
org.apache.aries.util |
Maven central |
org.apache.aries.spifly:org.apache.aries.spifly.dynamic.bundle-1.0.1.jar |
org.apache.aries.spifly.dynamic.bundle |
Maven central |
javax.annotation:javax.annotation-api-1.2.jar |
javax.annotation-api |
Maven central |
jta api version 1.1.1 (eg org.apache.geronimo.specs:geronimo-jta_1.1_spec-1.1.1.jar)* |
Maven central |
|
javax mail api version 1.4.1 (eg org.eclipse.jetty.orbit:javax.mail.glassfish-1.4.1.v201005082020.jar)* |
Maven central |
|
jetty-jndi |
org.eclipse.jetty.jndi |
Distribution lib/ |
jetty-plus |
org.eclipse.jetty.plus |
Distribution lib/ |
jetty-annotations |
org.eclipse.jetty.annotations |
Distribution lib/ |
+重要
如果你想使用注解,你需要部署这些依赖包。
+提示
你可以部署这些jar包的最后一个版本,然而特定的版本已知是正确的,经过测试并在OSGi环境下运行过。
即便你的web应用不使用注解,你也应该部署这些jar包,因为你的应用可能依赖Jetty的模块或者使用 javax.servlet.ServletContainerInitializer。这个接口需要注解的支持。
29.3 Weld
Weld可以用来增加servlet、Listeners 、Filters对CDI的支持。在Jetty9中配置是非常简单的。
1、启动时确保模块调用cdi
2、确保你的WEB-INF/web.xml包含如下内容:
<listener> <listener-class>org.jboss.weld.environment.servlet.Listener</listener-class> </listener> <resource-env-ref> <description>Object factory for the CDI Bean Manager</description> <resource-env-ref-name>BeanManager</resource-env-ref-name> <resource-env-ref-type>javax.enterprise.inject.spi.BeanManager</resource-env-ref-type> </resource-env-ref>
这当你启动Jetty的时候,你将看到以下输出:
2015-06-18 12:13:54.924:INFO::main: Logging initialized @485ms
2015-06-18 12:13:55.231:INFO:oejs.Server:main: jetty-9.3.1-SNAPSHOT
2015-06-18 12:13:55.264:INFO:oejdp.ScanningAppProvider:main: Deployment monitor [file:///tmp/cdi-demo/webapps/] at interval 1
2015-06-18 12:13:55.607:WARN:oeja.AnnotationConfiguration:main: ServletContainerInitializers: detected. Class hierarchy: empty
Jun 18, 2015 12:13:55 PM org.jboss.weld.environment.servlet.EnhancedListener onStartup
INFO: WELD-ENV-001008: Initialize Weld using ServletContainerInitializer
Jun 18, 2015 12:13:55 PM org.jboss.weld.bootstrap.WeldStartup <clinit>
INFO: WELD-000900: 2.2.9 (Final)
Jun 18, 2015 12:13:55 PM org.jboss.weld.environment.servlet.deployment.WebAppBeanArchiveScanner scan
WARN: WELD-ENV-001004: Found both WEB-INF/beans.xml and WEB-INF/classes/META-INF/beans.xml. It‘s not portable to use both locations at the same time. Weld is going to use file:/tmp/jetty-0.0.0.0-8080-cdi-webapp.war-_cdi-webapp-any-8161614308407422636.dir/webapp/WEB-INF/beans.xml.
Jun 18, 2015 12:13:55 PM org.jboss.weld.bootstrap.WeldStartup startContainer
INFO: WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously.
Jun 18, 2015 12:13:55 PM org.jboss.weld.interceptor.util.InterceptionTypeRegistry <clinit>
WARN: WELD-001700: Interceptor annotation class javax.ejb.PostActivate not found, interception based on it is not enabled
Jun 18, 2015 12:13:55 PM org.jboss.weld.interceptor.util.InterceptionTypeRegistry <clinit>
WARN: WELD-001700: Interceptor annotation class javax.ejb.PrePassivate not found, interception based on it is not enabled
Jun 18, 2015 12:13:56 PM org.jboss.weld.bootstrap.MissingDependenciesRegistry handleResourceLoadingException
Jun 18, 2015 12:13:56 PM org.jboss.weld.environment.servlet.WeldServletLifecycle findContainer
INFO: WELD-ENV-001002: Container detection skipped - custom container class loaded: org.jboss.weld.environment.jetty.JettyContainer.
Jun 18, 2015 12:13:56 PM org.jboss.weld.environment.jetty.JettyContainer initialize
INFO: WELD-ENV-001200: Jetty 7.2+ detected, CDI injection will be available in Servlets and Filters. Injection into Listeners should work on Jetty 9.1.1 and newer.
Jun 18, 2015 12:13:56 PM org.jboss.weld.environment.servlet.Listener contextInitialized
INFO: WELD-ENV-001006: org.jboss.weld.environment.servlet.EnhancedListener used for ServletContext notifications
Jun 18, 2015 12:13:56 PM org.jboss.weld.environment.servlet.EnhancedListener contextInitialized
INFO: WELD-ENV-001009: org.jboss.weld.environment.servlet.Listener used for ServletRequest and HttpSession notifications
2015-06-18 12:13:56.535:INFO:oejsh.ContextHandler:main: Started [email protected]{/cdi-webapp,file:///tmp/jetty-0.0.0.0-8080-cdi-webapp.war-_cdi-webapp-any-8161614308407422636.dir/webapp/,AVAILABLE}{/cdi-webapp.war}
2015-06-18 12:13:56.554:INFO:oejs.ServerConnector:main: Started [email protected]{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2015-06-18 12:13:56.587:INFO:oejus.SslContextFactory:main: x509={jetty.eclipse.org=jetty} wild={} alias=null for [email protected](file:///tmp/cdi-demo/etc/keystore,file:///tmp/cdi-demo/etc/keystore)
2015-06-18 12:13:56.821:INFO:oejs.ServerConnector:main: Started [email protected]{SSL,[ssl, http/1.1]}{0.0.0.0:8443}
2015-06-18 12:13:56.822:INFO:oejs.Server:main: Started @2383ms
29.4 Metro
Metro是Web Service的参考实现,你可以简单的使用Metro对你的web service和你的应用进行整合。步骤如下:
- 下载Metro 安装包,解压到硬盘,使用$metro.home来代替解压路径。
- 创建$jetty.home/lib/metro文件夹。
- 将$metro.home/lib下的jar包拷贝到$jetty.home/lib/metro下面。
- 编辑start.ini文件,在结尾处增加 OPTIONS=metro。
这就是所有的步骤,完成后即可把Jetty和Metro进行整合。下面是启动后的输出:
[2093] java -jar start.jar
2013-07-26 15:47:53.480:INFO:oejs.Server:main: jetty-9.0.4.v20130625
2013-07-26 15:47:53.549:INFO:oejdp.ScanningAppProvider:main: Deployment monitor [file:/home/user/jetty-distribution-9.3.11.v20160721/webapps/] at interval 1
Jul 26, 2013 3:47:53 PM com.sun.xml.ws.transport.http.servlet.WSServletContextListener contextInitialized
INFO: WSSERVLET12: JAX-WS context listener initializing
Jul 26, 2013 3:47:56 PM com.sun.xml.ws.server.MonitorBase createRoot
INFO: Metro monitoring rootname successfully set to: com.sun.metro:pp=/,type=WSEndpoint,name=/metro-async-AddNumbersService-AddNumbersImplPort
Jul 26, 2013 3:47:56 PM com.sun.xml.ws.transport.http.servlet.WSServletDelegate <init>
INFO: WSSERVLET14: JAX-WS servlet initializing
2013-07-26 15:47:56.800:INFO:oejsh.ContextHandler:main: Started [email protected]{/metro-async,file:/tmp/jetty-0.0.0.0-8080-metro-async.war-_metro-async-any-/webapp/,AVAILABLE}{/metro-async.war}
2013-07-26 15:47:56.853:INFO:oejs.ServerConnector:main: Started [email protected]{HTTP/1.1}{0.0.0.0:8080}
三十、Ant 和 Jetty
Ant Jetty插件是Jetty9 jetty-ant模块下的一部分。这个插件让通过Ant启动Jetty成为可能,并且将Jetty应用嵌入到你的构建过程中。它提供绝大部分Maven同样的功能。需要添加以下依赖:
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-ant</artifactId> </dependency>
30.1 项目前期准备
为了在Ant中运行你的Jetty,你需要Jetty安装包和jetty-ant的jar包。步骤如下:
- 下载Jetty安装包,并解压到本地。
- 获得the jetty-ant Jar。
- 在你的项目中新建jetty-lib/文件夹。
- 拷贝Jetty所有的jar包到jetty-lib文件夹下,拷贝时注意去掉层级关系,所有的jar包都要在jetty-lib路径下。
- 拷贝 jetty-ant.jar到jetty-lib文件夹下。
- 在项目中新建jetty-temp文件夹。
现在你已经准备好编辑或创建 build.xml 文件了。
30.2 build.xml 文件准
以一个空的build.xml文件中开始。
<project name="Jetty-Ant integration test" basedir="."> </project>
增加一个<taskdef>标签来引用所有可用的Jetty task
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> </project>
接下来你要为运行的Jetty增加新的任务
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run /> </target> </project>
这是需要的最少的配置。现在你可以启动Jetty,默认端口为8080。
30.3 通过Ant启动Jetty
在命令行中输入以下内容:
> ant jetty.run
30.4 配置Jetty容器
一系列的配置属性可以帮助你设置Jetty的运行环境,这样你的web应用将有你需要的所有资源:
端口和连接:
为了配置启动端口你需要定一个连接。首先你需要配置一个<typedef> 标签用于定义连接:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="connector" classname="org.eclipse.jetty.ant.types.Connector" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <connectors> <connector port="8090"/> </connectors> </jetty.run> </target> </project>
+技巧
你可以将端口设置为0,那么Jetty启动时将自动分配一个可用的端口。你可以通过访问系统属性jetty.ant.server.port和jetty.ant.server.host获得启动的信息。
登录服务
如果你的应用需要身份认证和登录服务,你可以在Jetty容器中进行配置。这里有一个例子用来展示如何配置一个org.eclipse.jetty.security.HashLoginService服务:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="hashLoginService" classname="org.eclipse.jetty.security.HashLoginService" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <loginServices> <hashLoginService name="Test Realm" config="${basedir}/realm.properties"/> </loginServices> </jetty.run> </target> </project>
请求日志
requestLog 选项运行你为Jetty实例指定一个请求日志。你可以使用org.eclipse.jetty.server.NCSARequestLog或者你自己的实现类:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run requestLog="com.acme.MyFancyRequestLog"> </jetty.run> </target> </project>
临时目录
你可以把一个文件夹配置为临时目录用于存储文件,例如编译后的jsp,通过tempDirectory 选项进行配置:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run tempDirectory="${basedir}/jetty-temp"> </jetty.run> </target> </project>
其他context 处理器
你有可能会在你程序运行的同时想要运行其他的context 处理器,你可以通过<contextHandlers>进行指定,在用之前首先定义一个<typedef>:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="contextHandlers" classname="org.eclipse.jetty.ant.types.ContextHandlers" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <contextHandlers> <contextHandler resourceBase="${basedir}/stuff" contextPath="/stuff"/> </contextHandlers> </jetty.run> </target> </project>
系统参数
为了方便你可以定义系统参数,通过使用 <systemProperties>元素:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <systemProperties> <systemProperty name="foo" value="bar"/> </systemProperties> </jetty.run> </target> </project>
Jetty xml文件
如果你有大量的文件用来配置容器,你可以通过xml来配置管理:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run jettyXml="${basedir}/jetty.xml"> </jetty.run> </target> </project>
扫描文件改动
最有用的模块是运行Jetty插件来自动扫描文件改动并重启服务。scanIntervalSeconds 选项控制扫描的频率,默认值为0不扫描,下面例子指定扫描为5秒:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run scanIntervalSeconds="5"> </jetty.run> </target> </project>
停止运行
在普通模式中(daemon="false"),<jetty.run> 任务将会一直运行直到输入Ctrl+C,可以通过配置<jetty.stop>元素来配置停止端口:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run stopPort="9999" stopKey="9999"> </jetty.run> </target> <target name="jetty.stop"> <jetty.stop stopPort="9999" stopKey="9999" stopWait="10"/> </target> </project>
为了在Ant中停止Jetty的运行,输入以下:
> ant jetty.stop
30.5 部署一个web应用
可以为org.eclipse.jetty.ant.AntWebAppContext类增加一个<typedef>标签,并配置一个webApp名字,然后增加一个<webApp>标签来描述你需要运行的web项目。下面的例子部署了一个web应用,应用在foo/文件夹下,context的Path为“/”:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="webApp" classname="org.eclipse.jetty.ant.AntWebAppContext" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <webApp war="${basedir}/foo" contextPath="/"/> </jetty.run> </target> </project>
30.5.1 部署一个war文件
不需要解压一个war文件,可以很方便的部署一个war文件:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="webApp" classname="org.eclipse.jetty.ant.AntWebAppContext" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <webApp war="${basedir}/foo.war" contextPath="/"/> </jetty.run> </target> </project>
30.5.2 部署多个web应用
你也可以同时部署多个web应用,配置如下:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="webApp" classname="org.eclipse.jetty.ant.AntWebAppContext" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <webApp war="${basedir}/foo.war" contextPath="/"/> <webApp war="${basedir}/other contextPath="/other"/> <webApp war="${basedir}/bar.war" contextPath="/bar"/> </jetty.run> </target> </project>
30.6 配置web应用
org.eclipse.jetty.ant.AntWebAppContext类是 org.eclipse.jetty.webapp.WebAppContext类的扩展,你可以通过增加同属性set方法来配置它(不需要设置或新增前缀)。
这里有一个例子,指定了web.xml(与AntWebAppContext.setDescriptor()功能相同)和web应用的临时目录(与 AntWebAppContext.setTempDirectory()功能相同)。
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="webApp" classname="org.eclipse.jetty.ant.AntWebAppContext" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <webApp descriptor="${basedir}/web.xml" tempDirectory="${basedir}/my-temp" war="${basedir}/foo" contextPath="/"/> </jetty.run> </target> </project>
其他额外的对AntWebAppContext 配置如下;
额外的classes和jar包
如果你的web应用的classes和jar包不单单放在WEB-INF下面,你可以使用<classes> 和 <jar>元素来配置它们,例子如下:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="webApp" classname="org.eclipse.jetty.ant.AntWebAppContext" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <webApp descriptor="${basedir}/web.xml" tempDirectory="${basedir}/my-temp" war="${basedir}/foo" contextPath="/"> <classes dir="${basedir}/classes"> <include name="**/*.class"/> <include name="**/*.properties"/> </classes> <lib dir="${basedir}/jars"> <include name="**/*.jar"/> <exclude name="**/*.dll"/> </lib> </webApp> </jetty.run> </target> </project>
context 属性
Jetty允许你对web应用的ServletContext 设置属性。你可以在context xml文件中进行配置,为了方便也可以在build文件中进行配置:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="webApp" classname="org.eclipse.jetty.ant.AntWebAppContext" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <webApp war="${basedir}/foo" contextPath="/"> <attributes> <attribute name="my.param" value="123"/> </attributes> </webApp> </jetty.run> </target> </project>
jetty-env.xml 文件
如果你使用一些新特性如JNDJ在你的应用程序中,你需要在WEB-INF/jetty-env.xml文件中配置资源。你需要使用jettyEnvXml属性来告诉Ant配置文件放在那里:
<project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="webApp" classname="org.eclipse.jetty.ant.AntWebAppContext" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <webApp war="${basedir}/foo" contextPath="/" jettyEnvXml="${basedir}/jetty-env.xml"> <attributes> </webApp> </jetty.run> </target> </project>
context Xml文件
您可能会喜欢或甚至需要做一些高级配置您的Web应用程序以外的蚂蚁构建文件。在这种情况下,您可以使用ant插件在部署之前应用到您的Web应用程序的标准上下文XML配置文件。要注意,上下文XML文件的设置覆盖了在生成文件中定义的属性和嵌套元素的设置。???
project name="Jetty-Ant integration test" basedir="."> <path id="jetty.plugin.classpath"> <fileset dir="jetty-lib" includes="*.jar"/> </path> <taskdef classpathref="jetty.plugin.classpath" resource="tasks.properties" loaderref="jetty.loader" /> <typedef name="webApp" classname="org.eclipse.jetty.ant.AntWebAppContext" classpathref="jetty.plugin.classpath" loaderref="jetty.loader" /> <target name="jetty.run"> <jetty.run> <webApp war="${basedir}/foo" contextPath="/" contextXml="${basedir}/jetty-env.xml"> <attributes> </webApp> </jetty.run> </target> </project>
+附言
Jetty文档的目录详见:http://www.cnblogs.com/yiwangzhibujian/p/5832294.html
Jetty第一章翻译详见:http://www.cnblogs.com/yiwangzhibujian/p/5832597.html
Jetty第四章(21-22)详见:http://www.cnblogs.com/yiwangzhibujian/p/5845623.html
Jetty第四章(23)详见:http://www.cnblogs.com/yiwangzhibujian/p/5856857.html
Jetty第四章(24-27)详见:http://www.cnblogs.com/yiwangzhibujian/p/5858544.html
这次翻译的是第四部分的第28到30小节。翻译的过程中觉得Jetty的参考文档写的并不是特别好,偏向于理论,而且还有很多地方等待完善,虽然也有很多示例代码,但是都是一些代码片段,不太适合入门使用,所以我也打算在翻译快结束的时候,根据自己的理解写几篇用于实践的项目例子。