Write a Spring framework with 300 lines of code

Posted by CrusaderSean on Sun, 23 Jan 2022 03:10:50 +0100

Author: Tom bomb architecture

Original link: Handwritten a Spring framework with 300 lines of code. Although the sparrow is small, it has five dirty parts - Nuggets

This article is excerpted from Spring 5 core principles

1 custom configuration

1.1 configure application Properties file

For the convenience of parsing, we use application Properties instead of application XML file. The specific configuration contents are as follows:

scanPackage=com.tom.demo

1.2 configure web XML file

As we all know, all projects that depend on the Web container are read from the Web XML file. Let's configure the Web Content in XML:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
   xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
   xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
   version="2.4">
   <display-name>Gupao Web Application</display-name>
   <servlet>
      <servlet-name>gpmvc</servlet-name>
      <servlet-class>com.tom.mvcframework.v1.servlet.GPDispatcherServlet</servlet-class>
      <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>application.properties</param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
   </servlet>
   <servlet-mapping>
      <servlet-name>gpmvc</servlet-name>
      <url-pattern>/*</url-pattern>
   </servlet-mapping>
</web-app>

GPDispatcherServlet is the core function class that simulates the implementation of Spring.

1.3 user defined annotation

@GPService notes are as follows:

package com.tom.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPService {
    String value() default "";
}

@GPAutowired notes are as follows:

package com.tom.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPAutowired {
    String value() default "";
}

@GPController notes are as follows:

package com.tom.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPController {
    String value() default "";
}

@The GPRequestMapping annotation is as follows:

package com.tom.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPRequestMapping {
    String value() default "";
}

@GPRequestParam is annotated as follows:

package com.tom.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPRequestParam {
    String value() default "";
}

1.4 configuration notes

Configure the business implementation class DemoService:

package com.tom.demo.service.impl;
import com.tom.demo.service.IDemoService;
import com.tom.mvcframework.annotation.GPService;
/**
 * Core business logic
 */
@GPService
public class DemoService implements IDemoService{
   public String get(String name) {
      return "My name is " + name;
   }
}

Configure request entry class DemoAction:

package com.tom.demo.mvc.action;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.tom.demo.service.IDemoService;
import com.tom.mvcframework.annotation.GPAutowired;
import com.tom.mvcframework.annotation.GPController;
import com.tom.mvcframework.annotation.GPRequestMapping;
import com.tom.mvcframework.annotation.GPRequestParam;
@GPController
@GPRequestMapping("/demo")
public class DemoAction {
   @GPAutowired private IDemoService demoService;
   @GPRequestMapping("/query")
   public void query(HttpServletRequest req, HttpServletResponse resp,
                 @GPRequestParam("name") String name){
      String result = demoService.get(name);
      try {
         resp.getWriter().write(result);
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
   @GPRequestMapping("/add")
   public void add(HttpServletRequest req, HttpServletResponse resp,
               @GPRequestParam("a") Integer a, @GPRequestParam("b") Integer b){
      try {
         resp.getWriter().write(a + "+" + b + "=" + (a + b));
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
   @GPRequestMapping("/remove")
   public void remove(HttpServletRequest req,HttpServletResponse resp,
                  @GPRequestParam("id") Integer id){
   }
}

At this point, the configuration is complete.

2 container initialization version 1.0

All core logic is written in init() method, and the code is as follows:

package com.tom.mvcframework.v1.servlet;
import com.tom.mvcframework.annotation.GPAutowired;
import com.tom.mvcframework.annotation.GPController;
import com.tom.mvcframework.annotation.GPRequestMapping;
import com.tom.mvcframework.annotation.GPService;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;

public class GPDispatcherServlet extends HttpServlet {
    private Map<String,Object> mapping = new HashMap<String, Object>();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req,resp);}
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
   
    @Override
    public void init(ServletConfig config) throws ServletException {
        InputStream is = null;
        try{
            Properties configContext = new Properties();
            is = this.getClass().getClassLoader().getResourceAsStream(config.getInitParameter ("contextConfigLocation"));
            configContext.load(is);
            String scanPackage = configContext.getProperty("scanPackage");
            doScanner(scanPackage);
            for (String className : mapping.keySet()) {
                if(!className.contains(".")){continue;}
                Class<?> clazz = Class.forName(className);
                if(clazz.isAnnotationPresent(GPController.class)){
                    mapping.put(className,clazz.newInstance());
                    String baseUrl = "";
                    if (clazz.isAnnotationPresent(GPRequestMapping.class)) {
                        GPRequestMapping requestMapping = clazz.getAnnotation (GPRequestMapping.class);
                        baseUrl = requestMapping.value();
                    }
                    Method[] methods = clazz.getMethods();
                    for (Method method : methods) {
                        if(!method.isAnnotationPresent(GPRequestMapping.class)){  continue; }
                        GPRequestMapping requestMapping = method.getAnnotation (GPRequestMapping.class);
                        String url = (baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
                        mapping.put(url, method);
                        System.out.println("Mapped " + url + "," + method);
                    }
                }else if(clazz.isAnnotationPresent(GPService.class)){
                        GPService service = clazz.getAnnotation(GPService.class);
                        String beanName = service.value();
                        if("".equals(beanName)){beanName = clazz.getName();}
                        Object instance = clazz.newInstance();
                        mapping.put(beanName,instance);
                        for (Class<?> i : clazz.getInterfaces()) {
                            mapping.put(i.getName(),instance);
                        }
                }else {continue;}
            }
            for (Object object : mapping.values()) {
                if(object == null){continue;}
                Class clazz = object.getClass();
                if(clazz.isAnnotationPresent(GPController.class)){
                    Field [] fields = clazz.getDeclaredFields();
                    for (Field field : fields) {
                        if(!field.isAnnotationPresent(GPAutowired.class)){continue; }
                        GPAutowired autowired = field.getAnnotation(GPAutowired.class);
                        String beanName = autowired.value();
                        if("".equals(beanName)){beanName = field.getType().getName();}
                        field.setAccessible(true);
                        try {
                            field.set(mapping.get(clazz.getName()),mapping.get(beanName));
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        } catch (Exception e) {
        }finally {
            if(is != null){
                try {is.close();} catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.print("GP MVC Framework is init");
    }
    private void doScanner(String scanPackage) {
        URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll ("\\.","/"));
        File classDir = new File(url.getFile());
        for (File file : classDir.listFiles()) {
            if(file.isDirectory()){ doScanner(scanPackage + "." +  file.getName());}else {
                if(!file.getName().endsWith(".class")){continue;}
                String clazzName = (scanPackage + "." + file.getName().replace(".class",""));
                mapping.put(clazzName,null);
            }
        }
    }
}

3 request execution

The key is to implement the doGet() and doPost() methods. In fact, the doDispatch() method is invoked in the doGet() and doPost() methods. The specific code is as follows:

public class GPDispatcherServlet extends HttpServlet {
    private Map<String,Object> mapping = new HashMap<String, Object>();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req,resp);}
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            doDispatch(req,resp);
        } catch (Exception e) {
            resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
        }
    }
    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        String url = req.getRequestURI();
        String contextPath = req.getContextPath();
        url = url.replace(contextPath, "").replaceAll("/+", "/");
        if(!this.mapping.containsKey(url)){resp.getWriter().write("404 Not Found!!");return;}
        Method method = (Method) this.mapping.get(url);
        Map<String,String[]> params = req.getParameterMap();
        method.invoke(this.mapping.get(method.getDeclaringClass().getName()),new Object[]{req,resp,params.get("name")[0]});
    }
    @Override
    public void init(ServletConfig config) throws ServletException {
        ...
    }

}

4 optimize and implement version 2.0

It is optimized in version 1.0 and encapsulates the code in init() method by using common design patterns (factory pattern, singleton pattern, delegation pattern and policy pattern). According to the previous implementation idea, first build the basic framework, and then "fill meat and inject blood". The specific code is as follows:

//Initialization phase
@Override
public void init(ServletConfig config) throws ServletException {

    //1. Load configuration file
    doLoadConfig(config.getInitParameter("contextConfigLocation"));

    //2. Scan related classes
    doScanner(contextConfig.getProperty("scanPackage"));
    
    //3. Initialize the scanned classes and put them into the IoC container
    doInstance();
    
    //4. Complete dependency injection
    doAutowired();

    //5. Initialize HandlerMapping
    initHandlerMapping();

    System.out.println("GP Spring framework is init.");

}

Declare global member variables, where IoC container is the specific case of single instance during registration:

//Save application Contents in the properties configuration file
private Properties contextConfig = new Properties();

//Save all class names scanned
private List<String> classNames = new ArrayList<String>();

//The legendary IoC container, let's uncover its mystery
//In order to simplify the program, ConcurrentHashMap is not considered for the time being
//Mainly focus on design ideas and principles
private Map<String,Object> ioc = new HashMap<String,Object>();

//Save the correspondence between url and Method
private Map<String,Method> handlerMapping = new HashMap<String,Method>();

Implement the doLoadConfig() method:

//Load profile
private void doLoadConfig(String contextConfigLocation) {
    //Find the path where the Spring main configuration file is located directly through the classpath
    //And read it out and put it in the Properties object
    //Equivalent to scanpackage = com tom. The demo is saved in memory
    InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
    try {
        contextConfig.load(fis);
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if(null != fis){
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Implement the doScanner() method:

//Scan related classes
private void doScanner(String scanPackage) {
    //scanPackage = com.tom.demo, which stores the package path
    //To convert to a file path is actually to convert Replace with/
    URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll ("\\.","/"));
    File classPath = new File(url.getFile());
    for (File file : classPath.listFiles()) {
        if(file.isDirectory()){
            doScanner(scanPackage + "." + file.getName());
        }else{
            if(!file.getName().endsWith(".class")){ continue;}
            String className = (scanPackage + "." + file.getName().replace(".class",""));
            classNames.add(className);
        }
    }
}

Implement the doInstance() method, which is the specific implementation of the factory pattern:

private void doInstance() {
    //Initialize and prepare for DI
    if(classNames.isEmpty()){return;}

    try {
        for (String className : classNames) {
            Class<?> clazz = Class.forName(className);

            //What classes need to be initialized?
            //Annotated classes are initialized. How to judge?
            //In order to simplify the code logic, we mainly understand the design idea, and only use @ Controller and @ Service as examples,
            //@Component and others will not give examples one by one
            if(clazz.isAnnotationPresent(GPController.class)){
                Object instance = clazz.newInstance();
                //Spring default class name initial lowercase
                String beanName = toLowerFirstCase(clazz.getSimpleName());
                ioc.put(beanName,instance);
            }else if(clazz.isAnnotationPresent(GPService.class)){
                //1. Custom beanName
                GPService service = clazz.getAnnotation(GPService.class);
                String beanName = service.value();
                //2. The default class name is lowercase
                if("".equals(beanName.trim())){
                    beanName = toLowerFirstCase(clazz.getSimpleName());
                }

                Object instance = clazz.newInstance();
                ioc.put(beanName,instance);
                //3. Automatic assignment according to type, which is an opportunistic way
                for (Class<?> i : clazz.getInterfaces()) {
                    if(ioc.containsKey(i.getName())){
                        throw new Exception("The "" + i.getName() + "" is exists!!");
                    }
                    //The type of the interface is directly regarded as a key
                    ioc.put(i.getName(),instance);
                }
            }else {
                continue;
            }

        }
    }catch (Exception e){
        e.printStackTrace();
    }

}

In order to facilitate processing, I implemented the toLowerFirstCase() method to realize the lowercase of the class name. The specific code is as follows:

//Change the first letter of the class name to lowercase
private String toLowerFirstCase(String simpleName) {
    char [] chars = simpleName.toCharArray();
    //The reason for adding is that the ASCII codes of large and small letters differ by 32
    //And the ASCII code of upper case letters is smaller than that of lower case letters
    //In Java, arithmetic operation on char is actually arithmetic operation on ASCII code
    chars[0] += 32;
    return String.valueOf(chars);
}

Implement the doAutowired() method:

//Automatic dependency injection
private void doAutowired() {
    if(ioc.isEmpty()){return;}

    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        //Get all fields, including private, protected and default
        //Normally, ordinary OOP programming can only obtain fields of public type
        Field[] fields = entry.getValue().getClass().getDeclaredFields();
        for (Field field : fields) {
            if(!field.isAnnotationPresent(GPAutowired.class)){continue;}
            GPAutowired autowired = field.getAnnotation(GPAutowired.class);

            //If the user does not have a custom beanName, it will be injected according to the type by default
            //This place eliminates the judgment of the lowercase initial of the class name. As an after-school homework, please "friends" to realize it by yourself
            String beanName = autowired.value().trim();
            if("".equals(beanName)){
                //Get the type of the interface as the key, and use this key to get the value in the IoC container later
                beanName = field.getType().getName();
            }

            //If it is a type other than public, mandatory assignment is required as long as @ Autowired annotation is added
            //Reflection is called violent access
            field.setAccessible(true);

            try {
                //Dynamically assign values to fields using reflection mechanism
                field.set(entry.getValue(),ioc.get(beanName));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }


        }

    }


}

Implement initHandlerMapping() method. HandlerMapping is an application case of policy mode:

//Initialize the one-to-one relationship between url and Method
private void initHandlerMapping() {
    if(ioc.isEmpty()){ return; }

    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        Class<?> clazz = entry.getValue().getClass();

        if(!clazz.isAnnotationPresent(GPController.class)){continue;}


        //Save the @ GPRequestMapping("/demo") written on the class
        String baseUrl = "";
        if(clazz.isAnnotationPresent(GPRequestMapping.class)){
            GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
            baseUrl = requestMapping.value();
        }

        //Get all public type methods by default
        for (Method method : clazz.getMethods()) {
            if(!method.isAnnotationPresent(GPRequestMapping.class)){continue;}

            GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);
            //optimization
            String url = ("/" + baseUrl + "/" + requestMapping.value())
                        .replaceAll("/+","/");
            handlerMapping.put(url,method);
            System.out.println("Mapped :" + url + "," + method);

        }


    }


}

Now that the initialization is completed, let's implement the running logic and look at the code of doGet() and doPost() methods:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req,resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    //Operation phase
    try {
        doDispatch(req,resp);
    } catch (Exception e) {
        e.printStackTrace();
        resp.getWriter().write("500 Exection,Detail : " + Arrays.toString(e.getStackTrace()));
    }
}

The delegation mode is used in the doPost() method. The specific logic of the delegation mode is implemented in the doDispatch() method:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp)throws Exception {
    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replaceAll(contextPath,"").replaceAll("/+","/");
    if(!this.handlerMapping.containsKey(url)){
        resp.getWriter().write("404 Not Found!!");
        return;
    }
    Method method = this.handlerMapping.get(url);
    //The first parameter: the instance where the method is located
    //The second parameter: the argument required when calling

    Map<String,String[]> params = req.getParameterMap();
    //Opportunistic way
    String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
    method.invoke(ioc.get(beanName),new Object[]{req,resp,params.get("name")[0]});
    //System.out.println(method);
}

