自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從0開(kāi)始手寫(xiě)一個(gè)Spring MVC框架,向高手進(jìn)階!

開(kāi)發(fā) 后端
Spring框架對(duì)于Java后端程序員來(lái)說(shuō)再熟悉不過(guò)了,以前只知道它用的反射實(shí)現(xiàn)的,但了解之后才知道有很多巧妙的設(shè)計(jì)在里面。如果不看Spring的源碼,你將會(huì)失去一次和大師學(xué)習(xí)的機(jī)會(huì):它的代碼規(guī)范,設(shè)計(jì)思想很值得學(xué)習(xí)。

Spring框架對(duì)于Java后端程序員來(lái)說(shuō)再熟悉不過(guò)了,以前只知道它用的反射實(shí)現(xiàn)的,但了解之后才知道有很多巧妙的設(shè)計(jì)在里面。如果不看Spring的源碼,你將會(huì)失去一次和大師學(xué)習(xí)的機(jī)會(huì):它的代碼規(guī)范,設(shè)計(jì)思想很值得學(xué)習(xí)。

我們程序員大部分人都是野路子,不懂什么叫代碼規(guī)范。寫(xiě)了一個(gè)月的代碼,***還得其他老司機(jī)花3天時(shí)間重構(gòu),相信大部分老司機(jī)都很頭疼看新手的代碼。

廢話不多說(shuō),我們進(jìn)入今天的正題,在Web應(yīng)用程序設(shè)計(jì)中,MVC模式已經(jīng)被廣泛使用。SpringMVC以DispatcherServlet為核心,負(fù)責(zé)協(xié)調(diào)和組織不同組件以完成請(qǐng)求處理并返回響應(yīng)的工作,實(shí)現(xiàn)了MVC模式。點(diǎn)擊這里學(xué)習(xí) Spring MVC 常用注解。

想要實(shí)現(xiàn)自己的SpringMVC框架,需要從以下幾點(diǎn)入手:

  • 了解SpringMVC運(yùn)行流程及九大組件
  • 梳理自己的SpringMVC的設(shè)計(jì)思路
  • 實(shí)現(xiàn)自己的SpringMVC框架

一、了解SpringMVC運(yùn)行流程及九大組件

1、SpringMVC的運(yùn)行流程

 

⑴ 用戶發(fā)送請(qǐng)求至前端控制器DispatcherServlet

⑵ DispatcherServlet收到請(qǐng)求調(diào)用HandlerMapping處理器映射器。

⑶ 處理器映射器根據(jù)請(qǐng)求url找到具體的處理器,生成處理器對(duì)象及處理器攔截器(如果有則生成)一并返回給DispatcherServlet。

⑷ DispatcherServlet通過(guò)HandlerAdapter處理器適配器調(diào)用處理器

⑸ 執(zhí)行處理器(Controller,也叫后端控制器)。

⑹ Controller執(zhí)行完成返回ModelAndView

⑺ HandlerAdapter將controller執(zhí)行結(jié)果ModelAndView返回給DispatcherServlet

⑻ DispatcherServlet將ModelAndView傳給ViewReslover視圖解析器

⑼ ViewReslover解析后返回具體View

⑽ DispatcherServlet對(duì)View進(jìn)行渲染視圖(即將模型數(shù)據(jù)填充至視圖中)。

⑾ DispatcherServlet響應(yīng)用戶。從上面可以看出,DispatcherServlet有接收請(qǐng)求,響應(yīng)結(jié)果,轉(zhuǎn)發(fā)等作用。有了DispatcherServlet之后,可以減少組件之間的耦合度。

