Zwei Mal dieselbe Anwendung in 2 verschiedenen Browsern starten

Anfaengerin

Aktives Mitglied
Hallo,

ich habe eine SpringBoot-Anwendung, einen Rechner, den man auf localhost:8080 erreichen kann. Wenn man aber diese Adresse mit zwei verschiedenen Browsern aufruft, kommen die sich "in die Quere", d.h. eine 4, die ich in einem Browser eingegeben habe, erscheint auch im zweiten. Wie kann ich das umgehen?
 

KonradN

Super-Moderator
Mitarbeiter
Kannst Du das einmal im Detail ausführen? Wenn Du zwei unterschiedliche Browser hast (also z.B. Edge und Firefox), dann hast Du auf jeden Fall zwei unterschiedliche Sessions.

Wenn Du nur einen Browser verwendest, aber mehrere Tabs oder Fenster öffnest, dann hast Du nur eine Session. Dies kannst Du vermeiden, in dem Du unterschiedliche Profile verwendest. Dies kann z.B. über die Verwendung von InPrivate Fenstern erfolgen.

Generell ist aber auch die Frage, was Du genau machst. Was genau machst Du in der Spring Boot Anwendung? Was ist im Browser? Wenn Du im Browser eine 4 eingibst: Was machst Du damit genau? Evtl. kannst Du hier einmal mehr Details geben. Hier kommt dann vermutlich der Scope ins Spiel: Wenn Du in Spring Boot einen @Controller hast, dann ist das ein Bean mit Scope Singleton. Das bedeutet, es gibt genau eine Instanz eines solchen Controllers. Wenn Du also eine 4, die von einem Client gekommen ist, in dem Controller speicherst, dann wäre das geteilt zwischen allen Clients (Unabhängig von Browsern, Sessions, Systemen, ....)

Daher wäre meine Bitte, doch einmal mehr Details zu nennen, was Du genau machst und dann können wir die Details erläutern.
 

Anfaengerin

Aktives Mitglied
Kannst Du das einmal im Detail ausführen? Wenn Du zwei unterschiedliche Browser hast (also z.B. Edge und Firefox), dann hast Du auf jeden Fall zwei unterschiedliche Sessions.

Wenn Du nur einen Browser verwendest, aber mehrere Tabs oder Fenster öffnest, dann hast Du nur eine Session. Dies kannst Du vermeiden, in dem Du unterschiedliche Profile verwendest. Dies kann z.B. über die Verwendung von InPrivate Fenstern erfolgen.

Generell ist aber auch die Frage, was Du genau machst. Was genau machst Du in der Spring Boot Anwendung? Was ist im Browser? Wenn Du im Browser eine 4 eingibst: Was machst Du damit genau? Evtl. kannst Du hier einmal mehr Details geben. Hier kommt dann vermutlich der Scope ins Spiel: Wenn Du in Spring Boot einen @Controller hast, dann ist das ein Bean mit Scope Singleton. Das bedeutet, es gibt genau eine Instanz eines solchen Controllers. Wenn Du also eine 4, die von einem Client gekommen ist, in dem Controller speicherst, dann wäre das geteilt zwischen allen Clients (Unabhängig von Browsern, Sessions, Systemen, ....)

Daher wäre meine Bitte, doch einmal mehr Details zu nennen, was Du genau machst und dann können wir die Details erläutern.
Klar. Also es ist ein Webtaschenrechner, ich hab genau einen Controller und wenn ich mit dem Button "4" (0-9 sind möglich) klicke, erscheint im oberen Textfeld eine 4. Dann der Operator und dann die zweite Zahl, ganz simpel gehalten. Für das Ergebnis muss man "=" drücken und das erscheint dann im zweiten Textfeld.
 

Anfaengerin

Aktives Mitglied
Java:
package org.example.taschenrechner;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import static java.lang.Integer.parseInt;

@Controller
public class Controller1 {

    private String anzeige1 = "";
    private String anzeige = "";
    private String aktuelleZahl = "";
    private String operator = "";
    private String vorherigeZahl = "";
    long ergebnisAlsLong;

    @GetMapping("/")
    public String zeigeFormular() {
        return "index";
    }

