Servlet pour servir du contenu statique

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

Je cherche donc à inclure une petite servlet dans l’application Web pour servir son propre contenu statique (images, CSS, etc.). Le servlet doit avoir les propriétés suivantes:

  • Pas de dépendances externes
  • Simple et fiable
  • Prise en charge de l’en If-Modified-Since tête If-Modified-Since (c. If-Modified-Since -d getLastModified Méthode getLastModified personnalisée)
  • (Facultatif) prise en charge de l’encodage gzip, etags, …

Une telle servlet est-elle disponible quelque part? Le plus proche que je puisse trouver est l’ exemple 4-10 du livre de servlets.

Mise à jour: La structure de l’URL que je veux utiliser – au cas où vous vous poseriez la question – est simplement:

   main /*   default /static/*  

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

J’ai proposé une solution légèrement différente. C’est un peu piraté, mais voici le mapping:

  default *.html   default *.jpg   default *.png   default *.css   default *.js   myAppServlet /  

Cela ne fait que mapper tous les fichiers de contenu par extension au servlet par défaut, et tout le rest à “myAppServlet”.

Il fonctionne à la fois dans Jetty et Tomcat.

Dans ce cas, il n’est pas nécessaire d’implémenter complètement le servlet par défaut. Vous pouvez utiliser ce servlet simple pour envelopper la demande 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 Ssortingng getServletPath() { return ""; } }; rd.forward(wrapped, resp); } } 

J’ai eu de bons résultats avec FileServlet , car il supporte à peu près tous les HTTP (Etags, Chunking, etc.).

Modèle abstrait pour un servlet de ressource statique

Basé en partie sur ce blog de 2007, voici un modèle abstrait modernisé et hautement réutilisable pour une servlet qui traite correctement la mise en cache, ETag , If-None-Match et If-Modified-Since (mais pas de support Gzip et Range; simple, Gzip peut être fait avec un filtre ou via une 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 Ssortingng ETAG_HEADER = "W/\"%s-%s\""; private static final Ssortingng 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; } Ssortingng 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 null 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, Ssortingng fileName, long lastModified) { Ssortingng eTag = Ssortingng.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, Ssortingng eTag, long lastModified) { Ssortingng ifNoneMatch = request.getHeader("If-None-Match"); if (ifNoneMatch != null) { Ssortingng[] 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, Ssortingng fileName, long contentLength) { response.setHeader("Content-Type", getServletContext().getMimeType(fileName)); response.setHeader("Content-Disposition", Ssortingng.format(CONTENT_DISPOSITION_HEADER, fileName)); if (contentLength != -1) { response.setHeader("Content-Length", Ssortingng.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", Ssortingng.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 <mime-type> in web.xml. * @return The file name of the resource. */ public Ssortingng 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 -1 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 la servlet abstraite donnée et d’implémenter la méthode getStaticResource() selon le javadoc.

Exemple concret servant de système de fichiers:

Voici un exemple concret qui le sert via une URL comme /files/foo.ext 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 { Ssortingng pathInfo = request.getPathInfo(); if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) { throw new IllegalArgumentException(); } Ssortingng name = URLDecoder.decode(pathInfo.subssortingng(1), StandardCharsets.UTF_8.name()); final File file = new File(folder, Paths.get(name).getFileName().toSsortingng()); 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 Ssortingng getFileName() { return file.getName(); } @Override public long getContentLength() { return file.length(); } }; } } 

Exemple concret servant de firebase database:

Voici un exemple concret qui le sert via une URL comme /files/foo.ext de la firebase database via un appel de service EJB qui renvoie votre entité ayant une propriété de byte[] content :

 @WebServlet("/files/*") public class YourEntityResourceServlet extends StaticResourceServlet { @EJB private YourEntityService yourEntityService; @Override protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException { Ssortingng pathInfo = request.getPathInfo(); if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) { throw new IllegalArgumentException(); } Ssortingng name = URLDecoder.decode(pathInfo.subssortingng(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 Ssortingng getFileName() { return yourEntity.getName(); } @Override public long getContentLength() { return yourEntity.getContentLength(); } }; } } 

