Statuscode 404 zurückgeben


#1

Ich suche einen eleganten Weg, um bei nicht gefundenen Ressourcen einen 404-Statuscode zurückzugeben. Ein Ansatz wäre folgender:

public String getMeterDetails(@PathVariable("number") final Long number, final Model model) {
    final Meter result = meterService.findOne(number);
    if (result == null)
        throw new MeterNotFoundException();

    model.addAttribute("meter", result);
    return "meters/details";
}```

Die MeterNotFoundException könnte dann so aussehen:
```@ResponseStatus(HttpStatus.NOT_FOUND)
public class MeterNotFoundExcption extends Exception { }```

Oder die Exception wird mit einem ExceptionHandler verarbeitet.

An der Lösung mit der Exception gefällt mir allerdings nicht, dass dadurch für jede Klasse eine Exception nach dem Schema <XYZ>NotFoundException hinzukommt.

Alternativ könnte ich mir auch statt des Exception-werfens auch vorstellen, `return "forward:meters/notfound";` zurückzugeben und die zugehörige Controller-Methode mit `@ResponseStatus(HttpStatus.NOT_FOUND)` zu annotieren. Das hätte den Vorteil, dass ich in der View auch eine spezifische Meldung anzeigen kann und der Nutzer beispielsweise auf eine Übersichtsseite weitergeleitet werden könnte.

Man könnte der oben gezeigten Controller-Methode ja auch noch [japi]HttpServletResponse[/japi] übergeben lassen und darin dann mit `response.setStatus(HttpStatus.NOT_FOUND.value());` den Statuscode anpassen. Dann benötigt man auch keine weitere Methode (und damit auch verbunden eine öffentlich abrufbare Seite) im Controller mehr.

Gibt es noch einen eleganteren Weg, um den korrekten Statuscode zurückzugeben und den Controller möglichst "sauber" zu halten?

#2

was bedeutet hier ‘Meter’? ist das schlicht irgendeine fachliche Klasse oder ein Fachbegriff,
wäre “für jede Klasse eine Exception nach dem Schema NotFoundException hinzukommt” als nächstes UserStatusPicNotFoundExcption?

ich kenne mich in Spring nicht genug aus, so dass vielleicht nicht hilfreich, aber die Frage kommt mir zumindest:
wieso jeweils eigene Klassen, wieso nicht eine zentrale, wegen der Notwendigkeit komischer Annotations?

oder bedeutet return "forward:meters/notfound"; dass eine spezielle Meter-NotFound-Seite zu sehen ist statt einer zentralen für die Anwendung?


ich weiß wiederum nicht inwieweit mit Spring vereinbar, aber ich schlage eine allgemeine RUNTIMEException, RessourceNotFoundException, vor,
diese geht an eine zentrale Verarbeitungsstelle im Programm
(evtl. dazwischenliegende unvermeidbare individuellere catch-Ebenen müssen die Exception durchlassen oder weiterwerfen, nach evtl. nötigen Eigenarbeiten)

an dieser Stelle wird entweder gleich allgemein zu einer generischen 404-Seite weitergeleitet,
oder man kann alternativ noch vorhandene Meta-Informationen auswerten,

nicht unbedingt in der Exception, sondern allgemein zum Request irgendwo hinterlegt,
durch die Verarbeitung sollte bis dahin bekannt sein, welche genau Art Request es ist, z.B. ‘man befindet sich im Bereich des Meters’,

an zentraler Stelle in irgendeiner Enum kann effizient hinterlegt sein, dass im ‘Meter-Bereich’ im 404-Fall statt der generischen 404 eine bestimmte Seite ‘meters/notfound’ anzusteuern ist


#3

[QUOTE=SlaterB]was bedeutet hier ‘Meter’? ist das schlicht irgendeine fachliche Klasse oder ein Fachbegriff,
wäre “für jede Klasse eine Exception nach dem Schema NotFoundException hinzukommt” als nächstes UserStatusPicNotFoundExcption?[/quote]
Ja, Meter ist eine Domänenklasse.

[QUOTE=SlaterB;81575]ich kenne mich in Spring nicht genug aus, so dass vielleicht nicht hilfreich, aber die Frage kommt mir zumindest:
wieso jeweils eigene Klassen, wieso nicht eine zentrale, wegen der Notwendigkeit komischer Annotations?[/quote]
Jein. Wenn ich jeweils eine eigene Exception verwende, dann kann ich jeweils einen eigenen Exceptionhandler schreiben (das ist eigentlich nichts anderes als ein ausgelagerter Catch-Block) und daher auch jeweils auf eine eigene View weiterleiten.

forward:meters/notfound bedeutet, dass intern der Controller “umgebogen” wird und die ganze Requestabarbeitung nochmal so durchgeführt wird, als wenn die Seite http://www.example.com/meters/notfound aufgerufen worden wäre. Es wird also eine andere Controllermethode aufgerufen, die dann wiederum auf eine andere View weiterleitet.

[QUOTE=SlaterB;81575]ich weiß wiederum nicht inwieweit mit Spring vereinbar, aber ich schlage eine allgemeine RUNTIMEException, RessourceNotFoundException, vor,
diese geht an eine zentrale Verarbeitungsstelle im Programm [/quote]
Darüber habe ich auch schon nachgedacht. Prinzipiell ist das auch machbar (ich hatte sogar gehofft, dass Spring selbst eine ResourceNotFoundException definiert, ich konnte aber keine finden), dann ist allerdings nur eine Fehlerseite darstellbar. Die Information, dass ein Erfassungsgerät (= Meter) nicht gefunden wurde, ist dann ggf. verloren (wie man das teilweise umgehen kann, beschreibst du nachfolgend).

[QUOTE=SlaterB;81575]an dieser Stelle wird entweder gleich allgemein zu einer generischen 404-Seite weitergeleitet,
oder man kann alternativ noch vorhandene Meta-Informationen auswerten,

nicht unbedingt in der Exception, sondern allgemein zum Request irgendwo hinterlegt,
durch die Verarbeitung sollte bis dahin bekannt sein, welche genau Art Request es ist, z.B. ‘man befindet sich im Bereich des Meters’,

an zentraler Stelle in irgendeiner Enum kann effizient hinterlegt sein, dass im ‘Meter-Bereich’ im 404-Fall statt der generischen 404 eine bestimmte Seite ‘meters/notfound’ anzusteuern ist[/QUOTE]
Das wäre die unmittelbare Konsequenz aus diesem Vorgehen.


Ich finde meine zuletzt vorgeschlagene Option derzeit am interessantesten:

Konkret sieht das in meinem Controller jetzt so aus:

public String getMeterDetails(@PathVariable("number") final Long number, final Model model, final HttpServletResponse response) {
    final Meter result = meterService.findOne(number);
    if (result == null) {
        response.setStatus(HttpStatus.NOT_FOUND.value());
        return "meters/notfound";
    }

    model.addAttribute("meter", result);
    return "meters/details";
}```
Da habe ich jetzt aber wieder einen Schönheitsmakel. Es gibt recht viele Controllermethoden, die die ersten 5 Zeilen gemein haben:
```@RequestMapping(value = "/{number}/edit", method = RequestMethod.GET)
public String viewEditMeter(@PathVariable("number") final Long number, final Model model, final HttpServletResponse response) {
    final Meter meter = meterService.findOne(number);
    if (meter == null) {
        response.setStatus(HttpStatus.NOT_FOUND.value());
        return "meters/notfound";
    }

    model.addAttribute("meter", meter);
    return "meters/edit";
}```
Das kann ich auch nicht herausfakturieren, weil es zwei verschiedene Rückgabewerte geben müsste und der Behandlungscode so lang wäre, wie das Codefragment selbst. Mit einer Exception hätte ich das Problem nicht.

