注:本系列文章所用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