Let's create a super-simple web framework in Java

Overview

Today, we often hear words like "cloud native" and "microservices" in the web application community. In Java development, ** frameworks ** such as Spring Boot are often used to realize these architectures.

However, when I was not so familiar with Java behavior and suddenly started developing using such a ** framework **, "Why does it work in this way?" "Annotation? What is it?" "DI? Nullpo" You may end up in a situation like "Isn't it?"

In this article, in order to dispel such doubts, I hope that I can create a super ~~ similar ~~ simple framework by myself and understand the internal mechanism of the Java framework as much as possible. I will.

Final client sample

Spring Boot quick start With reference to https://projects.spring.io/spring-boot/#quick-start, the client programs that use the framework realized this time are as follows.

src/main/java/hello/SampleController.java


package hello;

...<import omitted>

@Controller
public class SampleController {

    @Resource
    private SampleService service;

    @RequestMapping("/hello")
    public String home() {
        return service.hello();
    }

    public static void main(String[] args) {
        EasyApplication.run(SampleController.class, args);
    }
}

Run this client program and expect it to display "Hello World!" When called from an http client (such as curl).

$ curl http://localhost:8080/hello
Hello World!

Premise

Techniques and words to use

(Additional notes)

Source code

https://github.com/hatimiti/easyframework Please follow the steps below to place the source code in your local environment.

$ cd ~
$ mkdir java
$ git clone https://github.com/hatimiti/easyframework.git
$ cd easyframework

Try to run

#Run with Gradle. It takes time for the first execution.
$ ./gradlew clean build run

> Task :run
Registered Components => {class [email protected], interface [email protected], class [email protected]}
Registered Controller => /hello - { "path": "/hello", "instance": [email protected]", "method": public java.lang.String hello.SampleController.home()" }
<============-> 93% EXECUTING [16s]
> :run

If the above situation occurs, launch another console and check with curl or wget.

$ curl http://localhost:8080/hello
Hello World!

* If the execution is successful, exit the original console with ** ctrl + C **. </ font>

File structure

The structure of the source code is as follows.

~/java/easyframework/src
|--main
|  |--java
|  |  |--easyframework
|  |  |  |--Component.java
|  |  |  |--Controller.java
|  |  |  |--EasyApplication.java
|  |  |  |--RequestMapping.java
|  |  |--hello
|  |  |  |--SampleController.java
|  |  |  |--SampleService.java
|  |  |  |--impl
|  |  |  |  |--SampleServiceImpl.java

Simple configuration diagram

Figure 1: Configuration

The sample is divided into the Client (user) side and the Framework (provider) side. As you read, always be aware of which layer you are referring to.

Annotation

** Annotations ** are features introduced in Java SE 5. What you usually see is "@Override" "[@Deprecated](https: / /docs.oracle.com/javase/jp/8/docs/api/java/lang/Deprecated.html) "" [@SuppressWarnings](https://docs.oracle.com/javase/jp/8/docs/ api / java / lang / SuppressWarnings.html) ”and“ [@FunctionalInterface](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/FunctionalInterface] added in Java 8 Isn't it around ".html)"? These are also annotations.

Annotations are also called "annotation types" and, as the name implies, are types that add (meaning) annotations to source code. For example, "@Override" means that the method is "overridden from the super class", and "@Deprecated" means that the method or type is "deprecated". Can be expressed in the source code.

Annotations are treated as types, so to create your own, define them using the keyword " @ interface </ font>" as well as "class" and "interface".

public @interface Hoge {
}

Annotations can also define attribute information.

public @interface Fuga {
    String value() default "";
}

By having attributes, you can give additional information when using annotations.

@Fuga(value = "/hello")
public void x() {
}

If the attribute is named value and you specify only value, you can omit the attribute name.

@Fuga("/hello")
public void x() {
}

If you set the default value with default at the time of definition, you can omit the specification.

@Fuga 
public void x() {
}

The following are the annotation definitions required in this article.

  • Details of the definition will be described later.

src/main/java/easyframework/Controller.java


package easyframework;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.Inherited;

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}

src/main/java/easyframework/Component.java


package easyframework;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.Inherited;

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}

src/main/java/easyframework/RequestMapping.java


package easyframework;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.Inherited;

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

So what's useful with these annotations? Certainly in the sample program

