Routes

Eine Sache, die mich gerade jetzt am Anfang (wo man viel herumprobiert) bei Ninja stört, ist dass die Routes “zentral” in einer gleichnamigen Klasse gemanaged werden, also etwa so:

public class Routes implements ApplicationRoutes {

    @Override
    public void init(Router router) {

        router.GET().route("/").with(ApplicationController.class, "index");
        router.POST().route("/login").with(UserController.class, "login");

        // Assets (pictures / javascript)
        router.GET().route("/assets/webjars/{fileName: .*}").with(AssetsController.class, "serveWebJars");
        router.GET().route("/assets/{fileName: .*}").with(AssetsController.class, "serveStatic");

        // Index / Catchall shows index page
        router.GET().route("/.*").with(ApplicationController.class, "index");
    }
}

Zum einen finde ich “String-als-Methoden-Referenz” ein wenig gewagt, und zum anderen ist es wirklich schwer, Controller, Freemarker-Template und Routes synchron zu halten - drei Stellen sind einfach eine zu viel. Also habe ich einen neuen Ansatz gewählt:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Gets.class)
public @interface Get {
    String value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Gets {
    Get[] value();
}

//analog Post- und Posts-Annotationen

public class RouteScanner {

    public static void scan(Router router, Class<?> controllerClass) {
        for(Method method : controllerClass.getMethods()) {
            for(Get get : method.getAnnotationsByType(Get.class)) {
                router.GET().route(get.value()).with(controllerClass, method.getName());
            }
            for(Post post : method.getAnnotationsByType(Post.class)) {
                router.POST().route(post.value()).with(controllerClass, method.getName());
            }
        }
    }
}

public class UserController{

   @Get("/login")
   public Result login(Context context) {...}
    ...  
}

public class Routes implements ApplicationRoutes {

