preface

The title is’ Implementing a Simple Java MVC Framework from Scratch ‘. Let’s just say the foreplay was a bit much. But all this foreplay is necessary, and there is no point in simply implementing an MVC function. You need Bean containers, IOC, AOP, and MVC to be a ‘framework’.

prepared

To implement MVC functionality, add some dependencies to POM.xml.

<properties>.<tomcat.version>8.5.31</tomcat.version>
    <jstl.version>1.2</jstl.version>
    <fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencies>.<! -- tomcat embed -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <version>${tomcat.version}</version>
    </dependency>

    <! -- JSTL -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>${jstl.version}</version>
        <scope>runtime</scope>
    </dependency>

    <! -- FastJson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
    </dependency>
</dependencies>
Copy the code
  • The tomcat-embed-jasper dependency introduces a built-in Tomcat package that spring-boot uses by default to start services directly. In addition to adding an embedded Tomcat, the package also introduces java.servlet-API and JSP-API. If you don’t want to use embedded Tomcat, you can remove tomcat-embed-jasper and introduce these two packages.

  • JSTL is used to parse JSP expressions, such as c:forEach statements written on JSP pages.

    <c:forEach items="${list}" var="user">
    	<tr>
            <td>${user.id}</td>
            <td>${user.name}</td>
    	</tr>
    </c:forEach>
    Copy the code
  • Fastjson is a JSON parsing package developed by Ali for converting entity classes to JSON. There are similar packages such as Gson and Jackson, here is not specific comparison, you can choose a favorite.

To realize the MVC

Implementation principle of MVC

First of all, we need to understand how MVC works. When we write projects using Spring-Boot, we usually write a series of controllers to implement links one by one, which is’ modern ‘writing. But before SpringMVC or even MVC frameworks like Struts2 were popular, they were implemented by writing servlets.

There is a Servlet for each request, which is then configured in web.xml, and the receiving and processing of requests are distributed over a large number of servlets, with very mixed code.

In order for people to write more business code and handle fewer requests, SpringMVC processes these requests through a central Servlet and then forwards them to the corresponding Controller, so there is only one Servlet that handles the requests. The passage below official documentation from the spring docs. Spring. IO/spring/docs…

Spring MVC, like many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing while actual work is performed by configurable, delegate components. This model is flexible and supports diverse workflows.

The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification using Java configuration or in web.xml. In turn the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.

Springmvc uses a central Servlet(DispatcherServlet) to implement operations on the control Controller. This Servlet is configured via Java or in web.xml, and it is used to find the mapping of the request (that is, find the corresponding Controller), view resolution (that is, the result of executing the Controller), exception handling (that is, unified handling of exceptions during execution), and so on

So the effects of implementing MVC are as follows:

  1. Through a central sevlet such asDispatcherServletTo receive all requests
  2. Find the corresponding controller according to the request
  3. Execute controller to obtain the result
  4. Parse the results of the Controller and go to the corresponding view
  5. If there are exceptions, they are handled in a unified manner

According to the above steps, we will start from steps 2, 3, 4 and 5, and finally implement 1 to complete MVC.

Create annotation

For easy implementation, create three annotations and an enumeration under com.zbw.mvC.Annotation package: RequestMapping, RequestParam, ResponseBody, RequestMethod.

package com.zbw.mvc.annotation;
import./** * HTTP request path */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    /** * Request path */
    String value(a) default "";

    /**
     * 请求方法
     */
    RequestMethod method(a) default RequestMethod.GET;
}
Copy the code
package com.zbw.mvc.annotation;

/** * HTTP request type */
public enum RequestMethod {
    GET, POST
}
Copy the code
package com.zbw.mvc.annotation;
import./** * Request method parameter name */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
    /** * Method parameter alias */
    String value(a) default "";

    /** * whether to send */
    boolean required(a) default true;
}
Copy the code
package com.zbw.mvc.annotation;
import./** * is used to flag return json */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

Copy the code

The purpose of these classes will not be explained, but they are the most common annotations in SpringMVC.

Create a ModelAndView

To make it easy to pass parameters to the front end, create a utility bean, equivalent to a simplified version of ModelAndView in Spring. This class is created under the com.zbw.mvC.bean package

package com.zbw.mvc.bean;
import./** * ModelAndView */
public class ModelAndView {

    /** * page path */
    private String view;

    /** * page data */
    private Map<String, Object> model = new HashMap<>();

