Implement a simple Spring MVC

Statement: for personal study notes only, please visit: Handwritten Spring MVC

 

If you need the source code, please click the link above.

 

General idea:

1. Spring MVC takes over all requests by registering the dispatcherservlet and configuring the URL pattern as / *. Similarly, if we need to implement the Mvc framework, we also need to implement it on the web Register a custom servlet in XML:

<servlet>
    <servlet-name>xmvc</servlet-name>
    <servlet-class>com.ph.xspring.servlet.XDispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <!--you can't use classpath*: -->
      <param-value>application.properties</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>xmvc</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

2. To implement MVC functions, you also need to configure some common annotations, such as @RequestMapping, @Controller, and so on. Here is a custom annotation

@Target({ElementType.TYPE}) //Restrict where annotations are used
@Retention(RetentionPolicy.RUNTIME) //Annotation retention policy. If you want to obtain annotation information through reflection, you need to keep annotation information in memory. You can only use runtime
@Documented  //For documentation
public @interface XController {
    String value()default "";
}

3. Next, you need to implement the contents of the xdispacerservlet, and read the configuration file, initialization parameters, IOC container initialization, dependency injection, and so on in this Servlet. These column initialization functions need to rewrite the init method in the HttpServlet and implement it in the init method.

4. After initialization, you need to receive and process the request from the browser, hand over the request in the service (GET, POST for example) method to dodispacer for processing, and return the result.

 

Let's parse the contents of xdispacerservlet:

1. Static variable

public class XDispatcherServlet extends HttpServlet {
    /*
    Property profile
     */
    private Properties contextConfig = new Properties();
    private List<String> classNamelist = new ArrayList<>();

    /**
     * IOC container
     */
    Map<String ,Object>iocMap = new HashMap<String,Object>();
    Map<String, Method>handlerMapping = new HashMap<String, Method>();

First, the above variables need to be initialized in the init method below.

contextConfig is used to represent configuration files, such as applicationContext

The classNameList is used to store the bytecode names of all java classes under the user specified package. It is used to obtain class information through reflection later.

iocMap is used to represent the IOC container. Objects injected through annotations or xml files (this article only injects through annotations) will be stored here.

handlerMapping is used to store the mapping relationship between the url and the corresponding method. For example, the "/Login" path in the controller corresponds to the Login() method. You can call the Login() method by accessing /Login.

2. Initialize

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

        //2.Scan related classes
        doScanner(contextConfig.getProperty("scan-package"));

        //3.initialization IOC Container to save all related class instances to IOC In container
        doInstance();

        //4.Dependency injection
        doAutowired();

        //5.initialization HandlerMapping
        initHandlerMapping();

Initialization has the above five steps, which will be explained one by one below.

2.1. Loading configuration files

    private void doLoadConfig(String contextConfigLocation) {
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);

        try {
            //Save in memory
            contextConfig.load(inputStream);
            System.out.println("[INFO-1] property file has been saved in contextConfig.");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(null != inputStream){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

The file name needs to be passed in. The file is placed in the resources directory. I have configured the packages to be scanned in the file (to be used in the next step), as follows

scan-package=com.ph

2.2 scanning related classes

       private void doScanner(String scanPackage) {

        URL resourcePath = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));
        System.out.println("INFO-2   "+resourcePath);
        if(resourcePath == null){
            return;
        }
        File classPath = new File(resourcePath.getFile());
        for(File file: classPath.listFiles()){
            if(file.isDirectory()){
                System.out.println("[INFO-2] {" + file.getName() + "} is a directory.");
                //Subdirectory recursion
                doScanner(scanPackage + "." + file.getName());
            }else{
                if(!file.getName().endsWith(".class")){
                    System.out.println("[INFO-2] {\" + file.getName() + \"} is not a class file.");
                    continue;
                }
                String className = (scanPackage+"." +file.getName()).replace(".class","");
               
                classNamelist.add(className);
                System.out.println("[INFO-2] {" + className + "} has been saved in classNameList.");
            }
        }

    }

Add all bytecode files under the specified package to the classNameList.

2.3 initialize IOC container

    private void doInstance() {
        if(classNamelist.isEmpty()){
            return;
        }
        try {
            for(String className: classNamelist){
                Class<?>clazz = Class.forName(className);

                if(clazz.isAnnotationPresent(XController.class)){
                    String beanName = toLowerFirstCase(clazz.getSimpleName());
                    Object instance = clazz.newInstance();

                    //Save in ioc container
                    iocMap.put(beanName, instance);
                    System.out.println("[INFO-3] {" + beanName + "} has been saved in iocMap.");

                }else if(clazz.isAnnotationPresent(XService.class)){
                    String beanName = toLowerFirstCase(clazz.getSimpleName());

                    //If the annotation contains a custom name
                    XService xService = clazz.getAnnotation(XService.class);
                    if("".equals(xService.value())){
                        beanName = xService.value();
                    }

                    Object instance = clazz.newInstance();
                    iocMap.put(beanName, instance);
                    System.out.println("[INFO-3] {" + beanName + "} has been saved in iocMap.");

                    //Find the interface of class(Inject all the interfaces that this class depends on)
                    for(Class<?> i :clazz.getInterfaces()){
                        if(iocMap.containsKey(i.getName())){
                            throw new Exception("The Bean Name is Exist.");
                        }
                        iocMap.put(i.getName(), instance);
                        System.out.println("[INFO-3] {" + i.getName() + "} has been saved in iocMap.");
                    }
                }
            }
        }catch(Exception e){

        }
    }

The injection here is realized by adding annotations. You need to judge which classes have been annotated (such as @Service, @Controller, etc.). If these annotations are available, you need to inject this class into the container. The corresponding is to add this class to the iocMap. If value is specified in the annotation, the value can be used as the bean name. Otherwise, the class name of the class (replace the initial letter of the class name with lowercase).

2.4 dependency injection

