[JAVA] Rails war schwierig, deshalb habe ich so etwas wie einen Spring Framework-Controller gemacht, um eine Pause einzulegen

Dieser Artikel ist der 19. Tag von Back freee Developers Advent Calender 2018.

Hallo. Ich bin Tamura Shingo und mache GYOMU Hack on Freee. Ich benutze Ruby bei der Arbeit, aber ich werde über Java schreiben, weil es hinter den Kulissen ist.

Motivation

Also habe ich beschlossen, so etwas wie einen Controller zu machen.

Frühlingshafter Typ → Vor dem Frühling → Vor dem Frühling → Neujahr? → Ich bin so glücklich, lass uns die sieben Kräuter kochen Es ist also Nanakusagayu Framework.

https://github.com/tamurashingo/NanakusagayuFramework

Ich habe mich nicht bei Maven registriert, daher können Sie es durch Herunterladen und mvn install verwenden.

Verfahren

  1. Lesen Sie alle Klassendateien unter einem bestimmten Paket
  2. Suchen Sie die Annotation @ Controller
  3. Suchen Sie die Annotation "@ GET" (diesmal wird nur "GET" unterstützt).
  4. Erstellen Sie eine Zuordnung zwischen dem Pfad und der aufzurufenden Methode
  5. Steg starten
  6. Wenn Zugriff besteht, starten Sie die Methode gemäß der Zuordnung in 4.

Anmerkung vorerst

Controller.java


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
    String value() default "";
}

GET.java


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GET {
    String value() default "";
}

Belassen Sie es als "RetentionPolicy.RUNTIME".

Holen Sie sich die Klassendatei unter das Paket

Dies ist die Konfiguration.

image.png

Initialisierung main

Initializer.java


/**
 *Rufen Sie eine Liste der Klassen unter dem Paket ab, zu dem die Referenzklasse gehört
 *
 */
public class Initializer {

    /**
     *Geben Sie die Referenzklasse an und rufen Sie die Klassenliste ab.
     *
     * @param className Name der Basisklasse
     * @Klassenliste zurückgeben
     * @löst InitializerException aus Fehler beim Abrufen der Klassenliste
     */
    public List<String> getClassList(String className) throws InitializerException {
        return getClassnames(className);
    }

    /**
     *Rufen Sie den Klassendateinamen aus den Klasseninformationen ab
     * @param cls Klasseninformationen
     * @Dateiname zurückgeben
     */
    private String getPathnameFromClass(Class<?> cls) {
        return cls.getCanonicalName().replace(".", "/") + ".class";
    }

    private List<String> getClassnames(String className) throws InitializerException {
        try {
            //Ermitteln Sie den Speicherort der Klasse aus dem Klassennamen
            Class<?> baseClass = Class.forName(className);
            ClassLoader cl = baseClass.getClassLoader();
            String pathname = getPathnameFromClass(baseClass);
            URL url = cl.getResource(pathname);

            if (url == null) {
                throw new InitializerException("not found class:" + baseClass.getCanonicalName());
            }

            //Rufen Sie den Klassennamen-Crawler aus der Klasse und ihrem Speicherort ab
            AbstractClassnameCrawler parser = ClassnameCrawlerFactory.create(baseClass, url);
            //Holen Sie sich eine Liste der Klassennamen
            return parser.getClassnameList();
        } catch (ClassNotFoundException ex) {
            throw new InitializerException("Initialisierungsfehler", ex);
        }
    }
}

Wenn Sie eine Klasse übergeben, die die Grundlage für das Laden eines Pakets bildet, erhalten Sie eine Liste der Klassen unter diesem Paket. Die Lesemethode unterscheidet sich geringfügig, je nachdem, ob sich die Standardklasse in der JAR-Datei oder in einem Verzeichnis wie Klassen befindet. Daher habe ich "AbstractClassnameClawer" erstellt und dort verarbeitet.

Schauen wir uns als nächstes die Implementierung an, wenn sie sich in der JAR-Datei befindet. (Die Datei ist fast gleich)

Holen Sie sich den vollständigen Klassennamen von Jar

ClassnameCrawlerFromJar.java


public class ClassnameCrawlerFromJar extends AbstractClassnameCrawler {

    /**Das Paket, zu dem die Referenzklasse gehört*/
    private String basePackageName;

    public ClassnameCrawlerFromJar(Class<?> baseClass, URL baseUrl) {
        super(baseClass, baseUrl);
        this.basePackageName = baseClass.getPackage().getName();
    }

    /**
     *Datei und Erweiterung.Klasse oder nicht
     */
    Predicate<JarEntry> isClassfile = jarFile -> !jarFile.isDirectory() && jarFile.getName().endsWith(".class");

