web-dev-qa-db-de.com

Servlet zum Bereitstellen statischer Inhalte

Ich implementiere eine Webapp auf zwei verschiedenen Containern (Tomcat und Jetty), aber ihre Standardservlets zum Bereitstellen des statischen Inhalts haben eine andere Art, die URL-Struktur zu behandeln, die ich verwenden möchte ( details ).

Daher möchte ich ein kleines Servlet in die webapp aufnehmen, um seinen eigenen statischen Inhalt (Bilder, CSS usw.) bereitzustellen. Das Servlet sollte die folgenden Eigenschaften haben:

  • Keine externen Abhängigkeiten
  • Einfach und zuverlässig
  • Unterstützung für If-Modified-Since header (d. H. Benutzerdefinierte getLastModified method)
  • (Optional) Unterstützung für die gzip-Codierung, etags, ...

Ist ein solches Servlet irgendwo verfügbar? Das nächste, was ich finden kann, ist example 4-10 aus dem Servlet-Buch.

Update: Die URL-Struktur, die ich verwenden möchte - falls Sie sich fragen - ist einfach:

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

Daher sollten alle Anforderungen an das Hauptservlet weitergeleitet werden, es sei denn, sie betreffen den Pfad static. Das Problem ist, dass das Standard-Servlet von Tomcat den ServletPath nicht berücksichtigt (also nach den statischen Dateien im Hauptordner sucht), während Jetty dies tut (also im Ordner static).

141
Bruno De Fraine

Am Ende habe ich meine eigene StaticServlet gerollt. Es unterstützt If-Modified-Since, gzip-Codierung und sollte auch statische Dateien aus Kriegsdateien bereitstellen können. Es ist kein sehr schwieriger Code, aber auch nicht ganz trivial.

Der Code ist verfügbar: StaticServlet.Java . Fühlen Sie sich frei zu kommentieren.

Update: Khurram fragt nach der ServletUtils-Klasse, auf die in StaticServlet verwiesen wird. Es ist einfach eine Klasse mit Hilfsmethoden, die ich für mein Projekt verwendet habe. Die einzige Methode, die Sie benötigen, ist coalesce (die mit der SQL-Funktion COALESCE identisch ist). Dies ist der Code:

public static <T> T coalesce(T...ts) {
    for(T t: ts)
        if(t != null)
            return t;
    return null;
}
20
Bruno De Fraine

Ich habe mir eine etwas andere Lösung ausgedacht. Es ist ein bisschen hack-ish, aber hier ist das Mapping:

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

Dies ordnet grundsätzlich alle Inhaltsdateien nach Erweiterung dem Standardservlet zu, und alles andere "myAppServlet". 

Es funktioniert sowohl in Jetty als auch in Tomcat. 

48
Taylor Gautier

In diesem Fall ist keine vollständige benutzerdefinierte Implementierung des Standard-Servlets erforderlich. Sie können dieses einfache Servlet verwenden, um die Anforderung in die Implementierung des Containers zu integrieren:


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

Ich habe gute Ergebnisse mit FileServlet erzielt, da es praktisch alles HTTP unterstützt (etags, chunking usw.).

29
Will Hartung

Abstrakte Vorlage für ein statisches Ressourcen-Servlet

Teilweise basiert auf diesem Blog von 2007, hier ist eine modernisierte und in hohem Maße wiederverwendbare abstrakte Vorlage für ein Servlet, das sich ordnungsgemäß mit dem Zwischenspeichern von ETag, If-None-Match und If-Modified-Since befasst (aber keine Unterstützung für Gzip und Range; nur um es einfach zu halten; mit einem Filter oder über eine Containerkonfiguration durchgeführt werden).

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

}

Verwenden Sie es zusammen mit der folgenden Schnittstelle, die eine statische Ressource darstellt.

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;

}

Alles, was Sie brauchen, ist, nur das angegebene abstrakte Servlet zu erweitern und die getStaticResource()-Methode entsprechend dem Javadoc zu implementieren.

Konkretes Beispiel aus dem Dateisystem:

Hier ist ein konkretes Beispiel, das über eine URL wie /files/foo.ext aus dem lokalen Festplatten-Dateisystem abgerufen wird:

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

}

Konkretes Beispiel aus einer Datenbank:

Hier ein konkretes Beispiel, das über eine URL wie /files/foo.ext aus der Datenbank über einen EJB-Serviceaufruf bereitgestellt wird, der Ihre Entität mit einer byte[] content-Eigenschaft zurückgibt:

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

}
25
BalusC

Nach den obigen Beispielinformationen denke ich, dass der gesamte Artikel auf einem fehlerhaften Verhalten in Tomcat 6.0.29 und früheren Versionen basiert. Siehe https://issues.Apache.org/bugzilla/show_bug.cgi?id=50026 . Aktualisieren Sie auf Tomcat 6.0.30, und das Verhalten zwischen (Tomcat | Jetty) sollte zusammengeführt werden. 

