用于提供静态内容的Servlet

时间:2008-09-25 08:04:28

标签: java jsp servlets java-ee

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

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

这样的servlet是否可用?我能找到的最接近的是servlet书中的example 4-10

更新:我想要使用的网址结构 - 如果您想知道 - 只是:

    <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文件夹中查找)。

14 个答案:

答案 0 :(得分:48)

我想出了一个略有不同的解决方案。这有点黑客,但这里是映射:

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

答案 1 :(得分:45)

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

答案 2 :(得分:29)

我使用FileServlet获得了良好的效果,因为它支持几乎所有的HTTP(etags,分块等)。

答案 3 :(得分:25)

静态资源servlet的抽象模板

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

If-Modified-Since

将它与下面代表静态资源的接口一起使用。

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));
            }
        }
    }

}

您只需要从给定的抽象servlet扩展并根据javadoc实现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; } 方法。

从文件系统提供的具体示例:

这是一个具体示例,通过本地磁盘文件系统中的getStaticResource()之类的URL提供服务:

/files/foo.ext

从数据库服务的具体示例:

这是一个具体示例,它通过EJB服务调用从数据库中通过类似@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属性的实体:

byte[] content

答案 4 :(得分:20)

我最终滚动了自己的StaticServlet。它支持If-Modified-Since,gzip编码,它也应该能够从war文件中提供静态文件。代码并不是很难,但它也不是完全无关紧要的。

代码可用:StaticServlet.java。随意评论。

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

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

答案 5 :(得分:11)

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

答案 6 :(得分:10)

我遇到了同样的问题,我通过使用Tomcat代码库中'default servlet'的代码解决了这个问题。

http://svn.apache.org/repos/asf/tomcat/trunk/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类的引用。

答案 7 :(得分:10)

试试这个

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

答案 8 :(得分:5)

我在网上找到了一些关于某些解决方法的精彩教程。它简单而有效,我在几个带有REST网址样式方法的项目中使用它:

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

答案 9 :(得分:4)

我是通过扩展tomcat DefaultServletsrc)并覆盖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>  

答案 10 :(得分:1)

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

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

答案 11 :(得分:1)

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

完成

答案 12 :(得分:0)

使用org.mortbay.jetty.handler.ContextHandler。您不需要其他组件,如StaticServlet。

在码头的家里,

$ cd contexts

$ 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的值设置为静态内容的文件路径。

它对我有用。

答案 13 :(得分:-1)

请参阅JSOS中的StaticFile:http://www.servletsuite.com/servlets/staticfile.htm