    /**
     *Gibt an, zu welchem Paket (untergeordnet) die Standardklasse gehört
     */
    Predicate<JarEntry> hasPackage = jarFile -> jarFile.getName().replace("/", ".").startsWith(basePackageName);

    /**
     * JarEntry(com/github/xxxx/xxx/XXXX.class)Der Klassenname(com.github.xxxx.xxx.XXXX)Konvertieren zu
     */
    Function<JarEntry, String> convertFilename = jarFile -> {
        String filename = jarFile.getName();
        // com/github/xxxx/xxx/XXXX.class -> com/github/xxxx/xxx/XXXX
        filename = filename.substring(0, filename.lastIndexOf(".class"));
        // com/github/xxxx/xxx/XXXX -> com.github.xxxx.xxx.XXXX
        return filename.replace("/", ".");
    };

    @Override
    public List<String> getClassnameList() throws InitializerException {
        String path = baseUrl.getPath(); // file:/path/to/jarfile!/path/to/class
        String jarPath = path.substring(5, path.indexOf("!")); // /path/to/jarfile
        try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, StandardCharsets.UTF_8.name()))) {
            Enumeration<JarEntry> entries = jar.entries();
            return Collections.list(entries).stream()
                    .filter(isClassfile)
                    .filter(hasPackage)
                    .map(convertFilename)
                    .collect(Collectors.toList());
        } catch (IOException ex) {
            throw new InitializerException("Fehler beim Lesen der Datei:" + jarPath, ex);
        }
    }
}

Werfen wir einen Blick auf getClassnameList.

Der tatsächliche Dateiname des JARs (/ path / to / jarfile) wird aus der URL des JARs (Datei: / path / to / jarfile! / Path / to / class) abgerufen, und ein JarFile-Objekt wird erstellt.

Collections.list(entries).stream()
    .filter(isClassfile)
    .filter(hasPackage)
    .map(convertFilename)
    .collect(Collectors.toList());

Ich mache das

Holen Sie sich Controller

Nachdem Sie eine Liste mit Dateinamen erstellt haben, können Sie den Controller aus dieser Liste abrufen. Es ist ein bisschen lang, aber ich werde alles auf einmal sagen.

ControllerScanner.java


public class ControllerScanner implements ComponentScanner {

    /**
     *Rooting-Informationen
     * String: /path
     * Object[0]: Controller instance
     * Object[1]: method instance
     */
    private Map<String, Object[]> pathMethodMap = new HashMap<>();

    public Map<String, Object[]> getRoute() {
        return this.pathMethodMap;
    }

    @Override
    public void componentScan(Class<?> cls) throws InitializerException {
        Controller controller = cls.getAnnotation(Controller.class);
        if (controller == null) {
            return;
        } else {
            createController(cls, controller);
        }
    }

    /**
     *Erstellen Sie Routing-Informationen
     *
     * @param cls
     * @param controller
     * @param <T>
     * @throws InitializerException
     */
    private <T> void createController(Class<?> cls, Controller controller) throws InitializerException {
        T inst = createInst(cls);
        getPathAndMethod(inst, controller.value());
    }

    /**
     *Instanziieren Sie eine Klasse.
     * (jetzt)Es wird nur der Standardkonstruktor unterstützt.
     *
     * @param cls Klasseninformationen
     * @param <T>Dummy-Parameter
     * @Instanz zurückgeben
     * @löst InitializerException aus Instanziieren fehlgeschlagen
     */
    private <T> T createInst(Class<?> cls) throws InitializerException {
        try {
            return (T)cls.getDeclaredConstructor().newInstance();
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new InitializerException("Controller-Erstellung fehlgeschlagen:" + cls.getCanonicalName(), ex);
        }
    }

    /**
     *Holen Sie sich die GET-Methode und erstellen Sie das Routing
     * @Instanz des param inst Controllers
     * @param basePath Der in Controller definierte Pfad
     * @param <T>Dummy-Informationen
     * @throws InitializerException
     */
    private <T> void getPathAndMethod(T inst, final String basePath) throws InitializerException {
        Class<?> cls = inst.getClass();

        for (Method method: cls.getDeclaredMethods()) {
            GET get = method.getAnnotation(GET.class);
            if (get == null) {
                continue;
            }
            String path = get.value();

            StringBuilder buf = new StringBuilder();
            if (basePath.isEmpty()) {
                if (path.isEmpty()) {
                    buf.append("/");
                } else if (!path.startsWith("/")) {
                    buf.append("/").append(path);
                } else {
                    buf.append(path);
                }

            } else {
                if (!basePath.startsWith("/")) {
                    buf.append("/");
                }
                buf.append(basePath);
                if (!path.isEmpty()) {
                    if (!path.startsWith("/")) {
                        buf.append("/");
                    }
                    buf.append(path);
                }
            }

            pathMethodMap.put(buf.toString(), new Object[]{ inst, method });
        }
    }
}