    public ModelAndView setView(String view) {
        this.view = view;
        return this;
    }
    public String getView(a) {
        return view;
    }
    public ModelAndView addObject(String attributeName, Object attributeValue) {
        model.put(attributeName, attributeValue);
        return this;
    }
    public ModelAndView addAllObjects(Map
       
         modelMap)
       ,> {
        model.putAll(modelMap);
        return this;
    }
    public Map<String, Object> getModel(a) {
        returnmodel; }}Copy the code

Implement the Controller dispenser

The Controller dispenser is similar to a Bean container, except that the latter holds beans and the former holds controllers, and then depending on some criteria you can simply retrieve the corresponding Controller.

Create a ControllerInfo class under the com.zbw. MVC package to hold some Controller information.

package com.zbw.mvc;
import./** * ControllerInfo Stores Controller information */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ControllerInfo {
    /** * Controller class */
    privateClass<? > controllerClass;/** * the execution method */
    private Method invokeMethod;

    /** * Method parameter alias corresponds to parameter type */
    privateMap<String, Class<? >> methodParameter; }Copy the code

Then create a PathInfo class to hold the request path and request method type

package com.zbw.mvc;
import./** * PathInfo stores HTTP related information */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PathInfo {
    /** * HTTP request method */
    private String httpMethod;

    /** * HTTP request path */
    private String httpPath;
}
Copy the code

Next, create the Controller dispenser class ControllerHandler

package com.zbw.mvc;
import./** * Controller dispenser */
@Slf4j
public class ControllerHandler {

    private Map<PathInfo, ControllerInfo> pathControllerMap = new ConcurrentHashMap<>();

    private BeanContainer beanContainer;

    public ControllerHandler(a) { beanContainer = BeanContainer.getInstance(); Set<Class<? >> classSet = beanContainer.getClassesByAnnotation(RequestMapping.class);for (Class<?> clz : classSet) {
            putPathController(clz);
        }
    }

    /** * Get ControllerInfo */
    public ControllerInfo getController(String requestMethod, String requestPath) {
        PathInfo pathInfo = new PathInfo(requestMethod, requestPath);
        return pathControllerMap.get(pathInfo);
    }

    /** * Add information to the requestControllerMap */
    private void putPathController(Class
        clz) {
        RequestMapping controllerRequest = clz.getAnnotation(RequestMapping.class);
        String basePath = controllerRequest.value();
        Method[] controllerMethods = clz.getDeclaredMethods();
        // 1. Iterate through the methods in Controller
        for (Method method : controllerMethods) {
            if (method.isAnnotationPresent(RequestMapping.class)) {
                // 2. Get the parameter name and parameter type of this methodMap<String, Class<? >> params =new HashMap<>();
                for (Parameter methodParam : method.getParameters()) {
                    RequestParam requestParam = methodParam.getAnnotation(RequestParam.class);
                    if (null == requestParam) {
                        throw new RuntimeException("Must have a parameter name specified by RequestParam");
                    }
                    params.put(requestParam.value(), methodParam.getType());
                }
                // 3. Get a RequestMapping annotation for this method
                RequestMapping methodRequest = method.getAnnotation(RequestMapping.class);
                String methodPath = methodRequest.value();
                RequestMethod requestMethod = methodRequest.method();
                PathInfo pathInfo = new PathInfo(requestMethod.toString(), basePath + methodPath);
                if (pathControllerMap.containsKey(pathInfo)) {
                    log.error("Url :{} Repeat registration", pathInfo.getHttpPath());
                    throw new RuntimeException("Duplicate URL registration");
                }
                // 4. Generate ControllerInfo and store it in the Map
                ControllerInfo controllerInfo = new ControllerInfo(clz, method, params);
                this.pathControllerMap.put(pathInfo, controllerInfo);
                log.info("Add Controller RequestMethod:{}, RequestPath:{}, Controller:{}, Method:{}", pathInfo.getHttpMethod(), pathInfo.getHttpPath(), controllerInfo.getControllerClass().getName(), controllerInfo.getInvokeMethod().getName()); }}}}Copy the code

The most complex method of this class is the putPathController() method called in the constructor. This method is also the core method of this class. It stores information from the Controller class into the pathControllerMap variable. To explain some of the functionality of this class:

  1. Get the Bean container in the constructorBeanContainerA singleton instance of
  2. Gets and traversesBeanContainerIs stored in theRequestMappingAnnotation tag class
  3. Iterate through the methods in this class to find theRequestMappingMethods for annotating tags
  4. Gets the parameter name and parameter type of this method, generatedControllerInfo
  5. According to theRequestMappingIn thevalue()andmethod()generatePathInfo
  6. The generatedPathInfoandControllerInfoAll variablespathControllerMapIn the
  7. Other classes are calledgetController()Method to get the corresponding controller

