Pregunta

Implemento una aplicación web en dos contenedores diferentes (Tomcat y Jetty), pero sus servlets predeterminados para servir el contenido estático tienen una forma diferente de manejar la estructura de URL que quiero usar ( detalles ).

Por lo tanto, estoy buscando incluir un pequeño servlet en la aplicación web para servir su propio contenido estático (imágenes, CSS, etc.). El servlet debe tener las siguientes propiedades:

¿Hay algún servlet disponible en algún lugar? Lo más cercano que puedo encontrar es example 4-10 del libro de servlets.

Actualización: La estructura de URL que quiero usar, en caso de que se lo pregunte, es simplemente:

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

Por lo tanto, todas las solicitudes deben pasarse al servlet principal, a menos que sean para la ruta static . El problema es que el servlet predeterminado de Tomcat no tiene en cuenta el ServletPath (por lo que busca los archivos estáticos en la carpeta principal), mientras que Jetty sí lo hace (también lo hace en la carpeta static ).

¿Fue útil?

Solución 5

Terminé rodando mi propio StaticServlet . Es compatible con If-Modified-Since , la codificación gzip y también debe poder servir archivos estáticos de archivos war. No es un código muy difícil, pero tampoco es completamente trivial.

El código está disponible: StaticServlet.java . Siéntete libre de comentar.

Actualización: Khurram pregunta acerca de la clase ServletUtils a la que se hace referencia en StaticServlet . Es simplemente una clase con métodos auxiliares que usé para mi proyecto. El único método que necesita es coalesce (que es idéntico a la función SQL COALESCE ). Este es el código:

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

Otros consejos

Se me ocurrió una solución ligeramente diferente. Es un poco hack-ish, pero aquí está el mapa:

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

Básicamente, esto solo asigna todos los archivos de contenido por extensión al servlet predeterminado, y todo lo demás a " myAppServlet "

Funciona tanto en Jetty como en Tomcat.

No hay necesidad de una implementación completamente personalizada del servlet predeterminado en este caso, puede usar este servlet simple para envolver la solicitud a la implementación del contenedor:


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

He tenido buenos resultados con FileServlet , ya que soporta prácticamente todo HTTP (etags, fragmentación, etc.).

Plantilla abstracta para un servlet de recursos estáticos

Parcialmente basado en este blog de 2007, aquí hay una versión modernizada y altamente plantilla abstracta reutilizable para un servlet que se ocupa de manera adecuada del almacenamiento en caché, ETag , If-None-Match y If-Modified-Since (pero no Gzip y soporte de rango; solo para mantenerlo simple; Gzip se puede hacer con un filtro o mediante una configuración de contenedor).

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

}

Úselo junto con la siguiente interfaz que representa un recurso estático.

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;

}

Todo lo que necesita es simplemente extenderse desde el servlet abstracto dado e implementar el método getStaticResource () de acuerdo con el javadoc.

Ejemplo concreto que sirve desde el sistema de archivos:

Aquí hay un ejemplo concreto que lo sirve a través de una URL como /files/foo.ext del sistema de archivos del disco 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();
            }
        };
    }

}

Ejemplo concreto que sirve desde la base de datos:

Aquí hay un ejemplo concreto que lo sirve a través de una URL como /files/foo.ext desde la base de datos a través de una llamada de servicio EJB que devuelve a su entidad un contenido de byte [code>] propiedad:

@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 juzgar por la información de ejemplo anterior, creo que todo este artículo se basa en un comportamiento de error en Tomcat 6.0.29 y versiones anteriores. Consulte https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . Actualice a Tomcat 6.0.30 y el comportamiento entre (Tomcat | Jetty) debería fusionarse.

prueba esto

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

Editar: Esto solo es válido para la especificación del servlet 2.5 y versiones posteriores.

Tuve el mismo problema y lo resolví utilizando el código del "servlet predeterminado" del código base de Tomcat.

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

El DefaultServlet es el servlet que sirve a los recursos estáticos (jpg, html, css, gif, etc.) en Tomcat.

Este servlet es muy eficiente y tiene algunas de las propiedades que definió anteriormente.

Creo que este código fuente es una buena manera de iniciar y eliminar la funcionalidad o las dependencias que no necesita.

  • Las referencias al paquete org.apache.naming.resources se pueden eliminar o reemplazar con el código java.io.File.
  • Las referencias al paquete org.apache.catalina.util son probablemente solo métodos / clases de utilidad que pueden duplicarse en su código fuente.
  • Las referencias a la clase org.apache.catalina.Globals se pueden insertar o eliminar.

Encontré un excelente tutorial en la web sobre algunas soluciones. Es simple y eficiente, lo usé en varios proyectos con el enfoque de estilos de urls REST:

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

Hice esto extendiendo el tomcat DefaultServlet ( src ) y anulando el método 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);
   }
}

... Y aquí están mis asignaciones 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>  

Para atender todas las solicitudes de una aplicación Spring, así como también de /favicon.ico y los archivos JSP de / WEB-INF / jsp / * que Spring's AbstractUrlBasedView le solicitará, puede volver a asignar el servlet jsp y el servlet predeterminado:

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

No podemos confiar en el * .jsp url-pattern en el mapeo estándar para el servlet jsp porque el patrón de ruta '/ *' coincide antes de que se verifique cualquier mapeo de extensión. Asignar el servlet jsp a una carpeta más profunda significa que primero coincide. La coincidencia '/favicon.ico' ocurre exactamente antes de la coincidencia del patrón de ruta. Las coincidencias de ruta más profundas funcionarán, o coincidencias exactas, pero ninguna coincidencia de extensión puede superar la coincidencia de ruta '/ *'. La asignación de '/' al servlet predeterminado no parece funcionar. Usted pensaría que el '/' exacto superaría el patrón de ruta '/ *' en el springapp.

La solución de filtro anterior no funciona para las solicitudes JSP reenviadas / incluidas desde la aplicación. Para hacerlo funcionar, tuve que aplicar el filtro a springapp directamente, momento en el que la comparación de patrones de url fue inútil ya que todas las solicitudes que van a la aplicación también van a sus filtros. Así que agregué la coincidencia de patrones al filtro y luego aprendí sobre el servlet 'jsp' y vi que no elimina el prefijo de ruta como lo hace el servlet predeterminado. Eso solucionó mi problema, que no era exactamente el mismo pero lo suficientemente común.

Se verificó la presencia de Tomcat 8.x: los recursos estáticos funcionan bien si la asignación del servlet raíz a " Para servlet 3.x se puede hacer por @WebServlet("")

Utilice org.mortbay.jetty.handler.ContextHandler. No necesita componentes adicionales como StaticServlet.

En la casa del embarcadero,

$ cd contexts

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

Establezca el valor de contextPath con su prefijo de URL, y establezca el valor de resourceBase como la ruta del archivo del contenido estático.

Funcionó para mí.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top