Lass uns genauer hinschauen.

componentScan

@Override
public void componentScan(Class<?> cls) throws InitializerException {
    Controller controller = cls.getAnnotation(Controller.class);
    if (controller == null) {
       return;
    } else {
       createController(cls, controller);
    }
}

Bringen Sie gegebenenfalls die Anmerkung "Controller" in "cls.getAnnotation (Controller.class)".

createController und createInst

private <T> void createController(Class<?> cls, Controller controller) throws InitializerException {
    T inst = createInst(cls);
    getPathAndMethod(inst, controller.value());
}

/**
 *Instanziieren Sie eine Klasse.
 * (jetzt)Es wird nur der Standardkonstruktor unterstützt.
 *
 * @param cls Klasseninformationen
 * @param <T>Dummy-Parameter
 * @Instanz zurückgeben
 * @löst InitializerException aus Instanziieren fehlgeschlagen
 */
private <T> T createInst(Class<?> cls) throws InitializerException {
    try {
        return (T)cls.getDeclaredConstructor().newInstance();
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
        throw new InitializerException("Controller-Erstellung fehlgeschlagen:" + cls.getCanonicalName(), ex);
    }
}

Die Methode hat am Anfang "", aber es ist ein Dummy, um den Compiler zu täuschen. Ich bekomme den Standardkonstruktor und instanziiere ihn.

Es scheint, dass es DI empfohlen wird, mit einem Konstruktor im aktuellen Spring Framework zu arbeiten, aber zu diesem Zweck ist es für die Head-Familie schwierig, da verschiedene Konstruktortypen überprüft werden müssen.

getPathAndMethod

/**
 *Holen Sie sich die GET-Methode und erstellen Sie das Routing
 * @Instanz des param inst Controllers
 * @param basePath Der in Controller definierte Pfad
 * @param <T>Dummy-Informationen
 * @throws InitializerException
 */
private <T> void getPathAndMethod(T inst, final String basePath) throws InitializerException {
    Class<?> cls = inst.getClass();

    for (Method method: cls.getDeclaredMethods()) {
        GET get = method.getAnnotation(GET.class);
        if (get == null) {
            continue;
        }
        String path = get.value();

        StringBuilder buf = new StringBuilder();
        if (basePath.isEmpty()) {
            if (path.isEmpty()) {
                buf.append("/");
            } else if (!path.startsWith("/")) {
                buf.append("/").append(path);
            } else {
                buf.append(path);
            }

        } else {
            if (!basePath.startsWith("/")) {
                buf.append("/");
            }
            buf.append(basePath);
            if (!path.isEmpty()) {
                if (!path.startsWith("/")) {
                    buf.append("/");
                }
                buf.append(path);
            }
        }

        pathMethodMap.put(buf.toString(), new Object[]{ inst, method });
    }
}

Holen Sie sich zunächst die Liste der Methoden mit der Annotation GET. Danach wird der tatsächliche Pfad durch Kombinieren des in "Controller" definierten Pfads und des in "GET" definierten Pfads erzeugt. Speichern Sie den Pfad sowie die Instanzen und Methoden, die beim letzten Zugriff auf diesen Pfad in der Karte gestartet werden.

Router

Derjenige, der die Anfrage empfängt und die Methode der entsprechenden Instanz aufruft. Ich mache es, um mit Jetty zu arbeiten.

Rotuer.java


public class Router extends AbstractHandler {

    /**
     *Routing-Informationen
     * String: /path
     * Object[0]: Controller instance
     * Object[1]: method instance
     */
    private Map<String, Object[]> routing;

    public Router(Map<String, Object[]> routing) {
        this.routing = routing;
    }

    @Override
    public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException {
        if (!routing.containsKey(s)) {
            throw new ServletException("page not found");
        }
        Object[] inst = routing.get(s);

        try {
            Method method = (Method) inst[1];
            httpServletResponse.setStatus(HttpServletResponse.SC_OK);
            method.invoke(inst[0], httpServletRequest, httpServletResponse);
            request.setHandled(true);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            throw new ServletException(ex);
        }
    }
}

Ich werde es überhaupt nicht erklären, aber bei Jetty kommt der Pfad in das erste Argument, so dass es einfach ist, nicht in HttpServletRequest zu schauen.

Startpunkt

Dies ist der sogenannte SpringApplication.run (xxx.class, args) -Teil.

NanakusagayuApplication.java


public class NanakusagayuApplication {

    private static ControllerScanner controllerScanner = new ControllerScanner();