    @PostMapping("/rechnen")
    public String rechnen(@RequestParam(value = "eingabe") String eingabe,
                          Model model) {

        /*if (eingabe.equals("<-")) {removeLastChar();}*/

        if (eingabe.equals("<-")) {
            removeLastChar();

            // Anzeige aktualisieren
            if (!aktuelleZahl.isEmpty()) {
                anzeige1 = aktuelleZahl;
            } else if (!operator.isEmpty()) {
                anzeige1 = vorherigeZahl + " " + operator;
            } else {
                anzeige1 = vorherigeZahl;
            }

            model.addAttribute("anzeige1", anzeige1);
            model.addAttribute("anzeige", anzeige);
            return "index";
        }

        if (eingabe.matches("[0-9]")) {
            aktuelleZahl += eingabe;
        }

        double zahl1 = vorherigeZahl.isEmpty() ? 0 : Double.parseDouble(vorherigeZahl);
        double zahl2 = aktuelleZahl.isEmpty() ? 0 : Double.parseDouble(aktuelleZahl);
        double ergebnis = 0;


        if (!vorherigeZahl.isEmpty() && !operator.isEmpty() && !aktuelleZahl.isEmpty()) {
            anzeige1 = vorherigeZahl + " " + operator + " " + aktuelleZahl;
        } else if (!vorherigeZahl.isEmpty() && !operator.isEmpty()) {
            anzeige1 = vorherigeZahl + " " + operator;
        } else if (!aktuelleZahl.isEmpty()) {
            anzeige1 = aktuelleZahl;
        } else {
            anzeige1 = "";
        }

        model.addAttribute("anzeige1", anzeige1);
        model.addAttribute("anzeige", anzeige);

        switch (eingabe) {
            case "+", "-", "*", "/" -> {
                if (!aktuelleZahl.isEmpty()) {

                    vorherigeZahl = aktuelleZahl;
                    aktuelleZahl = "";
                }
                operator = eingabe;

                anzeige1 = vorherigeZahl + " " + operator;
                model.addAttribute("anzeige1", anzeige1);
            }

            case "=" -> {

                switch (operator) {
                    case "+" -> ergebnis = zahl1 + zahl2;
                    case "-" -> ergebnis = zahl1 - zahl2;
                    case "*" -> ergebnis = zahl1 * zahl2;
                    case "/" -> {
                        if (zahl2 != 0) {
                            ergebnis = zahl1 / zahl2;
                        } else {
                            anzeige = "Fehler: Division durch Null!";
                            model.addAttribute("anzeige", anzeige);
                            vorherigeZahl = "";
                            aktuelleZahl = "";
                            operator = "";
                            return "index";
                        }
                    }
                    default -> {
                        anzeige = "";
                        model.addAttribute("anzeige", anzeige);
                        return "index";
                    }
                }

                anzeige = String.valueOf(ergebnis);
                model.addAttribute("anzeige", anzeige);
                ergebnisAlsLong = Math.round(ergebnis);
                vorherigeZahl = String.valueOf(ergebnisAlsLong);
                aktuelleZahl = "";
                operator = "";
            }
            default -> {
                anzeige = "";
                model.addAttribute("anzeige", anzeige);
            }
        }

        return "index";
    }

    public void removeLastChar() {
        if (!aktuelleZahl.isEmpty()) {
            aktuelleZahl = aktuelleZahl.substring(0, aktuelleZahl.length() - 1);
        } else if (!operator.isEmpty()) {
            operator = "";
        } else if (!vorherigeZahl.isEmpty()) {
            vorherigeZahl = vorherigeZahl.substring(0, vorherigeZahl.length() - 1);
        }
    }
}
 

KonradN

Super-Moderator
Mitarbeiter
Ok, dann ist es, wie schon genau beschrieben. Von der Klasse gibt es genau 1 Instanz. Die Variablen:
Java:
    private String anzeige1 = "";
    private String anzeige = "";
    private String aktuelleZahl = "";
    private String operator = "";
    private String vorherigeZahl = "";
    long ergebnisAlsLong;

gibt es also auch nur genau ein Mal. Alle Clients teilen sich diese.

Um diese Problematik zu lösen, gibt es mehrere Ansätze. Ein Ansatz wäre, den Scope zu ändern:
Die Änderung wäre sehr klein und überschaubar: Zu dem @Controller kommt einfach noch ein @SessionController @SessionScope

Das geht recht gut, so lange es mehrere Session sind. Also zwei unterschiedliche Browser oder so. Die Probleme sind aber wieder da, wenn jemand in zwei Tabs arbeiten würde.

Das ist aber eine Lösung, die so eher unüblich geworden ist. In modernen Anwendungen versucht man, es zu vermeiden, so einen Status zu merken oder zu speichern. Man spricht von Stateless. Damit das funktioniert, gibst Du alle Daten von der Seite als Parameter mit. Neben der Eingabe also auch die Anzeige. Wenn Du irgendwas zwischenspeichern willst, dann kannst Du das über unsichtbare Felder machen. Und im Controller schreibst Du dann auch alles in das model damit es in der Seite gespeichert wird.
So vermeidest Du, dass unnötige Instanzen aufgebaut werden. Du hast auch nicht das Problem, dass Daten verloren gehen, wenn eine Session auf einen Timeout läuft und so eine Controller Instanz aus dem Speicher entfernt wird. Und das Interface ist auch gut testbar.

