Pergunta

I implantar um webapp em dois recipientes diferentes (Tomcat e Jetty), mas seus servlets padrão para servir o conteúdo estático têm uma maneira diferente de lidar com a estrutura de URL que deseja usar ( detalhes ).

Por isso, estou olhando para incluir um pequeno servlet no webapp para servir o seu próprio conteúdo estático (imagens, CSS, etc.). O servlet deve ter as seguintes propriedades:

  • Não há dependências externas
  • Simples e confiável
  • Suporte para If-Modified-Since cabeçalho (ou seja personalizado getLastModified método)
  • (Opcional) suporte para codificação gzip, etags, ...

É tal servlet disponível em algum lugar? O mais próximo que posso encontrar é href="http://www.unix.org.ua/orelly/java-ent/servlet/ch04_04.htm#ch04-35758" 4-10 do livro servlet.

Update: A estrutura de URL que eu quero usar - no caso de você está querendo saber - é simplesmente:

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

Assim, todos os pedidos devem ser passados ??para o servlet principal, a menos que eles são para o caminho static. O problema é que servlet padrão do Tomcat não leva a ServletPath em conta (por isso procura os arquivos estáticos na pasta principal), enquanto Jetty faz (de modo que parece na pasta static).

Foi útil?

Solução 5

acabei rolando minha própria StaticServlet. Ele suporta If-Modified-Since, codificação gzip e deve ser capaz de servir arquivos estáticos a partir de guerra arquivos também. Não é um código muito difícil, mas não é totalmente trivial qualquer um.

O código está disponível: StaticServlet.java . Sinta-se livre para comentar.

Update: Khurram pergunta sobre a classe ServletUtils que é referenciado no StaticServlet. É simplesmente uma classe com métodos auxiliares que eu usei para o meu projeto. O único método que você precisa é coalesce (que é idêntico à função SQL COALESCE ). Este é o código:

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

Outras dicas

Eu vim com uma solução ligeiramente diferente. É um pouco truque-ish, mas aqui é o mapeamento:

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

Isso basicamente apenas mapeia todos os arquivos de conteúdo, por extensão, para o servlet padrão, e tudo mais para "myAppServlet".

Ele funciona tanto em Jetty e Tomcat.

Não há necessidade de implementação totalmente personalizado do servlet padrão neste caso, você pode usar este servlet simples para embrulhar pedido para a implementação do container:


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

Eu tive bons resultados com FileServlet , uma vez que suporta praticamente todos HTTP (etags, chunking, etc.).

modelo abstrato para um recurso estático servlet

Em parte baseado em neste blog a partir de 2007, aqui está um modernizado e altamente molde abstrato reutilizável para um servlet que corretamente lida com armazenamento em cache, ETag, If-None-Match e If-Modified-Since. (mas nenhum apoio Gzip e Gama, basta mantê-lo simples; Gzip poderia ser feito com um filtro ou através de configuração container)

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

}

Use-o juntamente com o abaixo da interface que representa um 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;

}

Tudo que você precisa é apenas se estende desde a dada servlet abstrata e implementar o método getStaticResource() acordo com o javadoc.

exemplo concreto que serve de sistema de arquivos:

Aqui está um exemplo concreto que serve-lo através de uma URL como /files/foo.ext do sistema de arquivos do 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();
            }
        };
    }

}

exemplo concreto que serve de base de dados:

Aqui está um exemplo concreto que serve-lo através de uma URL como /files/foo.ext do banco de dados através de uma chamada de serviço EJB que retorna a sua entidade dotada de uma propriedade 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 julgar pelo exemplo informações acima, eu acho que este artigo inteiro é baseado em um comportamento grampeado no Tomcat 6.0.29 e anteriores. Consulte https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . Actualiza para o Tomcat 6.0.30 e o comportamento entre (Tomcat | Jetty) deve se fundir.

Tente este

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

Edit:. Isso é válido apenas para o servlet 2.5 spec e up

Eu tive o mesmo problema e eu resolvi-lo usando o código do 'servlet default' da base de código Tomcat.

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

O DefaultServlet é o servlet que serve os recursos estáticos (jpg, html, css, gif etc) no Tomcat.

Este servlet é muito eficiente e tem algumas das propriedades que você definiu anteriormente.

Eu acho que este código-fonte, é uma boa maneira de começar e remover a funcionalidade ou depedencies você não precisa.

  • As referências ao pacote org.apache.naming.resources pode ser removido ou substituído com código java.io.File.
  • As referências ao pacote org.apache.catalina.util são Propably apenas métodos de utilitário / classes que podem ser duplicados em seu código-fonte.
  • As referências à classe org.apache.catalina.Globals pode ser embutido ou removido.

Eu achei ótimo tutorial na web sobre alguma solução. É simples e eficiente, eu usei-o em vários projectos com estilos RESTO urls aproximar:

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

Eu fiz isso estendendo o tomcat DefaultServlet ( src ) e substituindo o 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);
   }
}

... E, aqui estão minhas mapeamentos 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>  

Para atender a todos os pedidos de um aplicativo Primavera, bem como /favicon.ico e os arquivos JSP de / WEB-INF / jsp / * que AbstractUrlBasedView da Primavera irá solicitar você pode apenas remapear o servlet jsp e padrão servlet:

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

Não podemos contar com o * .jsp url-padrão sobre o mapeamento padrão para o servlet jsp porque o padrão caminho '/ *' é aplicado antes de qualquer mapeamento de extensão está marcada. Mapear o servlet jsp para um meio de pasta mais profundas é combinados em primeiro lugar. Matching '/favicon.ico' acontece exatamente antes de correspondência padrão caminho. partidas caminho mais profundas vai funcionar, ou correspondências exatas, mas nenhum resultado de extensão pode fazê-lo após a '/ *' match caminho. Mapping '/' para servlet padrão não parece trabalho. Você acha que a exata '/' iria bater o '* /' padrão caminho no springapp.

A solução de filtro acima não funciona para solicitações encaminhadas / incluídos JSP a partir da aplicação. Para fazê-lo funcionar eu tinha que aplicar o filtro para springapp diretamente, altura em que a correspondência url-padrão era inútil como todas as solicitações que vão para o aplicativo também ir para os seus filtros. Então eu adicionei correspondência padrão para o filtro e, em seguida, aprendeu sobre o 'jsp' servlet e viu que não remove o prefixo do caminho como o servlet padrão faz. Isso resolveu o meu problema, que não era exatamente o mesmo, mas bastante comum.

verificado para Tomcat 8.x: recursos estáticos trabalho OK se mapa servlet raiz para "". Para 3.x servlet isso poderia ser feito por @WebServlet("")

Use org.mortbay.jetty.handler.ContextHandler. Você não precisa de componentes adicionais como StaticServlet.

Na casa cais,

$ cd contextos

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

Definir o valor de contextPath com o prefixo de URL, e definir o valor de resourceBase como o caminho do conteúdo estático arquivo.

Ela trabalhou para mim.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top