Serwlet do obsługi zawartości statycznej


145

Wdrażam aplikację internetową w dwóch różnych kontenerach (Tomcat i Jetty), ale ich domyślne serwlety do obsługi treści statycznej mają inny sposób obsługi struktury adresu URL, którego chcę używać ( szczegóły ).

Dlatego chcę dołączyć mały serwlet do aplikacji internetowej, który będzie obsługiwał własną statyczną zawartość (obrazy, CSS itp.). Serwlet powinien posiadać następujące właściwości:

  • Brak zależności zewnętrznych
  • Prosty i niezawodny
  • Obsługa If-Modified-Sincenagłówka (tj. getLastModifiedMetoda niestandardowa )
  • (Opcjonalnie) obsługa kodowania gzip, etags, ...

Czy taki serwlet jest gdzieś dostępny? Najbliższy, jaki mogę znaleźć, to przykład 4-10 z książki serwletów.

Aktualizacja: struktura adresu URL, której chcę użyć - na wypadek, gdybyś się zastanawiał - to po prostu:

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

Dlatego wszystkie żądania powinny być przekazywane do głównego serwletu, chyba że dotyczą staticścieżki. Problem polega na tym, że domyślny serwlet Tomcata nie bierze pod uwagę ścieżki ServletPath (więc szuka plików statycznych w folderze głównym), podczas gdy Jetty robi to (więc szuka w staticfolderze).


Czy możesz bardziej szczegółowo opisać „strukturę adresu URL”, której chcesz użyć? Wyrzucenie własnego, w oparciu o połączony przykład 4-10, wydaje się banalnym wysiłkiem. Sam to robiłem wiele razy ...
Stu Thompson,

Edytowany moje pytanie do opracowania struktury adresów URL. I tak, skończyło się na rolowaniu własnego serwletu. Zobacz moją odpowiedź poniżej.
Bruno De Fraine,

1
Dlaczego nie używasz serwera WWW do treści statycznych?
Stephen

4
@Stephen: bo nie zawsze jest Apache przed Tomcat / Jetty. Aby uniknąć kłopotów związanych z oddzielną konfiguracją. Ale masz rację, mogę rozważyć tę opcję.
Bruno De Fraine

Po prostu nie mogę zrozumieć, dlaczego nie użyłeś takiego mapowania <servlet-mapping> <servlet-name> default </servlet-name> <url-pattern> / </url-pattern> </ servlet-mapping > do obsługi treści statycznych
Maciek Kreft

Odpowiedzi:


53

Wymyśliłem nieco inne rozwiązanie. To trochę hakerskie, ale oto mapowanie:

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

To po prostu odwzorowuje wszystkie pliki treści według rozszerzenia na domyślny serwlet, a wszystko inne na „myAppServlet”.

Działa zarówno w Jetty, jak i Tomcat.


13
właściwie możesz dodać więcej niż jeden tag wzorca adresu URL wewnątrz mapowania servelet;)
Fareed Alnamrouti

5
Servlet 2.5 i nowsze obsługują wiele znaczników wzorca
adresu

Po prostu uważaj na pliki indeksowe (index.html), ponieważ mogą one mieć pierwszeństwo przed serwletem.
Andres