This is the flow of this class, with one caveat:

In step 4, you must specify that all parameter names of the method are annotated with RequestParam annotations. This is because in Java, although we write the parameter names, such as String name, the ‘name’ field is erased when compiled into a class file. So a RequestParam is required to save the name.

But you don’t have to annotate every method in SpringMVC because spring uses * ASM *, a tool that takes parameter names and saves them before compiling. There is also a way to save parameter names that has been supported since java8, but the compiler’s parameters must be modified to support this. Both of these methods are complicated to implement or have limitations, so I’m not going to implement them here, but you can do it yourself

Implement the result executor

Next we implement the result executor, which implements steps 3, 4, and 5 in the MVC flow.

Create the class ResultRender under the com.zbw. MVC package

package com.zbw.mvc;
import./** * Result executor */
@Slf4j
public class ResultRender {

    private BeanContainer beanContainer;

    public ResultRender(a) {
        beanContainer = BeanContainer.getInstance();
    }

    /** * Implements the Controller method */
    public void invokeController(HttpServletRequest req, HttpServletResponse resp, ControllerInfo controllerInfo) {
        // 1. Get all parameters of HttpServletRequest
        Map<String, String> requestParam = getRequestParams(req);
        // 2. Instantiate the parameter values to be passed by the call method
        List<Object> methodParams = instantiateMethodArgs(controllerInfo.getMethodParameter(), requestParam);

        Object controller = beanContainer.getBean(controllerInfo.getControllerClass());
        Method invokeMethod = controllerInfo.getInvokeMethod();
        invokeMethod.setAccessible(true);
        Object result;
        // 3. Call the method through reflection
        try {
            if (methodParams.size() == 0) {
                result = invokeMethod.invoke(controller);
            } else{ result = invokeMethod.invoke(controller, methodParams.toArray()); }}catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 4. Parse the return value of the method and select return page or JSON
        resultResolver(controllerInfo, result, req, resp);
    }

    /** * Get the HTTP parameter */
    private Map<String, String> getRequestParams(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        // The GET and POST methods GET the request parameters this way
        request.getParameterMap().forEach((paramName, paramsValues) -> {
            if (ValidateUtil.isNotEmpty(paramsValues)) {
                paramMap.put(paramName, paramsValues[0]); }});// TODO:Body, Path, Header, etc
        return paramMap;
    }

    /** * instantiate method arguments */
    private List<Object> instantiateMethodArgs(Map
       
        > methodParams, Map
        
          requestParams)
        ,>
       ,> {
        returnmethodParams.keySet().stream().map(paramName -> { Class<? > type = methodParams.get(paramName); String requestValue = requestParams.get(paramName); Object value;if (null == requestValue) {
                value = CastUtil.primitiveNull(type);
            } else {
                value = CastUtil.convert(type, requestValue);
                // TODO:Implements parameter instantiation of non-native classes
            }
            return value;
        }).collect(Collectors.toList());
    }


