很多 Java web 应用和服务,包括开源的和商业化的(比如 Alfresco, iRise, Confluence等),都倾向于将 Apache Tomcat Servlet 引擎整个嵌入到他们的分发包中。Atlatisan公司甚至只支持他们自己提供的嵌入式Tomcat 包,不再提供 WAR/EAR 形式的分发包。这些安装包包含了整个 Tomcat 引擎和配置文件,看起来确实有点大材小用。在大多数配置中,默认的配置文件甚至从来不会变动。真的有办法可以在代码中启动 Tomcat 并且只需要 tomcat 的 jar 文件作为依赖么?在下面的教程中,我们将会对 Jetty (Jetty 是一个为此目的而设计的一种嵌入式 servlet 引擎)进行测试,同时还会展示如何将 Jetty 迁移到 Tomcat 。
我开始研究嵌入式 Tomcat 是因为 BigSense 项目,该项目是一个开源 web 服务,用于模拟传感器网络。我的目的是可以将其作为一个标准的 Linux 软件包进行分发,这样就可以作为一个服务启动,而不用依赖于 Tomcat 软件包。下面的例子使用的是 Scala 语言,BigSense 项目用的也是这种开发语言,但是你也可以轻松地将所有源码和概念转换为 Java 语言。
首先,创建一个 trait(类似于 Java 中的接口),里面包含两个简单的功能,用于启动和停止 web 服务器。端口号可以从配置文件中获取。这是我的实现中唯一可配置的了,但是你也可以对 context path 添加配置。
package io.bigsense.server /** * Created by sumit on 4/28/14. */ trait ServerTrait { lazy val httpPort = try { BigSenseServer.config.options("httpPort").toInt } catch { case e: NumberFormatException => Exit.invalidHttpPort 0 //makes compiler happy } def startServer() def stopServer() }
下面是我使用 Scala 对 Jetty 的实现。大部分是直接从 Jetty 的官方文档中摘出来的。所有的静态资源(图片,CSS 和 javascript 脚本)都被直接打包到了 jar 文件中,可以作为类路径的资源进行访问。如果使用构建工具,如 SBT,Gradel 或者 Maven,可以将这些文件放到项目的 src/main/resources 目录下。Jetty 的 WebAppContext 允许调用 setResourceBase 来使用项目的静态资源。这个例子还展示了如何使用给定的 Context 路径来添加一个 Servlet (在这个例子中,只有一个 servlet,匹配根目录,名字为 MasterServlet)。还可以看到一个 EventListener 的例子。web.xml 中的大部分标准配置在 Jetty 中都可以使用代码进行设置。
package io.bigsense.server import org.Eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.handler.DefaultHandler import org.eclipse.jetty.server.handler.HandlerCollection import org.eclipse.jetty.servlet.ServletContextHandler import org.eclipse.jetty.webapp.WebAppContext import io.bigsense.servlet.{DBUpdateListener, MasterServlet} class JettyServer extends ServerTrait { val server = new Server() val connector = new ServerConnector(server) connector.setPort(httpPort) server.setConnectors(Array(connector)) val context = new ServletContextHandler() context.setContextPath(BigSenseServer.webRoot) context.addServlet(new MasterServlet().getClass, "/*") context.addEventListener(new DBUpdateListener()) val fileContext = new WebAppContext() fileContext.setContextPath(BigSenseServer.contentRoot) fileContext.setResourceBase(BigSenseServer.getClass.getResource("/io/bigsense/web").toExternalForm) val handlers = new HandlerCollection() handlers.setHandlers(Array( fileContext, context, new DefaultHandler())) server.setHandler(handlers) override def startServer() { server.start server.join } override def stopServer() { server.stop } }
Tomcat 的实现比较复杂。也没有足够的关于使用嵌入式 Tomcat 和配置代码的文档。在下面的例子中,我创建了一个 org.apache.catalina.startup.Tomcat 实例。当向 Tomcat 中添加 Servlets 时,因为一些原因需要指定它的工作目录。我这里是以一种平台依赖的方式使用系统属性 java.io.tmpdir 来获取一个临时文件夹。(注意:在本地环境下运行的时候会产生一个空的 ./tomcat.8080 目录)。虽然我不清楚怎样添加一个事件监听器,但却意识到了监听器甚至不会使用它持有的 context,因此只是手动调用它而没有使用context。最后,我在 Tomcat 的文档中没有找到类似 Jetty 中的 setResourceBase 方法来获取静态资源,因此只能创建一个自己的 StaticContentServlet,接下来将会看到。
package io.bigsense.server import org.apache.catalina.startup.Tomcat import io.bigsense.servlet.{StaticContentServlet, DBUpdateListener, MasterServlet} import java.io.File /** * Created by sumit on 4/28/14. */ class TomcatServer extends ServerTrait { val tomcat = new Tomcat() tomcat.setPort(httpPort) val tmp = new File(System.getProperty("java.io.tmpdir")) val ctx = tomcat.addContext(BigSenseServer.webRoot,tmp.getAbsolutePath) Tomcat.addServlet(ctx,"bigsense",new MasterServlet()) ctx.addServletMapping("/*","bigsense") new DBUpdateListener().contextInitialized(null) val cCtx = tomcat.addContext(BigSenseServer.contentRoot,tmp.getAbsolutePath) Tomcat.addServlet(cCtx,"static",new StaticContentServlet) cCtx.addServletMapping("/*","static") override def startServer() = { tomcat.start() tomcat.getServer().await() } override def stopServer() = tomcat.stop }
这个获取静态资源的 servlet 只有一些基本功能。只是简单地找到类路径下的资源并返回。难点是正确地设置 Mime-Type。我尝试使用 javax.activation.FileTypeMap 基于扩展名来获取准确得 mime 类型,但是经常会得到错误的结果。因此,对于项目中已知静态文件的 mime 类型通过硬编码进行了实现。
package io.bigsense.servlet import javax.servlet.http.{HttpServletRequest, HttpServletResponse, HttpServlet} import javax.activation.FileTypeMap import com.google.common.io.ByteStreams import java.net.URLConnection import org.slf4j.LoggerFactory /** * needed to serve static resources for Tomcat. Not needed for Jetty * * Created by cassius on 29/04/14. */ class StaticContentServlet extends HttpServlet { val log = LoggerFactory.getLogger(this.getClass()) override def doGet(req : HttpServletRequest, resp : HttpServletResponse) { val resourcePath = "/io/bigsense/web/%s".format(req.getPathInfo.stripPrefix("/")) val resource = getClass.getResource(resourcePath) log.debug("Requesting static resource %s".format(resourcePath)) if(resource == null) { resp.setContentType("text/plain") resp.setStatus(HttpServletResponse.SC_NOT_FOUND) resp.getWriter.write("Not Found") resp.getWriter.close } else { resp.setContentType( getContentType(req.getPathInfo) ) ByteStreams.copy(getClass.getResourceAsStream(resourcePath), resp.getOutputStream) resp.getOutputStream.close } } def getContentType(fileName : String) = { fileName match { case x if x endsWith "js" => "application/javascript" case x if x endsWith "css" => "text/css" case _ => Option[String](URLConnection.guessContentTypeFromName(fileName)) match { case Some(s) => s case None => FileTypeMap.getDefaultFileTypeMap().getContentType(fileName) } } } }
依赖相当简单,只需添加需要的 Tomcat 和 Jetty 包就可以了。下面展示了在一个 buiuld.sbt 文件中的依赖,但这样的配置只能用于 Maven,Gradel或者Ivy。检查一下,然后确认你使用的是最近版本的 Jetty 以及/或者 Tomcat,因为它们可能会有变化。
... "org.eclipse.jetty" % "jetty-server" % "9.1.4.v20140401", "org.eclipse.jetty" % "jetty-servlet" % "9.1.4.v20140401", "org.eclipse.jetty" % "jetty-webapp" % "9.1.4.v20140401", "org.rogach" %% "scallop" % "0.9.5", "org.apache.tomcat.embed" % "tomcat-embed-core" % "7.0.53" , "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % "7.0.53" , "org.apache.tomcat.embed" % "tomcat-embed-jasper" % "7.0.53" , ...
从这里开始,创建一个 main 函数,然后启动你的服务器将会变得非常简单。我使用了一个名为 sbt-native-packager 的插件来创建 deb 和 rpm 文件,其中会用到相关的初始化脚本或 SystemD 服务文件。这么做允许你像安装一个标准的 Linux 安装包一样来安装 BigSense,作为标准服务独立于系统的 Tomcat,并且不需要多余的 war 或 ear 文件。
当然,这样做也有缺点。比如你有很多 web 应用都按这种方式进行部署,对于每个应用来讲就是启动一个完整 Tomcat 和 JVM 实例。即使 Tomcat 相对来讲(和 JBoss 或者 WebSphere 比起来)是轻量级的,仍然是比较重的,会耗费相当多的资源,这一点在虚拟机上尤为明显。
如果在你的空间中有很多 apps,使用系统中的 Tomcat 软件包,然后使用诸如 Fabric 之类的部署系统来维护、更新和部署你的 web 应用可能会更好点。如果你需要将应用打包给第三方,则使用嵌入式 Tomcat 是一个更好的解决方案。然而,当发现安全漏洞时,对于更新安装包来讲你就要小心了。对于嵌入式方法来讲,添加一个类似SSL的东西是比较复杂的,更好的解决方案是将类似 HAProxy 或者 Nginx 的软件作为前端代理来处理用户的 SSL 请求。
如果你是从零开始开发一个应用,应该考虑一下避免完全使用 Servlet 模式。在 JVM上,有很多为 web 服务和应用设计的异步框架,比如 Spray 和 Netty ,远超这个设计于 1995 年的 HTTP Servlet API 。
如果你由于软件分发打算学习如何将 Tomcat 嵌入到 web 应用中,希望这篇教程可以帮到你。请时刻牢记以下这点,对于你给出的 jar 包和平台版本的 API 的变化,这些例子可能需要做一些调整来适配这些变化。虽然我只是讲到了 Tomcat 和 Jetty , 其实还有其它的嵌入式 Servlet 引擎,可以用相似的方式实现,甚至对于更新的非 Servlet 引擎(比如 Spray 和 Netty)来讲都有对 Servlet 的包装,这样你就可以在一些比较老的 web 应用上使用它们了。
原文链接: penguindreams 翻译:ImportNew.com -飘扬叶
译文链接:[]