Simple implementation of spring MVC framework based on Java

Time:2021-5-4

preface

In this paper, bloggers implement a simple framework step by step from servlet to controller layer. With this framework, we can use the following basic annotations as spring does:

  • @XxgController
  • @XxgRequestMapping
  • @XxgParam
  • @XxgRequestBody

Before reading this article, you should probably understand the following:

  • BeanUtils
  • ObjectMapper
  • Servlet related knowledge

Idea: interceptor realizes route distribution. Using annotations?

reflection:

  1. The interceptor can intercept all request paths before the servlet
  2. You can find the method in the annotation that matches the request path
  3. Then, req and resp are forwarded to the method for execution

Question:

How does the interceptor find a way to use this annotation? Package scan? How to realize it?

analysis:

Package scanning involves IO flow, and the file class can recursively query all the files below. Let’s

You can filter:

  1. As long as the suffix is. Class file, and get its classname (including package path)
  2. Get this class through reflection, judge whether it has specified annotation, and then filter it again

In this way, when the interceptor intercepts the request path, we can match and call the method.

Lazy:

Because of the MVC design pattern, we generally put the API interfaces under the same package, so we can directly specify to scan the package, and ignore other packages

1、 Implementation of scan class version 1.0

public class FileScanner {
 private final String packetUrl = "com.dbc.review.controller";
 private final ClassLoader classLoader = FileScanner.class.getClassLoader();
 private List<Class> allClazz = new ArrayList<>(10); // Save all annotated classes in the package
​
 public List<Class> getAllClazz(){
   return this.allClazz;
 }
​
 public String getPacketUrl(){
   return this.packetUrl;
 }
​
 //Query all classes that use the given annotation
 //Recursively scan the package. If class is scanned, the class processing method is called to collect the desired class
 public void loadAllClass(String packetUrl) throws Exception{
     String url = packetUrl.replace(".","/");
     URL resource = classLoader.getResource(url);
     if (resource == null) {
        return;
    }
     String path = resource.getPath();
     File file = new File(URLDecoder.decode(path, "UTF-8"));
     if (!file.exists()) {
        return;
     }
     if (file.isDirectory()){
     File[] files = file.listFiles();
     if (files == null) {
        return;
     }
     for (File f : files) {
         String classname = f.getName().substring(0, f.getName().lastIndexOf("."));
         if (f.isDirectory()) {
            loadAllClass(packetUrl + "." + classname);
         }
         if (f.isFile() && f.getName().endsWith(".class")) {
             Class clazz = Class.forName(packetUrl + "." + classname);
             dealClass( clazz);
         }
    }
     }
 }
​
 private void dealClass(Class clazz) {
 if ((clazz.isAnnotationPresent(Controller.class))) {
 allClazz.add(clazz);
 }
 }
​
 //In real use, the processing method is obtained according to the request path and request method
 public boolean invoke(String url,String requestMethod) {
     for (Class clazz : allClazz){
     Controller controller = (Controller) clazz.getAnnotation(Controller.class);
     Method[] methods = clazz.getDeclaredMethods();
     for (Method method : methods) {
         RequestMapping requestMapping = method.getDeclaredAnnotation(RequestMapping.class);
         if (requestMapping == null) {
         continue;
         }
         for (String m : requestMapping.methods()) {
         m = m.toUpperCase();
         if (!m.toUpperCase().equals(requestMethod.toUpperCase())) {
         continue;
         }
         StringBuilder sb = new StringBuilder();
         String urlItem = sb.append(controller.url()).append(requestMapping.url()).toString();
         if (urlItem.equals(url)) {
         //Gets the method used to handle this API interface
         try {
        //                              Method. Getgenericparametertypes() // you can use this method to determine which parameters the method needs to pass
         method.invoke(clazz.newInstance());
         return true;
         } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
         e.printStackTrace();
         return false;
         }
     }
     }
     }
     }
     return false;
 }
