Tomcat und die Anzeige von Bildern innerhalb des Servlets

Ich verwende ein Servlet, nennen wir es BlaServlet, auf einem Tomcat-Server (Version 9.0.7), welches HTML ausliefert. Innerhalb des HTMLs möchte ich Bilder anzeigen, welche innerhalb von BlaServlet liegen.

Diese werden auch richtig ins war-File gepackt und landen dann nach dem autodeploy von Tomcat unter ...\apache-tomcat-9.0.7\webapps\Bla\resources\.

Erzeugt in der HTML-Seite wird der Link auf die dort liegende Datei logo.png so erzeugt:

    <img src="/resources/logo.png"
        alt="Logo"
        width="800" height="144" />

Es wird nur der in alt genannte Text angezeigt, nicht aber das Logo.
Ich habe es auch versucht mit

<!DOCTYPE html>
<html>
    <head>
        <title>Bild-Test</title>
    </head>

    <body>
        <p>1</p>
        <img src="/resources/logo.png" alt="Logo 1" />

        <p>2</p>
        <img src="/Bla/resources/logo.png" alt="Logo 2" />

        <p>3</p>
        <img src="Bla/resources/logo.png" alt="Logo 3" />

        <p>4</p>
        <img src="resources/logo.png" alt="Logo 4" />

        <p>5</p>
        <img src="<%=getServletContext().getContextPath()%>/resources/logo.png" alt="Logo 5" />
    </body>
</html>

Nichts davon zeigt ein Bild an… nach Recherche im Netz kam mir der Verdacht, ob es an der Datei web.xml und dem dortigen url-pattern liegen könnte. Das sieht wie folgt aus, mit Anmerkungen, was ich ausprobiert habe:

<?xml version = "1.0" encoding = "UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>BlaServlet</servlet-name>
        <servlet-class>de.xyz.webservlet.webserver.BlaServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>BlaServlet</servlet-name>
        <!-- <url-pattern>/*</url-pattern> --> <!-- lädt das Servlet, aber keine Bilder -->
        <!-- <url-pattern>/Bla</url-pattern> -->  <!-- lädt das Servlet nicht -->
        <url-pattern>/</url-pattern> <!-- lädt das Servlet, aber keine Bilder -->
    </servlet-mapping>
</web-app>

Ich könnte natürlich einen Apache-Server aufsetzen, die Bilder dorthin legen und von dort anzeigen, aber eigentlich kann das ja nur ein Konfigurationsproblem von mir sein.

Hat jemand vielleicht eine Idee? Ich habe schon sehr viel gegoogelt, aber nichts von dem was ich fand hat das Problem gelöst.

Aufgerufen wird das im Browser mit

http://.../Bla/

was auch das erzeugte HTML anzeigt, aber eben das Bild darin nicht.

Ich habe keine Ahnung von JavaEE/JakartaEE. Aber ich würde mal gucken, was die Text-Ausgabe von

ist. Passt die überhaupt?
Zu diesem Thema wäre sicher auch ein Wiki-Artikel interesant. Lust, den anzufertigen?

Wenn ich rausfinde, wie das geht, klar.

Ich bin erstmal 8 Tage im Urlaub, also bitte nicht über ausbleibende Reaktionen wundern. Aber danach kann ich jede Hilfe hier finden.

Das gibt

<%=getServletContext().getContextPath()%>

aus … Hmm.

Falls jemand hier noch eine Idee hat, wäre ich dankbar. Ansonsten muss ich wohl einen Apache-Webserver daneben legen, auf dem ich die Bilder unterbringe, aber nach meinem Verständnis müsste das eigentlich nicht sein.

Das dürfte aber nach meinem Verständnis nicht sein. Es sieht dann doch so aus, als würde dieses Code-Fragment gar nicht ausgeführt werden.

Was wird denn bspw ausgegeben, wenn du das in deinem Code schreibst:

<%= "www.byte-welt.net" %>

Bin mir nicht ganz sicher ob ich das Problem richtig verstanden habe, aber ich bin auf folgenden Wiki-Artikel bei Coderanch gestoßen: https://coderanch.com/wiki/659912/Resource-Url-Problems

Dort wird geraten den Context-Path dynamisch in die URL zu packen.

But we never want to hard-code the context path into our URLs. Bad idea! Very bad idea!

We want to dynamically grab the context path so that it doesn’t matter to the pages what the context path happens to be. To do so in JSP 2.0, we can use an EL expression as follows:

${pageContext.request.contextPath}

Greta, bei dir scheint der Ansatz ja der gleiche zu sein. Aber offenbar wird dieses in spitzige Prozentzeichen eingebettete gar nicht interpretiert.

Und auch auf die verpönte hart-kodierte Weise lässt sich das Bild nicht erreichen. Seltsam.

Ich hab mir mal mit

String contextPath = request.getServletContext().getContextPath();

den Context-Path ermittelt und an das Programm übergeben, welches das HTML erstellt. Dort steht einfach nur /Bla drin. Und natürlich wird das Bild nicht gefunden, auch nicht unter Verwendung des Context-Pathes.

Versucht man per Hand

http://.../Bla/resources/logo.png

anzusurfen, landet man nur auf der Hauptseite, als hätte man

http://.../Bla

eingegeben.

Ja, ist gar nicht ueberraschend, hast es ja so konfiguriert das alles nach / zum Serlvet geleitet wird:
<url-pattern>/</url-pattern>

Ansonsten hat @Greta vollkommen recht dass man den ContextPath nicht hardcoden sollte, aber dein momentanes Problem ist nicht der ContextPath, sondern das servlet-mapping.

Ich habe das Problem gelöst. Man muss sich um das Ausliefern der Bilder selber kümmern.
Entweder schreibt man sich dafür ein eigenes Servlet, siehe


(Ich habe mich dort an die vereinfachte Variante unter “Abstract template for a static resource servlet” gehalten)

und insbesondere

http://showcase.omnifaces.org/servlets/FileServlet

Oder man bastelt es in das eigene Servlet hinein, diesen Weg habe ich beschritten:

Ich habe in der web.xml nun im <servlet-mapping>

    <url-pattern>/*</url-pattern>
    <url-pattern>*.png</url-pattern>

drinstehen, dann habe ich in meiner Get-Methode des Servlets die Anforderungsfälle gesplittet:

@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) {
    String pathInfo = request.getPathInfo();

    if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
        handleNormalGet(request, response);
    }
    else {
        PictureServing serving = new PictureServing(request, response, pathInfo,
                getServletContext());
        serving.serve();
    }
}

Die Klasse PictureServing ist dann zusammengebastelt aus den Klassen oben im Link:

Die Klasse PictureServing
package de.XYZ.webserver;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class PictureServing {

    private static final long DEFAULT_EXPIRE_TIME_IN_MILLIS = TimeUnit.DAYS.toMillis(30);
    private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1);

    private static final String ETAG_HEADER = "W/\"%s-%s\"";
    private static final int DEFAULT_STREAM_BUFFER_SIZE = 102400;

    private static final String CONTENT_DISPOSITION_HEADER =
            "inline;filename=\"%1$s\"; filename*=UTF-8''%1$s";

    private final HttpServletRequest request;
    private final HttpServletResponse response;
    private final String pathInfo;
    private final ServletContext servletContext;
    private final File folder;

    public PictureServing(HttpServletRequest request, HttpServletResponse response, String pathInfo,
            ServletContext servletContext) {
        this.request = request;
        this.response = response;
        this.pathInfo = pathInfo;
        this.servletContext = servletContext;
        folder = new File("c:\\ABSOLUTER\\PFAD\\ZUM\\SERVLET\\apache-tomcat-9.0.7\\webapps\\BLA");
    }

    public void serve() {
        if (isSupportedPictureType()) {
            serveSupportedPictureType();
        }
        else {
            sendErrorNotFound();
        }
    }

    private boolean isSupportedPictureType() {
        return pathInfo.toLowerCase().endsWith(".png");
    }

    private void sendErrorNotFound() {
        try {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
        catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private void serveSupportedPictureType() {
        try {
            tryToServeSupportedPictureType();
        }
        catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private void tryToServeSupportedPictureType() throws IOException {
        String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
        File file = new File(folder, name);

        if (!file.exists()) {
            sendErrorNotFound();
            return;
        }

        //response.reset();

        String fileName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8.name());
        boolean notModified = setCacheHeaders(fileName, file.lastModified());

        if (notModified) {
            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        setContentHeaders(fileName, file.length());

        writeContent(file);
    }

    private boolean setCacheHeaders(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(eTag, lastModified);
    }

    private boolean notModified(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(String fileName, long contentLength) {
        response.setHeader("Content-Type", servletContext.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(File file) throws IOException {
        InputStream inputStream = new FileInputStream(file);
        try (
            ReadableByteChannel inputChannel = Channels.newChannel(inputStream);
            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 (file.length() == -1 && !response.isCommitted()) {
                response.setHeader("Content-Length", String.valueOf(size));
            }
        }
    }

}

Nee, also damit kann ich mich nicht anfreunden. @maki hat da schon recht.

Aber ich hab das ganze mal kurz nachgebaut und benötige kein extra FileServlet.

Erstmal ein ganz einfaches Servlet das zwei Bilder einbindet duke.png und static/duke.png


import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 *
 * @author ionutbaiu
 */

public class HelloWorld extends HttpServlet {