Vielleicht schwirrt hier ja ein Spring-Experte herum, der mir dafür die Musterlösung präsentieren kann ;-)

Edit: gerne auch einen Weg, wie ich (ohne AOP) eine Controllermethode nur ausführe, falls die übergebene ID gültig ist. Ich hatte da unter anderem an eine mit `@ModelAttribute` annotierte Methode gedacht, allerdings hapert es an der Umsetzung.

#4

Ich weiß noch nicht, ob es etwas bringt, aber ich habe mal ein Crossposting im Spring-Forum erstellt:
http://forum.spring.io/forum/spring-projects/web/743117-clean-way-to-return-customized-404-errorpages


#5

Für alle, die diesen Thread hier per Zufall lesen sollten: die Lösung war so einfach, dass ich sie einfach nicht wahrgenommen habe.

Es reicht eine einzige ResourceNotFoundException aus, weil diese Controllerspezifisch verarbeitet werden kann. Und das führt dazu, dass ich controllerspezifische Fehlermeldungen generieren kann. Genau das möchte ich haben.

*** Edit ***

Nochmal in Gänze:

public class ResourceNotFoundException extends RuntimeException { }```
```class MeterController {
    // ...
    @ExceptionHandler(ResourceNotFoundException.class)
    public String handleResourceNotFoundException() {
        return "meters/notfound";
    }
    
    // ...
    
    @RequestMapping(value = "/{number}/edit", method = RequestMethod.GET)
    public String viewEdit(@PathVariable("number") final Meter meter,
                           final Model model) {
        if (meter == null) throw new ResourceNotFoundException();

        model.addAttribute("meter", meter);
        return "meters/edit";
    }
}```

Das hat sich jetzt also auf einen schlanken Einzeiler verkürzt.