@Controller
public class SampleController {
...

Like, @Controller created earlier is added to the top of the normal class definition.

** Annotate the source code **, but that's all you need to do to comment using Javadoc. Yes, unlike Javadoc, the annotations are part of the source code, not comments </ font>. Since it is part of the source code, you can define how the target annotation works by using the reflection API described later </ font>. Become. Conversely, simply annotating it doesn't do anything, and it's treated the same as (or less than) Javadoc.

Here, if you want to handle unique annotations, you need to use the reflection API </ font>.

Meta annotation

In the above annotation definition, the annotation was added to the annotation definition itself. Annotations attached to such annotations are called ** meta annotations **.

# name attribute Overview
1 @Inherited - Indicates that annotation information is inherited by the child classes of the class to which this annotation is added.
2 @Target ElementType ElementType indicates where this annotation can be added on the source.
3 @Retention RetentionPolicy RetentionPolicy indicates how long this annotation information should be retained.
# attribute value Overview
1 ElementType ANNOTATION_TYPE Annotation type
2 CONSTRUCTOR constructor
3 FIELD field
4 LOCAL_VARIABLE local
5 METHOD Method
6 PACKAGE package
7 PARAMETER argument
8 TYPE Class, interface, enum
9 TYPE_PARAMETER Generics(Type parameters)
10 TYPE_USE Everywhere you use the mold
11 RetentionPolicy SOURCE At compile time: ○
class file: ×
runtime: ×
12 CLASS At compile time: ○
class file: ○
runtime: ×
13 RUNTIME At compile time: ○
class file: ○
runtime: ○

Regarding RetentionPolicy, when reading from the reflection API, it is necessary to set it to "RUNTIME", so I think that the number of RUNTIME specifications will naturally increase. All annotations created this time also specify RUNTIME.

Reflection API

In the following samples, ** Reflection API ** (hereinafter referred to as Reflection) is used, so I will introduce it before starting the explanation of the framework part. Reflection is an API for realizing "metaprogramming" in Java, and can handle meta information in a program. The meta information is mainly the class name, the type of the class, what package it is defined in, what fields it has and what type it has, what methods are defined and what the return value is. Information that can be read from the class definition (.class), such as. Reflection also allows you to set values in ** (including private) ** fields and execute methods. As a result, reflection allows you to control your Java program, for better or for worse, by ignoring Java syntax and encapsulation. The framework is designed so that users (clients) can easily develop it by using this reflection in no small measure.

Now let's take a look at a sample that uses reflection. (I'm using jshell, a Java 9 REPL) This is an example of calling an instance of the String class and displaying "Hello, Reflection." On the standard output.

When not using reflection


$ jshell
jshell> System.out.println(new String("Hello, Reflection."))
Hello, Reflection.

When using reflection


$ jshell
jshell> Class<String> strClass = String.class
strClass ==> class java.lang.String

jshell> java.lang.reflect.Constructor c = strClass.getConstructor(String.class)
c ==> public java.lang.String(java.lang.String)

jshell> System.out.println(c.newInstance("Hello, Reflection."))
Hello, Reflection.

In the example that does not use reflection, an instance of the String class is created using the new keyword, but in the example that uses reflection, it is created by calling the newInstance () method of the Constructor type without using the new keyword. .. Also, the base of the Constructor instance is an instance of type Class created by String.class. The Class type is explained in the next section.

In this way, it is possible to create an instance and call a method based on the class definition information.

java.lang.Class type

How to get an instance of Class type

There are three main ways to get an instance of Class type.

  1. .class: Get from type definition
  2. Class.getClass (): Get from instance
  3. Class # forName (String): Get from string
//<Type>.class:Get from type definition
jshell> Class<?> c1 = String.class
c1 ==> class java.lang.String

// Class.getClass():Get from instance
jshell> Class<?> c2 = "Hello".getClass()
c2 ==> class java.lang.String

// Class#forName(String):Get from a string
jshell> Class<?> c3 = Class.forName("java.lang.String")
c3 ==> class java.lang.String

//The acquired class information is the same
jshell> c1 == c2
$13 ==> true

jshell> c2 == c3
$14 ==> true

jshell> c1.equals(c2)
$15 ==> true

jshell> c2.equals(c3)
$16 ==> true

In particular, the specification of " .class" is often seen when generating a logger at the time of log output.

Logger LOG = LoggerFactory.getLogger(String.class);

Class Main methods defined in the class

You can get various meta information about the class by using the Class instance obtained by the above method. Below are the main methods defined in the Class class. The one used in this sample is excerpted.

  • Quoted from Javadoc (https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Class.html)
# Return value Method static Overview
1 Class<?> forName(String className) Returns a Class object associated with a class or interface with the specified string name.
2 Annotation[] getAnnotations() Returns the annotations present on this element.
3 <A extends Annotation> A getAnnotation(Class<A> annotationClass) If it exists, it returns an annotation of the specified type for this element, otherwise it returns null.
4 Field[] getDeclaredFields() Returns an array of Field objects that reflect all the fields declared by the class or interface represented by this Class object.
5 Field[] getFields() Returns an array holding a Field object that reflects all accessible public fields of the class or interface represented by this Class object.
6 Method[] getDeclaredMethods() Returns an array containing a Method object that reflects all the declared methods of the class or interface represented by this Class object. This includes public, protected, and default(package)Includes access and private methods, but excludes inherited methods.
7 Method[] getMethods() Returns an array containing a Method object that reflects all public methods of the class or interface represented by this Class object. This includes those declared in the class or interface, as well as those inherited from the superclass or superinterface.
8 boolean isInterface() Determines if the specified Class object represents an interface type.
9 boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) Returns true if an annotation of the specified type exists for this element, false otherwise.
10 T newInstance() Create a new instance of the class represented by this Class object.

