2008-09-25 2 views
138

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

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

  • внешних зависимостей
  • Простой и надежный
  • Поддержка для If-Modified-Since заголовка (то есть пользовательские getLastModified метод)
  • (опционально) поддержка кодирования GZIP, ETags ,. ..

Является ли такой сервлет доступным где-нибудь? Самое близкое, что я могу найти, это example 4-10 из книги сервлетов.

Update: Структура 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).

+0

Не могли бы вы рассказать о структуре URL-адреса, которую вы хотите использовать? Роллинг, основанный на связанном примере 4-10, кажется тривиальным усилием. Я делал это сам много раз ... – 2008-09-25 09:12:43

+0

Я отредактировал свой вопрос, чтобы уточнить структуру URL. И да, я закончил свой собственный сервлет. См. Мой ответ ниже. – 2008-09-25 12:17:20

+1

Почему вы не используете веб-сервер для статического контента? – Stephen 2008-10-02 19:10:09

ответ

19

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

Код доступен: StaticServlet.java. Не стесняйтесь комментировать.

Обновление: Хуррам спрашивает о классе 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 тот же? – Leonel 2008-09-25 17:10:05

+0

Спасибо за предупреждение об ошибке. web.xml - это то же самое, с заменой «по умолчанию» на имя StaticServlet. – 2008-09-25 17:25:50

10

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

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

The DefaultServlet это сервлет, который обслуживает статические ресурсы (JPG, HTML, CSS, GIF и т.д.) в Tomcat.

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

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

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

Кажется, что это зависит от множества вещей из `org.apache. *`. Как вы можете использовать его с Jetty? – 2008-09-25 08:27:59

+0

Вы правы, эта версия имеет слишком много зависимостей от Tomcat (она также поддерживает многие вещи, которые вам могут не понравиться.) Я отредактирую свой ответ. – 2008-09-25 08:40:28

0

Использование org.mortbay.jetty.handler.ContextHandler. Вам не нужны дополнительные компоненты, такие как StaticServlet.

На пристани дома,

$ кд контексты

$ ф 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 как fi le путь статического содержимого.

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

43

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


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

Я сделал это за счет расширения котом 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> 
27

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

1

Чтобы удовлетворить все запросы от Spring приложения, а также /favicon.ico и файлы JSP с/WEB-INF/JSP/*, что в Spring AbstractUrlBasedView будет просить, вы можете просто переназначить СПЯ сервлета и по умолчанию сервлет:

<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» и увидел, что он не удаляет префикс пути, как это делает сервлет по умолчанию. Это решило мою проблему, которая была не совсем такой же, но достаточно распространенной.

45

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

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

Он работает как в Причале, так и в Томкате.

10

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

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>  

Edit: Это справедливо только для сервлетов 2.5 спецификации и выше.

22

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

Частично на основе this blog с 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() в соответствии с документацией.

Конкретный пример выступающей из файловой системы:

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

} 
0

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

-1

Статические файлы обслуживаются сервлетом по умолчанию, и вы можете настроить отдельное расширение в сети.XML

<servlet-mapping> 
    <servlet-name>default</servlet-name> 
    <url-pattern>*.js</url-pattern> 
    <url-pattern>*.css</url-pattern> 
</servlet-mapping> 

если ваш файл не * .js, * .css и вы хотите, чтобы показать его в браузере, вам необходимо настроить MIME-отображение

<mime-mapping> 
    <extension>wsdl</extension> 
    <mime-type>text/xml</mime-type> 
</mime-mapping> 

и ваш (например: WSDL) файл будет отображаться как текст в браузере

Смежные вопросы