Play framework源碼解析 Part3:Play的初始化與啓動

注:本系列文章所用play版本爲1.2.6

在上一篇中,咱們分析了play的2種啓動方式,這一篇,咱們來看看Play類的初始化過程java

Play類

不管是Server仍是ServletWrapper方式運行,在他們的入口中都會運行Play.init()來對Play類進行初始化。那在解析初始化以前,咱們先來看看Play類是作什麼的,它裏面有什麼重要的方法。
首先要明確的一點是,Play類是整個Play framework框架的管理、配置中心,它存放了大部分框架須要的成員變量,例如id,配置信息,全部加載的class,使用的插件管理器等等。下圖就是Play類中的方法列表。web

Play類的方法列表

這其中加註釋的幾個方法是比較重要的,咱們下面便來從init開始一點點剖析Play類中的各個方法。apache

Play的初始化

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;
    }

如上面的代碼所示,初始化過程主要的順序爲:緩存

  1. 加載全部play.static中記錄的類
  2. 獲取框架路徑
  3. 讀取配置文件
  4. 初始化日誌
  5. 獲取java文件、模板文件路徑
  6. 加載模塊
  7. 加載插件
  8. 若爲prod模式進行預編譯

咱們來依次看看Play在這些過程當中作了什麼事情。服務器

加載play.static

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

  1. 讀取application.conf,將其中的項所有加入Properties
  2. 將全部配置項用正則過濾找出真實配置名,並找出所用id的配置項替換
  3. 將配置項中的${..}替換爲對應值
  4. 讀取@include引用的配置

咱們來看下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做爲日誌記錄工具,初始化過程的順序以下

  1. 查找配置文件中是否有日誌配置文件路徑,存在跳至4
  2. 查找是否有log4j.xml,存在跳至4
  3. 查找是否有log4j.properties,存在跳至4
  4. 根據文件後綴使用對應的日誌配置解析器
  5. 若是是測試模式則加上寫入測試結果文件的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);
        }
        //找不到配置文件就關閉日誌
        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運行時的方方面面,包括請求先後的處理,更新字節碼等等,具體的插件使用說明咱們放在以後的插件篇再說,這裏就說說加載插件的過程。
插件的加載過程以下:

  1. 查找java文件路徑下存在的play.plugins文件
  2. 讀取每個play.plugins,play.plugins中的記錄由2部分組成,一個是插件優先級,一個是類名,逐行讀取後根據優先級和類目組成LoadingPluginInfo,並加入List<LoadingPluginInfo>
  3. 根據優先級對List<LoadingPluginInfo>排序
  4. 根據排序完後的列表依次將插件加入至全部插件列表
  5. 初始化插件
  6. 更新插件列表

咱們先來看看將插件加入全部插件列表的過程

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預編譯

在細說java預編譯過程以前,我以爲有必要先來講一下play framework的類加載機制。
play.classloading.ApplicationClassloader是play框架的類加載器,全部play框架的代碼均由ApplicationClassloader來加載。使用自建的類加載器主要是爲了便於處理預編譯後的字節碼以及方便在dev模式下進行即時的熱更新。
play.classloading.ApplicationClasses類是應用代碼的容器,裏面存放了全部的class。
ApplicationClassloader調用經過查找ApplicationClasses來獲取對應的類代碼(具體來講沒有這麼簡單,由於這邊只談預編譯,因此具體的流程暫且不談,在以後的classloader篇再細說)
java預編譯的入口在ApplicationClassloader.getAllClasses();它的過程以下:

  1. 檢查插件是否有編譯源碼的方法,如有跳至4
  2. 掃描以前設定的javaPath下存在的java文件,讀取類名,建立ApplicationClass,ApplicationClass類內有類名、java文件、java代碼、編譯後字節碼、加強後字節碼等字段,ApplicationClasses中存放的就是一個個ApplicationClass
  3. 使用eclipse JDT對源碼進行編譯,編譯後的字節碼存到對應的ApplicationClass中
  4. 遍歷ApplicationClasses中的全部ApplicationClass,判斷其是否須要加強,須要加強的類用插件進行加強並寫入文件

play的編譯器使用的是play.classloading.ApplicationCompiler類,這裏對編譯過程就不作更多的闡述。
這裏有幾點值得提一下

  1. 上面步驟2中掃描java文件只根據java文件名來判斷類名,也就是說在步驟2的時候,ApplicationClasses容器中存放的class還只是單純的外部類,而內部類與匿名內部類還不包括在內,在步驟3使用jdt編譯時,ApplicationCompiler類會將匿名類與內部類都加到ApplicationClasses容器中,因此在第4步遍歷的時候也會將內部類與匿名類的字節碼文件加強而後加到文件中
  2. 因爲步驟1會檢查是否有插件會編譯源碼,也就意味着咱們能夠經過自寫插件來覆蓋play原有的編譯過程。這點我以爲很是不錯,由於在使用play作爲工程框架時,等到代碼量很大時,每次編譯過程都會很是漫長,由於play默認會將全部java文件所有從新編譯一遍,而這有時候是很沒必要要的,由於其實只要從新編譯更改的文件便可,針對這一點我寫了一個根據文件更改時間選擇編譯的插件,這個放到以後的play應用中再談。

下面就是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中存放了使用過的模板信息。
在模板預編譯過程當中,主要是步驟以下:

  1. 掃描Play.templatesPath目錄下全部模板文件
  2. 遍歷文件,用TemplateLoader.load來加載源文件,並進行模板編譯,產生Groovy源碼
  3. 對編譯後的Groovy源碼進行編譯,並用base64加密寫入文件

這裏只對模板預編譯流程作一個梳理,至於編譯的過程放在以後的模板篇再說

play framework的啓動

從上一篇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的初始化有不少相同的地方,包括加載配置,加載日誌等,啓動過程有不少有意思的地方

  1. play的啓動不會重置模塊列表,也就是說若是在dev模式下對配置文件進行修改增長了模塊必需要重啓服務器,熱更新是無效的。
  2. 在play的初始化過程當中,若是是使用容器打開服務器時就會加載預編譯文件,這時候已經讀取一遍預編譯源碼了,全部在啓動過程當中的getAllClasses就不會再去查找了
  3. 插件的onApplicationStart方法通常就是執行些插件初始化的工做,因此遇到錯誤就會中斷啓動過程,而afterApplicationStart是在啓動完成後在運行的,頗有意思的是,用@OnApplicationStart標識的job類是在afterApplicationStart階段運行的,而不是onApplicationStart階段。那爲何要叫這個名字

能夠看出路由的解析是在play的啓動過程當中進行的,具體過程就是讀取路徑下的路由文件,而後路由的具體解析過程放在模板篇一塊兒講吧

總結

Play類的初始化與啓動已經說的差很少了,下一篇咱們來看下ActionInvoker與mvc

相關文章
相關標籤/搜索