Example of getting a method of String class


$ jshell
jshell> String.class
$1 ==> class java.lang.String

// getMethods()Does not include private methods, but also includes definitions inherited from the parent class
jshell> Arrays.asList($1.getMethods()).forEach(System.out::println)
public boolean java.lang.String.equals(java.lang.Object)
public int java.lang.String.length()
public java.lang.String java.lang.String.toString()
public int java.lang.String.hashCode()
public void java.lang.String.getChars(int,int,char[],int)
public int java.lang.String.compareTo(java.lang.Object)
public int java.lang.String.compareTo(java.lang.String)
public int java.lang.String.indexOf(int)
public int java.lang.String.indexOf(java.lang.String)
public int java.lang.String.indexOf(java.lang.String,int)
public int java.lang.String.indexOf(int,int)
public static java.lang.String java.lang.String.valueOf(int)
public static java.lang.String java.lang.String.valueOf(char)
public static java.lang.String java.lang.String.valueOf(boolean)
public static java.lang.String java.lang.String.valueOf(float)
public static java.lang.String java.lang.String.valueOf(double)
public static java.lang.String java.lang.String.valueOf(java.lang.Object)
public static java.lang.String java.lang.String.valueOf(long)
public static java.lang.String java.lang.String.valueOf(char[])
public static java.lang.String java.lang.String.valueOf(char[],int,int)
public java.util.stream.IntStream java.lang.String.codePoints()
public boolean java.lang.String.isEmpty()
public char java.lang.String.charAt(int)
public int java.lang.String.codePointAt(int)
public int java.lang.String.codePointBefore(int)
public int java.lang.String.codePointCount(int,int)
public int java.lang.String.offsetByCodePoints(int,int)
public byte[] java.lang.String.getBytes(java.nio.charset.Charset)
public byte[] java.lang.String.getBytes()
public byte[] java.lang.String.getBytes(java.lang.String) throws java.io.UnsupportedEncodingException
public void java.lang.String.getBytes(int,int,byte[],int)
public boolean java.lang.String.contentEquals(java.lang.CharSequence)
public boolean java.lang.String.contentEquals(java.lang.StringBuffer)
public boolean java.lang.String.equalsIgnoreCase(java.lang.String)
public int java.lang.String.compareToIgnoreCase(java.lang.String)
public boolean java.lang.String.regionMatches(int,java.lang.String,int,int)
public boolean java.lang.String.regionMatches(boolean,int,java.lang.String,int,int)
public boolean java.lang.String.startsWith(java.lang.String)
public boolean java.lang.String.startsWith(java.lang.String,int)
public boolean java.lang.String.endsWith(java.lang.String)
public int java.lang.String.lastIndexOf(java.lang.String,int)
public int java.lang.String.lastIndexOf(java.lang.String)
public int java.lang.String.lastIndexOf(int,int)
public int java.lang.String.lastIndexOf(int)
public java.lang.String java.lang.String.substring(int,int)
public java.lang.String java.lang.String.substring(int)
public java.lang.CharSequence java.lang.String.subSequence(int,int)
public java.lang.String java.lang.String.concat(java.lang.String)
public java.lang.String java.lang.String.replace(java.lang.CharSequence,java.lang.CharSequence)
public java.lang.String java.lang.String.replace(char,char)
public boolean java.lang.String.matches(java.lang.String)
public boolean java.lang.String.contains(java.lang.CharSequence)
public java.lang.String java.lang.String.replaceFirst(java.lang.String,java.lang.String)
public java.lang.String java.lang.String.replaceAll(java.lang.String,java.lang.String)
public java.lang.String[] java.lang.String.split(java.lang.String)
public java.lang.String[] java.lang.String.split(java.lang.String,int)
public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.CharSequence[])
public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.Iterable)
public java.lang.String java.lang.String.toLowerCase(java.util.Locale)
public java.lang.String java.lang.String.toLowerCase()
public java.lang.String java.lang.String.toUpperCase(java.util.Locale)
public java.lang.String java.lang.String.toUpperCase()
public java.lang.String java.lang.String.trim()
public java.util.stream.IntStream java.lang.String.chars()
public char[] java.lang.String.toCharArray()
public static java.lang.String java.lang.String.format(java.util.Locale,java.lang.String,java.lang.Object[])
public static java.lang.String java.lang.String.format(java.lang.String,java.lang.Object[])
public static java.lang.String java.lang.String.copyValueOf(char[])
public static java.lang.String java.lang.String.copyValueOf(char[],int,int)
public native java.lang.String java.lang.String.intern()
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()

