Question

Je déploie une application Web sur deux conteneurs différents (Tomcat et Jetty), mais leurs servlets par défaut servant le contenu statique ont une manière différente de gérer la structure d'URL que je souhaite utiliser ( détails ).

Je cherche donc à inclure un petit servlet dans la webapp pour servir son propre contenu statique (images, CSS, etc.). Le servlet doit avoir les propriétés suivantes:

Un tel servlet est-il disponible quelque part? Le plus proche que je puisse trouver est exemple 4-10 dans le livre de servlets.

Mise à jour : la structure de l'URL que je souhaite utiliser - au cas où vous vous le demanderiez - est simplement:

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

Toutes les demandes doivent donc être transmises au servlet principal, sauf si elles concernent le chemin static . Le problème est que le servlet par défaut de Tomcat ne prend pas en compte ServletPath (il recherche donc les fichiers statiques dans le dossier principal), contrairement à Jetty (donc dans le dossier static ).

Était-ce utile?

La solution 5

J'ai fini par lancer mon propre StaticServlet . Il prend en charge le codage gzip If-Modified-Since et devrait également pouvoir servir les fichiers statiques à partir de fichiers war. Ce n'est pas un code très difficile, mais ce n’est pas non plus tout à fait trivial.

Le code est disponible: StaticServlet.java . N'hésitez pas à commenter.

Mise à jour: Khurram pose des questions sur la classe ServletUtils référencée dans StaticServlet . C'est simplement une classe avec des méthodes auxiliaires que j'ai utilisées pour mon projet. La seule méthode dont vous avez besoin est coalesce (identique à la fonction SQL COALESCE ). C'est le code:

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

Autres conseils

J'ai proposé une solution légèrement différente. C'est un peu bidouillé, mais voici la cartographie:

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

En gros, cela mappe simplement tous les fichiers de contenu par extension sur le servlet par défaut, et tout le reste sur "myAppServlet".

Cela fonctionne à la fois dans Jetty et Tomcat.

Dans ce cas, une implémentation complètement personnalisée du servlet par défaut n'est pas nécessaire. Vous pouvez utiliser ce servlet simple pour envelopper la requête dans l'implémentation du conteneur:


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

J'ai obtenu de bons résultats avec FileServlet . , car il supporte à peu près tout le HTTP (etags, chunking, etc.).

Modèle abstrait pour un servlet de ressource statique

En partie sur la base de ce blog de 2007, voici une version modernisée et hautement modèle abstrait réutilisable pour un servlet qui gère correctement la mise en cache, ETag , If-None-Match et If-Modified-Since (mais sans Gzip et prise en charge des gammes; pour simplifier les choses; Gzip peut être réalisé avec un filtre ou via la configuration de conteneur).

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

}

Utilisez-le avec l'interface ci-dessous représentant une ressource statique.

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;

}

Tout ce dont vous avez besoin est simplement d’étendre à partir du servlet abstrait donné et d’implémenter la méthode getStaticResource () conformément au javadoc.

Exemple concret servant à partir du système de fichiers:

Voici un exemple concret qui le sert via une URL telle que /files/foo.ext à partir du système de fichiers du disque local:

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

}

Exemple concret servant depuis la base de données:

Voici un exemple concret qui le sert via une URL telle que /files/foo.ext depuis la base de données via un appel de service EJB qui renvoie votre entité ayant un contenu byte [] propriété:

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

}

À en juger par les informations ci-dessus, je pense que tout cet article est basé sur un comportement erroné dans Tomcat 6.0.29 et les versions antérieures. Voir https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . . Mettez à niveau vers Tomcat 6.0.30 et le comportement entre (Tomcat | Jetty) devrait fusionner.

essayez ceci

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

Éditer: cela n’est valable que pour les spécifications du servlet 2.5 et supérieur.

J'ai eu le même problème et je l'ai résolu en utilisant le code de la 'servlet par défaut' de la base de code Tomcat.

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

Le DefaultServlet est le servlet qui dessert les ressources statiques. (jpg, html, css, gif, etc.) dans Tomcat.

Cette servlet est très efficace et possède certaines des propriétés que vous avez définies ci-dessus.

Je pense que ce code source est un bon moyen de démarrer et de supprimer les fonctionnalités ou dépendances dont vous n’avez pas besoin.

  • Les références au package org.apache.naming.resources peuvent être supprimées ou remplacées par du code java.io.File.
  • Les références au package org.apache.catalina.util ne sont probablement que des classes / méthodes utilitaires pouvant être dupliquées dans votre code source.
  • Les références à la classe org.apache.catalina.Globals peuvent être insérées ou supprimées.

J'ai trouvé un excellent tutoriel sur le Web concernant certaines solutions de contournement. C'est simple et efficace, je l'ai utilisé dans plusieurs projets avec l'approche des styles d'URL REST:

http://www.kuligowski.pl/java/rest-style-urls-and-url-mapping-for-static-content-apache-tomcat5

Je l'ai fait en étendant le DefaultServlet de Tomcat ( src ) et substituer la méthode 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);
   }
}

... Et voici mes mappages de servlets

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

Pour répondre à toutes les demandes d'une application Spring, ainsi que de /favicon.ico et des fichiers JSP de / WEB-INF / jsp / *, Spring's AbstractUrlBasedView vous demandera de remapper le servlet jsp et le servlet par défaut:

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

Nous ne pouvons pas compter sur le modèle * .jsp url sur le mappage standard pour le servlet jsp car le modèle de chemin "/ *" est mis en correspondance avant que tout mappage d'extension ne soit coché. Le mappage de la servlet jsp sur un dossier plus profond signifie que la correspondance est établie en premier. La correspondance '/favicon.ico' se produit exactement avant la correspondance du motif de chemin. Les correspondances de chemin plus profondes fonctionneront, ou des correspondances exactes, mais aucune correspondance d'extension ne pourra dépasser la correspondance de chemin '/ *'. Le mappage '/' sur la servlet par défaut ne semble pas fonctionner. Vous penseriez que le "/" exact battrait le modèle de chemin "/ *" sur springapp.

La solution de filtrage ci-dessus ne fonctionne pas pour les demandes JSP transférées / incluses de l'application. Pour que cela fonctionne, je devais appliquer le filtre directement à springapp. À ce stade, la correspondance de motif d'URL était inutile, car toutes les demandes qui vont à l'application vont également à ses filtres. J'ai donc ajouté une correspondance de motif au filtre, puis j'ai découvert le servlet 'jsp' et constaté qu'il ne supprime pas le préfixe du chemin d'accès comme le fait le servlet par défaut. Cela a résolu mon problème, qui n’était pas exactement le même mais assez commun.

Vérifié pour Tomcat 8.x: les ressources statiques fonctionnent correctement si le servlet racine est mappé sur """. Pour le servlet 3.x, cela pourrait être fait par @WebServlet (""))

Utilisez org.mortbay.jetty.handler.ContextHandler. Vous n'avez pas besoin de composants supplémentaires tels que StaticServlet.

À la maison de la jetée,

$ contextes cd

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

Définissez la valeur de contextPath avec votre préfixe d'URL et définissez la valeur de resourceBase en tant que chemin d'accès au contenu statique.

Cela a fonctionné pour moi.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top