Myślę, że to zły pomysł *.sth. Jeśli ktoś otrzyma adres URL example.com/index.jsp?g=.sth, otrzyma źródło pliku jsp. Albo nie mam racji? (Jestem nowy w Java EE) Zwykle używam wzorca /css/*
adresu

46

W tym przypadku nie ma potrzeby całkowicie niestandardowej implementacji domyślnego serwletu, można użyć tego prostego serwletu, aby opakować żądanie w implementację kontenera:


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

To pytanie ma zgrabny sposób mapowania / na kontroler i / statyczne na zawartość statyczną za pomocą filtra. Sprawdź upvoted odpowiedź po akceptowanej jednym: stackoverflow.com/questions/870150/...
David Carboni


30

Osiągnąłem dobre wyniki z serwletem FileServlet , ponieważ obsługuje on prawie cały protokół HTTP (etagi, fragmenty itp.).


Dzięki! godzin nieudanych prób i złych odpowiedzi, a to rozwiązało mój problem
Yossi Shasho

4
Chociaż w celu udostępnienia treści z folderu poza aplikacją (używam go do serwera folderu z dysku, powiedzmy C: \ resources) zmodyfikowałem ten wiersz: this.basePath = getServletContext (). GetRealPath (getInitParameter ("basePath ")); I zamieniłem go na: this.basePath = getInitParameter ("basePath");
Yossi Shasho

1
Zaktualizowana wersja jest dostępna pod adresem showcase.omnifaces.org/servlets/FileServlet
koppor

26

Abstrakcyjny szablon dla statycznego serwletu zasobów

Częściowo oparty na tym blogu z 2007 roku, oto zmodernizowany i wielokrotnego użytku szablon abstrakcyjny dla serwletu, który prawidłowo radzi sobie z buforowaniem ETag, If-None-Matchi If-Modified-Since(ale bez obsługi Gzip i Range; tylko dla uproszczenia; Gzip można zrobić z filtrem lub przez konfiguracja kontenera).

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

}

Używaj go razem z poniższym interfejsem reprezentującym statyczny zasób.

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;

}

Wszystko, czego potrzebujesz, to po prostu rozszerzenie z podanego abstrakcyjnego serwletu i zaimplementowanie getStaticResource()metody zgodnie z javadoc.

Konkretny przykład serwujący z systemu plików:

Oto konkretny przykład, który podaje go za pośrednictwem adresu URL, takiego jak /files/foo.extz lokalnego systemu plików dysku:

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

}

Konkretny przykład z bazy danych:

Oto konkretny przykład, który wyświetla go za pośrednictwem adresu URL, takiego jak /files/foo.extz bazy danych, za pośrednictwem wywołania usługi EJB, które zwraca jednostkę posiadającą byte[] contentwłaściwość:

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

}

1
Drogi @BalusC Myślę, że podejście jest podatny na hakera, który wysyłającego następującą prośbę mógłby poruszać koryta systemu plików: files/%2e%2e/mysecretfile.txt. To żądanie produkuje files/../mysecretfile.txt. Przetestowałem to na Tomcat 7.0.55. Nazywają to wspinaczką katalogową: owasp.org/index.php/Path_Traversal
Cristian Arteaga

1
@Cristian: Tak, możliwe. Zaktualizowałem przykład, aby pokazać, jak temu zapobiec.
BalusC

To nie powinno dostać pozytywnych głosów. Udostępnianie statycznych plików dla strony internetowej za pomocą takiego serwletu jest przepisem na bezpieczeństwo w przypadku katastrof. Wszystkie takie problemy zostały już rozwiązane i nie ma powodu, aby wdrażać nowy niestandardowy sposób, w którym prawdopodobnie wybuchną więcej nieodkrytych bomb zegarowych. Prawidłową ścieżką jest skonfigurowanie Tomcat / GlassFish / Jetty itp. Do obsługi treści, a nawet lepiej użyć dedykowanego serwera plików, takiego jak NGinX.
Leonhard Printz

@LeonhardPrintz: Usunę odpowiedź i powiadomię moich przyjaciół z Tomcat, gdy wskażesz problemy z bezpieczeństwem. Nie ma problemu.
BalusC

19

Skończyło się na tym, że skręciłem własne StaticServlet. Obsługuje If-Modified-Sincekodowanie gzip i powinien być w stanie obsługiwać również pliki statyczne z plików wojennych. Nie jest to bardzo trudny kod, ale nie jest też całkowicie trywialny.

Kod jest dostępny: StaticServlet.java . Zapraszam do komentowania.

Aktualizacja: Khurram pyta o ServletUtilsklasę, do której się odwołuje StaticServlet. To po prostu klasa z metodami pomocniczymi, których użyłem w swoim projekcie. Jedyną potrzebną metodą jest coalesce(która jest identyczna z funkcją SQL COALESCE). To jest kod:

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

2
Nie nazywaj swojej wewnętrznej klasy Błąd. Może to spowodować zamieszanie, ponieważ można go pomylić z java.lang.Error. Czy Twój plik web.xml jest taki sam?
Leonel

Dziękujemy za ostrzeżenie o błędzie. web.xml jest taki sam, z „default” zamienionym na nazwę StaticServlet.
Bruno De Fraine,

1
Jeśli chodzi o sposób łączyć, można go zastąpić (wewnątrz Servlet klasy) przez commons-lang StringUtils.defaultString (String, String)
Mike Minicki

Metodę transferStreams () można również zastąpić metodą Files.copy (is, os);
Gerrit Brink,

Dlaczego to podejście jest tak popularne? Dlaczego ludzie ponownie wdrażają statyczne serwery plików w ten sposób? Jest tak wiele luk w zabezpieczeniach, które tylko czekają na odkrycie i tak wiele funkcji prawdziwych statycznych serwerów plików, które nie są zaimplementowane.
Leonhard Printz

12

Sądząc po przykładowych informacjach powyżej, myślę, że cały ten artykuł jest oparty na błędnym zachowaniu w Tomcat 6.0.29 i wcześniejszych. Zobacz https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . Zaktualizuj do Tomcat 6.0.30, a zachowanie między (Tomcat | Jetty) powinno się scalić.


1
Z tego też rozumiem svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/. Nareszcie, po zaznaczeniu tego WONTFIX +3 lata temu!
Bruno De Fraine

12

Spróbuj tego

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

Edycja: dotyczy to tylko specyfikacji serwletu 2.5 i nowszych.


Wygląda na to, że to nie jest prawidłowa konfiguracja.
Gedrox

10

Miałem ten sam problem i rozwiązałem go używając kodu „domyślnego serwletu” z bazy kodu Tomcat.

https://github.com/apache/tomcat/blob/master/java/org/apache/catalina/servlets/DefaultServlet.java

DefaultServlet jest aplet, który służy zasoby statyczne (jpg, html, css, gif itp) w Tomcat.

Ten serwlet jest bardzo wydajny i ma pewne właściwości zdefiniowane powyżej.

Myślę, że ten kod źródłowy jest dobrym sposobem na rozpoczęcie i usunięcie funkcjonalności lub zależności, których nie potrzebujesz.

  • Odniesienia do pakietu org.apache.naming.resources można usunąć lub zastąpić kodem java.io.File.
  • Odniesienia do pakietu org.apache.catalina.util są prawdopodobnie tylko narzędziami / klasami narzędzi, które można powielić w kodzie źródłowym.
  • Odwołania do klasy org.apache.catalina.Globals można wstawiać lub usuwać.

Wydaje się, że zależy to od wielu rzeczy z org.apache.*. Jak możesz go używać z Jetty?
Bruno De Fraine,

Masz rację, ta wersja ma zbyt wiele zależności od Tomcata (ca i obsługuje również wiele rzeczy, których możesz nie chcieć.
Zmienię


4

Zrobiłem to, rozszerzając tomcat DefaultServlet ( src ) i nadpisując metodę 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);
   }
}

... A oto moje mapowania serwletów

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

1

Aby obsłużyć wszystkie żądania z aplikacji Spring, a także /favicon.ico i pliki JSP z / WEB-INF / jsp / *, których zażąda Spring's AbstractUrlBasedView, możesz po prostu przemapować serwlet jsp i domyślny serwlet:

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

Nie możemy polegać na wzorcu adresu URL * .jsp w standardowym mapowaniu serwletu jsp, ponieważ wzorzec ścieżki „/ *” jest dopasowywany przed sprawdzeniem mapowania rozszerzenia. Mapowanie serwletu jsp do głębszego folderu oznacza, że ​​jest on dopasowywany jako pierwszy. Dopasowanie „/favicon.ico” ma miejsce dokładnie przed dopasowaniem wzorca ścieżki. Głębsze dopasowania ścieżek będą działać lub dokładne dopasowania, ale żadne dopasowania rozszerzeń nie mogą wyjść poza dopasowanie ścieżki „/ *”. Mapowanie „/” do domyślnego serwletu nie działa. Można by pomyśleć, że dokładne „/” pokonałoby wzorzec ścieżki „/ *” na springapp.

Powyższe rozwiązanie filtrujące nie działa w przypadku przekazanych / dołączonych żądań JSP z aplikacji. Aby to zadziałało, musiałem zastosować filtr bezpośrednio do springapp, w którym to momencie dopasowanie wzorca adresu URL było bezużyteczne, ponieważ wszystkie żądania, które trafiają do aplikacji, również trafiają do jej filtrów. Dodałem więc dopasowywanie wzorców do filtru, a potem dowiedziałem się o aplecie „jsp” i zobaczyłem, że nie usuwa on przedrostka ścieżki, tak jak robi to domyślny aplet. To rozwiązało mój problem, który nie był dokładnie taki sam, ale dość powszechny.


1

Zaznaczone dla Tomcat 8.x: zasoby statyczne działają poprawnie, jeśli serwlet główny jest odwzorowany na „”. W przypadku serwletu 3.x można to zrobić przez@WebServlet("")


0

Użyj org.mortbay.jetty.handler.ContextHandler. Nie potrzebujesz dodatkowych komponentów, takich jak StaticServlet.

W domu na molo

$ cd konteksty

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

Ustaw wartość contextPath z prefiksem adresu URL i ustaw wartość resourceBase jako ścieżkę do pliku zawartości statycznej.

U mnie to zadziałało.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.