[JAVA] Les rails étaient difficiles, alors j'ai fait quelque chose comme un contrôleur Spring Framework pour faire une pause

Cet article est le 19ème jour du Back freee Developers Advent Calender 2018.

Bonjour. Je suis tamura shingo en train de pirater GYOMU sur freee. J'utilise Ruby au travail, mais je vais écrire sur Java parce que c'est dans les coulisses.

Motivation

--Ruby on Rails était difficile à acheminer --Spring Framework est toujours bon ~ ――Mais dans quelle mesure avez-vous compris le Spring Framework?

J'ai donc décidé de faire quelque chose comme un contrôleur.

Un gars printanier → Avant le printemps → Avant le printemps → Nouvel an? → Je suis si heureux, faisons bouillir les sept herbes Il s'agit donc du cadre Nanakusagayu.

https://github.com/tamurashingo/NanakusagayuFramework

Je ne me suis pas enregistré auprès de Maven, vous pouvez donc l'utiliser en téléchargeant et en mvn install.

procédure

  1. Lisez tous les fichiers de classe sous un certain package
  2. Trouvez l'annotation @ Controller
  3. Recherchez l'annotation «@ GET» (seul «GET» est pris en charge cette fois)
  4. Créez un mappage entre le chemin et la méthode à appeler
  5. Départ de la jetée
  6. Lorsqu'il y a accès, démarrez la méthode selon le mappage en 4.

Annotation pour le moment

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 "";
}

Laissez-le comme RetentionPolicy.RUNTIME.

Récupérez le fichier de classe sous le package

Voici la configuration.

image.png

Initialisation principale

Initializer.java


/**
 *Obtenir une liste des classes sous le package auquel appartient la classe de référence
 *
 */
public class Initializer {

    /**
     *Spécifiez la classe de référence et obtenez la liste des classes.
     *
     * @param className Nom de la classe de base
     * @retourner la liste des classes
     * @throws InitializerException Impossible d'obtenir la liste des classes
     */
    public List<String> getClassList(String className) throws InitializerException {
        return getClassnames(className);
    }

    /**
     *Obtenez le nom du fichier de classe à partir des informations de classe
     * @informations de classe param cls
     * @retour du nom du fichier
     */
    private String getPathnameFromClass(Class<?> cls) {
        return cls.getCanonicalName().replace(".", "/") + ".class";
    }

    private List<String> getClassnames(String className) throws InitializerException {
        try {
            //Obtenez l'emplacement de la classe à partir du nom de la classe
            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());
            }

            //Récupère le robot d'exploration de nom de classe à partir de la classe et de son emplacement
            AbstractClassnameCrawler parser = ClassnameCrawlerFactory.create(baseClass, url);
            //Obtenez une liste de noms de classe
            return parser.getClassnameList();
        } catch (ClassNotFoundException ex) {
            throw new InitializerException("Échec de l'initialisation", ex);
        }
    }
}

