做过web项目的小伙伴,对于SpringMVC,Struts2都是在熟悉不过了,再就是我们比较古老的servlet,我们先来复习一下我们的servlet生命周期。

servlet生命周期

上述文字摘自http://c.biancheng.net/view/3989.html

   整个过程是比较复杂的,而且我们的参数是通过问号的形式来传递的,比如http://boke?id=1234,id为1234来传递的,如果我们要http://boke/1234这样来传递参数,servlet是做不到的,我们来看一下我们SpringMVC还有哪些优势。

1.基于注解方式的URL映射。比如http://boke/type/{articleType}/id/{articleId}

2.表单参数自动映射,我们不在需要request.getParament得到参数,参数可以通过name属性来自动映射到我们的控制层下。

3.缓存的处理,SprinMVC提供了缓存来提高我们的效率。

4.全局异常处理,通过过滤器也可以实现,只不过SprinMVC的方法会更简单一些。

5.拦截器的实现,通过过滤器也可以实现,只不过SprinMVC的方法会更简单一些。

6.下载处理

我们来对比一下SprinMVC的流程图。

SprinMVC的流程图

下面我们先熟悉一下源码,来个实例,来一个最精简启动SpringMVC。

最精简启动SpringMVC

建立Maven项目就不说了啊,先设置我们的pom文件

  1. <dependencies>
  2. <dependency>
  3. <groupId>javax.servlet</groupId>
  4. <artifactId>javax.servlet-api</artifactId>
  5. <version>3.1.0</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework</groupId>
  9. <artifactId>spring-webmvc</artifactId>
  10. <version>4.3.8.RELEASE</version>
  11. </dependency>
  12. </dependencies>

再来编写我们的Web.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns="http://java.sun.com/xml/ns/javaee"
  4. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  5. id="WebApp_ID" version="3.0">
  6. <display-name>spring mvc</display-name>
  7. <servlet>
  8. <servlet-name>dispatcherServlet</servlet-name>
  9. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  10. <init-param>
  11. <param-name>contextConfigLocation</param-name>
  12. <param-value>
  13. classpath:/spring-mvc.xml
  14. </param-value>
  15. </init-param>
  16. </servlet>
  17. <servlet-mapping>
  18. <servlet-name>dispatcherServlet</servlet-name>
  19. <url-pattern>/</url-pattern>
  20. </servlet-mapping>
  21. </web-app>

我们来简单些一个Controller

  1. package com.springmvcbk.controller;
  2. import org.springframework.web.servlet.ModelAndView;
  3. import org.springframework.web.servlet.mvc.Controller;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletResponse;
  6. public class SpringmvcbkController implements Controller {
  7. public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
  8. ModelAndView modelAndView = new ModelAndView();
  9. modelAndView.setViewName("/WEB-INF/page/index.jsp");
  10. modelAndView.addObject("name","张三");
  11. return modelAndView;
  12. }
  13. }

写一个index.jsp页面吧。

  1. <%@ page language="java" contentType="text/html; charset=UTF-8"
  2. pageEncoding="UTF-8" %>
  3. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  4. <html>
  5. <head>
  6. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  7. <title>Insert title here</title>
  8. </head>
  9. <body>
  10. good man is love
  11. ${name}
  12. </body>
  13. </html>

最后还有我们的spring-mvc.xml

  1. <?xml version="1.0" encoding="GBK"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xmlns:mvc="http://www.springframework.org/schema/mvc"
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/context/spring-context.xsd
  10. http://www.springframework.org/schema/mvc
  11. http://www.springframework.org/schema/mvc/spring-mvc.xsd">
  12. <bean name="/hello" class="com.springmvcbk.controller.SpringmvcbkController"/>
  13. </beans>

注意自己的路径啊,走起,测试一下。

这样我们最精简的SpringMVC就配置完成了。讲一下这段代码是如何执行的,上面图我们也看到了,请求过来优先去找我们的dispatchServlet,也就是我们Spring-MVC.xml配置文件,通过name属性来找的。找到我们对应的类,我们的继承我们的Controller接口来处理我们的请求,也就是图中的3,4,5步骤。然后再把结果塞回给dispatchServlet。返回页面,走起。

