Reference: Two-hour handwritten spring MVC framework
Catalog
I. Preface:
For Spring framework, the most important are IoC and AOP. This paper makes some simple analysis of DI.
tip: It has been unclear before about the difference between dependency injection and control inversion. By reading Spring source code and blogs of some big bloggers on the Internet, I have some understanding. According to the understanding, I have sorted out the following contents about handwritten simple Spring framework (only for Ioc).
Regarding the relationship between IOC and DI, I think this article is very easy to understand. Understanding of IOC and DI
2. Thinking Map
Before starting the three stages of appeal, we need to create a web project. For environmental reasons, there is no maven tool. All projects can only be opened by importing jar packages using the most basic web projects.
Relevant dependencies on javax.servlet-api are required only for jar packages that need to be imported.
Engineering catalogue:
Where the configuration file (config.properties) is placed in the resources directory.
There are the following main package s in src:
- annotation package (custom annotations)
- entity package (POJO class)
- servlet (custom Dispatcher Serlvet location, understandable as the main entry)
web Packet
Controller
Service (Service J interface)
Impl
3. Code Analysis
The above thought map is divided into three main parts:
1. Configuration phase:
In the configuration phase, we mainly write web.xml, which is as follows:
- Configure web.xml
- Setting init-param
- Setting url-pattern
- Configure Annotation
<servlet> <servlet-name>springmvc</servlet-name> <servlet-class>com.diy.spring.servlet.DIYDispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>config.properties</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
The appeal profile mainly defines a contextConfigLocation constant, which corresponds to config.properties, i.e. the config.properties, and then intercepts all requests by setting < load-on-starup > 1, i.e. the initial load at web startup, and finally configuring < servlet-mapping > to intercept all requests.
2. Initialization stage:
The main initialization methods are as follows:
- Loading configuration files
- Scanning related classes
- IOC container initialization
- Create an instance and save it in the IOC container
- Perform DI operations (scan IOC containers, automatic assignment of attributes without assignment)
- Initialize Handler Mapping (mapping a url to the corresponding method)
The following is an analysis one by one:
When starting configuration initialization, you need to create a custom Dispatcher Serlvet.
In this article, it is defined as DIY Dispatcher Servlet:
At this point, the DIYDispatcher Servlet needs to inherit the class HttpServlet and override the init(), doPost(), doGet() methods.
init() method: mainly defines the method needed for initialization, the specific method body will be given below.
@Override public void init(ServletConfig config) throws ServletException { // 1. Load the configuration file doLoadConfig(config.getInitParameter("contextConfigLocation")); // 2. Scanning class packages doScanner(contextConfig.getProperty("scan-package")); // 3. Initialize the ioc container and put the class class class into it doInstance(); // 4. Dependency Injection doAutowired(); // 5. Initialize handerMapping initHanderMapping(); }
(1) Load configuration file: the doLoadConfig () method, mainly reads the config.properties file, and loads the obtained configuration file through the load method in the Properties class.
- config.properties:
scan-package=com.diy.spring
- Declare a Properties object in the DIYDipathcerSerlvet class
private Properties contextConfig = new Properties();
- The doLoadConfig () method:
private void doLoadConfig(String location) { InputStream is = this.getClass().getClassLoader().getResourceAsStream(location); try { //Loading configuration files contextConfig.load(is); } catch (IOException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } }
(2) Scan related classes (i.e. package names defined in config.properties)
- doScanner method ()
private void doScanner(String packageName) { //Convert all package paths to file paths URL resource = this.getClass().getClassLoader(). getResource("/" + packageName.replaceAll("\\.", "/")); File classDir = new File(resource.getFile()); for (File classFile : classDir.listFiles()) { // If the current directory is a directory, recursion if (classFile.isDirectory()) { doScanner(packageName + "." + classFile.getName()); } else { // If it's a file, add it directly to classNames String className = (packageName + "." + classFile. getName()).replaceAll(".class", ""); classNames.add(className); } } }
In the modification method, the incoming parameter packageName is the value of the corresponding "scan-package" field in the config configuration file, namely com.diy.spring.
Then the routing rules are regularized by replaceAll method. Splice into a file path.
Then iterate through the obtained file path. If it is a directory, recursively, continue to query the following files. If it's a file, remove the. class suffix and place it in the list collection (defined as a list collection of classes that need to be scanned)
(3) Initialize the IOC container (doInstance() method)
private void doInstance() { if (classNames.isEmpty()) { return; } try { for (String className : classNames) { Class<?> clazz = Class.forName(className); if (clazz.isAnnotationPresent(DIYController.class)) { // Handling initial lowercase String key = lowerFirstCase(clazz.getSimpleName()); iocMap.put(key, clazz.newInstance()); } else if (clazz.isAnnotationPresent(DIYService.class)) { DIYService service = clazz.getAnnotation(DIYService.class); String beanName = service.value(); // 1. Default use if ("".equals(beanName)) { beanName = lowerFirstCase(clazz.getSimpleName()); } // 2. Use service with parameters when there are parameters Object instance = clazz.newInstance(); iocMap.put(beanName, instance); Class<?>[] interfaces = clazz.getInterfaces(); for (Class<?> i : interfaces) { iocMap.put(i.getName(), instance); } } else { continue; } } } catch (Exception e) { e.printStackTrace(); } }
This is mainly to scan the name value of the class name as the most key, and then put the corresponding instantiated object as value into iocMap (a set of map s defined).
tips: In this case, the first letter is lowercase for the class name. According to the reference article, the operation of capitalizing the first letter is achieved by adding 32 to ASCII.
/** * Processing initial lowercase, ASCII value plus 32 * @param simpleName * @return */ private String lowerFirstCase(String simpleName) { char[] chars = simpleName.toCharArray(); chars[0] += 32; return String.valueOf(chars); }
This method is much faster and cleaner than using substring() to handle initials in lowercase, but I feel very limited. What if someone names a class name with initials in lowercase? (Although the normal naming convention would not have sent such a thing.)
In short. For the doInstance() method, the main thing is to put the scanned classes into the ioc container, where the initial lowercase is handled for Controller, and the Service handles the case where the user adds parameters himself, such as @DIYService("mySerivve").
(4) Dependency Injection (doAutowired() method)
private void doAutowired() { if (iocMap.isEmpty()) return; //Loop all the classes in the IOC container, and then assign the attributes that need to be automatically assigned for (Map.Entry<String, Object> entry : iocMap.entrySet()) { //Get all the attributes in the instance object //Dependency Injection Field[] fields = entry.getValue().getClass().getDeclaredFields(); for (Field field : fields) { if (!field.isAnnotationPresent(DIYAutowired.class)) { continue; } //1. Default use //2. Prefer parameters with parameters DIYAutowired autowired = field.getAnnotation(DIYAutowired.class); String beanName = autowired.value().trim(); if ("".equals(beanName)) { beanName = field.getType().getName(); } //Setting Private Attribute Access Permissions, Violent Access field.setAccessible(true); try { field.set(entry.getValue(), iocMap.get(beanName)); } catch (IllegalAccessException e) { e.printStackTrace(); continue; } } } }
This is similar to handling annotations like @DIYService, which deals mainly with the way users customize parameters, such as @DIYAutowired("myAutowired"), and then binds the corresponding attribute values to their iocMap (ioc container) instantiated objects (value is obtained by key).
(5) Finally, the handerMapping is initialized:
For this, I felt that it would be better to use map sets at that time, but found that when Invoke, the first parameter didn't know how to get it....
So I read the article carefully again and found that it was Tom's course video in Goubao School. So I went to see it again and found that it benefited a lot. Moreover, I had an understanding of why I did not use Map collection. So, here's a list set for analysis. For those who still want to use Map, you can try it. It may not be so simple. It's better to use the method given by the teacher. / (An oblique smile)
First. Define a handlerList collection first
private List<Handler> handlerMapping = new ArrayList<Handler>();
initHandlerMapping method:
private void initHanderMapping() { if (iocMap.isEmpty()) return; for (Map.Entry<String, Object> entry : iocMap.entrySet()) { Class<?> clazz = entry.getValue().getClass(); if (!clazz.isAnnotationPresent(DIYController.class)) { continue; } String baseUrl = ""; //Get the url configuration in Controller if (clazz.isAnnotationPresent(DIYRequestMapping.class)) { DIYRequestMapping requestMapping = clazz.getAnnotation(DIYRequestMapping.class); baseUrl = requestMapping.value(); } //Get the url configuration in Method Method[] methods = clazz.getMethods(); for (Method method : methods) { //Skipping without @DIYRequestMapping annotation if (!method.isAnnotationPresent(DIYRequestMapping.class)) { continue; } //Mapping URL DIYRequestMapping requestMapping = method.getAnnotation(DIYRequestMapping.class); String methodUrl = ("/"+baseUrl + requestMapping.value()).replaceAll("/+", "/"); Pattern pattern = Pattern.compile(methodUrl); // handlerMapping.put(methodUrl, method); handlerMapping.add(new Handler(pattern, entry.getValue(), method)); System.out.println("mapped:" + methodUrl + "-->" + method); } } }
Here, when invoke is used for reflection mechanism, the corresponding first parameter can not be obtained (the key value of the class is unknown, can not be found). In the reference article, a Handler class is created, in which the corresponding instances of the preservation methods and the methods of preserving the mappings are defined concretely.
private class Handler { protected Object controller; //Examples of preservation methods protected Method method;//Method of saving mapping protected Pattern pattern; protected Map<String, Integer> paramIndexMapping;//Parametric order protected Handler(Pattern pattern, Object controller, Method method) { this.pattern = pattern; this.controller = controller; this.method = method; paramIndexMapping = new HashMap<String, Integer>(); putParamIndexMapping(method); } private void putParamIndexMapping(Method method) { Annotation[][] pa = method.getParameterAnnotations(); for (int i = 0; i < pa.length; i++) { for (Annotation a : pa[i]) { if (a instanceof DIYRequestParam) { String paramName = ((DIYRequestParam) a).value(); if (!"".equals(paramName)) { paramIndexMapping.put(paramName, i); } } } } Class<?>[] paramTypes = method.getParameterTypes(); for (int i = 0; i < paramTypes.length; i++) { Class<?> type = paramTypes[i]; if (type == HttpServletRequest.class || type == HttpServletResponse.class) { paramIndexMapping.put(type.getName(), i); } } } }
3. Operation phase
In the final run-time stage, we mainly deal with the doPost/doGet method, then match Handler Mapping, call method.invoke() through reflection, and finally output the result to the browser using reponse.getWriter.wirte() method.
The overridden doPost/doGet method:
@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 { //Start matching to the corresponding method try { doDispatcher(req, resp); } catch (Exception e) { //Handling exceptions resp.getWriter().write("500 Exception.Details:\r\n" + Arrays.toString(e.getStackTrace()) .replaceAll("\\[|\\]", "").replaceAll(",\\s", "\r\n")); } }
Customized doDispatcher method:
public void doDispatcher(HttpServletRequest req, HttpServletResponse res) { try { Handler handler = getHandler(req); if (handler == null) { res.getWriter().write("404 not found."); return; } //Get the parameter list of the method Class<?>[] paramTypes = handler.method.getParameterTypes(); //Save parameter values Object[] paramValues = new Object[paramTypes.length]; //Get the list of parameters for the request Map<String, String[]> params = req.getParameterMap(); for (Entry<String, String[]> param : params.entrySet()) { String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", ""); if (!handler.paramIndexMapping.containsKey(param.getKey())) { continue; } int index = handler.paramIndexMapping.get(param.getKey()); paramValues[index] = convert(paramTypes[index], value); } int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName()); paramValues[reqIndex] = req; int resIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName()); paramValues[resIndex] = res; handler.method.invoke(handler.controller, paramValues); } catch (Exception e) { e.printStackTrace(); } String url = req.getRequestURI(); String contextPath = req.getContextPath(); url = url.replace(contextPath, "").replaceAll("/+", "/"); } private Object convert(Class<?> type, String value) { if (Integer.class == type) { return Integer.valueOf(value); } return value; } private Handler getHandler(HttpServletRequest req) { if (handlerMapping.isEmpty()) { return null; } String url = req.getRequestURI(); String contextPath = req.getContextPath(); url = url.replace(contextPath, "").replaceAll("/+", "/"); for (Handler handler : handlerMapping) { Matcher matcher = handler.pattern.matcher(url); if (!matcher.matches()) { continue; } return handler; } return null; }
This code is not very thorough (especially for Handler classes), so wait until you understand it before adding the explanation (mainly for fear of misleading others, after all, people crawling from the pit.) I hope if you see it, you can comment and explain it. Thank you!
IV. CONCLUSION OF SUMMARY
Through this handwritten mini-Spring framework, you can complete control inversion and dependency injection, but you can create controller, service to experiment, the specific code is not pasted, too much space, this article is mainly your notes, I try to understand and study each piece of code, but there are still some places not very clear. Clear. So I hope that the big guys can see it and help me understand the mistake (especially the creation of Handler, and the understanding of the custom method doDispathcer() in the doPost method).
Finally, although some parts of the article are not very well understood, I have a deeper impression and understanding of Spring's dependency injection and inversion of control. I hope I have time to come back and revise the article and add some unknown parts to it.