 private void doAutowired() {
        if(iocMap.isEmpty())
            return;
        for(Map.Entry<String, Object>entry : iocMap.entrySet()){
            //Get each class Fields in
            Field[]fields = entry.getValue().getClass().getDeclaredFields();

            for(Field field: fields){
                if(!field.isAnnotationPresent(XAutowired.class)){
                    continue;
                }
                //If there is a comment on this field @Autowired
                System.out.println("[INFO-4] Existence XAutowired.");

                //Get the class corresponding to the annotation
                XAutowired xAutowired = field.getAnnotation(XAutowired.class);
                String beanName = xAutowired.value().trim();

                //obtain XAutowired Value of annotation
                if("".equals(beanName)){
                    System.out.println("[INFO] xAutowired.value() is null");
                    //If the annotation has no value, the field name of the field is taken
                    beanName = field.getType().getName();
                }

                //As long as the annotation is added, it needs to be loaded, whether it is private still protect
                field.setAccessible(true);

                try{
                    //entry.getValue()What you get is a class object, icoMap.get(beanName)Get the object of the class to be injected
                    //To understand, here controller Assignment of the field to be injected in.
                    field.set(entry.getValue(), iocMap.get(beanName));
                    System.out.println("[INFO-4] field set {" + entry.getValue() + "} - {" + iocMap.get(beanName) + "}.");
                }catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }

    }

The so-called dependency injection here refers to parsing the @autowired annotation@ Autowired is marked on the variable. We need to take the corresponding instance from the IOC container and assign it to this variable.

For example:

    @XAutowired
    XTestService testService;

You need to find an instance of XTestService in the iocMap container and assign it to the variable testService. Then in the controller, you can call the service testService.

2.5. Mapping between initialization url and method

    //5,initialization handlerMapping
    private void initHandlerMapping() {
        if(iocMap.isEmpty()){
            return;
        }
        for(Map.Entry<String, Object> entry : iocMap.entrySet()){
            Class<?>clazz = entry.getValue().getClass();
            //Must have controller label
            if(!clazz.isAnnotationPresent(XController.class)){
                continue;
            }
            String baseUrl = "";
            if(clazz.isAnnotationPresent(XRequestMapping.class)){
                XRequestMapping xRequestMapping = clazz.getAnnotation(XRequestMapping.class);
                baseUrl = xRequestMapping.value();
            }

            for(Method method: clazz.getMethods()){
                if(!method.isAnnotationPresent(XRequestMapping.class)){
                    continue;
                }
                XRequestMapping xRequestMapping = method.getAnnotation(XRequestMapping.class);
                String url = ("/" + baseUrl + "/" + xRequestMapping.value()).replaceAll("/+","/");
                handlerMapping.put(url, method);
                System.out.println("[INFO-5] handlerMapping put {" + url + "} - {" + method + "}.");
            }
        }
    }

The value value in the @XRequestMapping annotation corresponds to the method. When a request comes, the corresponding method can be called through the requested url.

3. Process request

After initialization, you can process the request from the browser.

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //7,Operation phase
        try {
            doDispach(req, resp);
        } catch (Exception e) {
            e.printStackTrace();
            resp.getWriter().write("500 Exception Detail:\n" + Arrays.toString(e.getStackTrace()));
        }
    }
    private void doDispach(HttpServletRequest request, HttpServletResponse response) throws InvocationTargetException, IllegalAccessException {
        String url = request.getRequestURI();
        String contextPath = request.getContextPath();

        url = url.replaceAll(contextPath, "").replaceAll("/+","/");
        System.out.println("[INFO]request url ----> "+url);
        if(!(this.handlerMapping.containsKey(url))){
            try {
                response.getWriter().write("404 NOT FOUND");
                return;
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }

        //obtain url Corresponding method
        Method method = this.handlerMapping.get(url);
        System.out.println("[INFO]method ----> "+method);

        String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
        System.out.println("[INFO]iocMap.get(beanName)->" + iocMap.get(beanName));

        //The first parameter is the acquisition method. The following parameters are directly added to multiple parameters and correspond in sequence
        method.invoke(iocMap.get(beanName),request, response);
        System.out.println("[INFO] method.invoke put {" + iocMap.get(beanName) + "}.");
    }

Call the corresponding methods through the url, and pass in the request and response objects.

4. Writing test classes

@XController
@XRequestMapping("/test")
public class TestController {

    @XAutowired
    XTestService testService;

    @XRequestMapping("/getUserInfo")
    public void  getUserInfo(HttpServletRequest request, HttpServletResponse response){
        String userInfo = testService.getUserInfo();
        try {
            response.getWriter().write(userInfo);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

 

@XService
public class XTestServiceImpl implements XTestService {
    @Override
    public String getUserInfo() {
        return "admin:123";
    }
}

5. Configure tomcat and start it. Access in the browser:

http://localhost:8080/test/getUserInfo

Posted by phpnewbie1979 on Tue, 31 May 2022 16:44:31 +0530