这个是我们表层的理解,后续我们逐渐会深入的,我们再来看另外一种实现方式。

  1. package com.springmvcbk.controller;
  2. import org.springframework.web.HttpRequestHandler;
  3. import javax.servlet.ServletException;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.IOException;
  7. public class SpringmvcbkController2 implements HttpRequestHandler {
  8. public void handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
  9. httpServletRequest.setAttribute("name","李斯");
  10. httpServletRequest.getRequestDispatcher("/WEB-INF/page/index.jsp").forward(httpServletRequest,httpServletResponse);
  11. }
  12. }

这种方式也是可以的。

整个过程是如何实现的?
1. dispatchServlet 如何找到对应的Control?
2. 如何执行调用Control 当中的业务方法?
在面试中要回答好上述问题,就必须得弄清楚spring mvc 的体系组成。

spring mvc 的体系组成

  只是举了几个例子的实现,SpringMVC还有很多的实现方法。我们来看一下内部都有什么核心的组件吧。

HandlerMapping->url与控制器的映谢

HandlerAdapter->控制器执行适配器

ViewResolver->视图仓库

view->具体解析视图

HandlerExceptionResolver->异常捕捕捉器

HandlerInterceptor->拦截器

稍后我们会逐个去说一下这些组件,我们看一下我们的UML类图吧,讲解一下他们之间是如果先后工作调用的。

 图没上色,也没写汉字注释,看着有点蒙圈….我来说一下咋回事。HTTPServlet发出请求,我们的DispatcherServlet拿到请求去匹配我们的HandlerMapping,经过HandlerMapping下的HandlerExecutionChain,HandlerInterceptor生成我们的Handl,返回给DispatcherServlet,拿到了Handl,给我们的Handl传递给HandlerAdapter进行处理,得到我们的View再有DispatcherServlet传递给ViewResolver,经由View处理,返回response请求。

  我们先来看看我们的Handler是如何生产的。

Handler

 这个是SpringMVC自己的继承UML图,最下层的两个是我们常用的,一个是通过name来注入的,一个是通过注解的方式来注入的,他是通过一系列的HandlerInterceptor才生成我们的Handler。

目前主流的三种mapping 如下

1. SimpleUrlHandlerMapping:基于手动配置url与control映谢
2. BeanNameUrlHandlerMapping:  基于ioc name 中已 “/” 开头的Bean时行 注册至映谢.
3. RequestMappingHandlerMapping:基于@RequestMapping注解配置对应映谢

另外两个不说了,太常见不过了。我们来尝试自己配置一个SimpleUrlHandlerMapping

  1. <?xml version="1.0" encoding="GBK"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xmlns:mvc="http://www.springframework.org/schema/mvc"
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/context/spring-context.xsd
  10. http://www.springframework.org/schema/mvc
  11. http://www.springframework.org/schema/mvc/spring-mvc.xsd">
  12. <bean name="hello2" class="com.springmvcbk.controller.SpringmvcbkController2"/>
  13.  
  14. <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
  15. <property name="urlMap">
  16. <props>
  17. <prop key="hello.do">hello2</prop>
  18. </props>
  19. </property>
  20. </bean>
  21. </beans>

注意SimpleUrlHandlerMapping是没有/的,而我们的BeanNameUrlHandlerMapping必须加/的。

 

我们来走一下动态代码,只看取得Handler这段,(初始化的阶段可以自己研究一下)

  1. 1 /**
  2. 2 * Return the HandlerExecutionChain for this request.
  3. 3 * <p>Tries all handler mappings in order.
  4. 4 * @param request current HTTP request
  5. 5 * @return the HandlerExecutionChain, or {@code null} if no handler could be found
  6. 6 */
  7. 7 protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
  8. 8 for (HandlerMapping hm : this.handlerMappings) {
  9. 9 if (logger.isTraceEnabled()) {
  10. 10 logger.trace(
  11. 11 "Testing handler map [" + hm + "] in DispatcherServlet with name \'" + getServletName() + "\'");
  12. 12 }
  13. 13 HandlerExecutionChain handler = hm.getHandler(request);
  14. 14 if (handler != null) {
  15. 15 return handler;
  16. 16 }
  17. 17 }
  18. 18 return null;
  19. 19 }