Si vous passez une classe qui sert de base au chargement d'un package, vous obtiendrez une liste des classes sous ce package. La méthode de lecture est légèrement différente selon que la classe standard est dans le fichier jar ou dans un répertoire comme les classes, donc j'ai créé ʻAbstractClassnameClawer` et l'ai traité là.

Ensuite, regardons l'implémentation lorsqu'elle se trouve dans le fichier jar. (Le fichier est presque le même)

Obtenez le nom complet de la classe de Jar

ClassnameCrawlerFromJar.java


public class ClassnameCrawlerFromJar extends AbstractClassnameCrawler {

    /**Le package auquel appartient la classe de référence*/
    private String basePackageName;

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

    /**
     *Fichier et extension.classe ou pas
     */
    Predicate<JarEntry> isClassfile = jarFile -> !jarFile.isDirectory() && jarFile.getName().endsWith(".class");

    /**
     *Si le package (subordonné) auquel appartient la classe standard
     */
    Predicate<JarEntry> hasPackage = jarFile -> jarFile.getName().replace("/", ".").startsWith(basePackageName);

    /**
     * JarEntry(com/github/xxxx/xxx/XXXX.class)Le nom de la classe(com.github.xxxx.xxx.XXXX)Convertir en
     */
    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("Erreur de lecture de fichier:" + jarPath, ex);
        }
    }
}

Jetons un œil à getClassnameList.

Le nom de fichier réel du fichier jar (/ chemin / vers / fichierjar) est obtenu à partir de l'URL du fichier jar (fichier: / chemin / vers / fichierjar! / Chemin / vers / classe), et un objet JarFile est créé.

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

Je fais ça.

Obtenir le contrôleur

Maintenant que vous avez une liste de noms de fichiers, récupérez le contrôleur dans cette liste. C'est un peu long, mais je vais tout mettre en même temps.

ControllerScanner.java


public class ControllerScanner implements ComponentScanner {

    /**
     *Informations d'enracinement
     * 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);
        }
    }

    /**
     *Créer des informations de routage
     *
     * @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());
    }

    /**
     *Instanciez une classe.
     * (maintenant)Seul le constructeur par défaut est pris en charge.
     *
     * @informations de classe param cls
     * @param <T>Paramètres factices
     * @instance de retour
     * @throws InitializerException Échec de l'instanciation
     */
    private <T> T createInst(Class<?> cls) throws InitializerException {
        try {
            return (T)cls.getDeclaredConstructor().newInstance();
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new InitializerException("La création du contrôleur a échoué:" + cls.getCanonicalName(), ex);
        }
    }

    /**
     *Obtenez la méthode GET et créez le routage
     * @Instance du contrôleur param inst
     * @param basePath Le chemin défini dans Controller
     * @param <T>Informations factices
     * @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 });
        }
    }
}

Regardons de plus près.

componentScan

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

Apportez l'annotation Controller, le cas échéant, dans cls.getAnnotation (Controller.class).

createController et createInst

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

/**
 *Instanciez une classe.
 * (maintenant)Seul le constructeur par défaut est pris en charge.
 *
 * @informations de classe param cls
 * @param <T>Paramètres factices
 * @instance de retour
 * @throws InitializerException Échec de l'instanciation
 */
private <T> T createInst(Class<?> cls) throws InitializerException {
    try {
        return (T)cls.getDeclaredConstructor().newInstance();
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
        throw new InitializerException("La création du contrôleur a échoué:" + cls.getCanonicalName(), ex);
    }
}

La méthode a «» au début, mais c'est un faux pour tromper le compilateur. J'obtiens le constructeur par défaut et je l'instancie.

Il semble qu'il soit recommandé de faire une DI avec un constructeur dans le récent Framework Spring, mais pour cela, c'est difficile pour la famille principale car il est nécessaire de vérifier différents types de constructeurs.

getPathAndMethod

/**
 *Obtenez la méthode GET et créez le routage
 * @Instance du contrôleur param inst
 * @param basePath Le chemin défini dans Controller
 * @param <T>Informations factices
 * @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 });
    }
}

Tout d'abord, récupérez la liste des méthodes avec l'annotation GET. Après cela, le chemin réel est généré en combinant le chemin défini dans Controller et le chemin défini dans GET. Stockez le chemin et les instances et méthodes qui seront lancées la dernière fois que vous accédez à ce chemin dans la carte.

Routeur

Celui qui reçoit la demande et appelle la méthode de l'instance appropriée. Je le fais pour travailler avec Jetty.

Rotuer.java


public class Router extends AbstractHandler {

    /**
     *Informations de routage
     * 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);
        }
    }
}

Je ne vais pas l'expliquer du tout, mais avec Jetty, le chemin vient dans le premier argument, il est donc facile de ne pas regarder dans HttpServletRequest.

Point de départ

C'est la partie dite «SpringApplication.run (xxx.class, args)».

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();

        //Obtenir la liste des cours
        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();
    }
}

Nous avons préparé des scanners de composants afin que nous puissions manipuler autre chose que le contrôleur. (Mais il est indéniable que c'est une petite omission)

Courir

Créer un contrôleur ...

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>Bonjour</h1>");
    }
}

Faire le principal

Main.java


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

Si vous l'exécutez et accédez à http: // localhsot: 3344 / test / say ...

待望のhello world

venu.

en conclusion

Malgré les restrictions ci-dessus, c'était assez difficile. Cependant, j'ai senti qu'il était possible de le faire avec les fonctions Java standard, plutôt que de jouer avec le code d'octet.

Je voulais vraiment faire un FatJar exécutable avec mvn package, mais j'ai abandonné parce que je ne pouvais pas le faire avant le calendrier de l'Avent.

Demain 20, kei-0226 parlera d'affronter des domaines difficiles. J'ai hâte d'y être.

Recommended Posts

Les rails étaient difficiles, alors j'ai fait quelque chose comme un contrôleur Spring Framework pour faire une pause
Une demi-année d'auto-apprentissage fait du SPA avec Rails + Nuxt.js, alors jetez un œil
J'étais un peu accro à la comparaison S3 Checksum, alors prenez note.
[Mise à jour] Il était difficile de passer de la série httpclient 3.x à la version 4.5, je vais donc rédiger un résumé des modifications
J'ai créé une fonction pour enregistrer des images avec l'API dans Spring Framework. Partie 1 (édition API)
J'ai créé une fonction pour enregistrer des images avec l'API dans Spring Framework. Partie 2 (édition client)
J'ai essayé de faire une version japonaise de la transmission automatique du courrier de Rails / devise
Je veux connaître la méthode du contrôleur où l'exception a été levée dans le ExceptionHandler de Spring Boot
J'ai essayé d'implémenter le traitement Ajax de la fonction similaire dans Rails
J'ai fait un petit bijou pour poster le texte du mode org sur qiita
J'ai créé un outil pour afficher la différence du fichier CSV
[Rails] Je souhaite envoyer des données de différents modèles dans un formulaire
Je veux afficher des images avec REST Controller de Java et Spring!
J'avais des problèmes au travail, j'ai donc créé un plug-in IntelliJ
J'étais accro à un simple test de Jedis (bibliothèque Java-> Redis)
[Circle CI] J'étais accro au test automatique de Circle CI (rails + mysql) [Memo]
J'ai pu obtenir OCJP Silver SE 11 en toute sécurité, donc un résumé
Rails6 Je veux créer un tableau de valeurs avec une case à cocher
[Spring Boot] Si vous utilisez Spring Boot, il était pratique d'utiliser de nombreux utilitaires.
J'ai essayé de cloner une application Web pleine de bugs avec Spring Boot
J'ai fait un exemple de la façon d'écrire un délégué dans Swift UI 2.0 à l'aide de MapKit