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

相关文章
相关标签/搜索