注:本系列文章所用play版本爲1.2.6
Play framework是個輕量級的RESTful框架,致力於讓java程序員實現快速高效開發,它具備如下幾個方面的優點:java
這裏附上Play framework的文檔地址,官方有更爲詳盡的功能敘述。Play framework文檔python
play framework的初始化很是簡單,只要下載了play的軟件包後,在命令行中運行play new xxx便可初始化一個項目。
自動生成的項目結構以下:
程序員
運行play程序也很是簡單,在項目目錄下使用play run便可運行。數據庫
爲了更好的瞭解play framework的運做原理,咱們來從play framework的啓動腳本開始分析,分析啓動腳本有助於咱們瞭解play framework的運行過程。
play framework1.2.6軟件包解壓後的文件以下:
play的啓動腳本是使用python編寫的,腳本的入口爲play軟件包根目錄下的play文件,下面咱們將從play這個腳本的主入口開始分析。編程
play腳本在開頭引入了3個類,分別爲play.cmdloader,play.application,play.utils,從添加的系統參數中能夠看出play啓動腳本的存放路徑爲 framework/pym
cmdloader.py的主要做用爲加載framework/pym/commands下的各個腳本文件,用於以後對命令參數的解釋運行
application.py的主要做用爲解析項目路徑下conf/中的配置文件、加載模塊、拼接最後運行的java命令tomcat
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'framework', 'pym')) from play.cmdloader import CommandLoader from play.application import PlayApplication from play.utils import *
在腳本的開頭,有這麼一段代碼,意味着只要在play主程序根目錄下建立一個名爲id的文件,便可設置默認的框架id安全
play_env["id_file"] = os.path.join(play_env['basedir'], 'id') if os.path.exists(play_env["id_file"]): play_env["id"] = open(play_env["id_file"]).readline().strip() else: play_env["id"] = ''
命令參數的分隔由如下代碼完成,例如使用了play run --%test,那麼參數列表就是["xxxx\play","run","--%test"]
,這段代碼也說明了,play的命令格式爲 play cmd [app_path] [--options]
服務器
application_path = None remaining_args = [] if len(sys.argv) == 1: application_path = os.getcwd() if len(sys.argv) == 2: application_path = os.getcwd() remaining_args = sys.argv[2:] if len(sys.argv) > 2: if sys.argv[2].startswith('-'): application_path = os.getcwd() remaining_args = sys.argv[2:] else: application_path = os.path.normpath(os.path.abspath(sys.argv[2])) remaining_args = sys.argv[3:]
在play參數中,有一個ignoreMissing,這個參數的全稱實際上是ignoreMissingModules,做用就是在當配置文件中配置有模塊可是在play目錄下並無找到時是否忽略,如需忽略那麼在啓動時須要加入--force
mvc
ignoreMissing = False if remaining_args.count('--force') == 1: remaining_args.remove('--force') ignoreMissing = True
腳本經過play_app = PlayApplication(application_path, play_env, ignoreMissing)
和cmdloader = CommandLoader(play_env["basedir"])
來進行PlayApplication類和CommandLoader類的初始化。
在PlayApplication類的初始化過程當中,它建立了PlayConfParser類用來解析配置文件,這也就是說play的配置文件解析是在腳本啓動階段進行的,這也是爲何修改配置文件沒法實時生效須要重啓的緣由
在PlayConfParser中有一個常量DEFAULTS儲存了默認的http端口和jpda調試端口,分別爲9000和8000,須要添加默認值能夠修改DEFAULTS,DEFAULTS內的值只有當配置文件中找不到時纔會生效。app
DEFAULTS = { 'http.port': '9000', 'jpda.port': '8000' }
值得一提的是,配置文件中的http.port的優先級是小於命令參數中的http.port的
#env爲命令參數 if env.has_key('http.port'): self.entries['http.port'] = env['http.port']
CommandLoader類的功能很簡單,就是遍歷framework/pym/commands下的.py文件,依次加載而後讀取他的全局變量COMMANDS和MODULE儲存用於以後的命令處理和模塊加載。
回到play腳本中,在PlayApplication類和CommandLoader類初始化完成以後,play進行"--deps"參數的檢查,若是存在--deps,則調用play.deps.DependenciesManager
類來進行依賴的檢查、更新。DependenciesManager的解析將放到後話詳解。
if remaining_args.count('--deps') == 1: cmdloader.commands['dependencies'].execute(command='dependencies', app=play_app, args=['--sync'], env=play_env, cmdloader=cmdloader) remaining_args.remove('--deps')
接下來,play便正式進行啓動過程,play按照如下的順序進行加載:
if play_command in cmdloader.commands: for name in cmdloader.modules: module = cmdloader.modules[name] if 'before' in dir(module): module.before(command=play_command, app=play_app, args=remaining_args, env=play_env) status = cmdloader.commands[play_command].execute(command=play_command, app=play_app, args=remaining_args, env=play_env, cmdloader=cmdloader) for name in cmdloader.modules: module = cmdloader.modules[name] if 'after' in dir(module): module.after(command=play_command, app=play_app, args=remaining_args, env=play_env) sys.exit(status)
下面,咱們來看看play經常使用命令的運行過程...
在本節的一開始,我決定先把play全部的可用命令先列舉一下,以便讀者能夠選擇性閱讀。
命令名稱 | 命令所在文件 | 做用 |
---|---|---|
antify | ant.py | 初始化ant構建工具的build.xml文件 |
run | base.py | 運行程序 |
new | base.py | 新建play應用 |
clean | base.py | 刪除臨時文件,即清空tmp文件夾 |
test | base.py | 運行測試程序 |
autotest、auto-test | base.py | 自動運行全部測試項目 |
id | base.py | 設置項目id |
new,run | base.py | 新建play應用並啓動 |
clean,run | base.py | 刪除臨時文件並運行 |
modules | base.py | 顯示項目用到的模塊,注:這裏顯示的模塊只是在項目配置文件中引用的模塊,命令參數中添加的模塊不會顯示 |
check | check.py | 檢查play更新 |
classpath、cp | classpath.py | 顯示應用的classpath |
start | daemon.py | 在後臺運行play程序 |
stop | daemon.py | 中止正在運行的程序 |
restart | daemon.py | 重啓正在運行的程序 |
pid | daemon.py | 顯示運行中的程序的pid |
out | daemon.py | 顯示輸出 |
dependencies、deps | deps.py | 運行DependenciesManager更新依賴 |
eclipsify、ec | eclipse.py | 建立eclipse配置文件 |
evolutions | evolutions.py | 運行play.db.Evolutions進行數據庫演變檢查 |
help | help.py | 輸出全部play的可用命令 |
idealize、idea | intellij.py | 生成idea配置文件 |
javadoc | javadoc.py | 生成javadoc |
new-module、nm | modulesrepo.py | 建立新模塊 |
list-modules、lm | modulesrepo.py | 顯示play社區中的模塊 |
build-module、bm | modulesrepo.py | 打包模塊 |
add | modulesrepo.py | 將模塊添加至項目 |
install | modulesrepo.py | 安裝模塊 |
netbeansify | netbeans.py | 生成netbeans配置文件 |
precompile | precompile.py | 預編譯 |
secret | secret.py | 生成secret key |
status | status.py | 顯示運行中項目的狀態 |
version | version.py | 顯示play framework的版本號 |
war | war.py | 將項目打包爲war文件 |
run應該是咱們平時用的最多的命令了,run命令的做用其實很簡單,就是根據命令參數拼接java參數,而後調用java來運行play.server.Server,run函數的代碼以下:
def run(app, args): #app即爲play腳本中建立的PlayApplication類 global process #這裏檢查是否存在conf/routes和conf/application.conf app.check() print "~ Ctrl+C to stop" print "~ " java_cmd = app.java_cmd(args) try: process = subprocess.Popen (java_cmd, env=os.environ) signal.signal(signal.SIGTERM, handle_sigterm) return_code = process.wait() signal.signal(signal.SIGINT, handle_sigint) if 0 != return_code: sys.exit(return_code) except OSError: print "Could not execute the java executable, please make sure the JAVA_HOME environment variable is set properly (the java executable should reside at JAVA_HOME/bin/java). " sys.exit(-1) print
app.java_cmd(args)的實現代碼以下:
def java_cmd(self, java_args, cp_args=None, className='play.server.Server', args = None): if args is None: args = [''] memory_in_args=False #檢查java參數中是否有jvm內存設置 for arg in java_args: if arg.startswith('-Xm'): memory_in_args=True #若是參數中無jvm內存設置,那麼在配置文件中找是否有jvm內存設置,若仍是沒有則在環境變量中找是否有JAVA_OPTS #這裏其實有個問題,這裏假定的是JAVA_OPTS變量裏只存了jvm內存設置,若是JAVA_OPTS還存了其餘選項,那對運行可能有影響 if not memory_in_args: memory = self.readConf('jvm.memory') if memory: java_args = java_args + memory.split(' ') elif 'JAVA_OPTS' in os.environ: java_args = java_args + os.environ['JAVA_OPTS'].split(' ') #獲取程序的classpath if cp_args is None: cp_args = self.cp_args() #讀取配置文件中的jpda端口 self.jpda_port = self.readConf('jpda.port') #讀取配置文件中的運行模式 application_mode = self.readConf('application.mode').lower() #若是模式是prod,則用server模式編譯 if application_mode == 'prod': java_args.append('-server') # JDK 7 compat # 使用新class校驗器 (不知道做用) java_args.append('-XX:-UseSplitVerifier') #查找配置文件中是否有java安全配置,若是有則加入java參數中 java_policy = self.readConf('java.policy') if java_policy != '': policyFile = os.path.join(self.path, 'conf', java_policy) if os.path.exists(policyFile): print "~ using policy file \"%s\"" % policyFile java_args.append('-Djava.security.manager') java_args.append('-Djava.security.policy==%s' % policyFile) #加入http端口設置 if self.play_env.has_key('http.port'): args += ["--http.port=%s" % self.play_env['http.port']] #加入https端口設置 if self.play_env.has_key('https.port'): args += ["--https.port=%s" % self.play_env['https.port']] #設置文件編碼 java_args.append('-Dfile.encoding=utf-8') #設置編譯命令 (這邊使用了jregex/Pretokenizer類的next方法,不知道有什麼用) java_args.append('-XX:CompileCommand=exclude,jregex/Pretokenizer,next') #若是程序模式在dev,則添加jpda調試器參數 if self.readConf('application.mode').lower() == 'dev': if not self.play_env["disable_check_jpda"]: self.check_jpda() java_args.append('-Xdebug') java_args.append('-Xrunjdwp:transport=dt_socket,address=%s,server=y,suspend=n' % self.jpda_port) java_args.append('-Dplay.debug=yes') #拼接java參數 java_cmd = [self.java_path(), '-javaagent:%s' % self.agent_path()] + java_args + ['-classpath', cp_args, '-Dapplication.path=%s' % self.path, '-Dplay.id=%s' % self.play_env["id"], className] + args return java_cmd
start命令與run命令很相似,執行步驟爲:
stop命令即關閉當前進程,這裏要提一下,play有個註解叫OnApplicationStop,即會在程序中止時觸發,而OnApplicationStop的實現主要是調用了Runtime.getRuntime().addShutdownHook();
來完成
使用play test命令可讓程序進入測試模式,test命令和run命令其實差異不大,惟一的區別就在於使用test命令時,腳本會將play id自動替換爲test,當play id爲test時會自動引入testrunner模塊,testrunner模塊主要功能爲添加@tests路由並實現了test測試頁面,他的具體實現過程後續再談。
autotest命令的做用是自動測試全部的測試用例,他的執行順序是這樣的:
autotest的腳本的步驟1和2我以爲是有問題的,應該換下順序,否則若是程序正在向tmp文件夾插入臨時文件,那麼tmp文件夾就刪除失敗了。
關閉程序的代碼調用了http://localhost:%http_port/@kill
進行關閉,@kill的實現方法在play.CorePlugin下,注意,@kill在prod模式下無效(這是廢話)
因爲使用了python做爲啓動腳本,沒法經過java內部變量值判斷程序是否開啓,只能查看控制檯的輸出日誌,因此在使用test做爲id重啓後,腳本使用下面的代碼判斷程序是否徹底啓動:
try: #這裏啓動程序 play_process = subprocess.Popen(java_cmd, env=os.environ, stdout=sout) except OSError: print "Could not execute the java executable, please make sure the JAVA_HOME environment variable is set properly (the java executable should reside at JAVA_HOME/bin/java). " sys.exit(-1) #打開日誌輸出文件 soutint = open(os.path.join(app.log_path(), 'system.out'), 'r') while True: if play_process.poll(): print "~" print "~ Oops, application has not started?" print "~" sys.exit(-1) line = soutint.readline().strip() if line: print line #若出現'Server is up and running'則正常啓動 if line.find('Server is up and running') > -1: # This line is written out by Server.java to system.out and is not log file dependent soutint.close() break
firephoque類的實現過程咱們以後再詳解。
咱們使用new來建立一個新項目,play new的使用方法爲play new project-name [--with] [--name]
。
with參數爲項目所使用的模塊。
name爲項目名,這裏要注意一點,projectname和--name的參數能夠設置爲不一樣值,projectname是項目創建的文件夾名,--name的值爲項目配置文件中的application.name。
若是不加--name,腳本會提示你是否使用projectname做爲名字,在確認以後,腳本會將resources/application-skel中的全部文件拷貝到projectname文件夾下,而後用輸入的name替換項目配置文件下的application.name,並生成一個64位的secretKey替換配置文件中的secretKey。
接着,腳本會查找使用的模塊中是否存在dependencies.yml,並將dependencies.yml中的內容加入項目的dependencies.yml中,並調用DependenciesManager檢查依賴狀態。
new函數的主要代碼以下:
print "~ The new application will be created in %s" % os.path.normpath(app.path) if application_name is None: application_name = raw_input("~ What is the application name? [%s] " % os.path.basename(app.path)) if application_name == "": application_name = os.path.basename(app.path) copy_directory(os.path.join(env["basedir"], 'resources/application-skel'), app.path) os.mkdir(os.path.join(app.path, 'app/models')) os.mkdir(os.path.join(app.path, 'lib')) app.check() replaceAll(os.path.join(app.path, 'conf/application.conf'), r'%APPLICATION_NAME%', application_name) replaceAll(os.path.join(app.path, 'conf/application.conf'), r'%SECRET_KEY%', secretKey()) print "~"
clean命令很是簡單,就是刪除整個tmp文件夾
不少時候,咱們須要使用tomcat等服務器容器做爲服務載體,這時候就須要將play應用打包爲war
war的使用參數是play war project-name [-o/--output][filename] [--zip] [--exclude][exclude-directories]
使用-o或--output來指定輸出文件夾,使用--zip壓縮爲war格式,使用--exclude來包含另外須要打包的文件夾
要注意的是,必須在項目目錄外進行操做,否則會失敗
在參數處理完畢後,腳本正式開始打包過程,分爲2個步驟:1.預編譯。2:打包
預編譯即用到了precompile命令,precompile命令與run命令幾乎同樣,只是在java參數中加入了precompile=yes
,這裏要注意下,這裏加入的precompile值是yes,不是true,因此Play類中的usePrecompiled是false這裏搞錯了,Play類中的usePrecompiled檢查的參數是precompiled,而不是precompile
讓咱們來看一下加入了這個java參數對程序的影響。
與預編譯有關的代碼主要是下面2段:
static boolean preCompile() { if (usePrecompiled) { if (Play.getFile("precompiled").exists()) { classloader.getAllClasses(); Logger.info("Application is precompiled"); return true; } Logger.error("Precompiled classes are missing!!"); fatalServerErrorOccurred(); return false; } //這裏開始預編譯 try { Logger.info("Precompiling ..."); Thread.currentThread().setContextClassLoader(Play.classloader); long start = System.currentTimeMillis(); //getAllClasses方法較長,就不貼了,下面一段代碼在getAllClasses方法中進入 classloader.getAllClasses(); if (Logger.isTraceEnabled()) { Logger.trace("%sms to precompile the Java stuff", System.currentTimeMillis() - start); } if (!lazyLoadTemplates) { start = System.currentTimeMillis(); //編譯模板 TemplateLoader.getAllTemplate(); if (Logger.isTraceEnabled()) { Logger.trace("%sms to precompile the templates", System.currentTimeMillis() - start); } } return true; } catch (Throwable e) { Logger.error(e, "Cannot start in PROD mode with errors"); fatalServerErrorOccurred(); return false; } }
public byte[] enhance() { this.enhancedByteCode = this.javaByteCode; if (isClass()) { // before we can start enhancing this class we must make sure it is not a PlayPlugin. // PlayPlugins can be included as regular java files in a Play-application. // If a PlayPlugin is present in the application, it is loaded when other plugins are loaded. // All plugins must be loaded before we can start enhancing. // This is a problem when loading PlayPlugins bundled as regular app-class since it uses the same classloader // as the other (soon to be) enhanched play-app-classes. boolean shouldEnhance = true; try { CtClass ctClass = enhanceChecker_classPool.makeClass(new ByteArrayInputStream(this.enhancedByteCode)); if (ctClass.subclassOf(ctPlayPluginClass)) { shouldEnhance = false; } } catch( Exception e) { // nop } if (shouldEnhance) { Play.pluginCollection.enhance(this); } } //主要是這一段,他將加強處理後的字節碼寫入了文件,加強處理在以後會深刻展開 if (System.getProperty("precompile") != null) { try { // emit bytecode to standard class layout as well File f = Play.getFile("precompiled/java/" + (name.replace(".", "/")) + ".class"); f.getParentFile().mkdirs(); FileOutputStream fos = new FileOutputStream(f); fos.write(this.enhancedByteCode); fos.close(); } catch (Exception e) { e.printStackTrace(); } } return this.enhancedByteCode; }
預編譯過程結束後,腳本正式開始打包過程,打包過程比較簡單,就是講預編譯後的字節碼文件、模板文件、配置文件、使用的類庫、使用的模塊類庫等移動至WEB-INF文件夾中,若是使用了--zip,那麼腳本會將生成的文件夾用zip格式打包。
secret命令能生成一個新的secret key
status命令是用於實時顯示程序的運行狀態,腳本的運做十分簡單,步驟以下:
http://localhost:%http_port/@status
;若是沒有 --secret,則從配置文件中讀取secret key@status的實現和@kill同樣在CorePlugin類中,這在以後再進行詳解。
Play的啓動腳本分析至此就結束了,從腳本的分析過程當中咱們能夠稍微探究下Play在腳本啓動階段有何行爲,這對咱們進行腳本改造或者啓動優化仍是很是有幫助的。下一篇,咱們來看看Play的啓動類是如何運做的。。