2、SpringMVC的九大組件 

  1. protected void initStrategies(ApplicationContext context) {    
  2. //用于處理上傳請(qǐng)求。處理方法是將普通的request包裝成            MultipartHttpServletRequest,后者可以直接調(diào)用getFile方法獲取File.  
  3.  initMultipartResolver(context);    
  4. //SpringMVC主要有兩個(gè)地方用到了Locale:一是ViewResolver視圖解析的時(shí)候;二是用到國(guó)際化資源或者主題的時(shí)候。  
  5.  initLocaleResolver(context);   
  6. //用于解析主題。SpringMVC中一個(gè)主題對(duì)應(yīng) 一個(gè)properties文件,里面存放著跟當(dāng)前主題相關(guān)的所有資源、//如圖片、css樣式等。SpringMVC的主題也支持國(guó)際化, 
  7.  initThemeResolver(context);    
  8. //用來(lái)查找Handler的。  
  9.  initHandlerMappings(context);    
  10. //從名字上看,它就是一個(gè)適配器。Servlet需要的處理方法的結(jié)構(gòu)卻是固定的,都是以request和response為參數(shù)的方法。//如何讓固定的Servlet處理方法調(diào)用靈活的Handler來(lái)進(jìn)行處理呢?這就是HandlerAdapter要做的事情  
  11.  initHandlerAdapters(context);    
  12. //其它組件都是用來(lái)干活的。在干活的過(guò)程中難免會(huì)出現(xiàn)問(wèn)題,出問(wèn)題后怎么辦呢?//這就需要有一個(gè)專門的角色對(duì)異常情況進(jìn)行處理,在SpringMVC中就是HandlerExceptionResolver。  
  13.  initHandlerExceptionResolvers(context);    
  14. //有的Handler處理完后并沒(méi)有設(shè)置View也沒(méi)有設(shè)置ViewName,這時(shí)就需要從request獲取ViewName了,//如何從request中獲取ViewName就是RequestToViewNameTranslator要做的事情了。  
  15.  initRequestToViewNameTranslator(context);  
  16. //ViewResolver用來(lái)將String類型的視圖名和Locale解析為View類型的視圖。//View是用來(lái)渲染頁(yè)面的,也就是將程序返回的參數(shù)填入模板里,生成html(也可能是其它類型)文件。  
  17.  initViewResolvers(context);   
  18. //用來(lái)管理FlashMap的,F(xiàn)lashMap主要用在redirect重定向中傳遞參數(shù)。  
  19.  initFlashMapManager(context);   

二、梳理SpringMVC的設(shè)計(jì)思路

本文只實(shí)現(xiàn)自己的@Controller、@RequestMapping、@RequestParam注解起作用,其余SpringMVC功能讀者可以嘗試自己實(shí)現(xiàn)。

1、讀取配置

從圖中可以看出,SpringMVC本質(zhì)上是一個(gè)Servlet,這個(gè) Servlet 繼承自 HttpServlet。FrameworkServlet負(fù)責(zé)初始化SpringMVC的容器,并將Spring容器設(shè)置為父容器。因?yàn)楸疚闹皇菍?shí)現(xiàn)SpringMVC,對(duì)于Spring容器不做過(guò)多講解。點(diǎn)擊這里學(xué)習(xí) Spring MVC 常用注解。

為了讀取web.xml中的配置,我們用到ServletConfig這個(gè)類,它代表當(dāng)前Servlet在web.xml中的配置信息。通過(guò)web.xml中加載我們自己寫(xiě)的MyDispatcherServlet和讀取配置文件。

2、初始化階段

在前面我們提到DispatcherServlet的initStrategies方法會(huì)初始化9大組件,但是這里將實(shí)現(xiàn)一些SpringMVC的最基本的組件而不是全部,按順序包括:

  • 加載配置文件
  • 掃描用戶配置包下面所有的類
  • 拿到掃描到的類,通過(guò)反射機(jī)制,實(shí)例化。并且放到ioc容器中(Map的鍵值對(duì)  beanName-bean) beanName默認(rèn)是首字母小寫(xiě)
  • 初始化HandlerMapping,這里其實(shí)就是把url和method對(duì)應(yīng)起來(lái)放在一個(gè)k-v的Map中,在運(yùn)行階段取出

3、運(yùn)行階段