In the above code, although dodispatch () completes dynamic delegation and makes reflection calls, the processing of url parameters is still static. In fact, it is a little complicated to realize the dynamic acquisition of url parameters. We can optimize the implementation of doDispatch() method. The code is as follows:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp)throws Exception {
    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replaceAll(contextPath,"").replaceAll("/+","/");
    if(!this.handlerMapping.containsKey(url)){
        resp.getWriter().write("404 Not Found!!");
        return;
    }

    Method method = this.handlerMapping.get(url);
    //First parameter: the instance where the method is located
    //The second parameter: the argument required when calling
    Map<String,String[]> params = req.getParameterMap();
    //Gets the formal parameter list of the method
    Class<?> [] parameterTypes = method.getParameterTypes();
    //Save the url parameter list of the request
    Map<String,String[]> parameterMap = req.getParameterMap();
    //The location where the assignment parameters are saved
    Object [] paramValues = new Object[parameterTypes.length];
    //Dynamic assignment according to parameter position
    for (int i = 0; i < parameterTypes.length; i ++){
        Class parameterType = parameterTypes[i];
        if(parameterType == HttpServletRequest.class){
            paramValues[i] = req;
            continue;
        }else if(parameterType == HttpServletResponse.class){
            paramValues[i] = resp;
            continue;
        }else if(parameterType == String.class){

            //Annotated parameters in the extraction method
            Annotation[] [] pa = method.getParameterAnnotations();
            for (int j = 0; j < pa.length ; j ++) {
                for(Annotation a : pa[i]){
                    if(a instanceof GPRequestParam){
                        String paramName = ((GPRequestParam) a).value();
                        if(!"".equals(paramName.trim())){
                            String value = Arrays.toString(parameterMap.get(paramName))
                                    .replaceAll("\\[|\\]","")
                                    .replaceAll("\\s",",");
                            paramValues[i] = value;
                        }
                    }
                }
            }

        }
    }
    //Opportunistic way
    //Obtain the Class where the Method is located through reflection. After obtaining the Class, you also need to obtain the name of the Class
    //Then call toLowerFirstCase to get beanName
    String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
    method.invoke(ioc.get(beanName),new Object[]{req,resp,params.get("name")[0]});
}

It's not easy to be original. It's cool to insist. I've seen it here. Little partners remember to like, collect and watch it. Pay attention to it three times a button! If you think the content is too dry, you can share and forward it to your friends!

​​​

Topics: Java Spring Back-end Programmer architecture