摘要:使用自建的類加載器主要是為了便于處理預(yù)編譯后的字節(jié)碼以及方便在模式下進(jìn)行即時(shí)的熱更新。
注:本系列文章所用play版本為1.2.6
在上一篇中,我們分析了play的2種啟動(dòng)方式,這一篇,我們來(lái)看看Play類的初始化過(guò)程
Play類無(wú)論是Server還是ServletWrapper方式運(yùn)行,在他們的入口中都會(huì)運(yùn)行Play.init()來(lái)對(duì)Play類進(jìn)行初始化。那在解析初始化之前,我們先來(lái)看看Play類是做什么的,它里面有什么重要的方法。
首先要明確的一點(diǎn)是,Play類是整個(gè)Play framework框架的管理、配置中心,它存放了大部分框架需要的成員變量,例如id,配置信息,所有加載的class,使用的插件管理器等等。下圖就是Play類中的方法列表。
這其中加注釋的幾個(gè)方法是比較重要的,我們下面便來(lái)從init開始一點(diǎn)點(diǎn)剖析Play類中的各個(gè)方法。
Play的初始化public static void init(File root, String id) { // Simple things Play.id = id; Play.started = false; Play.applicationPath = root; // 加載所有 play.static 中的記錄的類 initStaticStuff(); //猜測(cè)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()); //設(shè)置臨時(shí)文件夾 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)"); } } } // 設(shè)置運(yùn)行模式 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); // 設(shè)置文件路徑 VirtualFile appRoot = VirtualFile.open(applicationPath); roots.add(appRoot); javaPath = new CopyOnWriteArrayList(); javaPath.add(appRoot.child("app")); javaPath.add(appRoot.child("conf")); // 設(shè)置模板路徑 if (appRoot.child("app/views").exists()) { templatesPath = new ArrayList (2); templatesPath.add(appRoot.child("app/views")); } else { templatesPath = new ArrayList (1); } // 設(shè)置路由文件 routes = appRoot.child("conf/routes"); // 設(shè)置模塊路徑 modulesRoutes = new HashMap (16); // 加載模塊 loadModules(); // 模板路徑中加入框架自帶的模板文件 templatesPath.add(VirtualFile.open(new File(frameworkPath, "framework/templates"))); // 初始化classloader classloader = new ApplicationClassloader(); // Fix ctxPath if ("/".equals(Play.ctxPath)) { Play.ctxPath = ""; } // 設(shè)置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直接啟動(dòng) if (mode == Mode.PROD || System.getProperty("precompile") != null) { mode = Mode.PROD; //預(yù)編譯 if (preCompile() && System.getProperty("precompile") == null) { start(); } else { return; } } else { Logger.warn("You"re running Play! in DEV mode"); } pluginCollection.onApplicationReady(); Play.initialized = true; }
如上面的代碼所示,初始化過(guò)程主要的順序?yàn)椋?/p>
加載所有play.static中記錄的類
獲取框架路徑
讀取配置文件
初始化日志
獲取java文件、模板文件路徑
加載模塊
加載插件
若為prod模式進(jìn)行預(yù)編譯
我們來(lái)依次看看Play在這些過(guò)程中做了什么事情。
加載play.staticPlay在初始化過(guò)程中會(huì)調(diào)用initStaticStuff()方法來(lái)檢查代碼目錄下是否存在play.static文件,如果存在,那么就逐行讀取文件中記錄的類,并通過(guò)反射加載類中的靜態(tài)初始化代碼段。至于作用嗎,不知道有什么用,這段代碼的優(yōu)先級(jí)太高了,早于初始化過(guò)程運(yùn)行,若在初始化過(guò)程結(jié)束后運(yùn)行還可以用來(lái)覆寫Play類中的配置信息。或者自己寫插件然后在play.static中初始化插件依賴?或者綁定新的數(shù)據(jù)源?
public static void initStaticStuff() { // Play! plugings Enumeration獲取框架路徑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); } } }
獲取框架路徑很簡(jiǎn)單,就是判斷是用jar模式運(yùn)行還是直接用文件運(yùn)行,然后讀取對(duì)應(yīng)的路徑
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); } }讀取配置文件
首先要說(shuō)明一下,我們這里討論的是在Play類初始化過(guò)程中的讀取配置文件過(guò)程,為什么要指出這一點(diǎn)呢,因?yàn)榕渲梦募x取后會(huì)調(diào)用插件的onConfigurationRead方法,而在初始化過(guò)程中,配置文件是優(yōu)先于插件加載的,所以在初始化過(guò)程中插件的方法并不會(huì)生效。等到Play調(diào)用start方法啟動(dòng)服務(wù)器時(shí),會(huì)重新讀取配置文件,那時(shí)候插件列表已經(jīng)更新完畢,會(huì)執(zhí)行onConfigurationRead方法。
配置文件的讀取和啟動(dòng)腳本中的解析方式基本一樣,步驟就是下面幾步:
讀取application.conf,將其中的項(xiàng)全部加入Properties
將所有配置項(xiàng)用正則過(guò)濾找出真實(shí)配置名,并找出所用id的配置項(xiàng)替換
將配置項(xiàng)中的${..}替換為對(duì)應(yīng)值
讀取@include引用的配置
我們來(lái)看下play對(duì)${..}替換過(guò)程
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; //重點(diǎn)是下面這個(gè)判斷 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},我們還可以通過(guò)使用參數(shù)和修改環(huán)境變量來(lái)添加可替換的值
初始化日志Play的日志處理放在Logger類中,默認(rèn)使用log4j作為日志記錄工具,初始化過(guò)程的順序如下
查找配置文件中是否有日志配置文件路徑,存在跳至4
查找是否有l(wèi)og4j.xml,存在跳至4
查找是否有l(wèi)og4j.properties,存在跳至4
根據(jù)文件后綴使用對(duì)應(yīng)的日志配置解析器
如果是測(cè)試模式則加上寫入測(cè)試結(jié)果文件的Appender
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); } //找不到配置文件就關(guān)閉日志 if (log4jConf == null) { Properties shutUp = new Properties(); shutUp.setProperty("log4j.rootLogger", "OFF"); PropertyConfigurator.configure(shutUp); } else if (Logger.log4j == null) { //判斷日志配置文件是否在應(yīng)用目錄下,加這條是因?yàn)閜lay軟件包目錄下有默認(rèn)的日志配置文件 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; } //根據(jù)不同的類型解析 if (isXMLConfig) { DOMConfigurator.configure(log4jConf); } else { PropertyConfigurator.configure(log4jConf); } Logger.log4j = org.apache.log4j.Logger.getLogger("play"); // 測(cè)試模式下,將日志追加到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的優(yōu)先級(jí)高于log4j.properties,這里可以發(fā)現(xiàn)一個(gè)問題,Play的日志初始化完全是針對(duì)log4j來(lái)進(jìn)行的,但是翻看Logger類的代碼可以看到,所有的日志輸出方法都會(huì)先判斷是否使用java.util.logging.Logger來(lái)輸出日志,在Logger類中也有一個(gè)forceJuli字段來(lái)判斷是否使用,但是這個(gè)字段一直沒有使用過(guò),也就是說(shuō)只要不手動(dòng)改,那forceJuli一直是false。
如果沒有做任何配置,那么直接使用Logger來(lái)輸出日志,那么顯示的類名就只會(huì)是Play,需要在配置文件中加入application.log.recordCaller=true來(lái)將日志輸出時(shí)顯示的類變?yōu)檎{(diào)用類名
Play類中記錄的路徑有5種,分別為根目錄路徑,java文件路徑,模板文件路徑,主路由路徑,模塊路由路徑。文件路徑的添加很簡(jiǎn)單,就是將文件路徑記錄在對(duì)應(yīng)的變量中,這里要提的一點(diǎn)是,conf文件夾是被加入到j(luò)ava文件路徑中的,所以寫在conf文件夾中的java源碼也是可以使用的。當(dāng)然,在conf文件夾里寫源碼大概會(huì)被罵死
Play通過(guò)調(diào)用loadModules()來(lái)加載所有模塊,使用的模塊在3個(gè)地方查找,1是系統(tǒng)環(huán)境變量,環(huán)境變量MODULES記錄的模塊將加載,2是配置文件中記錄的模塊,配置信息為module.開頭的模塊被加載,3是應(yīng)用目錄下的modules文件夾下的模塊被加載。在查找完畢后,會(huì)判斷是否使用測(cè)試模式,是則加入_testrunner模塊,并判斷是否為dev模式,是則會(huì)加入_docviewer模塊。
這里我們來(lái)看下Play將模塊文件加入應(yīng)用是如何做的
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()); } }
可以看出引入模塊其實(shí)就是在各個(gè)路徑下加入模塊路徑
加載插件插件(plugins)是Play framework框架非常重要的組成部分,插件作用于Play運(yùn)行時(shí)的方方面面,包括請(qǐng)求前后的處理,更新字節(jié)碼等等,具體的插件使用說(shuō)明我們放在之后的插件篇再說(shuō),這里就說(shuō)說(shuō)加載插件的過(guò)程。
插件的加載過(guò)程如下:
查找java文件路徑下存在的play.plugins文件
讀取每一個(gè)play.plugins,play.plugins中的記錄由2部分組成,一個(gè)是插件優(yōu)先級(jí),一個(gè)是類名,逐行讀取后根據(jù)優(yōu)先級(jí)和類目組成LoadingPluginInfo,并加入List
根據(jù)優(yōu)先級(jí)對(duì)List
根據(jù)排序完后的列表依次將插件加入至所有插件列表
初始化插件
更新插件列表
我們先來(lái)看看將插件加入所有插件列表的過(guò)程
protected boolean addPlugin( PlayPlugin plugin ){ synchronized( lock ){ //判斷插件列表是否存在插件 if( !allPlugins.contains(plugin) ){ allPlugins.add( plugin ); //根據(jù)優(yōu)先級(jí)排序 Collections.sort(allPlugins); //創(chuàng)建只讀的插件列表 allPlugins_readOnlyCopy = createReadonlyCopy( allPlugins); //啟用插件 enablePlugin(plugin); return true; } } return false; }
這是啟用插件的方法
public boolean enablePlugin( PlayPlugin plugin ){ synchronized( lock ){ //檢查是否存在插件 if( allPlugins.contains( plugin )){ //檢查插件是否已在啟動(dòng)列表 if( !enabledPlugins.contains( plugin )){ //加入啟動(dòng)插件 enabledPlugins.add( plugin ); //排序 Collections.sort( enabledPlugins); //創(chuàng)建只讀列表 enabledPlugins_readOnlyCopy = createReadonlyCopy( enabledPlugins); //更新插件列表 updatePlayPluginsList(); Logger.trace("Plugin " + plugin + " enabled"); return true; } } } return false; }
這里有一個(gè)問題是,既然加入插件列表時(shí)會(huì)進(jìn)行排序,那對(duì)List
在插件加入完畢后,會(huì)循環(huán)列表進(jìn)行插件的初始化
for( PlayPlugin plugin : getEnabledPlugins()){ if( isEnabled(plugin)){ initializePlugin(plugin); } }
這里為什么要在循環(huán)里再判斷一遍插件是否啟用呢,是為了讓高優(yōu)先級(jí)插件可以禁用低優(yōu)先級(jí)插件。
預(yù)編譯預(yù)編譯是為了在prod模式下加快加載速度,預(yù)編譯方法的作用是將已經(jīng)預(yù)編譯好的文件讀入或?qū)ξ募M(jìn)行預(yù)編譯,對(duì)文件預(yù)編譯包括了java文件的預(yù)編譯以及模板文件的預(yù)編譯,我們分別來(lái)看看
java預(yù)編譯在細(xì)說(shuō)java預(yù)編譯過(guò)程之前,我覺得有必要先來(lái)說(shuō)一下play framework的類加載機(jī)制。
play.classloading.ApplicationClassloader是play框架的類加載器,所有play框架的代碼均由ApplicationClassloader來(lái)加載。使用自建的類加載器主要是為了便于處理預(yù)編譯后的字節(jié)碼以及方便在dev模式下進(jìn)行即時(shí)的熱更新。
play.classloading.ApplicationClasses類是應(yīng)用代碼的容器,里面存放了所有的class。
ApplicationClassloader調(diào)用通過(guò)查找ApplicationClasses來(lái)獲取對(duì)應(yīng)的類代碼(具體來(lái)說(shuō)沒有這么簡(jiǎn)單,因?yàn)檫@邊只談?lì)A(yù)編譯,所以具體的流程暫且不談,在之后的classloader篇再細(xì)說(shuō))
java預(yù)編譯的入口在ApplicationClassloader.getAllClasses();它的過(guò)程如下:
檢查插件是否有編譯源碼的方法,若有跳至4
掃描之前設(shè)定的javaPath下存在的java文件,讀取類名,創(chuàng)建ApplicationClass,ApplicationClass類內(nèi)有類名、java文件、java代碼、編譯后字節(jié)碼、增強(qiáng)后字節(jié)碼等字段,ApplicationClasses中存放的就是一個(gè)個(gè)ApplicationClass
使用eclipse JDT對(duì)源碼進(jìn)行編譯,編譯后的字節(jié)碼存到對(duì)應(yīng)的ApplicationClass中
遍歷ApplicationClasses中的所有ApplicationClass,判斷其是否需要增強(qiáng),需要增強(qiáng)的類用插件進(jìn)行增強(qiáng)并寫入文件
play的編譯器使用的是play.classloading.ApplicationCompiler類,這里對(duì)編譯過(guò)程就不做更多的闡述。
這里有幾點(diǎn)值得提一下
上面步驟2中掃描java文件只根據(jù)java文件名來(lái)判斷類名,也就是說(shuō)在步驟2的時(shí)候,ApplicationClasses容器中存放的class還只是單純的外部類,而內(nèi)部類與匿名內(nèi)部類還不包括在內(nèi),在步驟3使用jdt編譯時(shí),ApplicationCompiler類會(huì)將匿名類與內(nèi)部類都加到ApplicationClasses容器中,所以在第4步遍歷的時(shí)候也會(huì)將內(nèi)部類與匿名類的字節(jié)碼文件增強(qiáng)然后加到文件中
由于步驟1會(huì)檢查是否有插件會(huì)編譯源碼,也就意味著我們可以通過(guò)自寫插件來(lái)覆蓋play原有的編譯過(guò)程。這點(diǎn)我覺得非常不錯(cuò),因?yàn)樵谑褂胮lay做為工程框架時(shí),等到代碼量很大時(shí),每次編譯過(guò)程都會(huì)非常漫長(zhǎng),因?yàn)閜lay默認(rèn)會(huì)將所有java文件全部重新編譯一遍,而這有時(shí)候是很不必要的,因?yàn)槠鋵?shí)只要重新編譯更改的文件即可,針對(duì)這一點(diǎn)我寫了一個(gè)根據(jù)文件更改時(shí)間選擇編譯的插件,這個(gè)放到之后的play應(yīng)用中再談。
下面就是java預(yù)編譯的主要代碼
//判斷是否有插件會(huì)進(jìn)行編譯 if(!Play.pluginCollection.compileSources()) { Listall = new ArrayList (); //在javaPath中找所有類 for (VirtualFile virtualFile : Play.javaPath) { all.addAll(getAllClasses(virtualFile)); } List classNames = new ArrayList (); //將所有類名組成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); } } //調(diào)用編譯器編譯 Play.classes.compiler.compile(classNames.toArray(new String[classNames.size()])); } //遍歷所有類,添加至allClasses,即ApplicationClasses容器中 for (ApplicationClass applicationClass : Play.classes.all()) { //loadApplicationClass方法關(guān)鍵代碼如下 Class clazz = loadApplicationClass(applicationClass.name); if (clazz != null) { allClasses.add(clazz); } }
long start = System.currentTimeMillis(); //從ApplicationClasses容器中找對(duì)應(yīng)的ApplicationClass ApplicationClass applicationClass = Play.classes.getApplicationClass(name); if (applicationClass != null) { //isDefinable方法就是判斷applicationClass是否已經(jīng)編譯且存在對(duì)應(yīng)的javaClass if (applicationClass.isDefinable()) { return applicationClass.javaClass; } //查找之前是否存在編譯結(jié)果 byte[] bc = BytecodeCache.getBytecode(name, applicationClass.javaSource); if (Logger.isTraceEnabled()) { Logger.trace("Compiling code for %s", name); } //判斷applicationClass是一個(gè)類還是package-info if (!applicationClass.isClass()) { definePackage(applicationClass.getPackage(), null, null, null, null, null, null, null); } else { loadPackage(name); } //如果之前的編譯結(jié)果存在,就用之前的 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編譯過(guò)了或者編譯后有字節(jié)碼,進(jìn)行字節(jié)碼增強(qiáng) if (applicationClass.javaByteCode != null || applicationClass.compile() != null) { //進(jìn)行字節(jié)碼增強(qiáng) 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); }模板預(yù)編譯
不同于java的預(yù)編譯,模板的預(yù)編譯結(jié)果雖然也會(huì)存放在precompile文件夾,但是在運(yùn)行過(guò)程中模板并不是一次性全部加載的,模板的加載主要通過(guò)play.templates.TemplateLoader來(lái)進(jìn)行,TemplateLoader中存放了使用過(guò)的模板信息。
在模板預(yù)編譯過(guò)程中,主要是步驟如下:
掃描Play.templatesPath目錄下所有模板文件
遍歷文件,用TemplateLoader.load來(lái)加載源文件,并進(jìn)行模板編譯,產(chǎn)生Groovy源碼
對(duì)編譯后的Groovy源碼進(jìn)行編譯,并用base64加密寫入文件
這里只對(duì)模板預(yù)編譯流程做一個(gè)梳理,至于編譯的過(guò)程放在之后的模板篇再說(shuō)
play framework的啟動(dòng)從上一篇server與servletWrapper中我們可以發(fā)現(xiàn),play framework并不是一定在腳本啟動(dòng)之后便啟動(dòng)服務(wù)器,在我們使用dev模式進(jìn)行開發(fā)時(shí)也會(huì)發(fā)現(xiàn),play總是需要接受到一個(gè)請(qǐng)求后才會(huì)有真正的啟動(dòng)流程。我們這一節(jié)就來(lái)看看play的啟動(dòng)過(guò)程是怎么樣的,這個(gè)啟動(dòng)與server或servletWrapper的啟動(dòng)的區(qū)別在于,play啟動(dòng)后便真正開始業(yè)務(wù)處理,而server與servletWrapper的啟動(dòng)僅僅是啟動(dòng)了監(jiān)聽端口,當(dāng)然要清楚的是servletWrapper啟動(dòng)時(shí)也會(huì)自動(dòng)啟動(dòng)play
public static synchronized void start() { try { //如果已經(jīng)啟動(dòng)了,先停止,這里是為了dev模式的熱更新 if (started) { stop(); } //如果不是獨(dú)立的server,即如果不是放在servlet容器中運(yùn)行,注冊(cè)關(guān)閉事件 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模式啟動(dòng),重新加載所有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")); // 設(shè)置語(yǔ)言 langs = new ArrayList(Arrays.asList(configuration.getProperty("application.langs", "").split(","))); if (langs.size() == 1 && langs.get(0).trim().length() == 0) { langs = new ArrayList (16); } // 重新加載模板 TemplateLoader.cleanCompiledCache(); // 設(shè)置secretKey secretKey = configuration.getProperty("application.secret", "").trim(); if (secretKey.length() == 0) { Logger.warn("No secret key defined. Sessions will not be encrypted"); } // 設(shè)置默認(rèn)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(); // 運(yùn)行插件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(); // 運(yùn)行插件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的啟動(dòng)和play的初始化有很多相同的地方,包括加載配置,加載日志等,啟動(dòng)過(guò)程有很多有意思的地方
play的啟動(dòng)不會(huì)重置模塊列表,也就是說(shuō)如果在dev模式下對(duì)配置文件進(jìn)行修改增加了模塊必須要重啟服務(wù)器,熱更新是無(wú)效的。
在play的初始化過(guò)程中,如果是使用容器打開服務(wù)器時(shí)就會(huì)加載預(yù)編譯文件,這時(shí)候已經(jīng)讀取一遍預(yù)編譯源碼了,所有在啟動(dòng)過(guò)程中的getAllClasses就不會(huì)再去查找了
插件的onApplicationStart方法一般就是執(zhí)行些插件初始化的工作,所以遇到錯(cuò)誤就會(huì)中斷啟動(dòng)過(guò)程,而afterApplicationStart是在啟動(dòng)完成后在運(yùn)行的,很有意思的是,用@OnApplicationStart標(biāo)識(shí)的job類是在afterApplicationStart階段運(yùn)行的,而不是onApplicationStart階段。那為什么要叫這個(gè)名字
可以看出路由的解析是在play的啟動(dòng)過(guò)程中進(jìn)行的,具體過(guò)程就是讀取路徑下的路由文件,然后路由的具體解析過(guò)程放在模板篇一起講吧
總結(jié)Play類的初始化與啟動(dòng)已經(jīng)說(shuō)的差不多了,下一篇我們來(lái)看下ActionInvoker與mvc
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/70955.html
摘要:注本系列文章所用版本為介紹是個(gè)輕量級(jí)的框架,致力于讓程序員實(shí)現(xiàn)快速高效開發(fā),它具有以下幾個(gè)方面的優(yōu)勢(shì)熱加載。在調(diào)試模式下,所有修改會(huì)及時(shí)生效。拋棄配置文件。約定大于配置。 注:本系列文章所用play版本為1.2.6 介紹 Play framework是個(gè)輕量級(jí)的RESTful框架,致力于讓java程序員實(shí)現(xiàn)快速高效開發(fā),它具有以下幾個(gè)方面的優(yōu)勢(shì): 熱加載。在調(diào)試模式下,所有修改會(huì)及時(shí)...
摘要:是一個(gè)抽象類,繼承了接口,它的方法是這個(gè)類的核心。因?yàn)榭赡苄枰粋€(gè)返回值,所以它同時(shí)繼承了接口來(lái)提供返回值。 注:本系列文章所用play版本為1.2.6 在上一篇中我們剖析了Play framework的啟動(dòng)原理,很容易就能發(fā)現(xiàn)Play framework的啟動(dòng)主入口在play.server.Server中,在本節(jié),我們來(lái)一起看看Server類中主要發(fā)生了什么。 Server類 既然是...
摘要:本文將著重介紹使用來(lái)部署一個(gè)基于的應(yīng)用程序會(huì)多么便捷,當(dāng)然這個(gè)過(guò)程主要基于插件。如你所見,這是一個(gè)基于的應(yīng)用程序。這個(gè)基于的應(yīng)用程序?qū)o(wú)法被訪問。總結(jié)可以如此簡(jiǎn)單地給一個(gè)基于的應(yīng)用程序建立,相信很多人都會(huì)像筆者一樣離不開它。 本文作者 Jacek Laskowski 擁有近20年的應(yīng)用程序開發(fā)經(jīng)驗(yàn),現(xiàn) CodiLime 的軟件開發(fā)團(tuán)隊(duì) Leader,曾從 IBM 取得多種資格認(rèn)證。在這...
摘要:為了使用最新的,升級(jí)到配置修改根據(jù)官網(wǎng)的升級(jí)指南,修改文件,更改插件版本號(hào)文件中,把和單獨(dú)加入。此文件為首頁(yè)的模板。推測(cè)可能是版本和版本的首頁(yè)模板不同,于是到官網(wǎng)下載版本的,找到并覆蓋項(xiàng)目的相應(yīng)文件。添加插件的語(yǔ)句至此,升級(jí)成功完成。 為了使用最新的Play WS Api,升級(jí)到play 2.6.21 1.配置修改 根據(jù)官網(wǎng)的升級(jí)指南,修改plugins.sbt文件,更改插件版本號(hào):a...
閱讀 1267·2023-04-25 23:22
閱讀 1668·2023-04-25 20:04
閱讀 2643·2021-11-22 15:24
閱讀 2801·2021-11-11 16:54
閱讀 1879·2019-08-30 14:03
閱讀 1480·2019-08-29 16:35
閱讀 1700·2019-08-26 10:29
閱讀 2643·2019-08-23 18:01