    /** * Controller method returns value resolution */
    private void resultResolver(ControllerInfo controllerInfo, Object result, HttpServletRequest req, HttpServletResponse resp) {
        if (null == result) {
            return;
        }
        boolean isJson = controllerInfo.getInvokeMethod().isAnnotationPresent(ResponseBody.class);
        if (isJson) {
            // Set the response header
            resp.setContentType("application/json");
            resp.setCharacterEncoding("UTF-8");
            // Write data to the response
            try (PrintWriter writer = resp.getWriter()) {
                writer.write(JSON.toJSONString(result));
                writer.flush();
            } catch (IOException e) {
                log.error("Forwarding request failed", e);
                // TODO:Unified handling of exceptions, 400, etc..}}else {
            String path;
            if (result instanceof ModelAndView) {
                ModelAndView mv = (ModelAndView) result;
                path = mv.getView();
                Map<String, Object> model = mv.getModel();
                if (ValidateUtil.isNotEmpty(model)) {
                    for(Map.Entry<String, Object> entry : model.entrySet()) { req.setAttribute(entry.getKey(), entry.getValue()); }}}else if (result instanceof String) {
                path = (String) result;
            } else {
                throw new RuntimeException("Return type invalid");
            }
            try {
                req.getRequestDispatcher("/templates/" + path).forward(req, resp);
            } catch (Exception e) {
                log.error("Forwarding request failed", e);
                // TODO:Unified handling of exceptions, 400, etc..}}}}Copy the code

The method in the Controller is called by calling the invokeController() method in the class and the corresponding page is parsed based on the result. The main process is:

  1. callgetRequestParams()Get the parameters in HttpServletRequest
  2. callinstantiateMethodArgs()Instantiates the parameter values to be passed in by the call method
  3. Call the target method of the target controller through reflection
  4. callresultResolver()Parse the return value of the method, choosing return page or JSON

These steps can be regarded as the core steps of MVC. However, due to the length problem, the functions of almost every step are simplified, such as

  • Step 1 Obtain parameters in HttpServletRequest. Only parameters sent by GET or POST are obtained. In fact, Body, Path, Header and other request parameters are not obtained
  • Step 2 Instantiate the values of the call method. Only the Java native parameters are implemented. The instantiation of the custom class is not implemented
  • Step 4 Unified exception handling is not implemented

Despite its flaws, an MVC flow is complete. Now it’s time to put these functions together.

Realize the DispatcherServlet

Finally, the DispatcherServlet class inherits from HttpServlet, through which all requests pass.

Create DispatcherServlet under com.zbw. MVC

package com.zbw.mvc;
import./** * DispatcherServlet all HTTP requests are forwarded by this Servlet */
@Slf4j
public class DispatcherServlet extends HttpServlet {

    private ControllerHandler controllerHandler = new ControllerHandler();

    private ResultRender resultRender = new ResultRender();

    /** * Execute the request */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Set the request encoding
        req.setCharacterEncoding("UTF-8");
        // Get the request method and request path
        String requestMethod = req.getMethod();
        String requestPath = req.getPathInfo();
        log.info("[DoodleConfig] {} {}", requestMethod, requestPath);
        if (requestPath.endsWith("/")) {
            requestPath = requestPath.substring(0, requestPath.length() - 1);
        }

        ControllerInfo controllerInfo = controllerHandler.getController(requestMethod, requestPath);
        log.info("{}", controllerInfo);
        if (null == controllerInfo) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return; } resultRender.invokeController(req, resp, controllerInfo); }}Copy the code

The ControllerHandler and ResultRender classes are called in this class, which first fetch the corresponding ControllerInfo based on the requested method and path, and then parse the corresponding view using ControllerInfo. You can then access the corresponding page or return the corresponding JSON information.

All requests are sent through the DispatcherServlet. This is because you need to configure web. XML. In the old days of SpringMVC + Spring + Mybatis, there were a lot of configuration files to write, one of which was web.xml, which was added to it. It is the DispatcherServlet that makes all requests go through the wildcard *.

<servlet>
	<servlet-name>springMVC</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
	<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
	<servlet-name>springMVC</servlet-name>
	<url-pattern>*</url-pattern>
</servlet-mapping>
Copy the code

We don’t need to do that though, and in honor of Spring-Boot, we’ll implement Tomcat embedded in the next section and start it from the launcher.

defects

This section of the code may not look very comfortable, this is because the current code is said to be implemented, but the code structure needs to be optimized.

First of all, the DispatcherServlet is a request dispatcher and there should be no logical code to handle Http

Secondly, we put MVC steps 3, 4 and 5 into a class, which is not good either. Originally, the functions of each step here are very complicated, and we put these steps into a class, which is not conducive to changing the functions of corresponding steps in the later stage.

There is also no current implementation of abnormal processing, can not return abnormal pages to the user.

These optimizations will be done in later chapters.


  • Implement a simple Java MVC framework from scratch (I)– Preface
  • Implement a simple Java MVC framework from scratch (two)- implement Bean container
  • Implement a simple Java MVC framework from scratch (iii)– implement IOC
  • Implement a simple Java MVC framework from scratch (four)– implement AOP
  • Implementing a simple Java MVC framework from scratch (5)– Introducing AspectJ to implement AOP pointcuts
  • Implement a simple Java MVC framework from scratch (6)– enhance AOP functionality
  • Implement a simple Java MVC framework from scratch (7)– Implement MVC
  • Implement a simple Java MVC framework from scratch (8)- Starter
  • Implement a simple Java MVC framework from scratch (9)– optimize THE MVC code

Source address :doodle

Implement a simple Java MVC framework from scratch (7)– Implement MVC