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

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);
    }
}
axtavt
источник
Этот вопрос имеет аккуратный способ сопоставления / с контроллером и / статического со статическим контентом с использованием фильтра. Проверьте ответ с голосованием после принятого: stackoverflow.com/questions/870150/…
Дэвид Карбони
1
HttpServletRequestWrapper javadoc
Ондра Жижка
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();
            }
        };
    }

}
BalusC
источник
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>  
delux247
источник
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 в качестве пути к файлу статического содержимого.

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

yogman
источник