tomcat 8.0.36
知識點:
- 通過配置loadOnStartup可以設置Servlet是否在Tomcat啟動時加載,以及按值大小進行有序加載,其最小有效值為0,最大有效值為Integer.MAX_VALUE。
- Jsp Servlet的類是org.apache.jasper.servlet.JspServlet。
- Jsp Servlet是強制性啟動時加載,其loadOnStartup的默認值,或其值是失效值時,將使用最大有效值。
- 通過配置Context或Host的failCtxIfServletStartFails屬性值,優先使用Context的,設置tomcat啟動時加載servlet時,是否忽略拋出的ServletException異常,如果不忽略,則tomcat啟動失敗,默認不忽略。
- servlet有多線程模式,和單線程模式,默認是多線程模式,單線程模式需要實現SingleThreadModel接口。單線程模式下,開啟一個實例池,同一時間,一個實例只分配給一個線程占用。
解說源碼:
Tomcat在啟動時,就會調用loadOnStartup,傳入的參數是一個Container[]的數組,這個數組代表的是從我們的web.xml,web-fragment.xml,注解,以及動態添加等方式配置的關於Servlet等信息的數組。
ok設置為false,表示Tomcat啟動失敗,將無法運作。即loadOnStartup的返回值將影響Tomcat是否運作。
if (!loadOnStartup(findChildren())) { ok = false; }
進入loadOnStartup方法后,這里首先定義了一個TreeMap,使用TreeMap的主要目的就是可以排序。
然后遍歷Servlet信息集,將遍歷中的Servlet信息轉化為實際類型Wrapper。
通過getLoadOnStartup方法得到Servlet的啟動序級,過濾掉啟動序級小於0的Servlet。
接下來,就把大於或等於0的啟動序級加入到TreeMap,映射的值是一個Wrapper表,即把Servlet分箱裝入到所屬啟動序級的箱子里面。
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>(); for (int i = 0; i < children.length; i++) { Wrapper wrapper = (Wrapper) children[i]; int loadOnStartup = wrapper.getLoadOnStartup(); if (loadOnStartup < 0) continue; Integer key = Integer.valueOf(loadOnStartup); ArrayList<Wrapper> list = map.get(key); if (list == null) { list = new ArrayList<>(); map.put(key, list); } list.add(wrapper); }
實際上Wrapper只是一個接口,真正實現Wrapper方法的是StandardWrapper,而StandardWrapper還實現了servlet規范中的ServletConfig接口。
public class StandardWrapper extends ContainerBase
implements ServletConfig, Wrapper
但是,最后傳遞給應用使用的ServletConfig並非StandardWrapper,而是StandardWrapperFacade,當然也是實現ServletConfig接口。
public final class StandardWrapperFacade
implements ServletConfig
正如名字上的意思,使用外觀模式再一次包裝StandardWrapper,然后調用其的同名方法。
protected final StandardWrapperFacade facade = new StandardWrapperFacade(this);
所以servlet的所有配置信息都存放在StandardWrapper,如成員變量loadOnStartup就是配置servlet中的一項,其默認值為-1。
protected int loadOnStartup = -1;
getLoadOnStartup方法主要獲取成員變量loadOnStartup值,但有特殊情況,成員變量isJspServlet是記錄配置的Servlet類是不是org.apache.jasper.servlet.JspServlet,即一個Servlet是不是Jsp Servlet就看它的類是不是這個類。
簡單的說,普通Servlet的啟動序級的默認值為-1,最小有效值為0,最大有效值為Integer.MAX_VALUE。Jsp Servlet的啟動序級的默認值為最大值,有效值與普通Servlet一致,並且其值失效時將使用最大值。
public int getLoadOnStartup() { if (isJspServlet && loadOnStartup < 0) { return Integer.MAX_VALUE; } else { return (this.loadOnStartup); } }
回到原來的getLoadOnStartup方法里,map已經把需要啟動時加載的Servlet分裝到按啟動序級排列好的箱子里面,接下來就是先按啟動序級順序,再按加入順序遍歷servlet,並且執行其load方法。
load方法如果拋出ServletException異常,將通過getComputedFailCtxIfServletStartFails獲取的值來決定是否讓Tomcat啟動失敗。
而獲取的值是Context配置的failCtxIfServletStartFails屬性,如果該屬性未配置,則使用Host配置failCtxIfServletStartFails的屬性,其默認值為false。
for (ArrayList<Wrapper> list : map.values()) { for (Wrapper wrapper : list) { try { wrapper.load(); } catch (ServletException e) { if (getComputedFailCtxIfServletStartFails()) { return false; } } } } return true;
進入load方法后,load方法是一個線程安全的方法,為什么需要線程安全?因為,servlet大多數情況下是不進行預加載,什么時候用到,就什么時候加載。換句話說,為了節省從來沒用到的servlet資源空間,所以等到有請求訪問到servlet時才加載。
所以這種后加載的方式,就有可能出現兩個請求,在同一時間訪問到同一個servlet時,這時候就需要線程安全了。那什么時候需要預加載,什么時候不需要預加載,這個問題可以思考到,為什么定義Jsp Servlet時,tomcat是強制性要預加載,這里不再往深敘述了。
成員變量instance就是配置的servlet類的實例,實例是需要加載與初始化,才能對請求做處理。
public synchronized void load() throws ServletException { instance = loadServlet(); if (!instanceInitialized) { initServlet(instance); } }
來到loadServlet方法,在加載實例之前,這里要驗證servlet實例是一個單例還是多例。如果是單例,並且實例已經存在,那就返回該實例。如果是多例,不管實例存與不存在,都將再創建實例。
判斷是使用單例還是多例,由成員變量singleThreadModel的值來判斷,其含意就是如果同一個servlet實例,只允許在同一時間讓一個請求(線程)訪問,所以叫單線程模式。反之,同一個servlet實例,允許同一時間,讓多個請求(線程)訪問,就叫多線程模式。
顯然,單線程與多線程的區別在於,servlet的成員變量共享與不共享,設計的重點就得往這里思考。那變量singleThreadModel默認值為false,即servlet模認是多線程模式,其如何改變為單線程模式在后面。
if (!singleThreadModel && (instance != null)) return instance;
接下來獲取實例管理器,然后通過servlet類加載並創建servlet,這個過實例化的過程,內容也是挺豐富多彩的,其中包括JNDI注入,POST構造方法調用等,這里就不往里面敘述了。
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
Servlet servlet = (Servlet) instanceManager.newInstance(servletClass);
判斷servlet是否單線程模式,如果serlvet實現了SingleThreadModel接口,那么成員變量singleThreadModel標記為true,同時創建出實例池。
if (servlet instanceof SingleThreadModel) { if (instancePool == null) { instancePool = new Stack<>(); } singleThreadModel = true; }
如果servlet需要得到Wrapper對象實例,那么servlet實現ContainerServlet接口,並且在Context的配置里設置屬性privileged為true,默認為false。
if ((servlet instanceof ContainerServlet) &&(Context) getParent()).getPrivileged()) { ((ContainerServlet) servlet).setWrapper(this); }
來到initServlet方法,如果servlet是多線程模式,則實例只執行一次初始化。反之,如果servlet是單線程模式,每次創建的實例,都會執行一次初始化。
private synchronized void initServlet(Servlet servlet) throws ServletException { if (instanceInitialized && !singleThreadModel) return; servlet.init(facade); instanceInitialized = true; }