​
 @Test
 public void test() throws Exception {
     //1. Instantiate in the static code block of filter
     FileScanner fileScanner = new FileScanner();
     //2. Start scanning
     fileScanner.loadAllClass(fileScanner.getPacketUrl());
     //3. After intercepting the request, call this method to execute
     //If there is no post request / test / post processing method defined under the package, false will be returned
     //True returned after successful execution
     fileScanner.invoke("/test/post","post");
     //4. If the execution fails and returns false, a 405 undefined method will be thrown.
    ​
     //Finally: this class does not implement the parameter transfer of controller
     //         For a moment, I think that it is not easy to get the parameter list according to the method and then transfer the corresponding parameters
     }
}

TestController

@Controller(url = "/test")
public class TestController {
​
 @RequestMapping(url = "/get",methods = "GET")
 public void get(){
 System.out.println(111);
 }
 @RequestMapping(url = "/post",methods = {"POST","get"})
 public void post(){
 System.out.println(22);
 }
​
 public void test(HttpServletRequest req, HttpServletResponse res){
 System.out.println(req.getPathInfo());
 }
}

Scanning version 2.0

Through version 1.0, we initially implement all controllers under recursive scanning package, and can access them through path mapping. But it is obvious that there are at least the following problems:

  1. When executing a method, the method cannot have parameters. Does not meet business needs
  2. Every time you visit, you have to deal with the class reflection repeatedly to find the path mapping method, which is inefficient.

To solve the above two problems, we make the following modifications in version 2.0:

  1. The controller, request mapping corresponding methods and the relevant information of the parameters corresponding to the methods are stored in a container. Scan the server when it first starts and assemble it into a container. In this way, it is more convenient to traverse this container than the container in version 1.0.
  2. Define the parameter type by annotation@XxgRequestBodyas well as@XxgParamDistinguish whether the parameters are taken from the request body or from the URL? Take it from the back. So as to obtain the data from the front end
  3. adoptObjectMapperAssemble different types of parameters, and finally call the method.invokeImplement method processing with or without parameters.

BeanDefinition

/**
 *It is used to store the related parameters and methods of the controller class
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BeanDefinition {
 private Class typeClazz; //  Class object
 private String typeName; //  Class name
 private Object annotation; //  annotation
 private String controllerUrlPath; //  Path of controller
 private List<MethodDefinition> methodDefinitions; //  Comments with requestmapping
}

MethodDefinition

/**
 *A class that describes a method
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MethodDefinition {
 private Class parentClazz; //  The class of the parent class
 private Method method; //  method
 private String methodName; //  Method name
 private Object annotation; //  Annotation 
 private String requestMappingUrlPath; // url
 private String[] allowedRequestMethods; // allowedRequestMethods
 private List<ParameterDefinition> parameterDefinitions;   //  parameter list
 private Object result;   //  Return data
}

ParameterDefinition

/**
 *A class that describes parameters
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ParameterDefinition {
 private Class paramClazz; //  Parameter class object
 private String paramName; //  Parameter name
 private Object paramType; //  Parameter type
 private boolean isRequestBody; //  Do you want to get the data in the body
}

Container for singleton mode

Method of giving scanning package and obtaining corresponding method according to URI

/**
 *Class used to store the corresponding relationship between request path and controller
 *Design a singleton model
 */
