用于提供静态内容的Servlet

152

我在两个不同的容器(Tomcat和Jetty)上部署web应用程序,但它们用于提供静态内容的默认servlet处理URL结构的方式不同,而我想使用相同的URL结构 (详情)。

因此,我希望在Web应用程序中包含一个小的servlet来提供自己的静态内容(图像、CSS等)。这个servlet应该具有以下属性:

是否有这样的servlet可用?我找到的最接近的是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>

除非是针对static路径的请求,否则所有请求都应该传递给主servlet。问题在于Tomcat的默认servlet不考虑ServletPath(因此它会在主文件夹中查找静态文件),而Jetty则考虑ServletPath(因此它将在static文件夹中查找)。


你能详细说明一下你想使用的“URL结构”吗?根据链接的示例4-10自己编写似乎是微不足道的工作。我自己已经做过很多次了... - Stu Thompson
我编辑了我的问题以详细说明URL结构。是的,最终我自己编写了一个servlet。请参见下面的答案。 - Bruno De Fraine
1
为什么不使用Web服务器来处理静态内容? - Stephen
4
@Stephen:因为并不总是在Tomcat/Jetty前有一个Apache服务器。为了避免单独进行配置的麻烦。但你说得没错,我可以考虑这个选项。 - Bruno De Fraine
我就是不明白,为什么你不使用像这样的映射来提供静态内容: default / - Maciek Kreft
14个回答

57

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

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

这基本上只将所有扩展名为content的文件映射到默认servlet,其他一切都映射到"myAppServlet"。

它适用于Jetty和Tomcat。


13
实际上,您可以在servlet-mapping中添加多个url-pattern标签 ;) - Fareed Alnamrouti
5
Servlet 2.5 及以上版本支持在 servlet-mapping 内部使用多个 url-pattern 标签。 - vivid_voidgroup
只需小心索引文件(index.html),因为它们可能优先于您的servlet。 - Andres
我认为使用 *.sth 是一个不好的主意。如果有人得到了网址 example.com/index.jsp?g=.sth,他将会获得 jsp 文件的源代码。或者我错了吗?(我在 Java EE 中是新手)我通常使用 url 模式 /css/* 等等。 - Platon Efimov

46

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

这个问题有一个巧妙的方法,可以使用过滤器将 / 映射到控制器,将 /static 映射到静态内容。在接受的答案之后,请查看得到赞同的答案:https://dev59.com/8XNA5IYBdhLWcg3wrf03 - David Carboni

30

静态资源servlet的抽象模板

这个模板部分基于2007年的这篇博客,是一个现代化且高度可重用的servlet抽象模板,它可以正确处理缓存、ETag、If-None-Match和If-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();
            }
        };
    }

}

从数据库中提供服务的具体实例:

这是一个具体的例子,通过 URL /files/foo.ext 从数据库中通过 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();
            }
        };
    }

}

1
亲爱的@BalusC,我认为您的方法容易受到黑客攻击,因为发送以下请求的黑客可以浏览文件系统:files/%2e%2e/mysecretfile.txt。这个请求会产生files/../mysecretfile.txt。我在Tomcat 7.0.55上进行了测试。他们称之为目录攀爬:https://www.owasp.org/index.php/Path_Traversal - Cristian Arteaga
3
可以的。我更新了示例以展示如何防止这种情况。 - BalusC
这篇文章不应该得到赞同。像这样使用Servlet为网页提供静态文件是一个安全灾难的配方。所有这些问题都已经得到解决,没有理由去实现一个新的自定义方式,可能会有更多未被发现的安全时间炸弹爆炸。正确的方法是配置Tomcat/GlassFish/Jetty等来提供内容,甚至更好的方法是使用专用的文件服务器,如NGinX。 - Leonhard Printz
2
@LeonhardPrintz:如果您指出了安全问题,我会删除答案并向Tomcat的朋友们报告。没问题。 - BalusC

30

我使用FileServlet获得了良好的结果,因为它支持几乎所有HTTP特性(如ETags、分块传输等)。


谢谢!无数小时的失败尝试和错误答案,这解决了我的问题。 - Yossi Shasho
4
为了从应用程序外部的文件夹提供内容(例如我使用它来从磁盘上的 C:\resources 文件夹提供内容),我修改了以下行:this.basePath = getServletContext().getRealPath(getInitParameter("basePath"));并将其替换为:this.basePath = getInitParameter("basePath"); - Yossi Shasho
1
可在http://showcase.omnifaces.org/servlets/FileServlet获取更新版本。 - koppor

20
我最终自己编写了一个 StaticServlet。它支持 If-Modified-Since、gzip 编码,并且应该能够从 war 文件中提供静态文件服务。这不是非常困难的代码,但也不是完全简单的。
代码可在此处获取: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;
}

2
不要将内部类命名为Error。这可能会导致混淆,因为您可能会将其误认为是java.lang.Error。另外,你的web.xml文件是否相同? - Leonel
感谢错误警告。web.xml文件相同,将“default”替换为StaticServlet的名称。 - Bruno De Fraine
1
关于coalesce方法,它可以被commons-lang StringUtils.defaultString(String, String)替换(在Servlet类内部)。 - Mike Minicki
transferStreams() 方法也可以被 Files.copy(is, os) 替换。 - Gerrit Brink
为什么这种方法如此受欢迎?为什么人们要重新实现这样的静态文件服务器?存在着许多安全漏洞等待被发现,而真正的静态文件服务器具有许多未实现的功能。 - Leonhard Printz

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规范及以上版本。


似乎这不是一个有效的配置。 - Gedrox

12

1
这也是我从 svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/ 中得出的理解。终于,在将此问题标记为WONTFIX+3年之后! - Bruno De Fraine

11

我遇到了同样的问题,通过使用来自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中使用它? - Bruno De Fraine
你说得对,这个版本对Tomcat有太多依赖(而且它也支持许多你可能不需要的东西)。我会编辑我的答案。 - Panagiotis Korros

5

4

我通过扩展tomcat的DefaultServlet (源代码),并覆盖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>  

这看起来很有前途,但对我没有用。在Tomcat9中,当我尝试访问资源时,getRelativePath()没有被调用。 - scharette

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接