    @Override
    public void init(Router router) {
        RouteScanner.scan(router, ApplicationController.class); 
        RouteScanner.scan(router, UserController.class); 
        ...
     }
}

Das scheint gut zu funktionieren. Vor allem beschwöre ich keine großartige Magie - ich muss immer noch sagen, welche Controller gescannt werden sollen und kann auch Routen per Hand eintragen, wenn ich will. Aber es können bei den annotierten Methoden keine Tippfehler mehr passieren, und ich muss normalerweise nur noch zwei Stellen im Auge behalten (Template und Controller).

Oder übersehe ich was?

Als ich den ersten Codeschnippsel sah, habe ich auch an eine ähnliche Lösung, wenn nicht sogar die gleiche gedacht.

Habe auch im Forum eine Frage gestellt, bei der es mehr oder weniger um dieses Thema geht. Der Unterschied war nur, dass ich dies in einer reinen Clientanwendung nutzen wollte. Aber im Prinzip ein reines Publish-Subscribe-Pattern, dass mit Annotationen konfiguriert wird.

http://forum.byte-welt.net/java-forum-erste-hilfe-vom-java-welt-kompetenz-zentrum/allgemeine-themen/13516-nutzt-ihr-aktiv-annotations.html?highlight=annotation

Resonanz war - Naja lies selber.

Ich wage es ja nicht zu Fragen, aber hast du schon mal was von Jax-RS bzw. Jersey gehört?

Das sieht nämlich genau so aus, wenn man nicht so genau hinschaut wie SlaterB

https://jersey.java.net/apidocs/latest/jersey/index.html

@Get("/login")

wäre

@PATH("/login")```

Bzw. wenn man Jax-rs einsetzt

@Path("/login")
class LoginController {

@GET // URL -> /login
Response login() {}

@GET("/admin") // URL -> /login/admin
Response adminLogin() {}

}


Die Annotation der Klasse wird dann dafür genutzt um alle annotierten Klassen in einem angegebenen Package zu scannen und zu registrieren.

Ich hatte schon irgendwo Code gesehen, der das ähnlich gemacht hat - keine Ahnung, ob das Jersey war. Ich denke, es gibt für beide Ansätze - zentral und annotiert - nachvollziehbare Argumente.

Den “Gesamt-Überblick” über das Routing vermisse ich jedenfalls nicht, weil ich das genauso gut beim Start in der Log-Ausgabe nachlesen kann.

da wäre die Unterscheidung in Get & Post, der Querypath, die aufzurufende Methode, zugegeben etwas schwierig abzubilden,
und überhaupt ein so wichtiges Konzept wie möglichen Grundaktionen der Webanwendung,
und wer weiß was noch alles gut dazukommen könnte, Sicherheitslevel wie Moderatorenbereich usw.

ist das keine gute Basis für eine eigene Modellierung, für den Anfang etwa in einer Enum Action mit Werten Login & Co.?
(falls später dynamisch Plugin auch derartiges ergänzen können sollen nicht mehr Enum)

warum so wichtige Daten unhandlich in Annotations verstecken?,
können freilich ausgelesen und gesammelt werden, so kommt es ja sicher auch zur Log-Ausgabe beim Start,
gehen die Annotations auch mehrfach, verschiedene Pfade erlaubt?

@Path("/login") und @GET("/admin") steht für ‚/login/admin‘?
das wäre zweistufig, auch dreistufig? mit echter Programmierung wie Enums hätte man volle Turing-Mächtigkeit,
auf andere Enums in beliebiger Rekursions-Länge verweisen, in init-Methoden Pfade komplex zusammenbauen, auf externe Konfigurationen reagieren usw., alles

ein interessanter Punkt:
eine Enum nur mit allgemeinen Infos wie Get & Post, Querypath, LoginRequired usw.
könnte von verschiedenen Webframeworks gleichermaßen als Struktur genutzt werden


Methoden wie public Result login(Context context) sind dem Framework geschuldet, das kann nicht anders, aber sind doch schrecklich,
wie viele derartige Methoden haben Context-Parameter? welche Verarbeitungen darauf stehen zigmal in verschiedenen Klassen/ Methoden?
oder verwenden bereits gemeinsame Basisklassen, aber erst etwas umständlich mit diesem Parameter neu begonnen

Initialisierung gehört in Basisklassen,
alle Aufrufe der Webanwendung kommen in einer Klasse, einer Methode an,
dort den Kontext aufbauen, zentrales Logging, allgemeine Überprüfungen wie ob User eingeloggt falls nötig,
letzte Aufrufe konsistent, aktueller Request etwa erlaubt in Folge von Formular,
sowas wie CSRF-Protection, falls für bestimmte logische Inhalte selber zu managen

abhängig von Enum wird an einer Stelle individuell weitergeleitet:

case (..) {
  switch Login:
    createUserController().login();
  break;
..

(wenn nicht mehr Enum muss es einzeln in den Objekten stehen, wie ja auch bei Enum immer Alternative)

Result als Rückgabetyp muss nicht sein, kann auch irgendwo im Kontext abgelegt und dann zentral abgeholt werden


nächste Stufe wäre evtl., die Klassen wie UserController bereits vom Web-Framework unabhängig zu machen:

  • die Request-Parameter falls kompliziert in Framwork-Form (ActionBeans und son Kram) in gemeinsame Datentypen umwandeln,
  • Aktionen wie login() arbeiten nur auf allgemeine Daten, Ergebnis sind nur allgemeine Daten, Fehlermeldungen, Enum-Wert zur Anzeige
  • je nach Framework Ergebnis wieder in komplizierte Beans verpacken, aus Enum-Wert in Framework-spefizische Anzeigeseiten leiten
    usw.

aber all das nur als allgemeine Anregung,
bin schon froh falls sich letztlich ein gewisser Forum-Kern sauber halten läßt ;),
die (bzw. eine von vielen :wink: ) Präsentationsschicht mit direkt angebundenen Controllern kann so spezifisch verloren sein wie sie will,
solange jedenfalls nicht zuviel wichtiger Code darin steht, das läßt sich erst beurteilen wenn konkretes zu sehen

[QUOTE=SlaterB]Methoden wie public Result login(Context context) sind dem Framework geschuldet, das kann nicht anders, aber sind doch schrecklich,
wie viele derartige Methoden haben Context-Parameter?[/QUOTE]

Da muss ich Ninja in Schutz nehmen, diese Argumente sind keine Pflicht. Das Framework “kennt” nur einige Typen (Session, Context, FlashScope…), und injiziert sie in die Methode, wenn sie diese als Argument hat. Zusätzlich kann man noch Pfad- und Headerbestandteile angeben und injizieren lassen, diese müssen aber entsprechend annotiert werden. Siehe Ninja - full stack web framework for Java -

Man kann sich sogar in den Mechanismus einklinken, und über eigene “Extraktoren” zusätzliche Argumente liefern: Ninja - full stack web framework for Java -

Bis auf die zentrale Routes habe ich wenig Kritikpunkte bei Ninja finden können. Ich finde es noch ein wenig fishy, dass die Templates in einem Package stehen müssen, das wie “ihr” Controller heißt. Bei den Transactional- und UnitOfWork-Annotations muss man noch aufpassen, dass man diese nicht versehentlich “schachtelt”. Ich habe auch eine Weile gebraucht, bis ich rausgefunden habe, wie man debuggt (nicht das Maven-Goal ninja-run, sondern jetty-run benutzen, dann wird es “normalerweise” von der IDE unterstützt). Ob die application.conf bei größeren Anwendungen unübersichtlich wird, muss ich noch sehen. Alles in allem eher Kleinigkeiten.

Ich sehe gerade, dass Ninja eine Erweiterung hat, die JAX/RS-artige Routenannotationen unterstützt (Ninja - full stack web framework for Java - ganz unten).

Gut zu wissen, aber solange nichts gegen meine minimale selbstgehäkelte Lösung spricht, ziehe ich mir keine neue Dependency rein.