【问题标题】:Servlet for serving static content用于提供静态内容的 Servlet
【发布时间】:2010-09-13 00:31:13
【问题描述】:

我在两个不同的容器(Tomcat 和 Jetty)上部署了一个 webapp,但是它们用于提供静态内容的默认 servlet 有不同的方式来处理我想要使用的 URL 结构 (details)。

因此,我希望在 webapp 中包含一个小型 servlet,以提供其自己的静态内容(图像、CSS 等)。 servlet 应具有以下属性:

这样的 servlet 在某处可用吗?我能找到的最接近的是 servlet book 中的 example 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 路径的。问题是 Tomcat 的默认 servlet 没有考虑 ServletPath(因此它会在主文件夹中查找静态文件),而 Jetty 会(因此它会在 static 文件夹中查找)。

【问题讨论】:

  • 您能否详细说明您要使用的“URL 结构”?根据链接的示例 4-10 自行滚动似乎是一项微不足道的工作。我自己做过很多次......
  • 我编辑了我的问题以详细说明 URL 结构。是的,我最终推出了自己的 servlet。请参阅下面的答案。
  • 为什么不用网络服务器来处理静态内容?
  • @Stephen:因为 Tomcat/Jetty 前面并不总是有 Apache。并且避免了单独配置的麻烦。但你是对的,我可以考虑这个选项。
  • 我就是不明白,你为什么不使用这样的映射 default/-pattern> 提供静态内容

标签: java jsp servlets jakarta-ee


【解决方案1】:

我遇到了同样的问题,我使用 Tomcat 代码库中的“默认 servlet”代码解决了这个问题。

https://github.com/apache/tomcat/blob/master/java/org/apache/catalina/servlets/DefaultServlet.java

DefaultServlet 是为 Tomcat 中的静态资源(jpg、html、css、gif 等)提供服务的 servlet。

这个 servlet 非常高效,并且具有您在上面定义的一些属性。

我认为此源代码是启动和删除您不需要的功能或依赖项的好方法。

  • 可以删除对 org.apache.naming.resources 包的引用或将其替换为 java.io.File 代码。
  • 对 org.apache.catalina.util 包的引用可能只是可以在源代码中复制的实用程序方法/类。
  • 可以内联或删除对 org.apache.catalina.Globals 类的引用。