我们找到我们的DispatcherServlet类的getHandler方法上。在源码的1150行,也就是我上图的第7行。打个断点。优先遍历我们handlerMappings集合,找到以后去取我们的handler。

HandlerExecutionChain handler = hm.getHandler(request);方法就是获得我们的Handler方法,这里只是获得了一个HandlerExecutionChain执行链,也就是说我们在找到handler的前后都可能做其它的处理。再来深入一下看getHandler方法。

这时会调用AbstractHandlerMapping类的getHandler方法,然后优先去AbstractUrlHandlerMapping的getHandlerInternal取得handler

  1. 1 /**
  2. 2 * Look up a handler for the URL path of the given request.
  3. 3 * @param request current HTTP request
  4. 4 * @return the handler instance, or {@code null} if none found
  5. 5 */
  6. 6 @Override
  7. 7 protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
  8. 8 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);//取得路径
  9. 9 Object handler = lookupHandler(lookupPath, request);//拿着路径去LinkedHashMap查找是否存在
  10. 10 if (handler == null) {
  11. 11 // We need to care for the default handler directly, since we need to
  12. 12 // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
  13. 13 Object rawHandler = null;
  14. 14 if ("/".equals(lookupPath)) {
  15. 15 rawHandler = getRootHandler();
  16. 16 }
  17. 17 if (rawHandler == null) {
  18. 18 rawHandler = getDefaultHandler();
  19. 19 }
  20. 20 if (rawHandler != null) {
  21. 21 // Bean name or resolved handler?
  22. 22 if (rawHandler instanceof String) {
  23. 23 String handlerName = (String) rawHandler;
  24. 24 rawHandler = getApplicationContext().getBean(handlerName);
  25. 25 }
  26. 26 validateHandler(rawHandler, request);
  27. 27 handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
  28. 28 }
  29. 29 }
  30. 30 if (handler != null && logger.isDebugEnabled()) {
  31. 31 logger.debug("Mapping [" + lookupPath + "] to " + handler);
  32. 32 }
  33. 33 else if (handler == null && logger.isTraceEnabled()) {
  34. 34 logger.trace("No handler mapping found for [" + lookupPath + "]");
  35. 35 }
  36. 36 return handler;
  37. 37 }

得到request的路径,带着路径去我们已经初始化好的LinkedHashMap查看是否存在。

  1. /**
  2. * Look up a handler instance for the given URL path.
  3. * <p>Supports direct matches, e.g. a registered "/test" matches "/test",
  4. * and various Ant-style pattern matches, e.g. a registered "/t*" matches
  5. * both "/test" and "/team". For details, see the AntPathMatcher class.
  6. * <p>Looks for the most exact pattern, where most exact is defined as
  7. * the longest path pattern.
  8. * @param urlPath URL the bean is mapped to
  9. * @param request current HTTP request (to expose the path within the mapping to)
  10. * @return the associated handler instance, or {@code null} if not found
  11. * @see #exposePathWithinMapping
  12. * @see org.springframework.util.AntPathMatcher
  13. */
  14. protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
  15. // Direct match?
  16. Object handler = this.handlerMap.get(urlPath);//拿着路径去LinkedHashMap查找是否存在
  17. if (handler != null) {
  18. // Bean name or resolved handler?
  19. if (handler instanceof String) {
  20. String handlerName = (String) handler;
  21. handler = getApplicationContext().getBean(handlerName);
  22. }
  23. validateHandler(handler, request);
  24. return buildPathExposingHandler(handler, urlPath, urlPath, null);
  25. }
  26. // Pattern match?
  27. List<String> matchingPatterns = new ArrayList<String>();
  28. for (String registeredPattern : this.handlerMap.keySet()) {
  29. if (getPathMatcher().match(registeredPattern, urlPath)) {
  30. matchingPatterns.add(registeredPattern);
  31. }
  32. else if (useTrailingSlashMatch()) {
  33. if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {
  34. matchingPatterns.add(registeredPattern +"/");
  35. }
  36. }
  37. }
  38. String bestMatch = null;
  39. Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
  40. if (!matchingPatterns.isEmpty()) {
  41. Collections.sort(matchingPatterns, patternComparator);
  42. if (logger.isDebugEnabled()) {
  43. logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns);
  44. }
  45. bestMatch = matchingPatterns.get(0);
  46. }
  47. if (bestMatch != null) {
  48. handler = this.handlerMap.get(bestMatch);
  49. if (handler == null) {
  50. if (bestMatch.endsWith("/")) {
  51. handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
  52. }
  53. if (handler == null) {
  54. throw new IllegalStateException(
  55. "Could not find handler for best pattern match [" + bestMatch + "]");
  56. }
  57. }
  58. // Bean name or resolved handler?
  59. if (handler instanceof String) {
  60. String handlerName = (String) handler;
  61. handler = getApplicationContext().getBean(handlerName);
  62. }
  63. validateHandler(handler, request);
  64. String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath);
  65. // There might be multiple \'best patterns\', let\'s make sure we have the correct URI template variables
  66. // for all of them
  67. Map<String, String> uriTemplateVariables = new LinkedHashMap<String, String>();
  68. for (String matchingPattern : matchingPatterns) {
  69. if (patternComparator.compare(bestMatch, matchingPattern) == 0) {
  70. Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
  71. Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
  72. uriTemplateVariables.putAll(decodedVars);
  73. }
  74. }
  75. if (logger.isDebugEnabled()) {
  76. logger.debug("URI Template variables for request [" + urlPath + "] are " + uriTemplateVariables);
  77. }
  78. return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables);
  79. }
  80. // No handler found...
  81. return null;
  82. }