每一次請(qǐng)求將會(huì)調(diào)用doGet或doPost方法,所以統(tǒng)一運(yùn)行階段都放在doDispatch方法里處理,它會(huì)根據(jù)url請(qǐng)求去HandlerMapping中匹配到對(duì)應(yīng)的Method,然后利用反射機(jī)制調(diào)用Controller中的url對(duì)應(yīng)的方法,并得到結(jié)果返回。按順序包括以下功能:

  • 異常的攔截
  • 獲取請(qǐng)求傳入的參數(shù)并處理參數(shù)
  • 通過(guò)初始化好的handlerMapping中拿出url對(duì)應(yīng)的方法名,反射調(diào)用。

三、實(shí)現(xiàn)自己的SpringMVC框架

工程文件及目錄:

 

首先,新建一個(gè)maven項(xiàng)目,在pom.xml中導(dǎo)入以下依賴: 

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" 
  2.  <modelVersion>4.0.0</modelVersion>  
  3.  <groupId>com.liugh</groupId>  
  4.  <artifactId>liughMVC</artifactId>  
  5.  <version>0.0.1-SNAPSHOT</version>  
  6.  <packaging>war</packaging>  
  7.    <properties>  
  8.    <project.build.sourceEncoding>UTF- 8</project.build.sourceEncoding>  
  9.    <maven.compiler.source>1.8</maven.compiler.source>  
  10.    <maven.compiler.target>1.8</maven.compiler.target>  
  11.    <java.version>1.8</java.version>  
  12.  </properties>  
  13.  <dependencies>  
  14.       <dependency>  
  15.         <groupId>javax.servlet</groupId>   
  16.       <artifactId>javax.servlet-api</artifactId>   
  17.       <version>3.0.1</version>   
  18.       <scope>provided</scope>  
  19.    </dependency>  
  20.       </dependencies>  
  21. </project> 