public class RequestPathContainer {
 private static List<BeanDefinition> requestList = new ArrayList<>();
 private static final ClassLoader classLoader = RequestPathContainer.class.getClassLoader();
 private static volatile RequestPathContainer instance = null;
​
 public static RequestPathContainer getInstance() {
     if (instance == null) {
        synchronized(RequestPathContainer.class){
            if (instance == null) {
                instance = new RequestPathContainer();
            }
         }
     }
     return instance;
 }
​
 private RequestPathContainer() {
​
 }
​
 public List<BeanDefinition> getRequestList() {
    return requestList;
 }
​
 //Scan package
 public void scanner(String packetUrl) throws UnsupportedEncodingException, ClassNotFoundException {
     String url = packetUrl.replace(".", "/");
     URL resource = classLoader.getResource(url);
     if (resource == null) {
         return;
     }
     String path = resource.getPath();
     File file = new File(URLDecoder.decode(path, "UTF-8"));
     if (!file.exists()) {
         return;
     }
     if (file.isDirectory()){
     File[] files = file.listFiles();
     if (files == null) {
        return;
     }
     for (File f : files) {
     if (f.isDirectory()) {
        scanner(packetUrl + "." + f.getName());
     }
     if (f.isFile() && f.getName().endsWith(".class")) {
         String classname = f.getName().replace(".class", ""); //  Remove the. Class suffix
         Class clazz = Class.forName(packetUrl + "." + classname);
         dealClass(clazz);
         }
     }
     }
 }
​
 //Filter the classes in the package and add them to the list
 private void dealClass(Class clazz) {
     if (!clazz.isAnnotationPresent(XxgController.class)) {
     //There is no controller annotation
        return;
     }
     List<MethodDefinition> methodDefinitions = new ArrayList<>();
     Method[] methods = clazz.getDeclaredMethods();
     for (Method method : methods) {
         //Method to method description class
         MethodDefinition methodDefinition = convertMethodToMethodDefinition(method, clazz);
         if (methodDefinition != null) {
            methodDefinitions.add(methodDefinition);
         }
         }
         if (methodDefinitions.size() == 0) {
            return;
         }
         //Set class description class
         BeanDefinition beanDefinition = convertBeanToBeanDefinition(clazz, methodDefinitions);
         requestList.add(beanDefinition);
    }
​
 //Get the execution method according to URI and request method
 public MethodDefinition getMethodDefinition(String uri, String method) {
     for (BeanDefinition beanDefinition: requestList) {
     if (!uri.contains(beanDefinition.getControllerUrlPath())) {
        continue;
     }
     List<MethodDefinition> methodDefinitions = beanDefinition.getMethodDefinitions();
     for (MethodDefinition methodDefinition: methodDefinitions) {
     StringBuilder sb = new StringBuilder().append(beanDefinition.getControllerUrlPath());
     sb.append(methodDefinition.getRequestMappingUrlPath());
     if (!sb.toString().equals(uri)) {
     continue;
     }
     String[] allowedRequestMethods = methodDefinition.getAllowedRequestMethods();
     for (String str : allowedRequestMethods) {
     if (str.toUpperCase().equals(method.toUpperCase())) {
     //If both the request path and the request method are satisfied, the method description class is returned
     return methodDefinition;
     }
     }
     }
     }
     return null;
 }
​
 /**
 *Convert the controller class to the description class of the class
 */
 private BeanDefinition convertBeanToBeanDefinition(Class clazz, List<MethodDefinition> methodDefinitions) {
     BeanDefinition beanDefinition = new BeanDefinition();
     beanDefinition.setTypeName(clazz.getName());
     beanDefinition.setTypeClazz(clazz);
     XxgController controller = (XxgController) clazz.getAnnotation(XxgController.class);
     beanDefinition.setAnnotation(controller);
     beanDefinition.setControllerUrlPath(controller.value());
     beanDefinition.setMethodDefinitions(methodDefinitions);//  Add method body
     return beanDefinition;
 }
​
 /**
 *Converts a method to a method description class
 */
 private MethodDefinition convertMethodToMethodDefinition(Method method, Class clazz) {
 if (!method.isAnnotationPresent(XxgRequestMapping.class)) {
 //There is no requestmapping annotation
 return null;
 }
 method.setAccessible(true);
 Parameter[] parameters = method.getParameters();
 //Set parameter description class
 List<ParameterDefinition> parameterDefinitions = new ArrayList<>();
 for ( Parameter parameter : parameters) {
 ParameterDefinition parameterDefinition = convertParamToParameterDefinition(parameter);
 parameterDefinitions.add(parameterDefinition);
 }
 //Set method description class
 MethodDefinition methodDefinition = new MethodDefinition();
 methodDefinition.setParameterDefinitions(parameterDefinitions);   //  Add parameter list
 methodDefinition.setMethod(method);
 methodDefinition.setMethodName(method.getName());
 methodDefinition.setResult(method.getReturnType());
 XxgRequestMapping requestMapping = method.getAnnotation(XxgRequestMapping.class);
 methodDefinition.setRequestMappingUrlPath(requestMapping.value());
 methodDefinition.setAnnotation(requestMapping);
 methodDefinition.setAllowedRequestMethods(requestMapping.methods());
 methodDefinition.setParentClazz(clazz);
 return methodDefinition;
 }
​
 /**
 *Converts a parameter to a parameter description class
 */
 private ParameterDefinition convertParamToParameterDefinition(Parameter parameter) {
 ParameterDefinition parameterDefinition = new ParameterDefinition();
 if ( parameter.isAnnotationPresent(XxgParam.class)) {
 parameterDefinition.setParamName(parameter.getAnnotation(XxgParam.class).value());
 } else {
 parameterDefinition.setParamName(parameter.getName());
 }
 parameterDefinition.setParamClazz(parameter.getType());
 parameterDefinition.setParamType(parameter.getType());
 parameterDefinition.setRequestBody(parameter.isAnnotationPresent(XxgRequestBody.class));
 return parameterDefinition;
 }
​
}