    /**
     * Processes requests for both HTTP <code>GET</code> and <code>POST</code>
     * methods.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            /* TODO output your page here. You may use following sample code. */
            out.println("<!DOCTYPE html>");
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet HelloWorld</title>");            
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>Servlet HelloWorld here at " + request.getContextPath() + "</h1>");
            out.println("<img src='"+request.getContextPath()+"/duke.png'>");
            out.println("<img src='"+request.getContextPath()+"/static/duke.png'>");
            out.println("</body>");
            out.println("</html>");
        }
    }

    // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
    /**
     * Handles the HTTP <code>GET</code> method.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    /**
     * Handles the HTTP <code>POST</code> method.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    /**
     * Returns a short description of the servlet.
     *
     * @return a String containing servlet description
     */
    @Override
    public String getServletInfo() {
        return "Short description";
    }// </editor-fold>

}

web.xml


<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	 version="3.1">
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    
    <servlet>
        <servlet-name>helloworldservlet</servlet-name>
        <servlet-class>helloworld.HelloWorld</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>helloworldservlet</servlet-name>
        <url-pattern>/</url-pattern>
        <url-pattern>/helloworld</url-pattern>
        <url-pattern>/hello/world</url-pattern>
    </servlet-mapping>        
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.png</url-pattern>
    </servlet-mapping>        
</web-app>

Bild liegt dann einmal im web-Folder und einmal im web-Folder in einem Unterordner static. Ist gerade unter Netbeans als Java Web-Project, mit Eclipse oder Maven kann sich das auch in Abhängigkeit vom jeweiligen build leicht unterscheiden.

Wichtig, ist das servlet-mapping mit dem servlet-name:default. Das ist quasi so ein eingebautes Servlet zum Dateien ausliefern. Was ja tatsächlich häufiger vorkommt :wink: Javascript und so, und deshalb auch vorhanden ist.

Im war-File habe ich dann das Bild einmal auf oberster eben und einmal in einem Ordner static.

Beide Bilder funktionieren wundervoll.

— Edit

Man sollte aber vor die Bilder den ContextPath hängen, weil ansonsten die Bilder relativ zur URL des Servlets addressiert werden müssen.

Das Servlet ist unter nun unter /, /helloworld und /hello/world erreichbar und die URL der Bilder ist immer die selbe, ausgehend vom ContextPath.

2 „Gefällt mir“

Schade, dass ich sowas vorher nicht gefunden habe. Mir kam es auch komisch vor, dass man so rudimentäre Dinge selbst implementieren muss.

Ich probiere das gleich mal aus und berichte, danke dafür!

Seltsamer Weise bekomme ich das Bild nicht zu sehen.

Die Url wird so erzeugt:

"<img src=\"" + contextPath + "/resources/logo.png\" />"

wobei contextPath per request.getServletContext().getContextPath() ermittelt wird. Ist das ein Unterschied zu deinem request.getContextPath()?

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.png</url-pattern>
</servlet-mapping>

hab ich in die web.xml übernommen und dafür das Mapping auf *.png in meinem Servlet entfernt.

Edit: Hab es hinbekommen, ich hatte es erst nur mit

<url-pattern>/Bla</url-pattern>

versucht, was nicht klappte (404 zur ganzen Anfrage). Dann mit

<url-pattern>/*</url-pattern>

versucht, was mein HTML anzeigte, aber nicht das Bild. Nun mit

<url-pattern>/</url-pattern>
<url-pattern>/Bla</url-pattern>

in dem Abschnitt meines Servlets in der web.xml funktioniert es. Vielen Dank, dann kann ich meine Klasse zur Auslieferung von Bildern wieder entsorgen.

Und vor allem kann ich dann auf den hartkodierten Pfad darin verzichten. Das ist super!

Vielen Dank, @ionutbaiu!

Ist manchmal ein bisschen tricky weil stellt man ja auch nicht täglich ein, mit ein wenig rumprobieren kommt man aber einigermassen dahinter.

So ein eigenes Servlet zum Dateien ausliefern macht meist nur Sinn, wenn man zum Beispiel Bilder und Dokument als Dateien auserhalb des war speichert. Also zum Beispiel ein Instagram baut. Allerdings würde man dann auch eher auf ein CDN zurückgreifen oder Microservices, wie oben schon mit einem eigenen Apache angesprochen, wenn das ganze eine gewisse Grösse erreicht. Oder wenn man noch Authorisierung und sowas vor diese Dateien schalten möchte.

1 „Gefällt mir“

In meinem Fall geht es wirklich nur um das Firmenlogo (und vielleicht noch ein, zwei andere, unveränderliche Bilder). Da scheinen sie mir im War-File gut aufgehoben zu sein.

Schön, dass ich nun eine so viel einfachere Lösung für das Problem habe.

Ich hatte das erste Mal mit einem Servlet zu tun und trotz Buch und viel Lesen im Netz bleiben manche Details etwas schwammig, obwohl sie irgendwo sicher genau definiert sind. Aber nun läuft es ja!