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


145

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

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

  • Нет внешних зависимостей
  • Просто и надежно
  • Поддержка If-Modified-Sinceзаголовка (т.е. пользовательский 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папке).


Не могли бы вы рассказать о «структуре URL», которую вы хотите использовать? Раскатать свои собственные, основываясь на связанном примере 4-10, кажется тривиальным усилием. Я сам делал это много раз ...
— Стю Томпсон

Я отредактировал свой вопрос, чтобы разработать структуру URL. И да, я закончила свой собственный сервлет. Смотрите мой ответ ниже.
— Бруно Де Фрейн,

1
Почему вы не используете веб-сервер для статического контента?
— Стивен

4
@Stephen: потому что не всегда есть Apache перед Tomcat / Jetty. И чтобы избежать хлопот отдельной конфигурации. Но вы правы, я мог бы рассмотреть этот вариант.
— Бруно Де Фрейн

Я просто не могу понять, почему вы не использовали отображение как этот <servlet-mapping> <servlet-name> default </ servlet-name> <url-pattern> / </ url-pattern> </ servlet-mapping > для обслуживания статического контента
— Maciek Kreft

Ответы:


53

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

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


13
на самом деле вы можете добавить более одного тега url-pattern в карте-сервлет;)
— Fareed Alnamrouti

5
Сервлет 2.5 и новее поддерживают множественные теги шаблонов URL в отображении сервлетов
— vivid_voidgroup

Просто будьте осторожны с индексными файлами (index.html), так как они могут иметь приоритет над вашим сервлетом.
— Андрес

Я думаю, что это плохая идея использовать *.sth. Если кто-то получит URL, example.com/index.jsp?g=.sthон получит источник файла JSP. Или я не прав? (Я новичок в Java EE) Я обычно использую шаблон URL /css/*и т. Д.
— SemperPeritus

46

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


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

Этот вопрос имеет аккуратный способ сопоставления / с контроллером и / статического со статическим контентом с использованием фильтра. Проверьте ответ с голосованием после принятого: stackoverflow.com/questions/870150/…
— Дэвид Карбони


30

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


Спасибо! часы неудачных попыток и неправильных ответов, и это решило мою проблему
— Йосси Шашо

4
Хотя для того, чтобы обслуживать контент из папки вне приложения (я использую его для сервера папки с диска, скажем, C: \ resources), я изменил эту строку: this.basePath = getServletContext (). GetRealPath (getInitParameter ("basePath «)); И заменил его следующим образом: this.basePath = getInitParameter ("basePath");
— Йосси Шашо

1
Обновленная версия доступна по адресу showcase.omnifaces.org/servlets/FileServlet
— koppor

26

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

Частично основанный на этом блоге с 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'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;

}

Все, что вам нужно, это просто выйти из данного абстрактного сервлета и реализовать 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, 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. Они называют это лазанием по каталогу
— Кристиан Артеага,

1
@ Кристиан: Да, возможно. Я обновил пример, чтобы показать, как это предотвратить.
— BalusC

Это не должно получить голосов. Такое представление статических файлов для веб-страницы с помощью сервлета - это рецепт для обеспечения безопасности при бедствиях. Все такие проблемы уже решены, и нет никаких оснований для внедрения нового пользовательского способа с использованием, вероятно, большего количества нераскрытых бомб замедленного действия. Правильный путь - настроить Tomcat / GlassFish / Jetty и т. Д. Для обслуживания контента или, что еще лучше, использовать специальный файловый сервер, такой как NGinX.
— Леонард Принц

@LeonhardPrintz: я удалю ответ и сообщу друзьям в Tomcat, как только вы укажете на проблемы безопасности. Нет проблем.
— BalusC

19

Я в конечном итоге прокатки мой собственный 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;
}

2
Не называйте свой внутренний класс Ошибка. Это может вызвать путаницу, так как вы можете ошибочно принять его за java.lang.Error. Также, ваш web.xml такой же?
— Леонель

Спасибо за предупреждение об ошибке. Файл web.xml такой же, с именем «default» заменено именем StaticServlet.
— Бруно Де Фрейн

1
Что касается метода coalesce, его можно заменить (внутри класса Servlet) на commons-lang StringUtils.defaultString (String, String)
— Майк Миницки,

Метод TransferStreams () также можно заменить на Files.copy (is, os);
— Геррит Бринк

Почему этот подход так популярен? Почему люди переопределяют статические файловые серверы, как это? Есть так много дыр в безопасности, которые ждут своего открытия, и так много возможностей реальных статических файловых серверов, которые не реализованы.
— Леонард Принц

12

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


1
Это также мое понимание от svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/. Наконец, после маркировки этого WONTFIX +3 года назад!
— Бруно Де Фрейн

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>    

Изменить: Это действительно только для спецификации сервлета 2.5 и выше.


Кажется, это не правильный конфиг.
— Gedrox

10

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

https://github.com/apache/tomcat/blob/master/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 могут быть встроены или удалены.

Кажется, это зависит от многих вещей org.apache.*. Как вы можете использовать его с Jetty?
— Бруно Де Фрейн

Вы правы, эта версия имеет слишком много зависимостей от Tomcat (и, кроме того, она поддерживает много вещей, которые вы, возможно, не захотите. Я отредактирую свой ответ.
— Panagiotis Korros


4

Я сделал это, расширив 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>  

1

Чтобы обслуживать все запросы из приложения Spring, а также /favicon.ico и файлов JSP из / WEB-INF / jsp / *, которые будет запрашивать SpringU 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' и увидел, что он не удаляет префикс пути, как это делает сервлет по умолчанию. Это решило мою проблему, которая была не совсем такой же, но достаточно распространенной.


1

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


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>

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

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


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.