java - addresourcehandlers - mvc resources spring 4
Servlet para servir contenido estático (15)
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 ( details ).
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:
- Sin dependencias externas
- Simple y confiable
- Compatibilidad con el encabezado
If-Modified-Since
(es decir, métodogetLastModified
personalizado) - (Opcional) soporte para codificación gzip, etags, ...
¿Hay un servlet disponible en algún lado? Lo más cercano que puedo encontrar es el ejemplo 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 se deben pasar al servlet principal, a menos que sean para la ruta static
. El problema es que el servlet predeterminado de Tomcat no tiene en cuenta ServletPath (por lo que busca los archivos estáticos en la carpeta principal), mientras que Jetty lo hace (por lo que se ve en la carpeta static
).
Plantilla abstracta para un servlet de recursos estáticos
En parte basado en este blog de 2007, aquí hay una plantilla abstracta modernizada y altamente reutilizable para un servlet que trata adecuadamente con el almacenamiento en caché, ETag
, If-None-Match
y If-Modified-Since
(pero no soporte de Gzip y Range, solo para mantenerlo simple, Gzip podría hacerse con un filtro o mediante la configuración del 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><mime-type></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 extender desde el servlet abstracto dado e implementar el método getStaticResource()
acuerdo con javadoc.
Ejemplo concreto sirviendo 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, name);
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
de la base de datos a través de una llamada de servicio EJB que devuelve a su entidad una propiedad de byte[] content
:
@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 bug en Tomcat 6.0.29 y anteriores. Ver 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.
Comprobado para Tomcat 8.x: los recursos estáticos funcionan bien si el mapa del servlet raíz es "". Para el servlet 3.x, podría ser hecho por @WebServlet("")
Encontré un excelente tutorial en la web sobre algunas soluciones. Es simple y eficiente, lo utilicé en varios proyectos con el enfoque de estilos de URL REST:
http://www.kuligowski.pl/java/rest-style-urls-and-url-mapping-for-static-content-apache-tomcat,5
He tenido buenos resultados con FileServlet , ya que es compatible con casi todos los HTTP (etags, fragmentación, etc.).
Hice esto extendiendo el Tomcat DefaultServlet ( http://svn.apache.org/repos/asf/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java ) 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 mapas de 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>
No es necesario implementar completamente el servlet predeterminado en este caso, puede usar este servlet simple para ajustar 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);
}
}
Para atender todas las solicitudes desde una aplicación de Spring, así como /favicon.ico y los archivos JSP de / WEB-INF / jsp / * que Spring''s AbstractUrlBasedView le solicitará, puede simplemente reasignar 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 ''/ *'' se corresponde antes de que se verifique cualquier mapeo de extensión. Al asignar el servlet jsp a una carpeta más profunda, significa que coincide primero. La coincidencia de ''/favicon.ico'' ocurre exactamente antes de la coincidencia de patrón de ruta. Las coincidencias de ruta más profundas funcionarán, o las coincidencias exactas, pero ninguna coincidencia de extensión puede superar la coincidencia de ruta ''/ *''. La asignación de ''/'' al servlet predeterminado no parece funcionar. Pensarías que el ''/'' exacto superaría el patrón de ruta ''/ *'' en springapp.
La solución de filtro anterior no funciona para solicitudes JSP enviadas / incluidas desde la aplicación. Para que funcione, tuve que aplicar el filtro a SpringApp directamente, momento en el cual el emparejamiento de url-pattern era 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 resolvió mi problema, que no era exactamente lo mismo pero lo suficientemente común.
Se me ocurrió una solución ligeramente diferente. Es un poco hack-ish, pero aquí está el mapeo:
<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 simplemente asigna todos los archivos de contenido al servlet predeterminado y todo lo demás a "myAppServlet".
Funciona tanto en Jetty como en Tomcat.
Terminé de rodar mi propio StaticServlet
. Admite la codificación If-Modified-Since
, gzip y también debe poder servir archivos estáticos de war-files. No es un código muy difícil, pero tampoco es del todo trivial.
El código está disponible: StaticServlet.java . Siéntete libre de comentar
Actualización: Khurram pregunta acerca de la clase ServletUtils
la que se hace referencia en StaticServlet
. Es simplemente una clase con métodos auxiliares que utilicé 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;
}
Tuve el mismo problema y lo resolví utilizando el código del ''servlet predeterminado'' de la base de código de Tomcat.
http://svn.apache.org/repos/asf/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java
El DefaultServlet es el servlet que sirve 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 comenzar y eliminar la funcionalidad o 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 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.
Use org.mortbay.jetty.handler.ContextHandler. No necesita componentes adicionales como StaticServlet.
En el embarcadero a casa,
$ 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í
Ver StaticFile en JSOS: http://www.servletsuite.com/servlets/staticfile.htm
los archivos estáticos son servlet por servlet predeterminado, y puede configurar la extensión por separado en web.xml
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
si su archivo no es * .js, * .css y desea mostrarlo en el navegador, debe configurar el mapeo de mimo
<mime-mapping>
<extension>wsdl</extension>
<mime-type>text/xml</mime-type>
</mime-mapping>
y su archivo (por ejemplo: wsdl) se mostrará como texto en el navegador
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 las especificaciones de servlet 2.5 y superiores.