Global Servlet

Instead of using interceptors, servlets are still used for routing distribution. This servlet listens/

public class DispatcherServlet extends HttpServlet {
 private ObjectMapper objectMapper = new ObjectMapper();
​
 @Override
 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
​
 //Coding settings
 resp.setContentType("text/json;charset=utf-8");
 RequestPathContainer requestPathContainer = RequestPathContainer.getInstance();
 MethodDefinition methodDefinition = requestPathContainer.getMethodDefinition(req.getRequestURI(), req.getMethod());
​
 if (methodDefinition == null) {
 resp.setStatus(404);
 Sendresponse (R. failed ("request path does not exist"), req, resp);
 return;
 }
​
 List<ParameterDefinition> parameterDefinitions = methodDefinition.getParameterDefinitions();
 List<Object> params = new ArrayList<>(parameterDefinitions.size());
 for (ParameterDefinition parameterDefinition : parameterDefinitions) {
 try {
 Object value = dealParam(parameterDefinition, req, resp);
 params.add(value);
 } catch (ParamException e) {
 resp.setStatus(404);
 sendResponse(R.failed(e.getMessage()), req, resp);
 return ;
 }
 }
​
 try {
 Object result = methodDefinition.getMethod().invoke(methodDefinition.getParentClazz().newInstance(), params.toArray());
 sendResponse(result, req, resp);
 } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
 e.printStackTrace();
 sendResponse(e.getMessage(), req, resp);
 }
​
 }
​
 /**
 *Processing parameters
 * @param parameterDefinition
 * @param req
 * @param resp
 */
 private Object dealParam(ParameterDefinition parameterDefinition, HttpServletRequest req, HttpServletResponse resp) throws ParamException, IOException {
 Object value;
 String data = "";
 if (parameterDefinition.isRequestBody()) {
 //Get data from the request body (the input stream of the request)
 data = getJsonString(req);
 } else if (parameterDefinition.getParamType() == HttpServletRequest.class) {
 return req;
 } else if (parameterDefinition.getParamType() == HttpServletResponse.class) {
 return resp;
 } else if (isJavaType(parameterDefinition)) {
 //Take the parameters from the URL
 data = req.getParameter(parameterDefinition.getParamName());
 if(data == null) {
 Throw new paramexception ("the server cannot get the request data, please check the request header, etc.);
 }
 } else {
 //Encapsulate the parameters in the request URL as objects
 try {
 Object obj = parameterDefinition.getParamClazz().newInstance();
 ConvertUtils.register(new DateConverter(), Date.class);
 BeanUtils.populate(obj, req.getParameterMap());
 return obj;
 } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
 Throw new paramexception ("the value corresponding to parameter '" + parameterdefinition. Getparamname() + "'") is not found;
 }
 }
 try {
 value = objectMapper.readValue(data, parameterDefinition.getParamClazz());
 } catch (JsonProcessingException e) {
 String errmsg = "parameter '" + parameterdefinition. Getparamname ()+
 "'required '" + parameterdefinition. Getparamtype ()+
 Type;
 throw new ParamException(errMsg);
 }
 return value;
 }