接著,我們?cè)赪EB-INF下創(chuàng)建一個(gè)web.xml,如下配置: 

  1. <?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  2.  xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"  
  3.  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"  
  4.  version="3.0" 
  5.  <servlet>  
  6.    <servlet-name>MySpringMVC</servlet-name 
  7.    <servlet-class>com.liugh.servlet.MyDispatcherServlet</servlet-class>  
  8.    <init-param>  
  9.      <param-name>contextConfigLocation</param-name 
  10.      <param-value>application.properties</param-value>  
  11.    </init-param>  
  12.    <load-on-startup>1</load-on-startup>  
  13.  </servlet>  
  14.  <servlet-mapping>  
  15.    <servlet-name>MySpringMVC</servlet-name 
  16.    <url-pattern>/*</url-pattern>  
  17.  </servlet-mapping></web-app> 

application.properties文件中只是配置要掃描的包到SpringMVC容器中。 

  1. scanPackage=com.liugh.core 

創(chuàng)建自己的Controller注解,它只能標(biāo)注在類上面: 

  1. package com.liugh.annotation;  
  2. import java.lang.annotation.Documented;  
  3. import java.lang.annotation.ElementType;  
  4. import java.lang.annotation.Retention;  
  5. import java.lang.annotation.RetentionPolicy;  
  6. import java.lang.annotation.Target;  
  7.  
  8. @Target(ElementType.TYPE)  
  9. @Retention(RetentionPolicy.RUNTIME)  
  10. @Documented  
  11. public @interface MyController {  
  12.  /**  
  13.     * 表示給controller注冊(cè)別名  
  14.     * @return  
  15.     */  
  16.    String value() default "" 
  17.  
  18.  
  19. RequestMapping注解,可以在類和方法上:  
  20. RequestParam注解,只能注解在參數(shù)上  
  21. package com.liugh.annotation;  
  22. import java.lang.annotation.Documented;  
  23. import java.lang.annotation.ElementType;  
  24. import java.lang.annotation.Retention;  
  25. import java.lang.annotation.RetentionPolicy;  
  26. import java.lang.annotation.Target;  
  27.  
  28. @Target(ElementType.PARAMETER)  
  29. @Retention(RetentionPolicy.RUNTIME)  
  30. @Documented  
  31. public @interface MyRequestParam {  
  32.  /**  
  33.     * 表示參數(shù)的別名,必填  
  34.     * @return  
  35.     */  
  36.    String value();  

然后創(chuàng)建MyDispatcherServlet這個(gè)類,去繼承HttpServlet,重寫(xiě)init方法、doGet、doPost方法,以及加上我們第二步分析時(shí)要實(shí)現(xiàn)的功能: 

  1. package com.liugh.servlet;  
  2. import java.io.File;  
  3. import java.io.IOException;  
  4. import java.io.InputStream;  
  5. import java.lang.reflect.Method;  
  6. import java.net.URL;  
  7. import java.util.ArrayList;  
  8. import java.util.Arrays;  
  9. import java.util.HashMap;  
  10. import java.util.List;  
  11. import java.util.Map;  
  12. import java.util.Map.Entry;  
  13. import java.util.Properties; 
  14.  
  15. import javax.servlet.ServletConfig;  
  16. import javax.servlet.ServletException;  
  17. import javax.servlet.http.HttpServlet;  
  18. import javax.servlet.http.HttpServletRequest;  
  19. import javax.servlet.http.HttpServletResponse;  
  20.  
  21. import com.liugh.annotation.MyController;  
  22. import com.liugh.annotation.MyRequestMapping;  
  23. public class MyDispatcherServlet extends HttpServlet{  
  24. private Properties properties = new Properties();  
  25.  
  26. private List<String> classNames = new ArrayList<>();    
  27. private Map<String, Object> ioc = new HashMap<>(); 
  28.  
  29. private Map<String, Method> handlerMapping = new  HashMap<>(); 
  30.  
  31. private Map<String, Object> controllerMap  =new HashMap<>();  
  32.  
  33. @Override  
  34. public void init(ServletConfig config) throws ServletException {    
  35.    //1.加載配置文件  
  36.   doLoadConfig(config.getInitParameter("contextConfigLocation"));   
  37.  
  38.   //2.初始化所有相關(guān)聯(lián)的類,掃描用戶設(shè)定的包下面所有的類  
  39.   doScanner(properties.getProperty("scanPackage"));   
  40.  
  41.   //3.拿到掃描到的類,通過(guò)反射機(jī)制,實(shí)例化,并且放到ioc容器中(k-v  beanName-bean) beanName默認(rèn)是首字母小寫(xiě)  
  42.   doInstance(); 
  43.  
  44.   //4.初始化HandlerMapping(將url和method對(duì)應(yīng)上)  
  45.   initHandlerMapping(); 
  46.  
  47.  
  48.  
  49. @Override  
  50. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  51.   this.doPost(req,resp);  
  52.  
  53. @Override  
  54. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  55.   try {  
  56.     //處理請(qǐng)求 
  57.     doDispatch(req,resp);  
  58.   } catch (Exception e) {  
  59.     resp.getWriter().write("500!! Server Exception");  
  60.   } 
  61.  
  62.  
  63. private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {  
  64.   if(handlerMapping.isEmpty()){  
  65.     return 
  66.   } 
  67.  
  68.   String url =req.getRequestURI();  
  69.   String contextPath = req.getContextPath();   
  70.  
  71.   url=url.replace(contextPath, "").replaceAll("/+""/");   
  72.  
  73.   if(!this.handlerMapping.containsKey(url)){  
  74.     resp.getWriter().write("404 NOT FOUND!");  
  75.     return 
  76.   }  
  77.   Method method =this.handlerMapping.get(url); 
  78.   //獲取方法的參數(shù)列表  
  79.   Class<?>[] parameterTypes = method.getParameterTypes();  
  80.  
  81.   //獲取請(qǐng)求的參數(shù)  
  82.   Map<String, String[]> parameterMap = req.getParameterMap(); 
  83.  
  84.   //保存參數(shù)值  
  85.   Object [] paramValues= new Object[parameterTypes.length]; 
  86.   //方法的參數(shù)列表  
  87.       for (int i = 0; i<parameterTypes.length; i++){    
  88.           //根據(jù)參數(shù)名稱,做某些處理    
  89.           String requestParam = parameterTypes[i].getSimpleName();   
  90.  
  91.           if (requestParam.equals("HttpServletRequest")){   
  92.               //參數(shù)類型已明確,這邊強(qiáng)轉(zhuǎn)類型    
  93.             paramValues[i]=req;  
  94.               continue;    
  95.           }    
  96.           if (requestParam.equals("HttpServletResponse")){    
  97.             paramValues[i]=resp;  
  98.               continue;    
  99.           }  
  100.           if(requestParam.equals("String")){  
  101.             for (Entry<String, String[]> param : parameterMap.entrySet()) {  
  102.              String value =Arrays.toString(param.getValue()).replaceAll("\[|\]""").replaceAll(",\s"",");  
  103.              paramValues[i]=value; 
  104.            }  
  105.           }  
  106.       }    
  107.   //利用反射機(jī)制來(lái)調(diào)用  
  108.   try {  
  109.     method.invoke(this.controllerMap.get(url), paramValues);//***個(gè)參數(shù)是method所對(duì)應(yīng)的實(shí)例 在ioc容器中  
  110.   } catch (Exception e) {  
  111.     e.printStackTrace(); 
  112.    }  
  113.  
  114. private void  doLoadConfig(String location){  
  115.   //把web.xml中的contextConfigLocation對(duì)應(yīng)value值的文件加載到流里面  
  116.   InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(location);  
  117.   try { 
  118.      //用Properties文件加載文件里的內(nèi)容  
  119.     properties.load(resourceAsStream);  
  120.   } catch (IOException e) {  
  121.     e.printStackTrace();  
  122.   }finally {  
  123.     //關(guān)流  
  124.     if(null!=resourceAsStream){  
  125.       try {  
  126.         resourceAsStream.close();  
  127.       } catch (IOException e) {  
  128.         e.printStackTrace();  
  129.       }  
  130.     }  
  131.   }  
  132.  
  133.  
  134. private void doScanner(String packageName) {  
  135.   //把所有的.替換成/  
  136.   URL url  =this.getClass().getClassLoader().getResource("/"+packageName.replaceAll("\.""/"));  
  137.   File dir = new File(url.getFile());  
  138.   for (File file : dir.listFiles()) {  
  139.     if(file.isDirectory()){  
  140.       //遞歸讀取包  
  141.       doScanner(packageName+"."+file.getName());  
  142.     }else 
  143.       String className =packageName +"." +file.getName().replace(".class""");  
  144.       classNames.add(className); 
  145.     }  
  146.   }  
  147.  
  148. private void doInstance() {  
  149.   if (classNames.isEmpty()) { 
  150.      return 
  151.   }    
  152.   for (String className : classNames) {  
  153.     try {  
  154.       //把類搞出來(lái),反射來(lái)實(shí)例化(只有加@MyController需要實(shí)例化)  
  155.       Class<?> clazz =Class.forName(className);  
  156.        if(clazz.isAnnotationPresent(MyController.class)){  
  157.         ioc.put(toLowerFirstWord(clazz.getSimpleName()),clazz.newInstance());  
  158.       }else 
  159.         continue 
  160.       }  
  161.  
  162.     } catch (Exception e) {  
  163.       e.printStackTrace();  
  164.       continue 
  165.     }  
  166.   } 
  167.  
  168. private void initHandlerMapping(){  
  169.   if(ioc.isEmpty()){  
  170.     return 
  171.   }  
  172.   try {  
  173.     for (Entry<String, Object> entry: ioc.entrySet()) {  
  174.       Class<? extends Object> clazz = entry.getValue().getClass();  
  175.       if(!clazz.isAnnotationPresent(MyController.class)){  
  176.         continue 
  177.       }  
  178.  
  179.       //拼url時(shí),是controller頭的url拼上方法上的url  
  180.       String baseUrl ="" 
  181.       if(clazz.isAnnotationPresent(MyRequestMapping.class)){  
  182.         MyRequestMapping annotation = clazz.getAnnotation(MyRequestMapping.class);  
  183.         baseUrl=annotation.value(); 
  184.       }  
  185.       Method[] methods = clazz.getMethods();  
  186.       for (Method method : methods) {  
  187.         if(!method.isAnnotationPresent(MyRequestMapping.class)){  
  188.           continue 
  189.         }  
  190.         MyRequestMapping annotation = method.getAnnotation(MyRequestMapping.class);  
  191.         String url = annotation.value();
  192.  
  193.         url =(baseUrl+"/"+url).replaceAll("/+""/");  
  194.         handlerMapping.put(url,method);  
  195.         controllerMap.put(url,clazz.newInstance());  
  196.         System.out.println(url+","+method);  
  197.       } 
  198.  
  199.     } 
  200.  
  201.   } catch (Exception e) {  
  202.     e.printStackTrace();  
  203.   }  
  204.  
  205.  
  206. /**  
  207.  * 把字符串的首字母小寫(xiě)  
  208.  * @param name  
  209.  * @return  
  210.  */  
  211. private String toLowerFirstWord(String name){  
  212.   char[] charArray = name.toCharArray();  
  213.   charArray[0] += 32;  
  214.   return String.valueOf(charArray);  

這里我們就開(kāi)發(fā)完了自己的SpringMVC,現(xiàn)在我們測(cè)試一下: 

  1. package com.liugh.core.controller;  
  2. import java.io.IOException;  
  3. import javax.servlet.http.HttpServletRequest;  
  4. import javax.servlet.http.HttpServletResponse; 
  5. import com.liugh.annotation.MyController;  
  6. import com.liugh.annotation.MyRequestMapping;  
  7. import com.liugh.annotation.MyRequestParam;  
  8.  
  9. @MyController  
  10. @MyRequestMapping("/test" 
  11. public class TestController { 
  12.   @MyRequestMapping("/doTest" 
  13.    public void test1(HttpServletRequest request, HttpServletResponse response,  
  14.        @MyRequestParam("param") String param){  
  15.     System.out.println(param);  
  16.      try {  
  17.            response.getWriter().write( "doTest method success! param:"+param);  
  18.        } catch (IOException e) {  
  19.            e.printStackTrace();  
  20.        }  
  21.    }  
  22.  
  23.   @MyRequestMapping("/doTest2" 
  24.    public void test2(HttpServletRequest request, HttpServletResponse response){  
  25.        try {  
  26.            response.getWriter().println("doTest2 method success!");  
  27.        } catch (IOException e) {  
  28.            e.printStackTrace();  
  29.        }  
  30.    } 
  31.  

訪問(wèn)

http://localhost:8080/liughMVC/test/doTest?param=liugh如下:

訪問(wèn)一個(gè)不存在的試試:

 

到這里我們就大功告成了!水平有限,文章難免有錯(cuò)誤,歡迎犧牲自己寶貴時(shí)間的讀者,就本文內(nèi)容直抒己見(jiàn),我的目的僅僅是希望對(duì)讀者有所幫助。 

責(zé)任編輯:龐桂玉 來(lái)源: Java技術(shù)棧
相關(guān)推薦

2019-05-13 15:05:34

TomcatWeb Server協(xié)議

2020-01-09 11:11:35

RPC框架調(diào)用遠(yuǎn)程

2017-05-08 14:27:49

PHP框架函數(shù)框架

2023-10-16 22:03:36

日志包多線程日志包

2020-11-02 08:19:18

RPC框架Java

2020-04-07 15:12:07

微服務(wù)架構(gòu)數(shù)據(jù)

2012-06-04 18:02:56

社區(qū)

2020-12-23 09:48:37

數(shù)據(jù)工具技術(shù)

2022-07-06 19:00:00

微服務(wù)框架鏈路

2020-09-27 14:13:50

Spring BootJava框架

2024-08-02 09:49:35

Spring流程Tomcat

2022-10-08 08:34:34

JVM加載機(jī)制代碼

2021-02-20 09:45:02

RPC框架Java

2021-12-27 08:27:17

SpringMVC面試

2013-12-18 13:30:19

Linux運(yùn)維Linux學(xué)習(xí)Linux入門

2010-08-03 09:15:05

ScalaSpring

2019-08-21 17:41:29

操作系統(tǒng)軟件設(shè)計(jì)

2015-07-28 11:02:15

androidapp開(kāi)發(fā)

2015-08-24 11:03:14

android建項(xiàng)目

2022-03-09 09:43:01

工具類線程項(xiàng)目
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)