// getDeclaredMethods()Includes private methods, but does not include definitions inherited from the parent class
jshell> Arrays.asList($1.getDeclaredMethods()).forEach(System.out::println)
byte java.lang.String.coder()
public boolean java.lang.String.equals(java.lang.Object)
public int java.lang.String.length()
public java.lang.String java.lang.String.toString()
public int java.lang.String.hashCode()
public void java.lang.String.getChars(int,int,char[],int)
public int java.lang.String.compareTo(java.lang.Object)
public int java.lang.String.compareTo(java.lang.String)
public int java.lang.String.indexOf(int)
static int java.lang.String.indexOf(byte[],byte,int,java.lang.String,int)
public int java.lang.String.indexOf(java.lang.String)
public int java.lang.String.indexOf(java.lang.String,int)
public int java.lang.String.indexOf(int,int)
static void java.lang.String.checkIndex(int,int)
public static java.lang.String java.lang.String.valueOf(int)
public static java.lang.String java.lang.String.valueOf(char)
public static java.lang.String java.lang.String.valueOf(boolean)
public static java.lang.String java.lang.String.valueOf(float)
public static java.lang.String java.lang.String.valueOf(double)
public static java.lang.String java.lang.String.valueOf(java.lang.Object)
public static java.lang.String java.lang.String.valueOf(long)
public static java.lang.String java.lang.String.valueOf(char[])
public static java.lang.String java.lang.String.valueOf(char[],int,int)
private static java.lang.Void java.lang.String.rangeCheck(char[],int,int)
public java.util.stream.IntStream java.lang.String.codePoints()
public boolean java.lang.String.isEmpty()
public char java.lang.String.charAt(int)
public int java.lang.String.codePointAt(int)
public int java.lang.String.codePointBefore(int)
public int java.lang.String.codePointCount(int,int)
public int java.lang.String.offsetByCodePoints(int,int)
public byte[] java.lang.String.getBytes(java.nio.charset.Charset)
void java.lang.String.getBytes(byte[],int,byte)
public byte[] java.lang.String.getBytes()
public byte[] java.lang.String.getBytes(java.lang.String) throws java.io.UnsupportedEncodingException
public void java.lang.String.getBytes(int,int,byte[],int)
public boolean java.lang.String.contentEquals(java.lang.CharSequence)
public boolean java.lang.String.contentEquals(java.lang.StringBuffer)
private boolean java.lang.String.nonSyncContentEquals(java.lang.AbstractStringBuilder)
public boolean java.lang.String.equalsIgnoreCase(java.lang.String)
public int java.lang.String.compareToIgnoreCase(java.lang.String)
public boolean java.lang.String.regionMatches(int,java.lang.String,int,int)
public boolean java.lang.String.regionMatches(boolean,int,java.lang.String,int,int)
public boolean java.lang.String.startsWith(java.lang.String)
public boolean java.lang.String.startsWith(java.lang.String,int)
public boolean java.lang.String.endsWith(java.lang.String)
public int java.lang.String.lastIndexOf(java.lang.String,int)
public int java.lang.String.lastIndexOf(java.lang.String)
public int java.lang.String.lastIndexOf(int,int)
public int java.lang.String.lastIndexOf(int)
static int java.lang.String.lastIndexOf(byte[],byte,int,java.lang.String,int)
public java.lang.String java.lang.String.substring(int,int)
public java.lang.String java.lang.String.substring(int)
public java.lang.CharSequence java.lang.String.subSequence(int,int)
public java.lang.String java.lang.String.concat(java.lang.String)
public java.lang.String java.lang.String.replace(java.lang.CharSequence,java.lang.CharSequence)
public java.lang.String java.lang.String.replace(char,char)
public boolean java.lang.String.matches(java.lang.String)
public boolean java.lang.String.contains(java.lang.CharSequence)
public java.lang.String java.lang.String.replaceFirst(java.lang.String,java.lang.String)
public java.lang.String java.lang.String.replaceAll(java.lang.String,java.lang.String)
public java.lang.String[] java.lang.String.split(java.lang.String)
public java.lang.String[] java.lang.String.split(java.lang.String,int)
public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.CharSequence[])
public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.Iterable)
public java.lang.String java.lang.String.toLowerCase(java.util.Locale)
public java.lang.String java.lang.String.toLowerCase()
public java.lang.String java.lang.String.toUpperCase(java.util.Locale)
public java.lang.String java.lang.String.toUpperCase()
public java.lang.String java.lang.String.trim()
public java.util.stream.IntStream java.lang.String.chars()
public char[] java.lang.String.toCharArray()
public static java.lang.String java.lang.String.format(java.util.Locale,java.lang.String,java.lang.Object[])
public static java.lang.String java.lang.String.format(java.lang.String,java.lang.Object[])
public static java.lang.String java.lang.String.copyValueOf(char[])
public static java.lang.String java.lang.String.copyValueOf(char[],int,int)
public native java.lang.String java.lang.String.intern()
private boolean java.lang.String.isLatin1()
static void java.lang.String.checkOffset(int,int)
static void java.lang.String.checkBoundsOffCount(int,int,int)
static void java.lang.String.checkBoundsBeginEnd(int,int,int)
static byte[] java.lang.String.access$100(java.lang.String)
static boolean java.lang.String.access$200(java.lang.String)