11
Jeff Stice-Hall

Ich hatte das gleiche Problem und habe es gelöst, indem ich den Code des "Standard-Servlets" der Tomcat-Codebase verwendet habe.

http://svn.Apache.org/repos/asf/Tomcat/trunk/Java/org/Apache/catalina/servlets/DefaultServlet.Java

Das DefaultServlet ist das Servlet, das die statischen Ressourcen (jpg, html, css, gif usw.) in Tomcat bereitstellt.

Dieses Servlet ist sehr effizient und verfügt über einige der oben definierten Eigenschaften.

Ich denke, dass dieser Quellcode eine gute Möglichkeit ist, die nicht benötigten Funktionen oder Abhängigkeiten zu starten und zu entfernen.

  • Verweise auf das Paket org.Apache.naming.resources können entfernt oder durch Java.io.File-Code ersetzt werden.
  • Verweise auf das Paket org.Apache.catalina.util sind wahrscheinlich nur Dienstprogrammmethoden/-klassen, die in Ihrem Quellcode dupliziert werden können.
  • Verweise auf die Klasse org.Apache.catalina.Globals können eingebettet oder entfernt werden.
10

versuche dies 

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

Bearbeiten: Dies gilt nur für Servlet 2.5-Spezifikationen und höher.

10

Ich habe im Web ein tolles Tutorial über eine Problemumgehung gefunden. Es ist einfach und effizient, ich habe es in mehreren Projekten mit dem URL-Stilansatz REST verwendet:

http://www.kuligowski.pl/Java/rest-style-urls-and-url-mapping-for-static-content-Apache-Tomcat,5

5
user164757

Ich habe dies gemacht, indem ich das Tomcat DefaultServlet ( src ) erweitert und die getRelativePath () - Methode überschrieben habe.

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

... Und hier sind meine Servlet-Mappings

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

Für Tomcat 8.x geprüft: Statische Ressourcen funktionieren in Ordnung, wenn das Root-Servlet "" ..__ zugeordnet ist. Für Servlet 3.x kann dies durch @WebServlet("") erfolgen

1
GKislin

Um alle Anfragen einer Spring-App sowie von /favicon.ico und den JSP-Dateien aus/WEB-INF/jsp/* zu bedienen, die Spring's AbstractUrlBasedView anfordert, können Sie das Jsp-Servlet und das Standard-Servlet neu zuordnen:

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

Wir können uns nicht auf das * .jsp-URL-Muster in der Standardzuordnung für das jsp-Servlet verlassen, da das Pfadmuster '/ *' abgeglichen wird, bevor Erweiterungszuordnungen geprüft werden. Wenn Sie das jsp-Servlet einem tieferen Ordner zuordnen, wird es zuerst abgeglichen. Das Abgleichen von '/favicon.ico' geschieht genau vor dem Abgleich des Pfadmusters. Tiefere Pfadübereinstimmungen funktionieren oder exakte Übereinstimmungen, aber keine Erweiterungsübereinstimmungen können den Pfad "/ *" übersteigen. Das Zuordnen von '/' zum Standard-Servlet scheint nicht zu funktionieren. Man könnte meinen, das exakte '/' würde das '/ *' Pfadmuster in springapp schlagen.

Die oben genannte Filterlösung funktioniert nicht für weitergeleitete/eingeschlossene JSP-Anforderungen der Anwendung. Damit es funktioniert, musste ich den Filter direkt auf springapp anwenden. Zu diesem Zeitpunkt war das Abgleichen der URL-Muster nutzlos, da alle Anforderungen, die an die Anwendung gesendet werden, auch an die Filter gehen. Also habe ich dem Filter Pattern Matching hinzugefügt und dann etwas über das 'jsp'-Servlet erfahren und festgestellt, dass das Pfad-Präfix nicht wie das Standard-Servlet entfernt wird. Das hat mein Problem gelöst, das nicht genau das gleiche war, aber häufig genug.

1
clish

statische Dateien werden vom Standard-Servlet bereitgestellt, und Sie können eine separate Erweiterung in web.xml konfigurieren

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

wenn Ihre Datei nicht * .js, * .css ist und Sie sie im Browser anzeigen möchten, müssen Sie das Mime-Mapping konfigurieren

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

und Ihre (zum Beispiel: wsdl) -Datei wird als Text im Browser angezeigt

0
zond

Verwenden Sie org.mortbay.jetty.handler.ContextHandler. Sie benötigen keine zusätzlichen Komponenten wie StaticServlet.

Am Steg nach Hause,

$ cd-Kontexte

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

Legen Sie den Wert von contextPath mit Ihrem URL-Präfix fest, und legen Sie den Wert von resourceBase als Dateipfad des statischen Inhalts fest.

Es hat für mich funktioniert.

0
yogman