Сервлет для обслуживания статического содержимого

Я развертываю webapp на двух разных контейнерах (Tomcat и Jetty), но их сервлеты по умолчанию для обслуживания статического контента имеют другой способ обработки структуры URL, которую я хочу использовать (подробнее).

Поэтому я хочу включить небольшой сервлет в webapp для обслуживания своего собственного статического контента (изображения, CSS и т.д.). Сервлет должен иметь следующие свойства:

  • Нет внешних зависимостей
  • Простой и надежный
  • Поддержка If-Modified-Since header (т.е. пользовательский getLastModified)
  • (Необязательно) поддержка кодирования gzip, etags,...

Является ли такой сервлет доступным где-нибудь? Ближайший я могу найти пример 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. Проблема заключается в том, что сервлет Tomcat по умолчанию не учитывает ServletPath (поэтому он ищет статические файлы в основной папке), в то время как Jetty делает (поэтому он выглядит в папке static).

Ответ 1

Я закончил свой собственный StaticServlet. Он поддерживает кодировку If-Modified-Since, gzip и должен также обеспечивать статические файлы из военных файлов. Это не очень сложный код, но он также не является тривиальным.

Доступен код: 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;
}

Ответ 2

Я придумал немного другое решение. Это немного хак-иш, но вот отображение:

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

Это в основном просто отображает все файлы содержимого по расширению до сервлета по умолчанию, а все остальное - в "myAppServlet".

Он работает как в Jetty, так и в Tomcat.

Ответ 3

В этом случае нет необходимости в полностью настраиваемой реализации сервлета по умолчанию, вы можете использовать этот простой сервлет для переноса запроса в реализацию контейнера:


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

Ответ 4

У меня были хорошие результаты с FileServlet, так как он поддерживает почти все HTTP (etags, chunking и т.д.).

Ответ 5

Абстрактный шаблон для сервлета статического ресурса

Частично основанный на этом блоге от 2007 года, здесь представлен модернизированный и очень многоразовый абстрактный шаблон для сервлета, который правильно обрабатывает кеширование, 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 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;

}

Все, что вам нужно, просто распространяется от данного абстрактного сервлета и реализует метод getStaticResource() в соответствии с javadoc.

Пример конкретного примера из файловой системы:

Вот конкретный пример, который служит ему через URL, например /files/foo.ext, из локальной файловой системы на диске:

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

}

Конкретный пример из базы данных:

Вот конкретный пример, который служит ему через 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();
            }
        };
    }

}

Ответ 6

У меня была та же проблема, и я решил ее, используя код "сервлета по умолчанию" из кодовой базы Tomcat.

http://svn.apache.org/repos/asf/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java

DefaultServlet - это сервлет, который обслуживает статические ресурсы (jpg, html, css, gif и т.д.) в Tomcat.

Этот сервлет очень эффективен и имеет некоторые свойства, определенные вами выше.

Я думаю, что этот исходный код - хороший способ начать и удалить функциональные возможности или настройки, которые вам не нужны.

  • Ссылки на пакет org.apache.naming.resources можно удалить или заменить кодом java.io.File.
  • Ссылки на пакет org.apache.catalina.util являются, возможно, только полезными методами/классами, которые могут быть дублированы в вашем исходном коде.
  • Ссылки на класс org.apache.catalina.Globals могут быть встроены или удалены.

Ответ 7

Судя по приведенной выше информации о примере, я думаю, что вся эта статья основана на прослушивании поведения в Tomcat 6.0.29 и ранее. См. https://issues.apache.org/bugzilla/show_bug.cgi?id=50026. Обновите Tomcat 6.0.30, и поведение между (Tomcat | Jetty) должно слиться.

Ответ 8

попробуйте это

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

Изменить: это справедливо только для сервлета 2.5 spec и up.

Ответ 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-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 и JSP файлы из /WEB -INF/jsp/*, которые Spring AbstractUrlBasedView запросит, вы можете просто переназначить сервлет jsp и сервлет по умолчанию

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

Мы не можем полагаться на url-шаблон *.jsp на стандартное сопоставление для сервлета jsp, потому что шаблон пути '/*' сопоставляется перед проверкой любого расширения. Сопоставление сервлета jsp с более глубокой папкой означает, что оно соответствует первому. Соответствие '/favicon.ico' точно происходит до соответствия шаблону пути. Более глубокие совпадения пути будут работать, или точные совпадения, но никакие совпадения в расширении не могут пройти мимо пути "/*". Отображение '/' для сервлета по умолчанию не работает. Вы могли бы подумать, что точное '/' будет бить шаблон пути//'на springapp.

Вышеупомянутое решение фильтра не работает для пересылаемых/включенных JSP-запросов из приложения. Чтобы заставить его работать, мне пришлось применить фильтр к springapp напрямую, после чего совпадение URL-адресов было бесполезным, так как все запросы, поступающие в приложение, также попадают в его фильтры. Поэтому я добавил соответствие шаблону фильтру, а затем узнал о сервлете "jsp" и увидел, что он не удаляет префикс пути, как это делает сервлет по умолчанию. Это решило мою проблему, которая была не совсем такой же, но достаточно распространенной.

Ответ 12

Использовать 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>

Задайте значение contextPath с вашим префиксом URL и установите значение resourceBase как путь к файлу статического содержимого.

Это сработало для меня.

Ответ 14

Проверено для Tomcat 8.x: статические ресурсы работают нормально, если корневой сервлет отображается на "". Для сервлета 3.x это можно сделать с помощью @WebServlet("")