天天看點

servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

點選關注“Java技術精選”,選擇“置頂或者星标”

精選最新技術文章,與你一起成長

servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

在本篇文章中,你将會了解到 Spring MVC 處理請求的過程。同時,你也會了解到 Servlet 相關的知識。以及 Spring MVC 的核心 DispatcherServlet 類的源碼分析。在掌握以上内容後,相信大家會對 Spring MVC 的原理有更深的認識。

如果大家對上面介紹的知識點感興趣的話,那下面不妨和我一起來去探索 Spring MVC 的原理。Let`s Go。

2.一個請求的旅行過程

在探索更深層次的原理之前,我們先來了解一下 Spring MVC 是怎麼處理請求的。弄懂了這個流程後,才能更好的了解具體的源碼。這裡我把 Spring MVC 處理請求的流程圖畫了出來,一起看一下吧:

servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

如上,每一個重要的步驟上面都有編号。我先來簡單分析一下上面的流程,然後再向大家介紹圖中出現的一些元件。我們從第一步開始,首先,使用者的浏覽器發出了一個請求,這個請求經過網際網路到達了我們的伺服器。Servlet 容器首先接待了這個請求,并将該請求委托給 DispatcherServlet 進行處理。接着 DispatcherServlet 将該請求傳給了處理器映射元件 HandlerMapping,并擷取到适合該請求的攔截器和處理器。在擷取到處理器後,DispatcherServlet 還不能直接調用處理器的邏輯,需要進行對處理器進行适配。

處理器适配成功後,DispatcherServlet 通過處理器擴充卡 HandlerAdapter 調用處理器的邏輯,并擷取傳回值 ModelAndView。之後,DispatcherServlet 需要根據 ModelAndView 解析視圖。解析視圖的工作由 ViewResolver 完成,若能解析成功,ViewResolver 會傳回相應的視圖對象 View。在擷取到具體的 View 對象後,最後一步要做的事情就是由 View 渲染視圖,并将渲染結果傳回給使用者。

以上就是 Spring MVC 處理請求的全過程,上面的流程進行了一定的簡化,比如攔截器的執行時機就沒說。不過這并不影響大家對主過程的了解。下來來簡單介紹一下圖中出現的一些元件:

  • DispatcherServlet

Spring MVC 的核心元件,是請求的入口,負責協調各個元件工作

  • HandlerMapping

内部維護了一些 映射,負責為請求找到合适的處理器

  • HandlerAdapter

處理器的擴充卡。Spring 中的處理器的實作多變,比如使用者處理器可以實作 Controller 接口,也可以用 @RequestMapping 注解将方法作為一個處理器等,這就導緻 Spring 不止到怎麼調用使用者的處理器邏輯。是以這裡需要一個處理器擴充卡,由處理器擴充卡去調用處理器的邏輯

  • ViewResolver

視圖解析器的用途不難了解,用于将視圖名稱解析為視圖對象 View。

  • View

視圖對象用于将模闆渲染成 html 或其他類型的檔案。比如 InternalResourceView 可将 jsp 渲染成 html。

從上面的流程中可以看出,Spring MVC 對各個元件的職責劃分的比較清晰。DispatcherServlet 負責協調,其他元件則各自做分内之事,互不幹擾。經過這樣的職責劃分,代碼會便于維護。同時對于源碼閱讀者來說,也會很友好。可以降低了解源碼的難度,使大家能夠快速理清主邏輯。這一點值得我們學習。

3.知其然,更要知其是以然

3.1 追根溯源之 Servlet

本章要向大家介紹一下 Servlet,為什麼要介紹 Servlet 呢?原因不難了解,Spring MVC 是基于 Servlet 實作的。是以要分析 Spring MVC,首先應追根溯源,弄懂 Servlet。Servlet 是 J2EE 規範之一,在遵守該規範的前提下,我們可将 Web 應用部署在 Servlet 容器下。這樣做的好處是什麼呢?我覺得可使開發者聚焦業務邏輯,而不用去關心 HTTP 協定方面的事情。比如,普通的 HTTP 請求就是一段有格式的文本,伺服器需要去解析這段文本才能知道使用者請求的内容是什麼。比如我對個人網站的 80 端口抓包,然後擷取到的 HTTP 請求頭如下:

servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

如果我們為了寫一個 Web 應用,還要去解析 HTTP 協定相關的内容,那會增加很多工作量。有興趣的朋友可以考慮使用 Java socket 編寫實作一個 HTTP 伺服器,體驗一下解析部分 HTTP 協定的過程。也可以參考我之前寫的文章 - 基于 Java NIO 實作簡單的 HTTP 伺服器。

如果我們寫的 Web 應用不大,不誇張的說,項目中對 HTTP 提供支援的代碼會比業務代碼還要多,這豈不是得不償失。當然,在現實中,有現成的架構可用,并不需要自己造輪子。如果我們基于 Servlet 規範實作 Web 應用的話,HTTP 協定的處理過程就不需要我們參與了。這些工作交給 Servlet 容器就行了,我們隻需要關心業務邏輯怎麼實作即可。

下面,我們先來看看 Servlet 接口及其實作類結構,然後再進行更進一步的說明。

servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

如上圖,我們接下來按照從上到下順序進行分析。先來看看最頂層的兩個接口是怎麼定義的。

3.1.1 Servlet 與 ServletConfig

先來看看 Servlet 接口的定義,如下:

public interface Servlet {

   public void init(ServletConfig config) throws ServletException;

   public ServletConfig getServletConfig();

   public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

   public String getServletInfo();

   public void destroy();

}

init 方法會在容器啟動時由容器調用,也可能會在 Servlet 第一次被使用時調用,調用時機取決 load-on-start 的配置。容器調用 init 方法時,會向其傳入一個 ServletConfig 參數。ServletConfig 是什麼呢?顧名思義,ServletConfig 是一個和 Servlet 配置相關的接口。舉個例子說明一下,我們在配置 Spring MVC 的 DispatcherServlet 時,會通過 ServletConfig 将配置檔案的位置告知 DispatcherServlet。比如:

   dispatcher

   org.springframework.web.servlet.DispatcherServlet

       contextConfigLocation

       classpath:application-web.xml

如上, 标簽内的配置資訊最終會被放入 ServletConfig 實作類對象中。DispatcherServlet 通過 ServletConfig 接口中的方法,就能擷取到 contextConfigLocation 對應的值。

Servlet 中的 service 方法用于處理請求。當然,一般情況下我們不會直接實作 Servlet 接口,通常是通過繼承 HttpServlet 抽象類編寫業務邏輯的。Servlet 中接口不多,也不難了解,這裡就不多說了。下面我們來看看 ServletConfig 接口定義,如下:

public interface ServletConfig {

   public String getServletName();

   public ServletContext getServletContext();

   public String getInitParameter(String name);

   public Enumeration<String> getInitParameterNames();

}

先來看看 getServletName 方法,該方法用于擷取 servlet 名稱,也就是 标簽中配置的内容。getServletContext 方法用于擷取 Servlet 上下文。如果說一個 ServletConfig 對應一個 Servlet,那麼一個 ServletContext 則是對應所有的 Servlet。ServletContext 代表目前的 Web 應用,可用于記錄一些全局變量,當然它的功能不局限于記錄變量。我們可通過 标簽向 ServletContext 中配置資訊,比如在配置 Spring 監聽器(ContextLoaderListener)時,就可以通過該标簽配置 contextConfigLocation。如下:

   contextConfigLocation

   classpath:application.xml

關于 ServletContext 就先說這麼多了,繼續介紹 ServletConfig 中的其他方法。getInitParameter 方法用于擷取 标簽中配置的參數值,getInitParameterNames 則是擷取所有配置的名稱集合,這兩個方法用途都不難了解。

以上是 Servlet 與 ServletConfig 兩個接口的說明,比較簡單。說完這兩個接口,我們繼續往下看,接下來是 GenericServlet。

3.1.2 GenericServlet

GenericServlet 實作了 Servlet 和 ServletConfig 兩個接口,為這兩個接口中的部分方法提供了簡單的實作。比如該類實作了 Servlet 接口中的 void init(ServletConfig) 方法,并在方法體内調用了内部提供了一個無參的 init 方法,子類可覆寫該無參 init 方法。除此之外,GenericServlet 還實作了 ServletConfig 接口中的 getInitParameter 方法,使用者可直接調用該方法擷取到配置資訊。而不用先擷取 ServletConfig,然後再調用 ServletConfig 的 getInitParameter 方法擷取。下面我們來看看 GenericServlet 部分方法的源碼:

public abstract class GenericServlet

   implements Servlet, ServletConfig, java.io.Serializable {

   // 省略部分代碼

   private transient ServletConfig config;

   public GenericServlet() { }

   public void init(ServletConfig config) throws ServletException {

       this.config = config;

       // 調用内部定義的無參 init 方法

       this.init();

   }

   public void init() throws ServletException { }

   public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

   public void destroy() { }

   public String getInitParameter(String name) {

       ServletConfig sc = getServletConfig();

       if (sc == null) {

           throw new IllegalStateException(

               lStrings.getString("err.servlet_config_not_initialized"));

       }

       return sc.getInitParameter(name);

   }

   public ServletConfig getServletConfig() {

       return config;

   }

   // 省略部分代碼

}

如上,GenericServlet 代碼比較簡單,配合着我寫注釋,很容易看懂。

GenericServlet 是一個協定無關的 servlet,是一個比較原始的實作,通常我們不會直接繼承該類。一般情況下,我們都是繼承 GenericServlet 的子類 HttpServlet,該類是一個和 HTTP 協定相關的 Servlet。那下面我們來看一下這個類。

3.1.3 HttpServlet

HttpServlet,從名字上就可看出,這個類是和 HTTP 協定相關。該類的關注點在于怎麼處理 HTTP 請求,比如其定義了 doGet 方法處理 GET 類型的請求,定義了 doPost 方法處理 POST 類型的請求等。我們若需要基于 Servlet 寫 Web 應用,應繼承該類,并覆寫指定的方法。doGet 和 doPost 等方法并不是處理的入口方法,是以這些方法需要由其他方法調用才行。其他方法是哪個方法呢?當然是 service 方法了。下面我們看一下這個方法的實作。如下:

@Override

public void service(ServletRequest req, ServletResponse res)

   throws ServletException, IOException {

   HttpServletRequest  request;

   HttpServletResponse response;

   if (!(req instanceof HttpServletRequest &&

           res instanceof HttpServletResponse)) {

       throw new ServletException("non-HTTP request or response");

   }

   request = (HttpServletRequest) req;

   response = (HttpServletResponse) res;

   // 調用重載方法,該重載方法接受 HttpServletRequest 和 HttpServletResponse 類型的參數

   service(request, response);

}

protected void service(HttpServletRequest req, HttpServletResponse resp)

   throws ServletException, IOException {

   String method = req.getMethod();

   // 處理 GET 請求

   if (method.equals(METHOD_GET)) {

       long lastModified = getLastModified(req);

       if (lastModified == -1) {

           // 調用 doGet 方法

           doGet(req, resp);

       } else {

           long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);

           if (ifModifiedSince < lastModified) {

               maybeSetLastModified(resp, lastModified);

               doGet(req, resp);

           } else {

               resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);

           }

       }

   // 處理 HEAD 請求

   } else if (method.equals(METHOD_HEAD)) {

       long lastModified = getLastModified(req);

       maybeSetLastModified(resp, lastModified);

       doHead(req, resp);

   // 處理 POST 請求

   } else if (method.equals(METHOD_POST)) {

       // 調用 doPost 方法

       doPost(req, resp);

   } else if (method.equals(METHOD_PUT)) {

       doPut(req, resp);

   } else if (method.equals(METHOD_DELETE)) {

       doDelete(req, resp);

   } else if (method.equals(METHOD_OPTIONS)) {

       doOptions(req,resp);

   } else if (method.equals(METHOD_TRACE)) {

       doTrace(req,resp);

   } else {

       String errMsg = lStrings.getString("http.method_not_implemented");

       Object[] errArgs = new Object[1];

       errArgs[0] = method;

       errMsg = MessageFormat.format(errMsg, errArgs);

       resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);

   }

}

如上,第一個 service 方法覆寫父類中的抽象方法,并沒什麼太多邏輯。所有的邏輯集中在第二個 service 方法中,該方法根據請求類型分發請求。我們可以根據需要覆寫指定的處理方法。

以上所述隻是 Servlet 規範中的一部分内容,這些内容是和本文相關的内容。對于 Servlet 規範中的其他内容,大家有興趣可以自己去探索。好了,關于 Servlet 方面的内容,這裡先說這麼多。

3.2 DispatcherServlet 族譜

我在前面說到,DispatcherServlet 是 Spring MVC 的核心。是以在分析這個類的源碼前,我們有必要了解一下它的族譜,也就是繼承關系圖。如下:

servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

如上圖,紅色框是 Servlet 中的接口和類,藍色框中則是 Spring 中的接口和類。關于 Servlet 内容前面已經說過,下面來簡單介紹一下藍色框中的接口和類,我們從最頂層的接口開始。

  • Aware

在 Spring 中,Aware 類型的接口用于向 Spring “索要”一些架構中的資訊。比如當某個 bean 實作了 ApplicationContextAware 接口時,Spring 在運作時會将目前的 ApplicationContext 執行個體通過接口方法 setApplicationContext 傳給該 bean。下面舉個例子說明,這裡我寫一個 SystemInfo API,通過該 API 傳回一些系統資訊。代碼如下:

@RestController

@RequestMapping("/systeminfo")

public class SystemInfo implements ApplicationContextAware, EnvironmentAware {

   private ApplicationContext applicationContext;

   private Environment environment;

   @Override

   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

       System.out.println(applicationContext.getClass());

       this.applicationContext = applicationContext;

   }

   @Override

   public void setEnvironment(Environment environment) {

       this.environment = environment;

   }

   @RequestMapping("/env")

   public String environment() {

       StandardServletEnvironment sse = (StandardServletEnvironment) environment;

       Map<String, Object> envs = sse.getSystemEnvironment();

       StringBuilder sb = new StringBuilder();

       sb.append("-------------------------++ System Environment ++-------------------------\n");

       List<String> list = new ArrayList<>();

       list.addAll(envs.keySet());

       for (int i = 0; i < 5 && i < list.size(); i++) {

           String key = list.get(i);

           Object val = envs.get(key);

           sb.append(String.format("%s = %s\n", key, val.toString()));

       }

       Map<String, Object> props = sse.getSystemProperties();

       sb.append("\n-------------------------++ System Properties ++-------------------------\n");

       list.clear();

       list.addAll(props.keySet());

       for (int i = 0; i < 5 && i < list.size(); i++) {

           String key = list.get(i);

           Object val = props.get(key);

           sb.append(String.format("%s = %s\n", key, val.toString()));

       }

       return sb.toString();

   }

   @RequestMapping("/beans")

   public String listBeans() {

       ListableBeanFactory lbf = applicationContext;

       String[] beanNames = lbf.getBeanDefinitionNames();

       StringBuilder sb = new StringBuilder();

       sb.append("-------------------------++ Bean Info ++-------------------------\n");

       Arrays.stream(beanNames).forEach(beanName -> {

           Object bean = lbf.getBean(beanName);

           sb.append(String.format("beanName  = %s\n", beanName));

           sb.append(String.format("beanClass = %s\n\n", bean.getClass().toString()));

       });

       return sb.toString();

   }

}

如上,SystemInfo 分别實作了 ApplicationContextAware 和 EnvironmentAware 接口,是以它可以在運作時擷取到 ApplicationContext 和 Environment 執行個體。下面我們調一下接口看看結果吧:

servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

如上,我們通過接口拿到了環境變量、配置資訊以及容器中所有 bean 的資料。這說明,Spring 在運作時向 SystemInfo 中注入了 ApplicationContext 和 Environment 執行個體。

  • EnvironmentCapable

EnvironmentCapable 僅包含一個方法定義 getEnvironment,通過該方法可以擷取到環境變量對象。我們可以将 EnvironmentCapable 和 EnvironmentAware 接口配合使用,比如下面的執行個體:

public class EnvironmentHolder implements EnvironmentCapable, EnvironmentAware {

   private Environment environment;

   @Override

   public void setEnvironment(Environment environment) {

       this.environment = environment;

   }

   @Override

   public Environment getEnvironment() {

       return environment;

   }

}

  • HttpServletBean

HttpServletBean 是 HttpServlet 抽象類的簡單拓展。HttpServletBean 覆寫了父類中的無參 init 方法,并在該方法中将 ServletConfig 裡的配置資訊設定到子類對象中,比如 DispatcherServlet。

  • FrameworkServlet

FrameworkServlet 是 Spring Web 架構中的一個基礎類,該類會在初始化時建立一個容器。同時該類覆寫了 doGet、doPost 等方法,并将所有類型的請求委托給 doService 方法去處理。doService 是一個抽象方法,需要子類實作。

  • DispatcherServlet

DispatcherServlet 主要的職責相信大家都比較清楚了,即協調各個元件工作。除此之外,DispatcherServlet 還有一個重要的事情要做,即初始化各種元件,比如 HandlerMapping、HandlerAdapter 等。

3.3 DispatcherServlet 源碼簡析

在第二章中,我們知道了一個 HTTP 請求是怎麼樣被 DispatcherServlet 處理的。本節,我們從源碼的角度對第二章的内容進行補充說明。這裡,我們直入主題,直接分析 DispatcherServlet 中的 doDispatch 方法。這裡我把請求的處理流程圖再貼一遍,大家可以對着流程圖閱讀源碼。

servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

   HttpServletRequest processedRequest = request;

   HandlerExecutionChain mappedHandler = null;

   boolean multipartRequestParsed = false;

   WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

   try {

       ModelAndView mv = null;

       Exception dispatchException = null;

       try {

           processedRequest = checkMultipart(request);

           multipartRequestParsed = (processedRequest != request);

           // 擷取可處理目前請求的處理器 Handler,對應流程圖中的步驟②

           mappedHandler = getHandler(processedRequest);

           if (mappedHandler == null || mappedHandler.getHandler() == null) {

               noHandlerFound(processedRequest, response);

               return;

           }

           // 擷取可執行處理器邏輯的擴充卡 HandlerAdapter,對應步驟③

           HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

           // 處理 last-modified 消息頭

           String method = request.getMethod();

           boolean isGet = "GET".equals(method);

           if (isGet || "HEAD".equals(method)) {

               long lastModified = ha.getLastModified(request, mappedHandler.getHandler());

               if (logger.isDebugEnabled()) {

                   logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);

               }

               if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {

                   return;

               }

           }

           // 執行攔截器 preHandle 方法

           if (!mappedHandler.applyPreHandle(processedRequest, response)) {

               return;

           }

           // 調用處理器邏輯,對應步驟④

           mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

           if (asyncManager.isConcurrentHandlingStarted()) {

               return;

           }

           // 如果 controller 未傳回 view 名稱,這裡生成預設的 view 名稱

           applyDefaultViewName(processedRequest, mv);

           // 執行攔截器 preHandle 方法

           mappedHandler.applyPostHandle(processedRequest, response, mv);

       }

       catch (Exception ex) {

           dispatchException = ex;

       }

       catch (Throwable err) {

           dispatchException = new NestedServletException("Handler dispatch failed", err);

       }

       // 解析并渲染視圖

       processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

   }

   catch (Exception ex) {

       triggerAfterCompletion(processedRequest, response, mappedHandler, ex);

   }

   catch (Throwable err) {

       triggerAfterCompletion(processedRequest, response, mappedHandler,

               new NestedServletException("Handler processing failed", err));

   }

   finally {

       if (asyncManager.isConcurrentHandlingStarted()) {

           // Instead of postHandle and afterCompletion

           if (mappedHandler != null) {

               mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);

           }

       }

       else {

           if (multipartRequestParsed) {

               cleanupMultipart(processedRequest);

           }

       }

   }

}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,

       HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

   boolean errorView = false;

   if (exception != null) {

       if (exception instanceof ModelAndViewDefiningException) {

           logger.debug("ModelAndViewDefiningException encountered", exception);

           mv = ((ModelAndViewDefiningException) exception).getModelAndView();

       }

       else {

           Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);

           mv = processHandlerException(request, response, handler, exception);

           errorView = (mv != null);

       }

   }

   if (mv != null && !mv.wasCleared()) {

       // 渲染視圖

       render(mv, request, response);

       if (errorView) {

           WebUtils.clearErrorRequestAttributes(request);

       }

   }

   else {

       if (logger.isDebugEnabled()) {...

   }

   if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {

       return;

   }

   if (mappedHandler != null) {

       mappedHandler.triggerAfterCompletion(request, response, null);

   }

}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {

   Locale locale = this.localeResolver.resolveLocale(request);

   response.setLocale(locale);

   View view;

   if (mv.isReference()) {

       // 解析視圖,對應步驟⑤

       view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);

       if (view == null) {

           throw new ServletException("Could not resolve view with name '" + mv.getViewName() +

                   "' in servlet with name '" + getServletName() + "'");

       }

   }

   else {

       view = mv.getView();

       if (view == null) {

           throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +

                   "View object in servlet with name '" + getServletName() + "'");

       }

   }

   if (logger.isDebugEnabled()) {...}

   try {

       if (mv.getStatus() != null) {

           response.setStatus(mv.getStatus().value());

       }

       // 渲染視圖,并将結果傳回給使用者。對應步驟⑥和⑦

       view.render(mv.getModelInternal(), request, response);

   }

   catch (Exception ex) {

       if (logger.isDebugEnabled()) {...}

       throw ex;

   }

}

以上就是 doDispatch 方法的分析過程,我已經做了較為詳細的注釋,這裡就不多說了。需要說明的是,以上隻是進行了簡單分析,并沒有深入分析每個方法調用。大家若有興趣,可以自己去分析一下 doDispatch 所調用的一些方法,比如 getHandler 和 getHandlerAdapter,這兩個方法比較簡單。從我最近所分析的源碼來看,我個人覺得處理器擴充卡 RequestMappingHandlerAdapter 應該是 Spring MVC 中最為複雜的一個類。該類用于對 @RequestMapping 注解的方法進行适配。該類的邏輯我暫時沒看懂,就不多說了,十分尴尬。關于該類比較詳細的分析,大家可以參考《看透Spring MVC》一書。

總結

到此,本篇文章的主體内容就說完了。本篇文章從一個請求的旅行過程進行分析,并在分析的過程中補充了 Servlet 和 DispatcherServlet 方面的知識。在最後,從源碼的角度分析了 DispatcherServlet 處理請求的過程。總的來算,算是做到了循序漸進。當然,限于個人能力,以上内容可能會有一些講的不好的地方,這裡請大家見諒。同時,也希望大家多多指教。

好了,本篇文章先到這裡。謝謝大家的閱讀。

推薦閱讀

  • 有點深度的聊聊JDK動态代理
  • 看不懂JDK8的流操作?5分鐘帶你入門
  • Maven的聚合和繼承有什麼差別?
  • 寫給程式員的裁員防身指南
servlet請求的執行過程_Spring MVC 原理探秘:一個請求的旅行過程

公衆号@Java技術精選,關注 Java 程式員的個人成長,分享最新技術資訊與技術幹貨。與你成長有關的,我們這裡都有。

↑↑原創不易,如果喜歡請轉發↑↑

繼續閱讀