注:本系列文章所用play版本爲1.2.6
在上一篇中,咱們分析了play的2種啓動方式,這一篇,咱們來看看Play類的初始化過程java
不管是Server仍是ServletWrapper方式運行,在他們的入口中都會運行Play.init()來對Play類進行初始化。那在解析初始化以前,咱們先來看看Play類是作什麼的,它裏面有什麼重要的方法。
首先要明確的一點是,Play類是整個Play framework框架的管理、配置中心,它存放了大部分框架須要的成員變量,例如id,配置信息,全部加載的class,使用的插件管理器等等。下圖就是Play類中的方法列表。web
這其中加註釋的幾個方法是比較重要的,咱們下面便來從init開始一點點剖析Play類中的各個方法。apache
public static void init(File root, String id) { // Simple things Play.id = id; Play.started = false; Play.applicationPath = root; // 加載全部 play.static 中的記錄的類 initStaticStuff(); //猜想play framework的路徑 guessFrameworkPath(); // 讀取配置文件 readConfiguration(); Play.classes = new ApplicationClasses(); // 初始化日誌 Logger.init(); String logLevel = configuration.getProperty("application.log", "INFO"); //only override log-level if Logger was not configured manually if( !Logger.configuredManually) { Logger.setUp(logLevel); } Logger.recordCaller = Boolean.parseBoolean(configuration.getProperty("application.log.recordCaller", "false")); Logger.info("Starting %s", root.getAbsolutePath()); //設置臨時文件夾 if (configuration.getProperty("play.tmp", "tmp").equals("none")) { tmpDir = null; Logger.debug("No tmp folder will be used (play.tmp is set to none)"); } else { tmpDir = new File(configuration.getProperty("play.tmp", "tmp")); if (!tmpDir.isAbsolute()) { tmpDir = new File(applicationPath, tmpDir.getPath()); } if (Logger.isTraceEnabled()) { Logger.trace("Using %s as tmp dir", Play.tmpDir); } if (!tmpDir.exists()) { try { if (readOnlyTmp) { throw new Exception("ReadOnly tmp"); } tmpDir.mkdirs(); } catch (Throwable e) { tmpDir = null; Logger.warn("No tmp folder will be used (cannot create the tmp dir)"); } } } // 設置運行模式 try { mode = Mode.valueOf(configuration.getProperty("application.mode", "DEV").toUpperCase()); } catch (IllegalArgumentException e) { Logger.error("Illegal mode '%s', use either prod or dev", configuration.getProperty("application.mode")); fatalServerErrorOccurred(); } if (usePrecompiled || forceProd) { mode = Mode.PROD; } // 獲取http使用路徑 ctxPath = configuration.getProperty("http.path", ctxPath); // 設置文件路徑 VirtualFile appRoot = VirtualFile.open(applicationPath); roots.add(appRoot); javaPath = new CopyOnWriteArrayList<VirtualFile>(); javaPath.add(appRoot.child("app")); javaPath.add(appRoot.child("conf")); // 設置模板路徑 if (appRoot.child("app/views").exists()) { templatesPath = new ArrayList<VirtualFile>(2); templatesPath.add(appRoot.child("app/views")); } else { templatesPath = new ArrayList<VirtualFile>(1); } // 設置路由文件 routes = appRoot.child("conf/routes"); // 設置模塊路徑 modulesRoutes = new HashMap<String, VirtualFile>(16); // 加載模塊 loadModules(); // 模板路徑中加入框架自帶的模板文件 templatesPath.add(VirtualFile.open(new File(frameworkPath, "framework/templates"))); // 初始化classloader classloader = new ApplicationClassloader(); // Fix ctxPath if ("/".equals(Play.ctxPath)) { Play.ctxPath = ""; } // 設置cookie域名 Http.Cookie.defaultDomain = configuration.getProperty("application.defaultCookieDomain", null); if (Http.Cookie.defaultDomain!=null) { Logger.info("Using default cookie domain: " + Http.Cookie.defaultDomain); } // 加載插件 pluginCollection.loadPlugins(); // 若是是prod直接啓動 if (mode == Mode.PROD || System.getProperty("precompile") != null) { mode = Mode.PROD; //預編譯 if (preCompile() && System.getProperty("precompile") == null) { start(); } else { return; } } else { Logger.warn("You're running Play! in DEV mode"); } pluginCollection.onApplicationReady(); Play.initialized = true; }
如上面的代碼所示,初始化過程主要的順序爲:緩存
咱們來依次看看Play在這些過程當中作了什麼事情。服務器
Play在初始化過程當中會調用initStaticStuff()方法來檢查代碼目錄下是否存在play.static文件,若是存在,那麼就逐行讀取文件中記錄的類,並經過反射加載類中的靜態初始化代碼段。至於做用嗎,不知道有什麼用,這段代碼的優先級過高了,早於初始化過程運行,若在初始化過程結束後運行還能夠用來覆寫Play類中的配置信息。或者本身寫插件而後在play.static中初始化插件依賴?或者綁定新的數據源?cookie
public static void initStaticStuff() { // Play! plugings Enumeration<URL> urls = null; try { urls = Play.class.getClassLoader().getResources("play.static"); } catch (Exception e) { } while (urls != null && urls.hasMoreElements()) { URL url = urls.nextElement(); try { BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8")); String line = null; while ((line = reader.readLine()) != null) { try { Class.forName(line); } catch (Exception e) { Logger.warn("! Cannot init static: " + line); } } } catch (Exception ex) { Logger.error(ex, "Cannot load %s", url); } } }
獲取框架路徑很簡單,就是判斷是用jar模式運行仍是直接用文件運行,而後讀取對應的路徑mvc
public static void guessFrameworkPath() { // Guess the framework path try { URL versionUrl = Play.class.getResource("/play/version"); // Read the content of the file Play.version = new LineNumberReader(new InputStreamReader(versionUrl.openStream())).readLine(); // This is used only by the embedded server (Mina, Netty, Jetty etc) URI uri = new URI(versionUrl.toString().replace(" ", "%20")); if (frameworkPath == null || !frameworkPath.exists()) { if (uri.getScheme().equals("jar")) { String jarPath = uri.getSchemeSpecificPart().substring(5, uri.getSchemeSpecificPart().lastIndexOf("!")); frameworkPath = new File(jarPath).getParentFile().getParentFile().getAbsoluteFile(); } else if (uri.getScheme().equals("file")) { frameworkPath = new File(uri).getParentFile().getParentFile().getParentFile().getParentFile(); } else { throw new UnexpectedException("Cannot find the Play! framework - trying with uri: " + uri + " scheme " + uri.getScheme()); } } } catch (Exception e) { throw new UnexpectedException("Where is the framework ?", e); } }
首先要說明一下,咱們這裏討論的是在Play類初始化過程當中的讀取配置文件過程,爲何要指出這一點呢,由於配置文件讀取後會調用插件的onConfigurationRead方法,而在初始化過程當中,配置文件是優先於插件加載的,因此在初始化過程當中插件的方法並不會生效。等到Play調用start方法啓動服務器時,會從新讀取配置文件,那時候插件列表已經更新完畢,會執行onConfigurationRead方法。
配置文件的讀取和啓動腳本中的解析方式基本同樣,步驟就是下面幾步:app
咱們來看下play對${..}替換過程框架
Pattern pattern = Pattern.compile("\\$\\{([^}]+)}"); for (Object key : propsFromFile.keySet()) { String value = propsFromFile.getProperty(key.toString()); Matcher matcher = pattern.matcher(value); StringBuffer newValue = new StringBuffer(100); while (matcher.find()) { String jp = matcher.group(1); String r; //重點是下面這個判斷 if (jp.equals("application.path")) { r = Play.applicationPath.getAbsolutePath(); } else if (jp.equals("play.path")) { r = Play.frameworkPath.getAbsolutePath(); } else { r = System.getProperty(jp); if (r == null) { r = System.getenv(jp); } if (r == null) { Logger.warn("Cannot replace %s in configuration (%s=%s)", jp, key, value); continue; } } matcher.appendReplacement(newValue, r.replaceAll("\\\\", "\\\\\\\\")); } matcher.appendTail(newValue); propsFromFile.setProperty(key.toString(), newValue.toString()); }
能夠看出除了${application.path}和${play.path},咱們還能夠經過使用參數和修改環境變量來添加可替換的值dom
Play的日誌處理放在Logger類中,默認使用log4j做爲日誌記錄工具,初始化過程的順序以下
public static void init() { //查找日誌配置文件路徑,沒有則用log4j.xml String log4jPath = Play.configuration.getProperty("application.log.path", "/log4j.xml"); URL log4jConf = Logger.class.getResource(log4jPath); boolean isXMLConfig = log4jPath.endsWith(".xml"); //日誌配置不存在,查找log4j.properties if (log4jConf == null) { // try again with the .properties isXMLConfig = false; log4jPath = Play.configuration.getProperty("application.log.path", "/log4j.properties"); log4jConf = Logger.class.getResource(log4jPath); } //找不到配置文件就關閉日誌 if (log4jConf == null) { Properties shutUp = new Properties(); shutUp.setProperty("log4j.rootLogger", "OFF"); PropertyConfigurator.configure(shutUp); } else if (Logger.log4j == null) { //判斷日誌配置文件是否在應用目錄下,加這條是由於play軟件包目錄下有默認的日誌配置文件 if (log4jConf.getFile().indexOf(Play.applicationPath.getAbsolutePath()) == 0) { // The log4j configuration file is located somewhere in the application folder, // so it's probably a custom configuration file configuredManually = true; } //根據不一樣的類型解析 if (isXMLConfig) { DOMConfigurator.configure(log4jConf); } else { PropertyConfigurator.configure(log4jConf); } Logger.log4j = org.apache.log4j.Logger.getLogger("play"); // 測試模式下,將日誌追加到test-result/application.log if (Play.runingInTestMode()) { org.apache.log4j.Logger rootLogger = org.apache.log4j.Logger.getRootLogger(); try { if (!Play.getFile("test-result").exists()) { Play.getFile("test-result").mkdir(); } Appender testLog = new FileAppender(new PatternLayout("%d{DATE} %-5p ~ %m%n"), Play.getFile("test-result/application.log").getAbsolutePath(), false); rootLogger.addAppender(testLog); } catch (Exception e) { e.printStackTrace(); } } } }
初始化的代碼能夠看出log4j.xml的優先級高於log4j.properties,這裏能夠發現一個問題,Play的日誌初始化徹底是針對log4j來進行的,可是翻看Logger類的代碼能夠看到,全部的日誌輸出方法都會先判斷是否使用java.util.logging.Logger來輸出日誌,在Logger類中也有一個forceJuli字段來判斷是否使用,可是這個字段一直沒有使用過,也就是說只要不手動改,那forceJuli一直是false。
若是沒有作任何配置,那麼直接使用Logger來輸出日誌,那麼顯示的類名就只會是Play,須要在配置文件中加入application.log.recordCaller=true來將日誌輸出時顯示的類變爲調用類名
Play類中記錄的路徑有5種,分別爲根目錄路徑,java文件路徑,模板文件路徑,主路由路徑,模塊路由路徑。文件路徑的添加很簡單,就是將文件路徑記錄在對應的變量中,這裏要提的一點是,conf文件夾是被加入到java文件路徑中的,因此寫在conf文件夾中的java源碼也是可使用的。固然,在conf文件夾裏寫源碼大概會被罵死
Play經過調用loadModules()來加載全部模塊,使用的模塊在3個地方查找,1是系統環境變量,環境變量MODULES記錄的模塊將加載,2是配置文件中記錄的模塊,配置信息爲module.開頭的模塊被加載,3是應用目錄下的modules文件夾下的模塊被加載。在查找完畢後,會判斷是否使用測試模式,是則加入_testrunner模塊,並判斷是否爲dev模式,是則會加入_docviewer模塊。
這裏咱們來看下Play將模塊文件加入應用是如何作的
public static void addModule(String name, File path) { VirtualFile root = VirtualFile.open(path); modules.put(name, root); if (root.child("app").exists()) { javaPath.add(root.child("app")); } if (root.child("app/views").exists()) { templatesPath.add(root.child("app/views")); } if (root.child("conf/routes").exists()) { modulesRoutes.put(name, root.child("conf/routes")); } roots.add(root); if (!name.startsWith("_")) { Logger.info("Module %s is available (%s)", name, path.getAbsolutePath()); } }
能夠看出引入模塊其實就是在各個路徑下加入模塊路徑
插件(plugins)是Play framework框架很是重要的組成部分,插件做用於Play運行時的方方面面,包括請求先後的處理,更新字節碼等等,具體的插件使用說明咱們放在以後的插件篇再說,這裏就說說加載插件的過程。
插件的加載過程以下:
咱們先來看看將插件加入全部插件列表的過程
protected boolean addPlugin( PlayPlugin plugin ){ synchronized( lock ){ //判斷插件列表是否存在插件 if( !allPlugins.contains(plugin) ){ allPlugins.add( plugin ); //根據優先級排序 Collections.sort(allPlugins); //建立只讀的插件列表 allPlugins_readOnlyCopy = createReadonlyCopy( allPlugins); //啓用插件 enablePlugin(plugin); return true; } } return false; }
這是啓用插件的方法
public boolean enablePlugin( PlayPlugin plugin ){ synchronized( lock ){ //檢查是否存在插件 if( allPlugins.contains( plugin )){ //檢查插件是否已在啓動列表 if( !enabledPlugins.contains( plugin )){ //加入啓動插件 enabledPlugins.add( plugin ); //排序 Collections.sort( enabledPlugins); //建立只讀列表 enabledPlugins_readOnlyCopy = createReadonlyCopy( enabledPlugins); //更新插件列表 updatePlayPluginsList(); Logger.trace("Plugin " + plugin + " enabled"); return true; } } } return false; }
這裏有一個問題是,既然加入插件列表時會進行排序,那對List<LoadingPluginInfo>的排序是否就不須要了呢,其實不是的,由於在加入插件列表時會反射生成PlayPlugin的一個實例,若是實例的靜態代碼段對插件進行了修改,就會出現問題。
在插件加入完畢後,會循環列表進行插件的初始化
for( PlayPlugin plugin : getEnabledPlugins()){ if( isEnabled(plugin)){ initializePlugin(plugin); } }
這裏爲何要在循環裏再判斷一遍插件是否啓用呢,是爲了讓高優先級插件能夠禁用低優先級插件。
預編譯是爲了在prod模式下加快加載速度,預編譯方法的做用是將已經預編譯好的文件讀入或對文件進行預編譯,對文件預編譯包括了java文件的預編譯以及模板文件的預編譯,咱們分別來看看
在細說java預編譯過程以前,我以爲有必要先來講一下play framework的類加載機制。
play.classloading.ApplicationClassloader是play框架的類加載器,全部play框架的代碼均由ApplicationClassloader來加載。使用自建的類加載器主要是爲了便於處理預編譯後的字節碼以及方便在dev模式下進行即時的熱更新。
play.classloading.ApplicationClasses類是應用代碼的容器,裏面存放了全部的class。
ApplicationClassloader調用經過查找ApplicationClasses來獲取對應的類代碼(具體來講沒有這麼簡單,由於這邊只談預編譯,因此具體的流程暫且不談,在以後的classloader篇再細說)
java預編譯的入口在ApplicationClassloader.getAllClasses();它的過程以下:
play的編譯器使用的是play.classloading.ApplicationCompiler類,這裏對編譯過程就不作更多的闡述。
這裏有幾點值得提一下
下面就是java預編譯的主要代碼
//判斷是否有插件會進行編譯 if(!Play.pluginCollection.compileSources()) { List<ApplicationClass> all = new ArrayList<ApplicationClass>(); //在javaPath中找全部類 for (VirtualFile virtualFile : Play.javaPath) { all.addAll(getAllClasses(virtualFile)); } List<String> classNames = new ArrayList<String>(); //將全部類名組成list for (int i = 0; i < all.size(); i++) { ApplicationClass applicationClass = all.get(i); if (applicationClass != null && !applicationClass.compiled && applicationClass.isClass()) { classNames.add(all.get(i).name); } } //調用編譯器編譯 Play.classes.compiler.compile(classNames.toArray(new String[classNames.size()])); } //遍歷全部類,添加至allClasses,即ApplicationClasses容器中 for (ApplicationClass applicationClass : Play.classes.all()) { //loadApplicationClass方法關鍵代碼以下 Class clazz = loadApplicationClass(applicationClass.name); if (clazz != null) { allClasses.add(clazz); } }
long start = System.currentTimeMillis(); //從ApplicationClasses容器中找對應的ApplicationClass ApplicationClass applicationClass = Play.classes.getApplicationClass(name); if (applicationClass != null) { //isDefinable方法就是判斷applicationClass是否已經編譯且存在對應的javaClass if (applicationClass.isDefinable()) { return applicationClass.javaClass; } //查找以前是否存在編譯結果 byte[] bc = BytecodeCache.getBytecode(name, applicationClass.javaSource); if (Logger.isTraceEnabled()) { Logger.trace("Compiling code for %s", name); } //判斷applicationClass是一個類仍是package-info if (!applicationClass.isClass()) { definePackage(applicationClass.getPackage(), null, null, null, null, null, null, null); } else { loadPackage(name); } //若是以前的編譯結果存在,就用以前的 if (bc != null) { applicationClass.enhancedByteCode = bc; applicationClass.javaClass = defineClass(applicationClass.name, applicationClass.enhancedByteCode, 0, applicationClass.enhancedByteCode.length, protectionDomain); resolveClass(applicationClass.javaClass); if (!applicationClass.isClass()) { applicationClass.javaPackage = applicationClass.javaClass.getPackage(); } if (Logger.isTraceEnabled()) { Logger.trace("%sms to load class %s from cache", System.currentTimeMillis() - start, name); } return applicationClass.javaClass; } //若是applicationClass編譯過了或者編譯後有字節碼,進行字節碼加強 if (applicationClass.javaByteCode != null || applicationClass.compile() != null) { //進行字節碼加強 applicationClass.enhance(); applicationClass.javaClass = defineClass(applicationClass.name, applicationClass.enhancedByteCode, 0, applicationClass.enhancedByteCode.length, protectionDomain); BytecodeCache.cacheBytecode(applicationClass.enhancedByteCode, name, applicationClass.javaSource); resolveClass(applicationClass.javaClass); if (!applicationClass.isClass()) { applicationClass.javaPackage = applicationClass.javaClass.getPackage(); } if (Logger.isTraceEnabled()) { Logger.trace("%sms to load class %s", System.currentTimeMillis() - start, name); } return applicationClass.javaClass; } Play.classes.classes.remove(name); }
不一樣於java的預編譯,模板的預編譯結果雖然也會存放在precompile文件夾,可是在運行過程當中模板並非一次性所有加載的,模板的加載主要經過play.templates.TemplateLoader來進行,TemplateLoader中存放了使用過的模板信息。
在模板預編譯過程當中,主要是步驟以下:
這裏只對模板預編譯流程作一個梳理,至於編譯的過程放在以後的模板篇再說
從上一篇server與servletWrapper中咱們能夠發現,play framework並非必定在腳本啓動以後便啓動服務器,在咱們使用dev模式進行開發時也會發現,play老是須要接受到一個請求後纔會有真正的啓動流程。咱們這一節就來看看play的啓動過程是怎麼樣的,這個啓動與server或servletWrapper的啓動的區別在於,play啓動後便真正開始業務處理,而server與servletWrapper的啓動僅僅是啓動了監聽端口,固然要清楚的是servletWrapper啓動時也會自動啓動play
public static synchronized void start() { try { //若是已經啓動了,先中止,這裏是爲了dev模式的熱更新 if (started) { stop(); } //若是不是獨立的server,即若是不是放在servlet容器中運行,註冊關閉事件 if( standalonePlayServer) { // Can only register shutdown-hook if running as standalone server if (!shutdownHookEnabled) { //registers shutdown hook - Now there's a good chance that we can notify //our plugins that we're going down when some calls ctrl+c or just kills our process.. shutdownHookEnabled = true; Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { Play.stop(); } }); } } //若是是dev模式啓動,從新加載全部class和插件 if (mode == Mode.DEV) { // Need a new classloader classloader = new ApplicationClassloader(); // Put it in the current context for any code that relies on having it there Thread.currentThread().setContextClassLoader(classloader); // Reload plugins pluginCollection.reloadApplicationPlugins(); } // 讀取配置文件 readConfiguration(); // 配置日誌 String logLevel = configuration.getProperty("application.log", "INFO"); //only override log-level if Logger was not configured manually if( !Logger.configuredManually) { Logger.setUp(logLevel); } Logger.recordCaller = Boolean.parseBoolean(configuration.getProperty("application.log.recordCaller", "false")); // 設置語言 langs = new ArrayList<String>(Arrays.asList(configuration.getProperty("application.langs", "").split(","))); if (langs.size() == 1 && langs.get(0).trim().length() == 0) { langs = new ArrayList<String>(16); } // 從新加載模板 TemplateLoader.cleanCompiledCache(); // 設置secretKey secretKey = configuration.getProperty("application.secret", "").trim(); if (secretKey.length() == 0) { Logger.warn("No secret key defined. Sessions will not be encrypted"); } // 設置默認web encoding String _defaultWebEncoding = configuration.getProperty("application.web_encoding"); if( _defaultWebEncoding != null ) { Logger.info("Using custom default web encoding: " + _defaultWebEncoding); defaultWebEncoding = _defaultWebEncoding; // Must update current response also, since the request/response triggering // this configuration-loading in dev-mode have already been // set up with the previous encoding if( Http.Response.current() != null ) { Http.Response.current().encoding = _defaultWebEncoding; } } // 加載全部class Play.classloader.getAllClasses(); // 加載路由 Router.detectChanges(ctxPath); // 初始化緩存 Cache.init(); // 運行插件onApplicationStart方法 try { pluginCollection.onApplicationStart(); } catch (Exception e) { if (Play.mode.isProd()) { Logger.error(e, "Can't start in PROD mode with errors"); } if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new UnexpectedException(e); } if (firstStart) { Logger.info("Application '%s' is now started !", configuration.getProperty("application.name", "")); firstStart = false; } // We made it started = true; startedAt = System.currentTimeMillis(); // 運行插件afterApplicationStart方法 pluginCollection.afterApplicationStart(); } catch (PlayException e) { started = false; try { Cache.stop(); } catch (Exception ignored) {} throw e; } catch (Exception e) { started = false; try { Cache.stop(); } catch (Exception ignored) {} throw new UnexpectedException(e); } }
能夠看出play的啓動和play的初始化有不少相同的地方,包括加載配置,加載日誌等,啓動過程有不少有意思的地方
能夠看出路由的解析是在play的啓動過程當中進行的,具體過程就是讀取路徑下的路由文件,而後路由的具體解析過程放在模板篇一塊兒講吧
Play類的初始化與啓動已經說的差很少了,下一篇咱們來看下ActionInvoker與mvc