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

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


没有外部依赖性
简单可靠
支持If-Modified-Since标头(即自定义getLastModified方法)
(可选)支持gzip编码,etag,...

这样的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>
 


因此,所有请求都应传递到主servlet,除非它们用于static路径。问题在于,Tomcat的默认Servlet未考虑ServletPath(因此它在主文件夹中查找静态文件),而Jetty则将其考虑(因此它在static文件夹中查找)。

评论

您能否详细说明要使用的“ URL结构”?根据链接的示例4-10,自己滚动似乎很简单。我已经做了很多次了...

我编辑了问题,以详细说明URL结构。是的,我最终滚动了自己的servlet。请参阅下面的答案。

为什么不将Web服务器用于静态内容?

@Stephen:因为在Tomcat / Jetty前面并不总是有Apache。并且避免了单独配置的麻烦。但是您是对的,我可以考虑使用该选项。

我只是不明白,为什么您不使用这样的映射 default / 提供静态内容

#1 楼

我想出了一个略有不同的解决方案。有点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标签;)

–著名的Alnamrouti
2012年1月28日14:41



Servlet 2.5及更高版本支持Servlet映射内的多个url模式标签

–vivid_voidgroup
2012年5月22日12:59

请小心索引文件(index.html),因为它们可能优先于servlet。

–安德烈斯
2015年12月11日15:43

我认为使用* .sth是个坏主意。如果有人获得URL example.com/index.jsp?g=.sth,他将获得jsp文件的源。还是我错了? (我是Java EE的新手)我通常使用url模式/ css / *等。

– SemperPeritus
17年6月2日在15:48

#2 楼

在这种情况下,无需完全自定义默认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映射到使用过滤器的静态内容。在接受的答案之后检查更新的答案:stackoverflow.com/questions/870150/…

– David Carboni
2012年10月26日6:34



HttpServletRequestWrapper javadoc

–恩德拉Žižka
16-3-22在1:22

#3 楼

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

评论


谢谢!数小时的失败尝试和错误答案,这解决了我的问题

–尤西·沙肖(Yossi Shasho)
2012年8月15日在7:38

尽管为了从应用程序外部的文件夹提供内容(我使用它来从磁盘将文件夹存储为服务器,例如C:\ resources),但我修改了以下行:this.basePath = getServletContext()。getRealPath(getInitParameter(“ basePath “));并替换为:this.basePath = getInitParameter(“ basePath”);

–尤西·沙肖(Yossi Shasho)
2012年8月15日7:39



更新的版本可在Showcase.omnifaces.org/servlets/FileServlet中找到。

– koppor
16年11月23日在9:19

#4 楼

静态资源servlet的抽象模板

部分基于2007年的博客,这是一个现代化且高度可重用的servlet抽象模板,可正确处理缓存,ETagIf-None-MatchIf-Modified-Since(但没有Gzip和范围支持;只是为了简单起见; 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();
            }
        };
    }

}


具体示例服务来自数据库:

这是一个具体示例,该示例通过EJB服务调用通过数据库中的/files/foo.ext之类的URL为它提供服务ich返回具有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。该请求产生文件/../mysecretfile.txt。我在Tomcat 7.0.55上进行了测试。他们称其为爬目录:owasp.org/index.php/Path_Traversal

– Cristian Arteaga
18年8月4日在0:37

@克里斯蒂安:是的,可能。我更新了示例以显示如何防止这种情况。

– BalusC
18年8月5日在13:53



这不应该得到投票。像这样使用Servlet为网页提供静态文件是明智的防灾方法。所有这些问题都已经得到解决,因此没有理由实施新的“自定义”方式,可能会触发更多未发现的安全定时炸弹。正确的路径是将Tomcat / GlassFish / Jetty等配置为提供内容,甚至最好使用专用文件服务器(如NGinX)。

– Leonhard Printz
3月23日下午16:26

@LeonhardPrintz:指出安全问题后,我将删除答案并向Tomcat的好友报告。没问题。

– BalusC
3月23日17:42



#5 楼

我最终滚动了自己的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;
}


评论


不要将您的内部类命名为Error。这可能会引起混乱,因为您可能会误认为java.lang.Error此外,您的web.xml是否相同?

– Leonel
08-09-25 at 17:10

感谢您的错误警告。 web.xml相同,但用StaticServlet的名称替换了“默认”。

– Bruno De Fraine
08-09-25 at 17:25

至于合并方法,可以用commons-lang StringUtils.defaultString(String,String)替换它(在Servlet类内部)。

–麦克·米尼克(Mike Minicki)
2011年7月25日在6:53

transferStreams()方法也可以替换为Files.copy(is,os);

– Gerrit Brink
2014年8月12日在6:25

为什么这种方法如此受欢迎?人们为什么要像这样重新实现静态文件服务器?有太多的安全漏洞正等待发现,还有许多实际的静态文件服务器功能尚未实现。

– Leonhard Printz
3月23日下午16:28

#6 楼

从上面的示例信息来看,我认为整篇文章都是基于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/的理解。终于在标记此WONTFIX + 3年前之后!

– Bruno De Fraine
2011-2-22在10:47

#7 楼

试试这个

<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
2012年11月22日7:09

#8 楼

我遇到了同样的问题,并通过使用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
08-09-25在8:27

没错,这个版本对Tomcat有太多依赖(因为它还支持许多您可能不想要的东西。我将编辑我的答案。

– Panagiotis Korros
08-09-25 at 8:40

#9 楼

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

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

#10 楼

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


#11 楼

要处理来自Spring应用程序的所有请求以及/favicon.ico和/ WEB-INF / jsp / *中Spring的AbstractUrlBasedView将请求的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-pattern匹配是无用的,因为所有发送给应用程序的请求也都转到了其过滤器。因此,我向过滤器添加了模式匹配,然后了解了“ jsp” servlet,并发现它不会像默认servlet那样删除路径前缀。那解决了我的问题,虽然不完全相同,但很常见。

#12 楼

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

#13 楼

使用org.mortbay.jetty.handler.ContextHandler。您不需要诸如StaticServlet之类的其他组件。

在码头首页,

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

对我有用。

#14 楼

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