用于提供静态内容的Servlet
我在两个不同的容器(Tomcat和Jetty)上部署Web应用程序,但是它们的默认servlet用于处理我想要使用的URL结构( 详细信息 )。
因此,我正在寻找在webapp中包含一个小的servlet来提供自己的静态内容(图像,CSS等)。 该servlet应该具有以下属性:
- 没有外部依赖性
- 简单可靠
- 支持
If-Modified-Since
标题(即自定义getLastModified
方法) - (可选)支持gzip编码,etags,…
这样的servlet可用吗? 我能find的最接近的是Servlet书中的例子4-10 。
更新:我想使用的URL结构 – 如果您想知道的话 – 很简单:
<servlet-mapping> <servlet-name>main</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/static/*</url-pattern> </servlet-mapping>
所以所有的请求都应该传递给主Servlet,除非它们是static
path。 问题是,Tomcat的默认servlet没有考虑到ServletPath(所以它查找主文件夹中的静态文件),而Jetty的(所以它看起来在static
文件夹)。
我想出了一个稍微不同的解决scheme。 这有点黑客,但这里是映射:
<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.jpg</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.png</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.css</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>myAppServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
这基本上只是将所有内容文件通过扩展映射到默认的servlet,而其他一切都映射到“myAppServlet”。
它可以在Jetty和Tomcat中使用。
在这种情况下,不需要完全自定义实现默认的servlet,可以使用这个简单的servlet将请求包装到容器的实现中:
package com.example; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class DefaultWrapperServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { RequestDispatcher rd = getServletContext().getNamedDispatcher("default"); HttpServletRequest wrapped = new HttpServletRequestWrapper(req) { public String getServletPath() { return ""; } }; rd.forward(wrapped, resp); } }
我用FileServlet获得了很好的结果,因为它支持几乎所有的HTTP(etags,chunking等)。
静态资源servlet的抽象模板
部分基于这个 2007年的博客 ,这里是一个现代化,高度可重用的抽象模板,适用于caching, ETag
, If-None-Match
和If-Modified-Since
(但没有Gzip和Range支持;只是为了保持它简单; Gzip可以通过filter或通过容器configuration完成)。
public abstract class StaticResourceServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1); private static final String ETAG_HEADER = "W/\"%s-%s\""; private static final String CONTENT_DISPOSITION_HEADER = "inline;filename=\"%1$s\"; filename*=UTF-8''%1$s"; public static final long DEFAULT_EXPIRE_TIME_IN_MILLIS = TimeUnit.DAYS.toMillis(30); public static final int DEFAULT_STREAM_BUFFER_SIZE = 102400; @Override protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException ,IOException { doRequest(request, response, true); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doRequest(request, response, false); } private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean head) throws IOException { response.reset(); StaticResource resource; try { resource = getStaticResource(request); } catch (IllegalArgumentException e) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } if (resource == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } String fileName = URLEncoder.encode(resource.getFileName(), StandardCharsets.UTF_8.name()); boolean notModified = setCacheHeaders(request, response, fileName, resource.getLastModified()); if (notModified) { response.sendError(HttpServletResponse.SC_NOT_MODIFIED); return; } setContentHeaders(response, fileName, resource.getContentLength()); if (head) { return; } writeContent(response, resource); } /** * Returns the static resource associated with the given HTTP servlet request. This returns <code>null</code> when * the resource does actually not exist. The servlet will then return a HTTP 404 error. * @param request The involved HTTP servlet request. * @return The static resource associated with the given HTTP servlet request. * @throws IllegalArgumentException When the request is mangled in such way that it's not recognizable as a valid * static resource request. The servlet will then return a HTTP 400 error. */ protected abstract StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException; private boolean setCacheHeaders(HttpServletRequest request, HttpServletResponse response, String fileName, long lastModified) { String eTag = String.format(ETAG_HEADER, fileName, lastModified); response.setHeader("ETag", eTag); response.setDateHeader("Last-Modified", lastModified); response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME_IN_MILLIS); return notModified(request, eTag, lastModified); } private boolean notModified(HttpServletRequest request, String eTag, long lastModified) { String ifNoneMatch = request.getHeader("If-None-Match"); if (ifNoneMatch != null) { String[] matches = ifNoneMatch.split("\\s*,\\s*"); Arrays.sort(matches); return (Arrays.binarySearch(matches, eTag) > -1 || Arrays.binarySearch(matches, "*") > -1); } else { long ifModifiedSince = request.getDateHeader("If-Modified-Since"); return (ifModifiedSince + ONE_SECOND_IN_MILLIS > lastModified); // That second is because the header is in seconds, not millis. } } private void setContentHeaders(HttpServletResponse response, String fileName, long contentLength) { response.setHeader("Content-Type", getServletContext().getMimeType(fileName)); response.setHeader("Content-Disposition", String.format(CONTENT_DISPOSITION_HEADER, fileName)); if (contentLength != -1) { response.setHeader("Content-Length", String.valueOf(contentLength)); } } private void writeContent(HttpServletResponse response, StaticResource resource) throws IOException { try ( ReadableByteChannel inputChannel = Channels.newChannel(resource.getInputStream()); WritableByteChannel outputChannel = Channels.newChannel(response.getOutputStream()); ) { ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE); long size = 0; while (inputChannel.read(buffer) != -1) { buffer.flip(); size += outputChannel.write(buffer); buffer.clear(); } if (resource.getContentLength() == -1 && !response.isCommitted()) { response.setHeader("Content-Length", String.valueOf(size)); } } } }
与下面的代表静态资源的接口一起使用。
interface StaticResource { /** * Returns the file name of the resource. This must be unique across all static resources. If any, the file * extension will be used to determine the content type being set. If the container doesn't recognize the * extension, then you can always register it as <code><mime-type></code> in <code>web.xml</code>. * @return The file name of the resource. */ public String getFileName(); /** * Returns the last modified timestamp of the resource in milliseconds. * @return The last modified timestamp of the resource in milliseconds. */ public long getLastModified(); /** * Returns the content length of the resource. This returns <code>-1</code> if the content length is unknown. * In that case, the container will automatically switch to chunked encoding if the response is already * committed after streaming. The file download progress may be unknown. * @return The content length of the resource. */ public long getContentLength(); /** * Returns the input stream with the content of the resource. This method will be called only once by the * servlet, and only when the resource actually needs to be streamed, so lazy loading is not necessary. * @return The input stream with the content of the resource. * @throws IOException When something fails at I/O level. */ public InputStream getInputStream() throws IOException; }
您只需从给定的抽象servlet进行扩展,然后根据javadoc实现getStaticResource()
方法即可。
从文件系统服务的具体示例:
下面是一个具体的例子,它通过一个像/files/foo.ext
这样的URL从本地磁盘文件系统中提供:
@WebServlet("/files/*") public class FileSystemResourceServlet extends StaticResourceServlet { private File folder; @Override public void init() throws ServletException { folder = new File("/path/to/the/folder"); } @Override protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException { String pathInfo = request.getPathInfo(); if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) { throw new IllegalArgumentException(); } String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name()); final File file = new File(folder, name); return !file.exists() ? null : new StaticResource() { @Override public long getLastModified() { return file.lastModified(); } @Override public InputStream getInputStream() throws IOException { return new FileInputStream(file); } @Override public String getFileName() { return file.getName(); } @Override public long getContentLength() { return file.length(); } }; } }
从数据库服务的具体示例:
下面是一个具体的例子,它通过一个类似于/files/foo.ext
的URL通过一个EJB服务调用从数据库提供服务,该服务调用返回具有byte[] content
属性的实体:
@WebServlet("/files/*") public class YourEntityResourceServlet extends StaticResourceServlet { @EJB private YourEntityService yourEntityService; @Override protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException { String pathInfo = request.getPathInfo(); if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) { throw new IllegalArgumentException(); } String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name()); final YourEntity yourEntity = yourEntityService.getByName(name); return (yourEntity == null) ? null : new StaticResource() { @Override public long getLastModified() { return yourEntity.getLastModified(); } @Override public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(yourEntityService.getContentById(yourEntity.getId())); } @Override public String getFileName() { return yourEntity.getName(); } @Override public long getContentLength() { return yourEntity.getContentLength(); } }; } }
我结束了我自己的StaticServlet
。 它支持If-Modified-Since
,gzip编码,它也应该能够从war文件中提供静态文件。 这不是非常困难的代码,但它也不是完全微不足道的。
代码是可用的: StaticServlet.java 。 随意评论。
更新: Khurram询问在ServletUtils
中引用的ServletUtils
类。 这只是一个辅助方法的类,我用于我的项目。 您需要的唯一方法是coalesce
(与SQL函数COALESCE
相同)。 这是代码:
public static <T> T coalesce(T...ts) { for(T t: ts) if(t != null) return t; return null; }
我遇到了同样的问题,通过使用Tomcat代码库中的“默认servlet”的代码解决了这个问题。
http://svn.apache.org/repos/asf/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java
DefaultServlet是在Tomcat中提供静态资源(jpg,html,css,gif等)的servlet。
这个servlet非常高效,并且有一些你在上面定义的属性。
我认为这个源代码是启动和删除不需要的function或依赖的好方法。
- org.apache.naming.resources包的引用可以被删除,或者replace为java.io.File代码。
- 对org.apache.catalina.util包的引用只是可用的方法/类,可以在源代码中复制。
- org.apache.catalina.Globals类的引用可以内联或者删除。
从上面的示例信息来看,我认为整篇文章是基于Tomcat 6.0.29及更早版本中的一个错误行为。 请参阅https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 。 升级到Tomcat 6.0.30和(Tomcat | Jetty)之间的行为应该合并。
尝试这个
<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> <url-pattern>*.css</url-pattern> <url-pattern>*.ico</url-pattern> <url-pattern>*.png</url-pattern> <url-pattern>*.jpg</url-pattern> <url-pattern>*.htc</url-pattern> <url-pattern>*.gif</url-pattern> </servlet-mapping>
编辑:这只适用于servlet 2.5规范和以上。
我在网上find了关于一些解决方法的很好的教程。 这是简单而高效的,我用它在几个项目中使用REST的URL样式方法:
http://www.kuligowski.pl/java/rest-style-urls-and-url-mapping-for-static-content-apache-tomcat,5
我通过扩展了Tomcat的DefaultServlet ( src )并覆盖了getRelativePath()方法来实现这一点。
package com.example; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import org.apache.catalina.servlets.DefaultServlet; public class StaticServlet extends DefaultServlet { protected String pathPrefix = "/static"; public void init(ServletConfig config) throws ServletException { super.init(config); if (config.getInitParameter("pathPrefix") != null) { pathPrefix = config.getInitParameter("pathPrefix"); } } protected String getRelativePath(HttpServletRequest req) { return pathPrefix + super.getRelativePath(req); } }
…这里是我的servlet映射
<servlet> <servlet-name>StaticServlet</servlet-name> <servlet-class>com.example.StaticServlet</servlet-class> <init-param> <param-name>pathPrefix</param-name> <param-value>/static</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>StaticServlet</servlet-name> <url-pattern>/static/*</url-pattern> </servlet-mapping>
为了满足来自Spring应用程序以及/favicon.ico的所有请求以及来自/ WEB-INF / jsp / *的JSP文件,Spring的AbstractUrlBasedView将请求您重新映射jsp servlet和默认servlet:
<servlet> <servlet-name>springapp</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>/WEB-INF/jsp/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/favicon.ico</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>springapp</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>
我们不能依赖jsp servlet的标准映射上的* .jsp url-pattern,因为在检查任何扩展映射之前,path模式'/ *'是匹配的。 将jsp servlet映射到更深的文件夹意味着它首先被匹配。 匹配'/favicon.ico'恰好发生在path模式匹配之前。 更深的path匹配将起作用,或完全匹配,但没有扩展匹配可以使其超过“/ *”path匹配。 将'/'映射到默认的servlet似乎不起作用。 你会认为确切的'/'会在springapp上击败'/ *'path模式。
上述filter解决scheme不适用于来自应用程序的转发/包含的JSP请求。 为了使它工作,我不得不直接应用filter到springapp,在这一点上,url-pattern匹配是无用的,因为去应用程序的所有请求也都去它的filter。 所以我在filter中添加了模式匹配,然后学习了“jsp”servlet,发现它并不像默认的servlet那样删除path前缀。 这解决了我的问题,这是不完全一样的,但共同的足够。
使用org.mortbay.jetty.handler.ContextHandler。 你不需要像StaticServlet这样的附加组件。
在docker的家中,
$ cd上下文
$ cp javadoc.xml static.xml
$ vi static.xml
…
<Configure class="org.mortbay.jetty.handler.ContextHandler"> <Set name="contextPath">/static</Set> <Set name="resourceBase"><SystemProperty name="jetty.home" default="."/>/static/</Set> <Set name="handler"> <New class="org.mortbay.jetty.handler.ResourceHandler"> <Set name="cacheControl">max-age=3600,public</Set> </New> </Set> </Configure>
使用您的URL前缀设置contextPath的值,并将resourceBase的值设置为静态内容的文件path。
它为我工作。
请参阅JSOS中的StaticFile: http : //www.servletsuite.com/servlets/staticfile.htm
检查Tomcat 8.x:静态资源工作确定是否将根servlet映射到“”。 对于servlet 3.x,可以通过@WebServlet("")