到这里其实我们就可以得到我们的Handler了,但是SpringMVC又经过了buildPathExposingHandler处理,经过HandlerExecutionChain,看一下是否需要做请求前处理,然后得到我们的Handler。得到Handler以后也并没有急着返回,又经过了一次HandlerExecutionChain处理才返回的。

 图中我们可以看到去和回来的时候都经过了HandlerExecutionChain处理的。就这样我们的handler就得到了。注意的三种mapping的方式可能有略微差异,但不影响大体流程。

HandlerAdapter

拿到我们的Handler,我们该查我们的HandlerAdapter了,也就是我们的适配器。我们回到我们的DispatchServlet类中

  1. /**
  2. * Return the HandlerAdapter for this handler object.
  3. * @param handler the handler object to find an adapter for
  4. * @throws ServletException if no HandlerAdapter can be found for the handler. This is a fatal error.
  5. */
  6. protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
  7. for (HandlerAdapter ha : this.handlerAdapters) {
  8. if (logger.isTraceEnabled()) {
  9. logger.trace("Testing handler adapter [" + ha + "]");
  10. }
  11. if (ha.supports(handler)) {
  12. return ha;
  13. }
  14. }
  15. throw new ServletException("No adapter for handler [" + handler +
  16. "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
  17. }

还是我们的循环调用,我们的适配器有四种,分别是AbstractHandlerMethodAdapter,HTTPRequestHandlerAdapter,SimpleControllerHandlerAdapter,SimpleServletHandlerAdapter

 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());方法开始处理我们的请求,返回ModelAndView。

返回以后,我们交给我们的ViewResolver来处理。

ViewResolver

 

ContentNegotiatingViewResolver下面还有很多子类,我就不展示了。 选择对应的ViewResolver解析我们的ModelAndView得我到我们的view进行返回。