J’ai fini par lancer mon propre StaticServlet . Il prend en charge l’encodage gzip If-Modified-Since et il devrait également pouvoir servir des fichiers statiques à partir de fichiers de guerre. Ce n’est pas un code très difficile, mais ce n’est pas totalement sortingvial non plus.

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

Mise à jour: Khurram demande 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 la coalesce (identique à la fonction SQL COALESCE ). Ceci est le code:

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

J’ai eu le même problème et je l’ai résolu en utilisant le code du «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

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

Ce 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 le code java.io.File.
  • Les références au package org.apache.catalina.util ne sont probablement que des méthodes / classes 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.

À en juger par l’exemple ci-dessus, je pense que cet article entier est basé sur un comportement bogue dans Tomcat 6.0.29 et versions antérieures. Voir http://soffr.miximages.com/java/error-2018-09-05.html *.jpg *.htc *.gif

Edit: Ceci n’est valable que pour la spécification de servlet 2.5 et plus.

J’ai trouvé un excellent didacticiel sur le Web à propos d’une solution 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-tomcat,5

Je l’ai fait en étendant tomcat DefaultServlet ( src ) et en remplaçant 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 Ssortingng pathPrefix = "/static"; public void init(ServletConfig config) throws ServletException { super.init(config); if (config.getInitParameter("pathPrefix") != null) { pathPrefix = config.getInitParameter("pathPrefix"); } } protected Ssortingng getRelativePath(HttpServletRequest req) { return pathPrefix + super.getRelativePath(req); } } 

… Et voici mes mappages de servlets

  StaticServlet com.example.StaticServlet  pathPrefix /static    StaticServlet /static/*  

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

   springapp org.springframework.web.servlet.DispatcherServlet 1   jsp /WEB-INF/jsp/*   default /favicon.ico   springapp /*  

Nous ne pouvons pas compter sur le modèle d’URL * .jsp sur le mappage standard pour le servlet jsp car le modèle de chemin ‘/ *’ est mis en correspondance avant que le mappage d’extension ne soit vérifié. Le mappage de la servlet jsp dans un dossier plus profond signifie qu’elle correspond en premier. La correspondance avec /favicon.ico se produit exactement avant la correspondance de modèle de chemin. Les correspondances de chemin plus profondes fonctionneront, ou les correspondances exactes, mais aucune correspondance d’extension ne pourra dépasser la correspondance de chemin ‘/ *’. Le mappage de ‘/’ au servlet par défaut ne semble pas fonctionner. Vous pensez que le ‘/’ exact bat le motif de chemin ‘/ *’ sur le Springapp.

La solution de filtrage ci-dessus ne fonctionne pas pour les requêtes JSP transférées / incluses depuis l’application. Pour que cela fonctionne, je devais appliquer le filtre directement à springapp. À ce moment-là, la correspondance de modèle d’URL était inutile car toutes les requêtes qui vont à l’application vont également à ses filtres. J’ai donc ajouté une correspondance de modèle au filtre, puis j’ai pris connaissance du servlet ‘jsp’ et j’ai constaté qu’il ne supprimait pas le préfixe de chemin d’access comme 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 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,

$ cd contexts

$ cp javadoc.xml static.xml

$ vi static.xml

  /static /static/   max-age=3600,public    

Définissez la valeur de contextPath avec votre préfixe d’URL et définissez la valeur de resourceBase comme chemin d’access au contenu statique.

Cela a fonctionné pour moi.

les fichiers statiques sont servis par défaut par servlet et vous pouvez configurer une extension séparée dans web.xml

  default *.js *.css  

Si votre fichier n’est pas * .js, * .css et que vous voulez le montrer dans le navigateur, vous devez configurer le mime-mapping

  wsdl text/xml  

et votre fichier (par exemple: wsdl) sera affiché sous forme de texte dans le navigateur