// getFields()Does not include private fields, but also includes definitions inherited from the parent class
jshell> Arrays.asList($1.getFields()).forEach(System.out::println)
public static final java.util.Comparator java.lang.String.CASE_INSENSITIVE_ORDER

// getDeclaredFields()Contains private, but does not include definitions inherited from the parent class
jshell> Arrays.asList($1.getDeclaredFields()).forEach(System.out::println)
private final byte[] java.lang.String.value
private final byte java.lang.String.coder
private int java.lang.String.hash
private static final long java.lang.String.serialVersionUID
static final boolean java.lang.String.COMPACT_STRINGS
private static final java.io.ObjectStreamField[] java.lang.String.serialPersistentFields
public static final java.util.Comparator java.lang.String.CASE_INSENSITIVE_ORDER
static final byte java.lang.String.LATIN1
static final byte java.lang.String.UTF16

Type definition for reflection

Reflection type definitions other than Class class are mainly ** [java.lang.reflect](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/reflect/package-summary. html) ** Defined under the package. This is also an excerpt of the one used in this sample.

# class Return value Method Overview
1 Field boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) Returns true if an annotation of the specified type exists for this element, false otherwise.
2 void setAccessible(boolean flag) Sets the accessible flag for this object to the specified boolean value.
3 void set(Object obj, Object value) Sets the field of the specified object argument represented by this Field object to the new value specified.
4 Method boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) Returns true if an annotation of the specified type exists for this element, false otherwise.
5 <T extends Annotation> T getAnnotation(Class<T> annotationClass) If it exists, it returns an annotation of the specified type for this element, otherwise it returns null.
6 Object invoke(Object obj, Object... args) Calls the underlying method represented by this Method object with the specified parameters for the specified object.

The points to be noted are as follows.

  • You must call ** setAccessible (true) ** to access private information.
  • To set a value in a field, call ** field.set (target instance, value you want to set) **.
  • If you want to execute the method, call ** method.invoke (target instance, argument ...) **.

Precautions when using reflection

  • Use only in the framework layer
  • As seen above, even private content is rewritten, so if a general developer starts using reflection, the class design and ideas that were originally envisioned may be destroyed. In actual development, I think it is better to ask the members who manage the framework layer and common layer to make corrections.
  • Be careful about performance
  • Reflection reads the definition information at ** runtime ** and processes it, so the performance is not as good as normal processing. As a result, some frameworks use compilation as a trigger to generate .classes and .javas to reduce their run-time impact.
  • If you still need to use it, consider caching the Field and Method information read by reflection.
  • Be aware of security so as not to create vulnerabilities
  • Since reflection can be instantiated from a character string, passing the request parameter from the client to the reflection API will create an unexpected vulnerability, so be careful about access from the outside. ..

Framework

From now on, I will introduce the structure of EasyFramework.java, which is the core of the framework. The first is the run () method, which is the only public method of this framework.

src/main/java/easyframework/EasyApplication.java


package easyframework;

public class EasyApplication {

...
      public static void run(Class<?> clazz, String... args) {
          scanComponents(clazz);
          injectDependencies();
          registerHTTPPaths();
          startHTTPServer();
      }
...

}

It calls the following four methods.

  • scanComponents (clazz) …… ** Search for and register components **.
  • injectDependencies () …… Sets the registered component to the corresponding field ** DI **.
  • registerHTTPPaths () …… Registers the path that will be the HTTP call point among the registered components.
  • startHTTPServer () …… Starts the server and listens for requests.

I will describe the details of each.

Component scan

