JSP Learning Notes - Custom MVC Framework

Keywords: Java JSON Apache Google

Implement a lightweight MVC framework based on Spring MVC. Knowledge involves reflection mechanism, use of annotations, and use of some third-party toolkits.

thinking

The main overall process is shown in the figure below.

As before, we defined a Dispatch Servlet to intercept requests (url requests at the end of. do are generally intercepted here);

After that, the Dispatch Servlet finds the corresponding method in Controller and executes it according to the url, returning a result.

Depending on the results returned, we use Dispatch Servlet to perform different operations (request forwarding, page redirection, return json data)

After reading this, the overall thinking should be very clear, the problem is coming:

  1. How to implement Dispatch Servlet according to the url to find the corresponding method?
  2. How can Dispatch Servlet perform different operations based on the returned results?

For the first problem, we can use annotations to implement it.

  1. Define a Controller annotation to mark the Controller class
  2. Define a Request Mapping annotation to mark the method of url matching

For the second question, we can set such a set of rules:

  1. The result returned is String, and starting with redirect: indicates that the Dispatch Servlet is going to perform page redirection
  2. The result returned is String, not redirect: which indicates that the Dispatch Servlet performs the request forwarding operation
  3. The result returned is a bean class, which is automatically converted to json data

Usually we will have the doGet method call the doPost method, so we just need to rewrite the doPost method in the Servlet.

From the above ideas, we can get a detailed flow chart (that is, the process in the doPost method)

Key Point Realization

Processing request url

//Get url address
String servletPath = req.getServletPath();
String requestUrl = StringUtils.substringBefore(servletPath,".do");

Use annotations to find the corresponding method

As I said before, we define two annotations, one is Controller and the other is Request Mapping.

Marking a class and method

@Controller
public class UserController{
    @RequestMapping("/user/login")
    public String login(HttpServletRequest request){
        return "login.jsp";
    }
    ...
}

We can then use the Lang3 open source library to find out if there is a Controller tag in the class, and then find out if the method is marked by RequestMapping, and save the url of the tag on the RequestMapping annotation, the full class name and the method name of the Controller.

Here I use a ClassMapping class to store the full class name and method name, then use url as key, ClassMapping object as value, and store it in a HashMap.

Although it can also be placed in the doPost method, it wastes resources because the doPost method may be executed many times. So, it's better to put it in init(), the initialization method of the Servlet, so that it can only be executed once.

I encapsulated a configuration class Configuration specifically designed to find URLs and their corresponding methods

Determine whether a class contains Controller annotations

controllerClass.isAnnotationPresent(Controller.class)

Get a list of methods with RequestMapping annotations in the class

Method[] methods = MethodUtils.getMethodsWithAnnotation(controllerClass, RequestMapping.class, true, true);

For more code, see the Configuration class in the following code section

Reference passing and reflection calling methods

First we get the parameter list type of the method, then we transfer the object into the Object array to reflect the invocation method.

To avoid errors, you need to have an empty check operation and whether it contains a key. The code posted below is omitted.

ClassMapping classMapping = classMappingMap.get(requestUrl);
Class<?> controllerClass = classMapping.getControllerClass();
Method method = classMapping.getMethod();

//Get a list of the parameter types of the method, and then process the parameters from the request based on this list.
Class<?>[] parameterTypes = method.getParameterTypes();
//The parameter values that need to be passed into the method after storage
Object[] paramValues = new Object[parameterTypes.length];