@Edit: Hatte mich bei der Annotation verschrieben - es muss natürlich SessionScope sein und nicht SessionController! Der Link beschreibt die Scopes aber sehr gut und gibt auch Beispiele ...
 
Zuletzt bearbeitet:

Anfaengerin

Aktives Mitglied
Wie sieht das denn aus, so eine Implementation einer Session Bean? Session Bean erstellen, in den Controller injizieren und dann mit Gettern/Settern Variablen aus dem Controller in der Session-Klasse speichern?
 

KonradN

Super-Moderator
Mitarbeiter
Wenn Du die erste Version nutzen willst, dann sollte es wirklich nur eine zusätzliche Annotation sein:

Java:
@Controller
@SessionScope
public class Controller1 {

(Ich habe gerade gesehen, dass ich beim Tippen mich verschrieben hatte - wenn man nicht gut genug schaut und man zu schnell tippt. Aus SessionScope beim Controller ist da irgendwie "SessionController" geworden .... Sorry dafür - ich habe keine Ahnung, wie es dazu gekommen ist und habe es noch einmal editiert.)

Der gegebene Link erklärt die verschiedenen Scopes aber sehr gut und gibt auch entsprechende Beispiele.
 

KonradN

Super-Moderator
Mitarbeiter
Evtl. einfach einmal ein paar Erläuterungen zu Controller / Beans. In dem gegebenen Link wird eine @Component mit @SessionScope versehen:
Java:
@SessionScope
@Component
public class UserPreferences {
    // ...
}
(Direkt kopiert von: https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html)

Hier ist dann wichtig zu verstehen, dass ein @Controller auch "nur" eine @Component sind:

Java:
@Target(TYPE)
@Retention(RUNTIME)
@Documented
@Component
public @interface Controller

Das kannst Du als eine Art "Vererbung" ansehen. Ein @Controller ist damit auch eine @Component und damit kann man diesen Scope auch entsprechend auf dem Controller direkt setzen.

Das Szenario von Dir ginge natürlich auch - Du kannst eine eigene Component bauen, welche SessionScope hat und diese dann in den Controller mit Singleton Scope injecten.

ABER: Vermutlich hast du zu Recht Bauchschmerzen, denn das scheint ja erst einmal nicht zu passen: Es wird ja beim Controller z.B. per Konstruktor Injection nur einmalig eine Instanz eingefügt und nicht jedes Mal pro Session. Hier muss man also noch etwas mehr machen:

a) Statt direkt die @Component CalculatorSessionData (einfach mal als Name für die Komonente gewählt) zu Injecten kann man hier die Komponente jedes Mal holen. Das ginge z.B. über eine ObjectFactory<T>, sprich: Du hast eine Variable ObjectFactory<CalculatorSessionData> calculatorSessionDataFactory; die Du injectest. Und immer, wenn Du in der Verarbeitung eines Requests damit arbeiten willst, holst Du Dir diese per get() Aufuf.

b) Spring Boot arbeitet viel mit Proxies. Auch hier hat Spring Boot eine Lösung, die es für Dich löst. Dazu schauen wir uns einfach einmal SessionScope an:
Man erkennt: Man kann eine proxyMode setzen. Und wenn man da einen TARGET_CLASS proxyMode setzt, dann ist sicher gestellt, dass es einen Proxy gibt, der dann auch u.a. die Scope Problematik lösen kann.
Das findet sich dann z.B. unter https://www.logicbig.com/tutorials/spring-framework/spring-core/scoped-proxy.html mit einem Beispiel.
Was man an der SessionScope Dokumentation auch sehen kann: Die @SessionScope Annotation entspricht auch der @Scope("session") Annotation. Das kann hilfreich sein zu wissen, damit Du bei Recherchen nicht verwirrt wirst. Denn das findet man auch öfters mal.

a) hat aus meiner Sicht den Vorteil, dass man dies sehr gut im Code nachvollziehen kann.
b) macht eine Art Magie, welche man nicht direkt an der Stelle der Nutzung erkennen kann, was ich nicht gut finde (Ein @Controller mit einer Injection von vermutlichen Session Daten führt erst einmal zu Bauchschmerzen bei mir!) aber dafür stellt es sicher, dass so ein Fehler eben nicht auftreten kann.

Wenn Du hier tiefer einsteigen willst: Ich kann dir nur das Buch von Christian Ullenboom "Spring Boot 3 und Spring Framework 6" empfehlen. Er gibt aus meiner Sicht eine extrem gute Übersicht über die einzelnen Bean Annotations und deren Verhalten. U.a. auch mit Fragestellungen wie: Wann wird ein Proxy erstellt und wann nicht ... Und sehr schön: Das Buch ist in Deutsch (gibt es auch als Englische Übersetzung, aber in der Mutterspache lässt sich sowas ja besser lesen....).
 
Ähnliche Java Themen

Ähnliche Java Themen


Oben