This framework manages the execution point (Controller) of HTTP request described later and the instance </ font> group required inside the framework to realize DI. Here, these ** instances under the control of the framework are referred to as "components" **. Similarly for Spring Boot, [@ComponentScan](https://github.com/spring-projects/spring-framework/blob/master/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan. By using java) annotation, it will automatically find the component to manage. At that time, which class is managed as a component is determined by whether the "@Controller" or "@Component" annotation is added.

Here, the search result is registered in the components field of EasyApplication.

src/main/java/easyframework/EasyApplication.java


public class EasyApplication {

    /** KEY=the class of the component, VALUE= A instance of the component */
    private final static Map<Class<?>, Object> components = new HashMap<>();

...

src/main/java/easyframework/EasyApplication.java


    private static List<Class<?>> scanClassesUnder(String packageName) {
        //Package name.Break/Convert to delimiter.
        String packagePath = packageName.replace('.', '/');
        //Search the resource of the target path from the class loader.
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        URL root = cl.getResource(packagePath);

        // .Get the class file.
        File[] files = new File(root.getFile())
            .listFiles((dir, name) -> name.endsWith(".class"));
        return Arrays.asList(files).stream()
                .map(file -> packageName + "." + file.getName().replaceAll(".class$", ""))
                //package name+Based on the class name, get the Class class instant and return it as a List.
                .map(fullName -> uncheck(() -> Class.forName(fullName)))
                .collect(Collectors.toList());
    }

src/main/java/easyframework/EasyApplication.java


    // @With Component [email protected] the type with Controller annotation as a component
    private static boolean hasComponentAnnotation(Class<?> clazz) {
        return clazz.isAnnotationPresent(Component.class)
                || clazz.isAnnotationPresent(Controller.class);
    }

    private static void scanComponents(Class<?> source) {
        List<Class<?>> classes = scanClassesUnder(source.getPackage().getName());
        classes.stream()
            .filter(clazz -> hasComponentAnnotation(clazz))
            .forEach(clazz -> uncheck(() -> {
                //Create and register an instance of the component target type
                Object instance = clazz.newInstance();
                components.put(clazz, instance);
            }));
        System.out.println("Registered Components => " + components);
    }

DI(Dependency Injection)

The reason I didn't get a NullPointerException when calling the method, even though I didn't create a new instance of type SampleService in the first sample, is because I used this ** DI ** mechanism.

src/main/java/hello/SampleController.java


    @Resource
    private SampleService service; //← Shouldn't it be null because it's not new?

    @RequestMapping("/hello")
    public String home() {
        return service.hello(); //← Why NullPointerException does not occur! ??
    }

It can be translated as DI = Dependency Injection, but what is ** Dependency **?

Think of it as dependency = object (instance) here. Therefore, DI is a mechanism for injecting objects, and it is possible to loosely couple classes. FactoryMethod is a design pattern for loosely coupling between classes, but DI doesn't even need to use factory methods or the new keyword, so it can be loosely coupled one step further.

The part that is DI in this framework is the following processing.

src/main/java/easyframework/EasyApplication.java


    private static void injectDependencies() {
        //Pre-scanned and registered components(components)Check all fields.
        components.forEach((clazz, component) -> {
            Arrays.asList(clazz.getDeclaredFields()).stream()
                // @Get only the fields with Resource annotation.
                .filter(field -> field.isAnnotationPresent(Resource.class))
                .forEach(field -> uncheck(() -> {
                    //Make it acccessible so that it can be handled even in private fields.
                    field.setAccessible(true);
                    //component(components)Get an instance of the type from and inject(Setting)To do.
                    field.set(component, components.get(field.getType()));
                }));
        });
    }

The definition of SampleService to be injected this time is already registered as a component because @Component is added as shown below.

src/main/java/hello/impl/SampleServiceImpl.java


@Component
public class SampleServiceImpl implements SampleService {
...

Since @Resource is added to the field of SampleController that is the injection destination and the type matches, the instance is injected by the injectDependencies () method that we saw earlier.

src/main/java/hello/SampleController.java


@Controller
public class SampleController {

    @Resource
    private SampleService service; //← Instances under framework management are injected here

...

With this mechanism, calling service.hello () will not result in a NullPointerException.

The framework that introduces the DI mechanism is called a "DI container". SpringFramework, Google Guice, Seasar2, etc. are DI containers.

The benefits of introducing DI are as follows.

  • The instance generation code (new keyword) of the joint part between classes is eliminated, and the required instance can be injected (assigned) at the required timing.
  • By defining the object to be injected in an external file (XML etc.), it can be a point to be extended later.
  • The framework can control the scope of the instance to be injected.
  • Scope is the scope of one instance.
  • singleton: Share one instance as a whole.
  • prototype: Create an instance for each injection.
  • session: Share the same instance during the same session.
  • request: Share the same instance during the same request.
  • Especially when singleton is selected, one instance is shared by multiple requests (multithread environment), so avoid having variable fields in the fields of objects managed by singleton. Let's do it. </ font> If you want to keep a variable field, you need to handle it with prototype.
  • It becomes easier to apply AOP from Framework.
  • The framework manages the instances, which makes it easier to intervene in cross-cutting processing.

HTTP request

This section handles the callpoint registration process when "http : // localhost: 8080 / hello" is called from an HTTP client. The part where the corresponding processing is performed in the framework is as follows.

Register the result in the controllers field of EasyApplication.

src/main/java/easyframework/EasyApplication.java


public class EasyApplication {

...
    /** KEY=path, VALUE= A instance and methods */
    private final static Map<String, HTTPController> controllers = new HashMap<>();

...

src/main/java/easyframework/EasyApplication.java


    private static void registerHTTPPaths() {
        //Check all components under framework management.
        components.entrySet().stream()
            //Among [email protected] to classes with Controller attached.
            .filter(kv -> kv.getKey().isAnnotationPresent(Controller.class))
            .forEach(kv ->
                //Get the method of Controller.
                Arrays.asList(kv.getKey().getMethods()).stream()
                    // @Limited to methods with RequestMapping annotation
                    .filter(m -> m.isAnnotationPresent(RequestMapping.class))
                    .forEach(m -> {
                        //Get information of RequestMapping annotation
                        RequestMapping rm = m.getAnnotation(RequestMapping.class);
                        //Gets the attribute value field that sets the HTTP call point.( rm.value() )
                        HTTPController c = new HTTPController(rm.value(), kv.getValue(), m);
                        //Register as Controller.
                        controllers.put(rm.value(), c);
                        System.out.println("Registered Controller => " + rm.value() + " - " + c);
                    })
            );
    }

The part that listens for a connection from an HTTP client and calls the method of the corresponding Controller is as follows.

    private static class EasyHttpServer implements Closeable {
...
        public void start() {
            while (true) {
                acceptRequest:
                //Wait for a request from a client.( server.accept() )
                try (Socket socket = server.accept();
                        BufferedReader br = new BufferedReader(
                            new InputStreamReader(socket.getInputStream(), "UTF-8"))) {

                    // br.readLine() => GET /hello HTTP/1.1
                    String path = br.readLine().split(" ")[1];
                    try (PrintStream os = new PrintStream(socket.getOutputStream())) {
                        //Requesting path/Extract the registered Controller based on hello.
                        HTTPController c = controllers.get(path);
                        if (c == null) {
                            os.println("404 Not Found (path = " + path + " ).");
                            break acceptRequest;
                        }
                        //Call the Controller method.( method.invoke )
                        os.println(c.method.invoke(c.instance));
                    }

                } catch (IOException | IllegalAccessException | InvocationTargetException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
...
    }

AOP

AOP stands for Aspect Oriented Programming and is a programming method that focuses on "cross-cutting concerns". In object-oriented programming, the processing specific to this object is handled together in individual definitions (classes, etc.), but processing that straddles them (cross-cutting concern) may be necessary. For example ...

  • Log output
  • Transaction management
  • Authentication process
  • Token check
  • Measurement of processing time
  • Validation check

Etc.

By using AOP, it is possible to insert processing across the board without editing the client program, which is the business program layer.

Here, in order to realize AOP, "[java.lang.reflect.Proxy](https://docs.oracle.com/javase/jp/8/docs/api/java/" provided as standard in Java lang / reflect / Proxy.html) "and" java.lang.reflect.InvocationHandler ) ”Is introduced.

If you specify the source object and the cross-cutting process (InvocationHandler) that you want to insert in the Proxy # newProxyInstance () method, a new instance that incorporates that process will be returned. Here, AOP is realized by instantly overwriting the created instance for DI.

When using Proxy for AOP, the target type must implement the interface. Here we want to intercept the SampleService interface.

src/main/java/easyframework/EasyApplication.java


    private static void registerAOP() {
        components.entrySet().stream()
            .filter(kv -> kv.getKey().isInterface())
            .forEach(kv -> components.put(kv.getKey(),
                Interceptor.createProxiedTarget(kv.getValue())));
        System.out.println("Registered AOP => " + components);
    }

src/main/java/easyframework/EasyApplication.java


package easyframework;

public class EasyApplication {

...
      public static void run(Class<?> clazz, String... args) {
          scanComponents(clazz);
          registerAOP(); //← Add this part
          injectDependencies();
          registerHTTPPaths();
          startHTTPServer();
      }
...

}

src/main/java/easyframework/Interceptor.java


package easyframework;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

class Interceptor implements InvocationHandler {

    private final Object target;

    public static Object createProxiedTarget(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new Interceptor(target));
    }

    private Interceptor(Object obj) {
        this.target = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
    }

}
  • In Proxy, it is necessary to prepare an interface for the part to be intercepted, so it is better to use the following library in the business program.
    • AspectJ: https://eclipse.org/aspectj/
    • Spring AOP: https://eclipse.org/aspectj/

Hands on

Addition of interceptor

  1. Create an easyframework.Transactional annotation.
  2. Add the @Transactional annotation to the hello.SampleService interface.
  • When attached to the type itself
  • When attached to a public method
  1. Implement the invoke method of easyframework.Interceptor according to the following specifications.
$ git checkout -b handsonAOP remotes/origin/handsonAOP

src/main/java/easyframework/Interceptor.java



    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(target, args);
        }

        return invoke(method, args);
    }

    private Object invoke(Method method, Object[] args) throws Throwable {
        /*
         *In the target method or type [email protected] Transactional annotation is added
         *Intercept the process that operates as follows.
         * 1.Before executing the corresponding method"Starts transaction."Is displayed on the standard output.
         * 2.When the corresponding method execution ends normally"Commit transaction."Is displayed on the standard output.
         * 3.When the corresponding method execution ends with an exception"Rollbak transaction."Is displayed on the standard output and the exception is thrown at the top.
         *
         * ※ @If Transactional is not added, return only the execution result of the corresponding method.
         * ※ @Transactional annotation is easyframework.Transactional.Create a new one as java.
         * ※ ./Run with gradlew clean build run and curl http://localhost:8080/Check with hello.
         */
         return null;
    }

The answer is in the addAOP branch.

$ git checkout remotes/origin/addAOP
$ less src/main/java/easyframework/Interceptor.java

Spring Boot dependencies

For reference, the Spring Boot sample [spring-boot-sample-tomcat](https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample- tomcat) Check what kind of libraries the project consists of.

$ git clone https://github.com/spring-projects/spring-boot.git
$ cd spring-boot/spring-boot-samples/spring-boot-sample-tomcat/
$ mvn dependency:tree
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Spring Boot Tomcat Sample 2.0.0.BUILD-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.10:tree (default-cli) @ spring-boot-sample-tomcat ---
[INFO] org.springframework.boot:spring-boot-sample-tomcat:jar:2.0.0.BUILD-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter:jar:2.0.0.BUILD-SNAPSHOT:compile
[INFO] |  +- org.springframework.boot:spring-boot:jar:2.0.0.BUILD-SNAPSHOT:compile
[INFO] |  +- org.springframework.boot:spring-boot-autoconfigure:jar:2.0.0.BUILD-SNAPSHOT:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-logging:jar:2.0.0.BUILD-SNAPSHOT:compile
[INFO] |  |  +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] |  |  |  \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] |  |  +- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
[INFO] |  |  \- org.slf4j:log4j-over-slf4j:jar:1.7.25:compile
[INFO] |  +- org.springframework:spring-core:jar:5.0.0.RC3:compile
[INFO] |  |  \- org.springframework:spring-jcl:jar:5.0.0.RC3:compile
[INFO] |  \- org.yaml:snakeyaml:jar:1.18:runtime
[INFO] +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.0.0.BUILD-SNAPSHOT:compile
[INFO] |  +- org.apache.tomcat.embed:tomcat-embed-core:jar:8.5.16:compile
[INFO] |  +- org.apache.tomcat.embed:tomcat-embed-el:jar:8.5.16:compile
[INFO] |  \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:8.5.16:compile
[INFO] +- org.springframework:spring-webmvc:jar:5.0.0.RC3:compile
[INFO] |  +- org.springframework:spring-aop:jar:5.0.0.RC3:compile
[INFO] |  +- org.springframework:spring-beans:jar:5.0.0.RC3:compile
[INFO] |  +- org.springframework:spring-context:jar:5.0.0.RC3:compile
[INFO] |  +- org.springframework:spring-expression:jar:5.0.0.RC3:compile
[INFO] |  \- org.springframework:spring-web:jar:5.0.0.RC3:compile
[INFO] \- org.springframework.boot:spring-boot-starter-test:jar:2.0.0.BUILD-SNAPSHOT:test
[INFO]    +- org.springframework.boot:spring-boot-test:jar:2.0.0.BUILD-SNAPSHOT:test
[INFO]    +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.0.0.BUILD-SNAPSHOT:test
[INFO]    +- com.jayway.jsonpath:json-path:jar:2.4.0:test
[INFO]    |  +- net.minidev:json-smart:jar:2.3:test
[INFO]    |  |  \- net.minidev:accessors-smart:jar:1.2:test
[INFO]    |  |     \- org.ow2.asm:asm:jar:5.0.4:test
[INFO]    |  \- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO]    +- junit:junit:jar:4.12:test
[INFO]    +- org.assertj:assertj-core:jar:3.8.0:test
[INFO]    +- org.mockito:mockito-core:jar:2.8.47:test
[INFO]    |  +- net.bytebuddy:byte-buddy:jar:1.6.14:test
[INFO]    |  +- net.bytebuddy:byte-buddy-agent:jar:1.6.14:test
[INFO]    |  \- org.objenesis:objenesis:jar:2.5:test
[INFO]    +- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO]    +- org.hamcrest:hamcrest-library:jar:1.3:test
[INFO]    +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO]    |  \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO]    \- org.springframework:spring-test:jar:5.0.0.RC3:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.507 s
[INFO] Finished at: 2017-08-20T17:50:33+09:00
[INFO] Final Memory: 21M/309M
[INFO] ------------------------------------------------------------------------

Constraints that cannot be realized by this framework

  • All DI components are singletons
  • DI is field injection only
  • DI components are generated only with the default constructor
  • Request parameter is not received and validator is not possible
  • HTTP method is GET only, not POST or PUT
  • Complex AOP is not possible
  • HotDeploy (Hot Reloading) not possible

There are many others.

The book that I used as a reference

Further details, such as those mentioned in this article, can be found in the books below. If you are interested, please read it.

** **

The site that I used as a reference

  • Create a simple HTTP server: http://qiita.com/opengl-8080/items/ca152658a0e52c786029
  • Various ways to get the list of classes under package: http://etc9.hatenablog.com/entry/2015/03/31/001620
  • TECHSCORE annotation: http://www.techscore.com/tech/Java/JavaSE/JavaLanguage/7/
  • Una's [email protected] Inherited operation check: http://unageanu.hatenablog.com/entry/20100712/1278946999

Recommended Posts