【讨论】:

  • 它似乎依赖于来自org.apache.* 的很多东西。如何将它与 Jetty 一起使用?
  • 你说得对,这个版本对 Tomcat 有太多依赖(它还支持很多你可能不想要的东西。我会编辑我的答案。
【解决方案2】:

我最终推出了自己的StaticServlet。它支持If-Modified-Since、gzip 编码,它也应该能够提供来自战争文件的静态文件。这不是很困难的代码,但也不是完全微不足道的。

代码可用:StaticServlet.java。欢迎发表评论。

更新: Khurram 询问StaticServlet 中引用的ServletUtils 类。它只是一个带有辅助方法的类,我用于我的项目。您需要的唯一方法是coalesce(与SQL 函数COALESCE 相同)。这是代码:

public static <T> T coalesce(T...ts) {
    for(T t: ts)
        if(t != null)
            return t;
    return null;
}

【讨论】:

  • 不要命名你的内部类错误。这可能会导致混淆,因为您可能会将其误认为 java.lang.Error 另外,您的 web.xml 是否相同?
  • 感谢您的错误警告。 web.xml 相同,“default”替换为 StaticServlet 的名称。
  • 至于coalesce方法,可以用commons-lang StringUtils.defaultString(String, String)替换(在Servlet类内部)
  • transferStreams()方法也可以换成Files.copy(is ,os);
  • 为什么这种方法如此受欢迎?为什么人们要像这样重新实现静态文件服务器?有很多安全漏洞等着被发现,还有很多真正的静态文件服务器的功能没有实现。
【解决方案3】:

使用 org.mortbay.jetty.handler.ContextHandler。您不需要像 StaticServlet 这样的额外组件。

在码头的家,

$ cd 上下文

$ cp javadoc.xml 静态.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>

将 contextPath 的值设置为您的 URL 前缀,并将 resourceBase 的值设置为静态内容的文件路径。

它对我有用。

【讨论】:

    【解决方案4】:

    在这种情况下,不需要完全自定义实现默认 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);
        }
    }
    

    【讨论】:

    【解决方案5】:

    查看 JSOS 中的静态文件:http://www.servletsuite.com/servlets/staticfile.htm

    【讨论】:

      【解决方案6】:

      我在网上找到了关于一些解决方法的很棒的教程。它简单高效,我在几个项目中使用了 REST urls 样式方法:

      http://www.kuligowski.pl/java/rest-style-urls-and-url-mapping-for-static-content-apache-tomcat,5

      【讨论】:

        【解决方案7】:

        我通过扩展 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>  
        

        【讨论】:

          【解决方案8】:

          FileServlet 取得了不错的效果,因为它支持几乎所有的 HTTP(etag、分块等)。

          【讨论】:

          • 谢谢!数小时的失败尝试和错误的答案,这解决了我的问题
          • 虽然为了从应用程序外部的文件夹中提供内容(我使用它从磁盘中提供文件夹,比如 C:\resources),但我修改了这一行:this.basePath = getServletContext( ).getRealPath(getInitParameter("basePath"));并将其替换为:this.basePath = getInitParameter("basePath");
          【解决方案9】:

          要为 Spring 应用程序以及 /favicon.ico 和 /WEB-INF/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 模式,因为在检查任何扩展映射之前,路径模式 '/*' 已匹配。将 jsp servlet 映射到更深的文件夹意味着它首先匹配。匹配 '/favicon.ico' 恰好发生在路径模式匹配之前。更深的路径匹配或完全匹配将起作用,但没有扩展匹配可以使其超过 '/*' 路径匹配。将“/”映射到默认 servlet 似乎不起作用。你会认为确切的 '/' 会击败 springapp 上的 '/*' 路径模式。

          上述过滤器解决方案不适用于来自应用程序的转发/包含 JSP 请求。为了让它工作,我必须直接将过滤器应用到 springapp,此时 url 模式匹配是无用的,因为进入应用程序的所有请求也会进入它的过滤器。因此,我向过滤器添加了模式匹配,然后了解了“jsp”servlet,发现它不会像默认 servlet 那样删除路径前缀。这解决了我的问题,这并不完全相同,但很常见。

          【讨论】:

            【解决方案10】:

            我想出了一个稍微不同的解决方案。这有点hack-ish,但这是映射:

            <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。

            【讨论】:

            • 实际上你可以在servelet-mapping中添加多个url-pattern标签;)
            • Servlet 2.5 和更新版本支持 servlet-mapping 中的多个 url-pattern 标签
            • 请注意索引文件 (index.html),因为它们可能优先于您的 servlet。
            • 我认为使用*.sth 是个坏主意。如果有人会得到 url example.com/index.jsp?g=.sth 他会得到 jsp 文件的来源。还是我错了? (我是 Java EE 新手)我通常使用 url 模式 /css/* 等。
            【解决方案11】:

            从上面的示例信息来看,我认为整篇文章都是基于 Tomcat 6.0.29 及更早版本中的错误行为。见https://issues.apache.org/bugzilla/show_bug.cgi?id=50026。升级到 Tomcat 6.0.30 和 (Tomcat|Jetty) 之间的行为应该合并。

            【讨论】:

            • 这也是我对svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/的理解。终于,在 3 年前标记了这个 WONTFIX 之后!
            【解决方案12】:

            试试这个

            <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 规范及更高版本有效。

            【讨论】:

            • 看来这不是一个有效的配置。
            【解决方案13】:

            静态资源 servlet 的抽象模板

            部分基于 2007 年的 this blog,这是一个现代化且高度可重用的抽象模板,适用于正确处理缓存的 servlet,ETagIf-None-MatchIf-Modified-Since(但不支持 Gzip 和 Range;只是为了保持简单;Gzip 可以通过过滤器或容器配置来完成。

            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>&lt;mime-type&gt;</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, Paths.get(name).getFileName().toString());
            
                    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();
                        }
                    };
                }
            
            }
            

            【讨论】:

            • 亲爱的@BalusC 我认为你的方法很容易受到黑客的攻击,他们发送以下请求可以通过文件系统导航:files/%2e%2e/mysecretfile.txt。此请求产生files/../mysecretfile.txt。我在 Tomcat 7.0.55 上对其进行了测试。他们称之为目录爬升:owasp.org/index.php/Path_Traversal
            • @Cristian:是的,可能。我更新了示例以展示如何防止这种情况发生。
            • 这不应该得到支持。像这样使用 Servlet 为网页提供静态文件是灾难安全的良方。所有这些问题都已经解决了,没有理由实施一种新的自定义方式,因为它可能会引爆更多未被发现的安全定时炸弹。正确的路径是配置 Tomcat/GlassFish/Jetty 等来提供内容,或者更好地使用像 NGinX 这样的专用文件服务器。
            • @LeonhardPrintz:一旦您指出安全问题,我将删除答案并向我在 Tomcat 的朋友报告。没问题。
            【解决方案14】:

            检查 Tomcat 8.x:如果根 servlet 映射到“”,静态资源可以正常工作。 对于 servlet 3.x,它可以由 @WebServlet("")

            完成

            【讨论】:

              猜你喜欢
              • 2011-03-13
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多