This article is excerpted from Spring 5 core principles
1 custom configuration
1.1 configure application Properties file
For the convenience of parsing, we use application Properties instead of application XML file. The specific configuration contents are as follows:
scanPackage=com.tom.demo
1.2 configuring web XML file
As we all know, all projects that depend on the Web container are read from the Web XML file. Let's configure the Web Content in XML:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <display-name>Gupao Web Application</display-name> <servlet> <servlet-name>gpmvc</servlet-name> <servlet-class>com.tom.mvcframework.v1.servlet.GPDispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>application.properties</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>gpmvc</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
GPDispatcherServlet is the core function class that simulates the implementation of Spring.
1.3 user defined annotation
@GPService notes are as follows:
package com.tom.mvcframework.annotation; import java.lang.annotation.*; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface GPService { String value() default ""; }
@GPAutowired notes are as follows:
package com.tom.mvcframework.annotation; import java.lang.annotation.*; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface GPAutowired { String value() default ""; }
@GPController notes are as follows:
package com.tom.mvcframework.annotation; import java.lang.annotation.*; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface GPController { String value() default ""; }
@The GPRequestMapping annotation is as follows:
package com.tom.mvcframework.annotation; import java.lang.annotation.*; @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface GPRequestMapping { String value() default ""; }
@GPRequestParam is annotated as follows:
package com.tom.mvcframework.annotation; import java.lang.annotation.*; @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface GPRequestParam { String value() default ""; }
1.4 configuration notes
Configure the business implementation class DemoService:
package com.tom.demo.service.impl; import com.tom.demo.service.IDemoService; import com.tom.mvcframework.annotation.GPService; /** * Core business logic */ @GPService public class DemoService implements IDemoService{ public String get(String name) { return "My name is " + name; } }
Configure request entry class DemoAction:
package com.tom.demo.mvc.action; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.tom.demo.service.IDemoService; import com.tom.mvcframework.annotation.GPAutowired; import com.tom.mvcframework.annotation.GPController; import com.tom.mvcframework.annotation.GPRequestMapping; import com.tom.mvcframework.annotation.GPRequestParam; @GPController @GPRequestMapping("/demo") public class DemoAction { @GPAutowired private IDemoService demoService; @GPRequestMapping("/query") public void query(HttpServletRequest req, HttpServletResponse resp, @GPRequestParam("name") String name){ String result = demoService.get(name); try { resp.getWriter().write(result); } catch (IOException e) { e.printStackTrace(); } } @GPRequestMapping("/add") public void add(HttpServletRequest req, HttpServletResponse resp, @GPRequestParam("a") Integer a, @GPRequestParam("b") Integer b){ try { resp.getWriter().write(a + "+" + b + "=" + (a + b)); } catch (IOException e) { e.printStackTrace(); } } @GPRequestMapping("/remove") public void remove(HttpServletRequest req,HttpServletResponse resp, @GPRequestParam("id") Integer id){ } }
At this point, the configuration is complete.
2 container initialization version 1.0
All core logic is written in init() method, and the code is as follows:
package com.tom.mvcframework.v1.servlet; import com.tom.mvcframework.annotation.GPAutowired; import com.tom.mvcframework.annotation.GPController; import com.tom.mvcframework.annotation.GPRequestMapping; import com.tom.mvcframework.annotation.GPService; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URL; import java.util.*; public class GPDispatcherServlet extends HttpServlet { private Map<String,Object> mapping = new HashMap<String, Object>(); @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 {} @Override public void init(ServletConfig config) throws ServletException { InputStream is = null; try{ Properties configContext = new Properties(); is = this.getClass().getClassLoader().getResourceAsStream(config.getInitParameter ("contextConfigLocation")); configContext.load(is); String scanPackage = configContext.getProperty("scanPackage"); doScanner(scanPackage); for (String className : mapping.keySet()) { if(!className.contains(".")){continue;} Class<?> clazz = Class.forName(className); if(clazz.isAnnotationPresent(GPController.class)){ mapping.put(className,clazz.newInstance()); String baseUrl = ""; if (clazz.isAnnotationPresent(GPRequestMapping.class)) { GPRequestMapping requestMapping = clazz.getAnnotation (GPRequestMapping.class); baseUrl = requestMapping.value(); } Method[] methods = clazz.getMethods(); for (Method method : methods) { if(!method.isAnnotationPresent(GPRequestMapping.class)){ continue; } GPRequestMapping requestMapping = method.getAnnotation (GPRequestMapping.class); String url = (baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/"); mapping.put(url, method); System.out.println("Mapped " + url + "," + method); } }else if(clazz.isAnnotationPresent(GPService.class)){ GPService service = clazz.getAnnotation(GPService.class); String beanName = service.value(); if("".equals(beanName)){beanName = clazz.getName();} Object instance = clazz.newInstance(); mapping.put(beanName,instance); for (Class<?> i : clazz.getInterfaces()) { mapping.put(i.getName(),instance); } }else {continue;} } for (Object object : mapping.values()) { if(object == null){continue;} Class clazz = object.getClass(); if(clazz.isAnnotationPresent(GPController.class)){ Field [] fields = clazz.getDeclaredFields(); for (Field field : fields) { if(!field.isAnnotationPresent(GPAutowired.class)){continue; } GPAutowired autowired = field.getAnnotation(GPAutowired.class); String beanName = autowired.value(); if("".equals(beanName)){beanName = field.getType().getName();} field.setAccessible(true); try { field.set(mapping.get(clazz.getName()),mapping.get(beanName)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } } catch (Exception e) { }finally { if(is != null){ try {is.close();} catch (IOException e) { e.printStackTrace(); } } } System.out.print("GP MVC Framework is init"); } private void doScanner(String scanPackage) { URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll ("\\.","/")); File classDir = new File(url.getFile()); for (File file : classDir.listFiles()) { if(file.isDirectory()){ doScanner(scanPackage + "." + file.getName());}else { if(!file.getName().endsWith(".class")){continue;} String clazzName = (scanPackage + "." + file.getName().replace(".class","")); mapping.put(clazzName,null); } } } }
3 request execution
The key is to implement the doGet() and doPost() methods. In fact, the doDispatch() method is invoked in the doGet() and doPost() methods. The specific code is as follows:
public class GPDispatcherServlet extends HttpServlet { private Map<String,Object> mapping = new HashMap<String, Object>(); @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 { try { doDispatch(req,resp); } catch (Exception e) { resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace())); } } private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception { String url = req.getRequestURI(); String contextPath = req.getContextPath(); url = url.replace(contextPath, "").replaceAll("/+", "/"); if(!this.mapping.containsKey(url)){resp.getWriter().write("404 Not Found!!");return;} Method method = (Method) this.mapping.get(url); Map<String,String[]> params = req.getParameterMap(); method.invoke(this.mapping.get(method.getDeclaringClass().getName()),new Object[]{req,resp,params.get("name")[0]}); } @Override public void init(ServletConfig config) throws ServletException { ... } }
4 optimize and implement version 2.0
It is optimized in version 1.0 and encapsulates the code in init() method by using common design patterns (factory pattern, singleton pattern, delegation pattern and policy pattern). According to the previous implementation idea, first build the basic framework, and then "fill meat and inject blood". The specific code is as follows:
//Initialization phase @Override public void init(ServletConfig config) throws ServletException { //1. Load configuration file doLoadConfig(config.getInitParameter("contextConfigLocation")); //2. Scan related classes doScanner(contextConfig.getProperty("scanPackage")); //3. Initialize the scanned classes and put them into the IoC container doInstance(); //4. Complete dependency injection doAutowired(); //5. Initialize HandlerMapping initHandlerMapping(); System.out.println("GP Spring framework is init."); }
Declare global member variables, where IoC container is the specific case of single instance during registration:
//Save application Contents in the properties configuration file private Properties contextConfig = new Properties(); //Save all class names scanned private List<String> classNames = new ArrayList<String>(); //The legendary IoC container, let's uncover its mystery //In order to simplify the program, ConcurrentHashMap is not considered for the time being //Mainly focus on design ideas and principles private Map<String,Object> ioc = new HashMap<String,Object>(); //Save the correspondence between url and Method private Map<String,Method> handlerMapping = new HashMap<String,Method>();
Implement the doLoadConfig() method:
//Load profile private void doLoadConfig(String contextConfigLocation) { //Find the path where the Spring main configuration file is located directly through the classpath //And read it out and put it in the Properties object //Equivalent to scanpackage = com tom. The demo is saved in memory InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation); try { contextConfig.load(fis); } catch (IOException e) { e.printStackTrace(); }finally { if(null != fis){ try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
Implement the doScanner() method:
//Scan related classes private void doScanner(String scanPackage) { //scanPackage = com.tom.demo, which stores the package path //To convert to a file path is actually to convert Replace with/ URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll ("\\.","/")); File classPath = new File(url.getFile()); for (File file : classPath.listFiles()) { if(file.isDirectory()){ doScanner(scanPackage + "." + file.getName()); }else{ if(!file.getName().endsWith(".class")){ continue;} String className = (scanPackage + "." + file.getName().replace(".class","")); classNames.add(className); } } }
Implement the doInstance() method, which is the specific implementation of the factory pattern:
private void doInstance() { //Initialize and prepare for DI if(classNames.isEmpty()){return;} try { for (String className : classNames) { Class<?> clazz = Class.forName(className); //What classes need to be initialized? //Annotated classes are initialized. How to judge? //In order to simplify the code logic, we mainly understand the design idea, and only use @ Controller and @ Service as examples, //@Component and others will not give examples one by one if(clazz.isAnnotationPresent(GPController.class)){ Object instance = clazz.newInstance(); //Spring default class name initial lowercase String beanName = toLowerFirstCase(clazz.getSimpleName()); ioc.put(beanName,instance); }else if(clazz.isAnnotationPresent(GPService.class)){ //1. Custom beanName GPService service = clazz.getAnnotation(GPService.class); String beanName = service.value(); //2. The default class name is lowercase if("".equals(beanName.trim())){ beanName = toLowerFirstCase(clazz.getSimpleName()); } Object instance = clazz.newInstance(); ioc.put(beanName,instance); //3. Automatic assignment according to type, which is an opportunistic way for (Class<?> i : clazz.getInterfaces()) { if(ioc.containsKey(i.getName())){ throw new Exception("The "" + i.getName() + "" is exists!!"); } //The type of the interface is directly regarded as a key ioc.put(i.getName(),instance); } }else { continue; } } }catch (Exception e){ e.printStackTrace(); } }
In order to facilitate processing, I implemented the toLowerFirstCase() method to realize the lowercase of the class name. The specific code is as follows:
//Change the first letter of the class name to lowercase private String toLowerFirstCase(String simpleName) { char [] chars = simpleName.toCharArray(); //The reason for adding is that the ASCII codes of large and small letters differ by 32 //And the ASCII code of upper case letters is smaller than that of lower case letters //In Java, arithmetic operation on char is actually arithmetic operation on ASCII code chars[0] += 32; return String.valueOf(chars); }
Implement the doAutowired() method:
//Automatic dependency injection private void doAutowired() { if(ioc.isEmpty()){return;} for (Map.Entry<String, Object> entry : ioc.entrySet()) { //Get all fields, including private, protected and default //Normally, ordinary OOP programming can only obtain fields of public type Field[] fields = entry.getValue().getClass().getDeclaredFields(); for (Field field : fields) { if(!field.isAnnotationPresent(GPAutowired.class)){continue;} GPAutowired autowired = field.getAnnotation(GPAutowired.class); //If the user does not have a custom beanName, it will be injected according to the type by default //This place eliminates the judgment of the lowercase initial of the class name. As an after-school homework, please "friends" to realize it by yourself String beanName = autowired.value().trim(); if("".equals(beanName)){ //Get the type of the interface as the key, and use this key to get the value in the IoC container later beanName = field.getType().getName(); } //If it is a type other than public, mandatory assignment is required as long as @ Autowired annotation is added //Reflection is called violent access field.setAccessible(true); try { //Dynamically assign values to fields using reflection mechanism field.set(entry.getValue(),ioc.get(beanName)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } }
Implement initHandlerMapping() method. HandlerMapping is an application case of policy mode:
//Initialize the one-to-one relationship between url and Method private void initHandlerMapping() { if(ioc.isEmpty()){ return; } for (Map.Entry<String, Object> entry : ioc.entrySet()) { Class<?> clazz = entry.getValue().getClass(); if(!clazz.isAnnotationPresent(GPController.class)){continue;} //Save the @ GPRequestMapping("/demo") written on the class String baseUrl = ""; if(clazz.isAnnotationPresent(GPRequestMapping.class)){ GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class); baseUrl = requestMapping.value(); } //Get all public type methods by default for (Method method : clazz.getMethods()) { if(!method.isAnnotationPresent(GPRequestMapping.class)){continue;} GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class); //optimization String url = ("/" + baseUrl + "/" + requestMapping.value()) .replaceAll("/+","/"); handlerMapping.put(url,method); System.out.println("Mapped :" + url + "," + method); } } }
Now that the initialization is completed, let's implement the running logic and look at the code of doGet() and doPost() methods:
@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 { //Operation phase try { doDispatch(req,resp); } catch (Exception e) { e.printStackTrace(); resp.getWriter().write("500 Exection,Detail : " + Arrays.toString(e.getStackTrace())); } }
The delegation mode is used in the doPost() method. The specific logic of the delegation mode is implemented in the doDispatch() method:
private void doDispatch(HttpServletRequest req, HttpServletResponse resp)throws Exception { String url = req.getRequestURI(); String contextPath = req.getContextPath(); url = url.replaceAll(contextPath,"").replaceAll("/+","/"); if(!this.handlerMapping.containsKey(url)){ resp.getWriter().write("404 Not Found!!"); return; } Method method = this.handlerMapping.get(url); //First parameter: the instance where the method is located //The second parameter: the argument required when calling Map<String,String[]> params = req.getParameterMap(); //Opportunistic way String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName()); method.invoke(ioc.get(beanName),new Object[]{req,resp,params.get("name")[0]}); //System.out.println(method); }
In the above code, although dodispatch () completes dynamic delegation and makes reflection calls, the processing of url parameters is still static. To achieve dynamic acquisition of url parameters, it is actually a little complicated. We can optimize the implementation of doDispatch() method. The code is as follows:
private void doDispatch(HttpServletRequest req, HttpServletResponse resp)throws Exception { String url = req.getRequestURI(); String contextPath = req.getContextPath(); url = url.replaceAll(contextPath,"").replaceAll("/+","/"); if(!this.handlerMapping.containsKey(url)){ resp.getWriter().write("404 Not Found!!"); return; } Method method = this.handlerMapping.get(url); //First parameter: the instance where the method is located //The second parameter: the argument required when calling Map<String,String[]> params = req.getParameterMap(); //Gets the formal parameter list of the method Class<?> [] parameterTypes = method.getParameterTypes(); //Save a list of url parameters for the request Map<String,String[]> parameterMap = req.getParameterMap(); //The location where the assignment parameter is saved Object [] paramValues = new Object[parameterTypes.length]; //Dynamic assignment according to parameter position for (int i = 0; i < parameterTypes.length; i ++){ Class parameterType = parameterTypes[i]; if(parameterType == HttpServletRequest.class){ paramValues[i] = req; continue; }else if(parameterType == HttpServletResponse.class){ paramValues[i] = resp; continue; }else if(parameterType == String.class){ //Annotated parameters in the extraction method Annotation[] [] pa = method.getParameterAnnotations(); for (int j = 0; j < pa.length ; j ++) { for(Annotation a : pa[i]){ if(a instanceof GPRequestParam){ String paramName = ((GPRequestParam) a).value(); if(!"".equals(paramName.trim())){ String value = Arrays.toString(parameterMap.get(paramName)) .replaceAll("\\[|\\]","") .replaceAll("\\s",","); paramValues[i] = value; } } } } } } //Opportunistic way //Obtain the Class where the Method is located through reflection. After obtaining the Class, you also need to obtain the name of the Class //Then call toLowerFirstCase to get beanName String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName()); method.invoke(ioc.get(beanName),new Object[]{req,resp,params.get("name")[0]}); }
Pay attention to WeChat official account Tom bomb structure and reply to "Spring" to get the complete source code.
This article is the original of "Tom bomb architecture". Please indicate the source for reprint. Technology lies in sharing, I share my happiness! If you have any suggestions, you can also leave comments or private letters. Your support is the driving force for me to adhere to my creation. Focus on WeChat official account Tom structure, get more dry cargo!
It's not easy to be original. It's cool to insist. I've seen it here. Little partners remember to like, collect and watch it. Pay attention to it three times a button! If you think the content is too dry, you can share and forward it to your friends!