Domanda

Distribuisco una webapp su due contenitori diversi (Tomcat e Jetty), ma i loro servlet predefiniti per servire il contenuto statico hanno un modo diverso di gestire la struttura dell'URL che voglio usare ( dettagli ).

Sto quindi cercando di includere un piccolo servlet nella webapp per servire il suo contenuto statico (immagini, CSS, ecc.). Il servlet dovrebbe avere le seguenti proprietà:

  • Nessuna dipendenza esterna
  • Semplice e affidabile
  • Supporto per If-Modified-Since header (ovvero personalizzato getLastModified metodo)
  • Supporto (opzionale) per la codifica gzip, etags, ...

Un tale servlet è disponibile da qualche parte? Il più vicino che riesco a trovare è esempio 4-10 dal libro servlet.

Aggiornamento: la struttura dell'URL che voglio usare - nel caso ti stia chiedendo - è semplicemente:

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

Quindi tutte le richieste devono essere passate al servlet principale, a meno che non siano per il percorso statico . Il problema è che il servlet predefinito di Tomcat non tiene conto di ServletPath (quindi cerca i file statici nella cartella principale), mentre Jetty lo fa (quindi cerca nella cartella static ).

È stato utile?

Soluzione 5

Ho finito per lanciare il mio StaticServlet . Supporta If-Modified-Since , codifica gzip e dovrebbe essere in grado di servire anche file statici da file di guerra. Non è un codice molto difficile, ma non è nemmeno del tutto banale.

Il codice è disponibile: StaticServlet.java . Sentiti libero di commentare.

Aggiornamento: Khurram chiede informazioni sulla classe ServletUtils a cui si fa riferimento in StaticServlet . È semplicemente una classe con metodi ausiliari che ho usato per il mio progetto. L'unico metodo di cui hai bisogno è coalesce (che è identico alla funzione SQL COALESCE ). Questo è il codice:

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

Altri suggerimenti

Ho trovato una soluzione leggermente diversa. È un po 'hack-ish, ma ecco la mappatura:

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

In pratica, questo mappa semplicemente tutti i file di contenuto per estensione al servlet predefinito e tutto il resto su "myAppServlet".

Funziona sia in Jetty che in Tomcat.

In questo caso non è necessaria un'implementazione completamente personalizzata del servlet predefinito, è possibile utilizzare questo semplice servlet per racchiudere la richiesta nell'implementazione del contenitore:


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

Ho avuto buoni risultati con FileServlet , poiché supporta praticamente tutto l'HTTP (etags, chunking, ecc.).

Modello astratto per un servlet di risorse statiche

Basato parzialmente su questo blog del 2007, ecco un sito modernizzato e altamente modello astratto riutilizzabile per un servlet che si occupa correttamente di memorizzazione nella cache, ETag , If-None-Match e If-Modified-Since (ma senza Gzip e supporto Range; solo per semplificare; Gzip potrebbe essere fatto con un filtro o tramite la configurazione del contenitore).

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

}

Usalo insieme all'interfaccia sottostante che rappresenta una risorsa statica.

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;

}

Tutto ciò che serve è solo estendere dal servlet astratto dato e implementare il metodo getStaticResource () secondo il javadoc.

Esempio concreto pubblicato dal file system:

Ecco un esempio concreto che lo serve tramite un URL come /files/foo.ext dal file system del disco locale:

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

}

Esempio concreto pubblicato dal database:

Ecco un esempio concreto che lo serve tramite un URL come /files/foo.ext dal database tramite una chiamata di servizio EJB che restituisce all'entità un byte [] content proprietà:

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

}

A giudicare dalle informazioni di esempio sopra, penso che questo intero articolo sia basato su un comportamento corretto in Tomcat 6.0.29 e precedenti. Vedi https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . Esegui l'upgrade a Tomcat 6.0.30 e il comportamento tra (Tomcat | Jetty) dovrebbe fondersi.

prova questo

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

Modifica: valido solo per le specifiche servlet 2.5 e successive.

Ho avuto lo stesso problema e l'ho risolto utilizzando il codice del 'servlet predefinito' dalla base di codici Tomcat.

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

Il DefaultServlet è il servlet che serve le risorse statiche (jpg, html, css, gif ecc.) in Tomcat.

Questo servlet è molto efficiente e ha alcune proprietà che hai definito sopra.

Penso che questo codice sorgente sia un buon modo per avviare e rimuovere le funzionalità o le dipendenze non necessarie.

  • I riferimenti al pacchetto org.apache.naming.resources possono essere rimossi o sostituiti con il codice java.io.File.
  • I riferimenti al pacchetto org.apache.catalina.util sono probabilmente solo metodi / classi di utilità che possono essere duplicati nel codice sorgente.
  • I riferimenti alla classe org.apache.catalina.Globals possono essere incorporati o rimossi.

Ho trovato un ottimo tutorial sul web su qualche soluzione alternativa. È semplice ed efficiente, l'ho usato in diversi progetti con l'approccio agli stili url REST:

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

L'ho fatto estendendo il tomcat DefaultServlet ( src ) e sovrascrivendo il metodo 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);
   }
}

... Ed ecco i miei mapping servlet

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

Per servire tutte le richieste da un'app Spring e da /favicon.ico e i file JSP da / WEB-INF / jsp / * richiesti da SpringUstructBasedView di Spring, puoi semplicemente rimappare il servlet jsp e il servlet predefinito:

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

Non possiamo fare affidamento sul modello di URL * .jsp sulla mappatura standard per il servlet jsp perché il modello di percorso '/ *' viene confrontato prima di controllare qualsiasi mappatura di estensione. Mappare il servlet jsp su una cartella più profonda significa che prima è abbinato. La corrispondenza "/favicon.ico" avviene esattamente prima della corrispondenza del modello di percorso. Corrispondenze di percorso più profonde funzioneranno o corrispondenze esatte, ma nessuna corrispondenza di estensione può superare la corrispondenza del percorso '/ *'. Il mapping '/' al servlet predefinito non sembra funzionare. Penseresti che l'esatto '/' batterebbe il pattern del percorso '/ *' su springapp.

La soluzione di filtro sopra non funziona per le richieste JSP inoltrate / incluse dall'applicazione. Per farlo funzionare ho dovuto applicare direttamente il filtro a springapp, a quel punto la corrispondenza del pattern url era inutile poiché tutte le richieste che vanno all'applicazione vanno anche ai suoi filtri. Quindi ho aggiunto il pattern matching al filtro e poi ho imparato a conoscere il servlet 'jsp' e ho visto che non rimuove il prefisso del percorso come fa il servlet predefinito. Ciò ha risolto il mio problema, che non era esattamente lo stesso ma abbastanza comune.

Controllato per Tomcat 8.x: le risorse statiche funzionano correttamente se la mappa del servlet root su " " ;. Per servlet 3.x potrebbe essere eseguito da @WebServlet("")

Usa org.mortbay.jetty.handler.ContextHandler. Non hai bisogno di componenti aggiuntivi come StaticServlet.

A casa del molo,

$ cd contesti

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

Imposta il valore di contextPath con il tuo prefisso URL e imposta il valore di resourceBase come percorso del file del contenuto statico.

Ha funzionato per me.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top