说到这一个请求的流程就算是大致结束了。我们来看两段核心的代码。

  1. /**
  2. * Process the actual dispatching to the handler.
  3. * <p>The handler will be obtained by applying the servlet\'s HandlerMappings in order.
  4. * The HandlerAdapter will be obtained by querying the servlet\'s installed HandlerAdapters
  5. * to find the first that supports the handler class.
  6. * <p>All HTTP methods are handled by this method. It\'s up to HandlerAdapters or handlers
  7. * themselves to decide which methods are acceptable.
  8. * @param request current HTTP request
  9. * @param response current HTTP response
  10. * @throws Exception in case of any kind of processing failure
  11. */
  12. protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  13. HttpServletRequest processedRequest = request;
  14. HandlerExecutionChain mappedHandler = null;
  15. boolean multipartRequestParsed = false;
  16. WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
  17. try {
  18. ModelAndView mv = null;
  19. Exception dispatchException = null;
  20. try {
  21. processedRequest = checkMultipart(request);
  22. multipartRequestParsed = (processedRequest != request);
  23. // Determine handler for the current request.
  24. mappedHandler = getHandler(processedRequest);
  25. if (mappedHandler == null || mappedHandler.getHandler() == null) {
  26. noHandlerFound(processedRequest, response);
  27. return;
  28. }
  29. // Determine handler adapter for the current request.
  30. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  31. // Process last-modified header, if supported by the handler.
  32. String method = request.getMethod();
  33. boolean isGet = "GET".equals(method);
  34. if (isGet || "HEAD".equals(method)) {
  35. long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
  36. if (logger.isDebugEnabled()) {
  37. logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
  38. }
  39. if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
  40. return;
  41. }
  42. }
  43. if (!mappedHandler.applyPreHandle(processedRequest, response)) {
  44. return;
  45. }
  46. // Actually invoke the handler.
  47. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  48. if (asyncManager.isConcurrentHandlingStarted()) {
  49. return;
  50. }
  51. applyDefaultViewName(processedRequest, mv);
  52. mappedHandler.applyPostHandle(processedRequest, response, mv);
  53. }
  54. catch (Exception ex) {
  55. dispatchException = ex;
  56. }
  57. catch (Throwable err) {
  58. // As of 4.3, we\'re processing Errors thrown from handler methods as well,
  59. // making them available for @ExceptionHandler methods and other scenarios.
  60. dispatchException = new NestedServletException("Handler dispatch failed", err);
  61. }
  62. processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
  63. }
  64. catch (Exception ex) {
  65. triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
  66. }
  67. catch (Throwable err) {
  68. triggerAfterCompletion(processedRequest, response, mappedHandler,
  69. new NestedServletException("Handler processing failed", err));
  70. }
  71. finally {
  72. if (asyncManager.isConcurrentHandlingStarted()) {
  73. // Instead of postHandle and afterCompletion
  74. if (mappedHandler != null) {
  75. mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
  76. }
  77. }
  78. else {
  79. // Clean up any resources used by a multipart request.
  80. if (multipartRequestParsed) {
  81. cleanupMultipart(processedRequest);
  82. }
  83. }
  84. }
  85. }

这个是DispatchServlet类里面doDispatch方法,也就是我们请求来的时候进行解析的方法。

  1. /**
  2. * Render the given ModelAndView.
  3. * <p>This is the last stage in handling a request. It may involve resolving the view by name.
  4. * @param mv the ModelAndView to render
  5. * @param request current HTTP servlet request
  6. * @param response current HTTP servlet response
  7. * @throws ServletException if view is missing or cannot be resolved
  8. * @throws Exception if there\'s a problem rendering the view
  9. */
  10. protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
  11. // Determine locale for request and apply it to the response.
  12. Locale locale = this.localeResolver.resolveLocale(request);
  13. response.setLocale(locale);
  14. View view;
  15. if (mv.isReference()) {
  16. // We need to resolve the view name.
  17. view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
  18. if (view == null) {
  19. throw new ServletException("Could not resolve view with name \'" + mv.getViewName() +
  20. "\' in servlet with name \'" + getServletName() + "\'");
  21. }
  22. }
  23. else {
  24. // No need to lookup: the ModelAndView object contains the actual View object.
  25. view = mv.getView();
  26. if (view == null) {
  27. throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
  28. "View object in servlet with name \'" + getServletName() + "\'");
  29. }
  30. }
  31. // Delegate to the View object for rendering.
  32. if (logger.isDebugEnabled()) {
  33. logger.debug("Rendering view [" + view + "] in DispatcherServlet with name \'" + getServletName() + "\'");
  34. }
  35. try {
  36. if (mv.getStatus() != null) {
  37. response.setStatus(mv.getStatus().value());
  38. }
  39. view.render(mv.getModelInternal(), request, response);
  40. }
  41. catch (Exception ex) {
  42. if (logger.isDebugEnabled()) {
  43. logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name \'" +
  44. getServletName() + "\'", ex);
  45. }
  46. throw ex;
  47. }
  48. }

这个是DispatchServlet类里面render方法,也就是我们处理完成要返回时的方法。大家有兴趣的可以逐行逐步的去走下流程。里面东西也不少的,这里就不一一讲解了。

 

最进弄了一个公众号,小菜技术,欢迎大家的加入

 

版权声明:本文为cxiaocai原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/cxiaocai/p/11630626.html