    public static void run(Class<?> cls, String[] args) throws Exception {
        Initializer init = new Initializer();

        //Klassenliste abrufen
        List<String> classList = init.getClassList(cls.getCanonicalName());
        init(classList);

        startServer();
    }

    private static void init(List<String> classList) throws InitializerException {
        ComponentScanner[] scannerList = new ComponentScanner[] {
                controllerScanner
        };

        try {
            for (String clsName : classList) {
                Class<?> cls = Class.forName(clsName);
                for (ComponentScanner scanner : scannerList) {
                    scanner.componentScan(cls);
                }
            }
        } catch (ClassNotFoundException ex) {
            throw new InitializerException(ex);
        }
    }

    private static void startServer() throws Exception {
        Server server = new Server(3344);
        server.setHandler(new Router(controllerScanner.getRoute()));
        server.start();
        server.join();
    }
}

Wir haben einige Komponentenscanner vorbereitet, damit wir andere als den Controller handhaben können. (Aber es ist nicht zu leugnen, dass es eine kleine Auslassung ist)

Lauf

Machen Sie einen Controller ...

TestController.java


@Controller("/test")
public class TestController {

    @GET("/say")
    public void hello(HttpServletRequest req, HttpServletResponse res) throws IOException {
        res.setContentType("text/html; charset=UTF-8");
        PrintWriter out = res.getWriter();
        out.println("<h1>Hallo</h1>");
    }
}

Machen Sie die Haupt

Main.java


public class Main {
    public static void main(String...args) throws Exception {
        NanakusagayuApplication.run(Main.class, args);
    }
}

Wenn Sie es ausführen und auf http: // localhsot: 3344 / test / say zugreifen ...

待望のhello world

kam.

abschließend

Trotz der oben genannten Einschränkungen war es ziemlich schwierig. Ich hatte jedoch das Gefühl, dass dies mit Standard-Java-Funktionen möglich war, anstatt mit Byte-Code herumzuspielen.

Ich wollte unbedingt ein ausführbares FatJar mit "mvn package" erstellen, aber ich gab auf, weil ich es nicht bis zum Adventskalender schaffen konnte.

Morgen, den 20., wird kei-0226 über die Konfrontation mit schwierigen Domänen sprechen. Ich freue mich darauf.

Recommended Posts

Rails war schwierig, deshalb habe ich so etwas wie einen Spring Framework-Controller gemacht, um eine Pause einzulegen
Ein halbes Jahr Selbststudium unerfahren gemacht SPA mit Rails + Nuxt.js, also schauen Sie bitte
Ich war ein wenig süchtig nach dem S3-Prüfsummenvergleich, machen Sie sich also eine Notiz.
[Aktualisierung] Es war schwierig, ein Upgrade von der httpclient 3.x-Serie auf 4.5 durchzuführen, daher werde ich eine Zusammenfassung der Änderungen schreiben
Ich habe eine Funktion zum Registrieren von Bildern bei der API in Spring Framework erstellt. Teil 1 (API Edition)
Ich habe eine Funktion zum Registrieren von Bildern bei der API in Spring Framework erstellt. Teil 2 (Client Edition)
Ich habe versucht, eine japanische Version der Automatik-Mail von Rails / devise zu erstellen
Ich möchte die Methode des Controllers kennen, bei der die Ausnahme im ExceptionHandler von Spring Boot ausgelöst wurde
Ich habe versucht, die Ajax-Verarbeitung der ähnlichen Funktion in Rails zu implementieren
Ich habe ein Juwel gemacht, um den Text des Org-Modus in Qiita zu posten
Ich habe ein Tool erstellt, um den Unterschied zwischen CSV-Dateien auszugeben
[Rails] Ich möchte Daten verschiedener Modelle in einem Formular senden
Ich möchte Bilder mit REST Controller von Java und Spring anzeigen!
Ich hatte Probleme bei der Arbeit und habe ein IntelliJ-Plug-In erstellt
Ich war süchtig nach einem einfachen Test von Jedis (Java-> Redis-Bibliothek)
[Circle CI] Ich war süchtig nach dem automatischen Test von Circle CI (Rails + MySQL) [Memo]
Ich konnte OCJP Silver SE 11 sicher erhalten, also eine Zusammenfassung
Rails6 Ich möchte ein Array von Werten mit einem Kontrollkästchen erstellen
[Spring Boot] Wenn Sie Spring Boot verwenden, war es praktisch, viele Dienstprogramme zu verwenden.
Ich habe versucht, eine Webanwendung voller Fehler mit Spring Boot zu klonen
Ich habe ein Beispiel erstellt, wie ein Delegat in Swift UI 2.0 mit MapKit geschrieben wird