​
 private void sendResponse(Object result, HttpServletRequest req, HttpServletResponse resp) throws IOException {
 if (result == null) {
 return;
 }
 resp.setContentType("text/json;charset=utf-8");
 objectMapper.writeValue(resp.getWriter(), result);
 }
​
 /**
 *Determine whether the parameter is a normal type
 * @return
 */
 private boolean isJavaType(ParameterDefinition parameterDefinition) {
 Object[] javaTypes = MyJavaType.getJavaTypes();
 for (Object item : javaTypes) {
 if (item.equals(parameterDefinition.getParamClazz())) {
 return true;
 }
 }
 return false;
 }
​
 /**
 *Gets the JSON string of the request header
 */
 private String getJsonString(HttpServletRequest req) throws IOException {
 BufferedReader br = new BufferedReader(new InputStreamReader(req.getInputStream(), "utf-8"));
 char[] chars = new char[1024];
 int len;
 StringBuilder sb = new StringBuilder();
 while ((len = br.read(chars)) != -1) {
 sb.append(chars, 0, len);
 }
 return sb.toString();
 }
}

Servlet context listener initialization container

@Override
 public void contextInitialized(ServletContextEvent servletContextEvent) {
 RequestPathContainer requestPathContainer = RequestPathContainer.getInstance();
 String configClassName = servletContextEvent.getServletContext().getInitParameter("config");
 Class appListenerClass = null;
 try {
 appListenerClass = Class.forName(configClassName);
 XxgScanner xxgScanner = (XxgScanner)appListenerClass.getAnnotation(XxgScanner.class);
 if (xxgScanner != null) {
 try {
 requestPathContainer.scanner(xxgScanner.value()); //  Scan the controller class and initialize the list
 } catch (UnsupportedEncodingException | ClassNotFoundException e) {
 e.printStackTrace();
 }
 }
 } catch (ClassNotFoundException e) {
 e.printStackTrace();
 }
 }

Remaining problems

Static resources are also blocked

Dealing with static resources

default servlet

Open Tomcat’sconf/web.xmlFile, you can find that Tomcat has one by defaultdefault servletIt has the following configuration:

 <servlet>
 <servlet-name>default</servlet-name>
 <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
 <init-param>
 <param-name>debug</param-name>
 <param-value>0</param-value>
 </init-param>
 <init-param>
 <param-name>listings</param-name>
 <param-value>false</param-value>
 </init-param>
 <load-on-startup>1</load-on-startup>
 </servlet>

However, it does not match servlet mapping, that is, the processing path. You can configure the following in web.xml of our project to process static resources:

<!--   Change the global interceptor matching / * to /. Must -- >
<!--  / Indicates that only paths that cannot be matched by other servlets are processed -- >
<servlet-mapping>
 <servlet-name>DispatcherServlet</servlet-name>
 <url-pattern>/</url-pattern>
 </servlet-mapping>
<!--   Static resources -- >
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.html</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.js</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.css</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.jpg</url-pattern>
 </servlet-mapping>

last

1、 In fact, this paper mainly does the following two operations

  1. When the server starts, it scans the controller package and assembles the classes, methods and parameters that meet our expectations into the container.
  2. The front end accesses the server and gets the method corresponding to the specified path in the container
    2.1 assemble access parameters into parameter list according to different types
    2.2 implementation of corresponding methods
    2.3 data returned by processing method

2、 Reference notes

  • Project implementation process

Version 1.0 was thought and completed by bloggers themselves.
In version 2.0, the blogger’s Xiao Gao explained his ideas to the blogger. After writing it, he saw the realization of Xiao Gao, and then integrated and improved it.

  • After writing the article, the blogger decoupled the different classes in the project and reconstructed the code, mainly to deal with the open close principle, single responsibility principle and so on.
  • code:https://github.com/dengbenche…

Portal

Next section:Simple implementation of Autowired attribute

Recommended Today

The bootstrap form label and input are displayed on one line

The bootstrap version used in this article is 4.4.1 1. Use form inline <form action=””> <div class=”form-group”> <div class=”form-inline”> < label class = “mb-2” > specification value: < / label > < input class = “form control MR-2 mb-2” type = “text” placeholder = “e.g. white” / > < input class = “form control MR-2 […]