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.
--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
.
@ Controller
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
.
Voici la configuration.
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)
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());
.class
--Un délimiteur de répertoire (/) avec un point (.) Commençant par le nom du package de base
--Le nom complet généré à partir du nom de la classe
--Recueillir dans une listeJe fais ça.
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)
.
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 «
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.
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
.
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)
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 ...
venu.
HttpServletRequest
et HttpServletResponse
.
--Spring Framework peut également convertir et transmettre des demandes à l'avance telles que des beans et des paramètres de cheminMalgré 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