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.
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.
@ 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 "";
}
Belassen Sie es als "RetentionPolicy.RUNTIME".
Dies ist die Konfiguration.
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)
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
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)".
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 "
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.
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.
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)
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 ...
kam.
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