//Processing the parameters from request, passing the parameters to the corresponding method in controller, calling the method
try {
    //Instantiate controller class
    Object o = controllerClass.newInstance();

    for (int i = 0; i < parameterTypes.length; i++) {
        //Here we only consider four cases, so there are only four types of method parameters for Controller species.
        if (ClassUtils.isAssignable(parameterTypes[i], HttpServletRequest.class)) {
            paramValues[i] = req;
        } else if (ClassUtils.isAssignable(parameterTypes[i], HttpServletResponse.class)) {
            paramValues[i] = resp;
        } else if (ClassUtils.isAssignable(parameterTypes[i], HttpSession.class)) {
            paramValues[i] = req.getSession(true);
        } else {
            //Turn to JavaBean
            if (parameterTypes[i] != null) {
                //Get the parameter value from request and convert it to javabean
                Map<String, String[]> parameterMap = req.getParameterMap();

                //Instantiate this JavaBean type
                Object bean = parameterTypes[i].newInstance();
                //Quickly convert values to bean s
                BeanUtils.populate(bean, parameterMap);
                paramValues[i] =bean;
            }
        }
    }

    //Call the method, get the return value, according to the return value, perform page Jump or return json data and other operations
    Object returnValue = MethodUtils.invokeMethod(o, true,method.getName(), paramValues);

Perform different operations based on the returned results

Here we use Google's gson framework for converting entity classes or lists into json data

if (returnValue instanceof String) {
    String value = (String) returnValue;

    if (value.startsWith("redirect:")) {
        //redirect
        resp.sendRedirect(req.getContextPath()+ StringUtils.substringAfter(value,"redirect:"));
    } else {
        //Request forwarding
        req.getRequestDispatcher(value).forward(req,resp);
    }
} else {
    //Return an object
    if (returnValue != null) {
        //Convert to json, ajax operation
        String json = new Gson().toJson(o);
        resp.getWriter().print(json);
    }

Use of Attention Points

Don't forget to configure Servlets. Like previous Servlets, you can configure them by configuring web.xml or annotations.

The annotations on the method RequestMapping need a "/ beginning" and the request url of the web page does not need a "/" beginning, but needs A. do ending.

Since we only implement four types of encapsulation, there are only four types of method parameters in the Controller class: request, response, session, and a JavaBean class.

Code

DispatchServlet

package mvc;

import com.google.gson.Gson;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * @author StarsOne
 * @date Create in  2019/8/9 0009 10:11
 * @description
 */
public class DispatcherServlet extends HttpServlet {
    private Map<String, ClassMapping> classMappingMap =null;
    private Logger logger = Logger.getLogger(DispatcherServlet.class);
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        //Getting map data in servlet initialization
        classMappingMap = new Configuration().config();
    }

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

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

        //Get url address
        String servletPath = req.getServletPath();
        String requestUrl = StringUtils.substringBefore(servletPath,".do");
        //According to the url address to match the map, find the corresponding controller class and method, and then reflect the parameter call.

        if (classMappingMap != null && classMappingMap.containsKey(requestUrl)) {
            ClassMapping classMapping = classMappingMap.get(requestUrl);
            Class<?> controllerClass = classMapping.getControllerClass();
            Method method = classMapping.getMethod();

            //Get a list of the parameter types of the method, and then process the parameters from the request based on this list.
            Class<?>[] parameterTypes = method.getParameterTypes();
            //The parameter values that need to be passed into the method after storage
            Object[] paramValues = new Object[parameterTypes.length];

            //Processing the parameters from request, passing the parameters to the corresponding method in controller, calling the method
            try {
                //Instantiate controller class
                Object o = controllerClass.newInstance();

                for (int i = 0; i < parameterTypes.length; i++) {

                    if (ClassUtils.isAssignable(parameterTypes[i], HttpServletRequest.class)) {
                        paramValues[i] = req;
                    } else if (ClassUtils.isAssignable(parameterTypes[i], HttpServletResponse.class)) {
                        paramValues[i] = resp;
                    } else if (ClassUtils.isAssignable(parameterTypes[i], HttpSession.class)) {
                        paramValues[i] = req.getSession(true);
                    } else {
                        //Turn to JavaBean
                        if (parameterTypes[i] != null) {
                            //Get the parameter value from request and convert it to javabean
                            Map<String, String[]> parameterMap = req.getParameterMap();

                            //Instantiate this JavaBean type
                            Object bean = parameterTypes[i].newInstance();
                            //Quickly convert values to bean s
                            BeanUtils.populate(bean, parameterMap);
                            paramValues[i] =bean;
                        }
                    }
                }

                //Call the method, get the return value, according to the return value, perform page Jump or return json data and other operations
                Object returnValue = MethodUtils.invokeMethod(o, true,method.getName(), paramValues);

                if (returnValue instanceof String) {
                    String value = (String) returnValue;

                    if (value.startsWith("redirect:")) {
                        //redirect
                        resp.sendRedirect(req.getContextPath()+ StringUtils.substringAfter(value,"redirect:"));
                    } else {
                        //Request forwarding
                        req.getRequestDispatcher(value).forward(req,resp);
                    }
                } else {
                    //Return an object
                    if (returnValue != null) {
                        //Convert to json, ajax operation
                        String json = new Gson().toJson(o);
                        resp.getWriter().print(json);
                    }
                }
            } catch (InstantiationException e) {
                logger.error("instantiation Controller Object error!");
            } catch (IllegalAccessException e) {
                logger.error("Illegal access method!");
            } catch (InvocationTargetException e) {
                logger.error("invocation target exception");
            } catch (NoSuchMethodException e) {
                logger.error("The method invoked does not exist!");
            }

        } else {
            throw new RuntimeException("url Non-existent" + requestUrl);
        }
    }
}

ClassMapping

Used to store full class names and method names

package mvc;

import java.lang.reflect.Method;

/**
 * Class class and corresponding Method method
 * @author StarsOne
 * @date Create in  2019/8/8 0008 22:13
 * @description
 */
public class ClassMapping {
    private Class<?> controllerClass;
    private Method method;

    public ClassMapping(Class<?> controllerClass, Method method) {
        this.controllerClass = controllerClass;
        this.method = method;
    }

    public Class<?> getControllerClass() {
        return controllerClass;
    }

    public void setControllerClass(Class<?> controllerClass) {
        this.controllerClass = controllerClass;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    @Override
    public String toString() {
        return controllerClass.getName() + "." + method.getName();
    }
}

Configuration

package mvc;

import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.log4j.Logger;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;

import mvc.Annotation.Controller;
import mvc.Annotation.RequestMapping;

/**
 * @author StarsOne
 * @date Create in  2019/8/8 0008 22:11
 * @description
 */
public class Configuration {
    Logger logger = Logger.getLogger(Configuration.class);

    private String getControllerPackage() {
        return ResourceBundle.getBundle("package").getString("packagePath");
    }
    public Map<String, ClassMapping> config() {
        Map<String, ClassMapping> classMappingMap = Collections.synchronizedMap(new HashMap<>());

        try {
            //According to the package name defined in the resource file, find the folder of the controller and get the class name.
            File file = new File(getClass().getResource("/").toURI());
            String controllerPackage = getControllerPackage();
            String controllerFullPath = file.getPath() + File.separator +controllerPackage.replaceAll("\\.",File.separator);
            //The folder where the controller class resides
            file = new File(controllerFullPath);
            //Filter files, only find class files
            String[] classNames = file.list((dir, name) -> name.endsWith(".class"));

            for (String className : classNames) {
                //Stitching the whole class name
                String controllerFullName = controllerPackage + "." + StringUtils.substringBefore(className,".class");

                Class controllerClass = ClassUtils.getClass(controllerFullName);
                //Does the class have controller annotations?
                if (controllerClass.isAnnotationPresent(Controller.class)) {
                    //Find all the methods in the controller class that indicate the RequestMapping annotation
                    Method[] methods = MethodUtils.getMethodsWithAnnotation(controllerClass, RequestMapping.class, true, true);

                    for (Method method : methods) {
                        //Get the path on the annotation
                        String path = method.getAnnotation(RequestMapping.class).value();
                        //The path is key, save
                        classMappingMap.put(path,new ClassMapping(controllerClass,method));
                    }
                }
            }

        } catch (URISyntaxException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return classMappingMap;
    }

    public static void main(String[] args) {
        new Configuration().config();
    }
}

annotation

Controller

package mvc.annotation;

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

/**
 * @author stars-one at 2019-08-09 08:50
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
    String value() default "";
}

RequestMapping

package mvc.annotation;

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

/**
 * @author stars-one at 2019-08-09 08:50
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String value() default "";
}

Posted by Sakesaru on Sun, 15 Sep 2019 02:57:11 -0700