Play framework源碼解析 Part2:Server與ServletWrapper

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

在上一篇中咱們剖析了Play framework的啓動原理,很容易就能發現Play framework的啓動主入口在play.server.Server中,在本節,咱們來一塊兒看看Server類中主要發生了什麼。java

Server類

既然是程序運行的主入口,那麼必然是由main方法進入的,Server類中的main方法十分簡單。源碼以下:web

public static void main(String[] args) throws Exception {
        File root = new File(System.getProperty("application.path"));
        //獲取參數中的precompiled
        if (System.getProperty("precompiled", "false").equals("true")) {
            Play.usePrecompiled = true;
        }
        //獲取參數中的writepid
        if (System.getProperty("writepid", "false").equals("true")) {
            //這個方法的做用是檢查當前目錄下是否存在server.pid文件,若存在代表當前已有程序在運行
            writePID(root);
        }
        //Play類的初始化
        Play.init(root, System.getProperty("play.id", ""));
        if (System.getProperty("precompile") == null) {
            //Server類初始化
            new Server(args);
        } else {
            Logger.info("Done.");
        }
    }

main方法執行的操做很簡單:bootstrap

  1. 獲取程序路徑
  2. 檢查是否存在precompiled參數
  3. 檢查是否存在writepid參數,若存在則檢查是否存在server.pid文件,若存在則代表已有程序在運行,不存在則將當前程序pid寫入server.pid
  4. play類初始化
  5. 檢查是否存在precompile參數項,若存在表示是個預編譯行爲,結束運行,若沒有則啓動服務

這其中最重要的即是Play類的初始化以及Server類的初始化
這裏咱們先來看Server類的初始化過程,如今能夠先簡單的將Play類的初始化理解爲Play框架中一些常量的初始化以及日誌、配置文件、路由信息等配置的讀取。
這裏貼一下Server類的初始化過程:緩存

public Server(String[] args) {
        //設置文件編碼爲UTF-8
        System.setProperty("file.encoding", "utf-8");
        //p爲Play類初始化過程當中讀取的配置文件信息
        final Properties p = Play.configuration;
        //獲取參數中的http與https端口信息,若不存在則用配置文件中的http與https端口信息
        httpPort = Integer.parseInt(getOpt(args, "http.port", p.getProperty("http.port", "-1")));
        httpsPort = Integer.parseInt(getOpt(args, "https.port", p.getProperty("https.port", "-1")));
        //若沒有配置則設置默認端口爲9000
        if (httpPort == -1 && httpsPort == -1) {
            httpPort = 9000;
        }
        //http與https端口不能相同
        if (httpPort == httpsPort) {
            Logger.error("Could not bind on https and http on the same port " + httpPort);
            Play.fatalServerErrorOccurred();
        }

        InetAddress address = null;
        InetAddress secureAddress = null;
        try {
            //獲取配置文件中的默認http地址,若不存在則在系統參數中查找
            //以前仍是參數配置大於配置文件,這裏不知道爲何又變成了配置文件的優先級高於參數配置,很迷
            if (p.getProperty("http.address") != null) {
                address = InetAddress.getByName(p.getProperty("http.address"));
            } else if (System.getProperties().containsKey("http.address")) {
                address = InetAddress.getByName(System.getProperty("http.address"));
            }

        } catch (Exception e) {
            Logger.error(e, "Could not understand http.address");
            Play.fatalServerErrorOccurred();
        }
        try {
            //同上,獲取https地址
            if (p.getProperty("https.address") != null) {
                secureAddress = InetAddress.getByName(p.getProperty("https.address"));
            } else if (System.getProperties().containsKey("https.address")) {
                secureAddress = InetAddress.getByName(System.getProperty("https.address"));
            }
        } catch (Exception e) {
            Logger.error(e, "Could not understand https.address");
            Play.fatalServerErrorOccurred();
        }
        //netty服務器啓動類初始化,使用nio服務器,無限制線程池
        //這裏的線程池是netty的主線程池與工做線程池,是處理鏈接的線程池,而Play實際執行業務操做的線程池在另外一個地方配置
        ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(
                Executors.newCachedThreadPool(), Executors.newCachedThreadPool())
        );
        try {
            //初始化http端口
            if (httpPort != -1) {
                //設置管道工廠類
                bootstrap.setPipelineFactory(new HttpServerPipelineFactory());
                //綁定端口
                bootstrap.bind(new InetSocketAddress(address, httpPort));
                bootstrap.setOption("child.tcpNoDelay", true);

                if (Play.mode == Mode.DEV) {
                    if (address == null) {
                        Logger.info("Listening for HTTP on port %s (Waiting a first request to start) ...", httpPort);
                    } else {
                        Logger.info("Listening for HTTP at %2$s:%1$s (Waiting a first request to start) ...", httpPort, address);
                    }
                } else {
                    if (address == null) {
                        Logger.info("Listening for HTTP on port %s ...", httpPort);
                    } else {
                        Logger.info("Listening for HTTP at %2$s:%1$s  ...", httpPort, address);
                    }
                }

            }

        } catch (ChannelException e) {
            Logger.error("Could not bind on port " + httpPort, e);
            Play.fatalServerErrorOccurred();
        }
        //下面是https端口服務器的啓動過程,和http一致
        bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(
                Executors.newCachedThreadPool(), Executors.newCachedThreadPool())
        );

        try {
            if (httpsPort != -1) {
                //這裏的管道工廠類變成了SslHttpServerPipelineFactory
                bootstrap.setPipelineFactory(new SslHttpServerPipelineFactory());
                bootstrap.bind(new InetSocketAddress(secureAddress, httpsPort));
                bootstrap.setOption("child.tcpNoDelay", true);

                if (Play.mode == Mode.DEV) {
                    if (secureAddress == null) {
                        Logger.info("Listening for HTTPS on port %s (Waiting a first request to start) ...", httpsPort);
                    } else {
                        Logger.info("Listening for HTTPS at %2$s:%1$s (Waiting a first request to start) ...", httpsPort, secureAddress);
                    }
                } else {
                    if (secureAddress == null) {
                        Logger.info("Listening for HTTPS on port %s ...", httpsPort);
                    } else {
                        Logger.info("Listening for HTTPS at %2$s:%1$s  ...", httpsPort, secureAddress);
                    }
                }

            }

        } catch (ChannelException e) {
            Logger.error("Could not bind on port " + httpsPort, e);
            Play.fatalServerErrorOccurred();
        }
        if (Play.mode == Mode.DEV) {
           // print this line to STDOUT - not using logger, so auto test runner will not block if logger is misconfigured (see #1222)
           //輸出啓動成功,以便進行自動化測試
           System.out.println("~ Server is up and running");
        }
    }

server類的初始化沒什麼好說的,重點就在於那2個管道工廠類,HttpServerPipelineFactory與SslHttpServerPipelineFactory服務器

HttpServerPipelineFactory

HttpServerPipelineFactory類做用就是爲netty服務器增長了各個ChannelHandler。websocket

public class HttpServerPipelineFactory implements ChannelPipelineFactory {

    public ChannelPipeline getPipeline() throws Exception {

        Integer max = Integer.valueOf(Play.configuration.getProperty("play.netty.maxContentLength", "-1"));

        ChannelPipeline pipeline = pipeline();
        PlayHandler playHandler = new PlayHandler();

        pipeline.addLast("flashPolicy", new FlashPolicyHandler()); 
        pipeline.addLast("decoder", new HttpRequestDecoder());
        pipeline.addLast("aggregator", new StreamChunkAggregator(max));
        pipeline.addLast("encoder", new HttpResponseEncoder());
        pipeline.addLast("chunkedWriter", playHandler.chunkedWriteHandler);
        pipeline.addLast("handler", playHandler);

        return pipeline;
    }
}

pipeline依次添加了flash處理器,http request解碼器,流區塊聚合器,http response編碼器,區塊寫入器,play處理器
flash處理器和流區塊聚合器這裏暫且不做詳細解析,讀者能夠理解爲這2者的做用分別爲傳輸flash流和合並請求頭中有Transfer-Encoding:chunked的請求數據。
PlayHandler類是Play將netty接收到的request轉換爲play的request並真正開始業務處理的入口類,咱們先暫時放下PlayHandler,先來看看SslHttpServerPipelineFactory作了什麼session

SslHttpServerPipelineFactory

SslHttpServerPipelineFactory是https使用的管道工廠類,他除了添加了一下處理器外,最重要的是根據配置建立了https的鏈接方式。mvc

public ChannelPipeline getPipeline() throws Exception {

        Integer max = Integer.valueOf(Play.configuration.getProperty("play.netty.maxContentLength", "-1"));
        String mode = Play.configuration.getProperty("play.netty.clientAuth", "none");

        ChannelPipeline pipeline = pipeline();

        // Add SSL handler first to encrypt and decrypt everything.
        SSLEngine engine = SslHttpServerContextFactory.getServerContext().createSSLEngine();
        engine.setUseClientMode(false);

        if ("want".equalsIgnoreCase(mode)) {
            engine.setWantClientAuth(true);
        } else if ("need".equalsIgnoreCase(mode)) {
            engine.setNeedClientAuth(true);
        }

        engine.setEnableSessionCreation(true);

        pipeline.addLast("flashPolicy", new FlashPolicyHandler());
        pipeline.addLast("ssl", new SslHandler(engine));
        pipeline.addLast("decoder", new HttpRequestDecoder());
        pipeline.addLast("aggregator", new StreamChunkAggregator(max));
        pipeline.addLast("encoder", new HttpResponseEncoder());
        pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());

        pipeline.addLast("handler", new SslPlayHandler());

        return pipeline;
    }

在設置完SSLEngine後,SslHttpServerPipelineFactory在decoder處理器前加了一個SslHandler來處理鏈接,這裏不是使用playHandler做爲處理器,而是使用SslPlayHandler,SslPlayHandler是PlayHandler的一個子類,就是重寫了鏈接和出錯時的處理,這裏很少作闡述。app

PlayHandler

PlayHandler的做用是解析http request的類型,而後根據不一樣類型來進行不一樣的處理,因爲PlayHandler內處理的東西較多,這裏先附上一張大致流程圖以供讀者參考
PlayHandle流程圖框架

messageReceived

messageReceived是一個Override方法,做用就是在netty消息傳遞至該handle時進行相應操做,方法代碼以下

@Override
    public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent messageEvent) throws Exception {
        if (Logger.isTraceEnabled()) {
            Logger.trace("messageReceived: begin");
        }

        final Object msg = messageEvent.getMessage();

        //判斷是否爲HttpRequest的信息,不是則不進行處理
        if (msg instanceof HttpRequest) {

            final HttpRequest nettyRequest = (HttpRequest) msg;

            // 若爲websocket的初次握手請求,則進行websocket握手過程
            if (HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.UPGRADE))) {
                websocketHandshake(ctx, nettyRequest, messageEvent);
                return;
            }

            try {
                //將netty的request轉爲Play的request
                final Request request = parseRequest(ctx, nettyRequest, messageEvent);

                final Response response = new Response();
                //Response.current爲ThreadLocal<Response>類型,保證每一個線程擁有單獨Response
                Http.Response.current.set(response);

                // Buffered in memory output
                response.out = new ByteArrayOutputStream();

                // Direct output (will be set later)
                response.direct = null;

                // Streamed output (using response.writeChunk)
                response.onWriteChunk(new Action<Object>() {

                    public void invoke(Object result) {
                        writeChunk(request, response, ctx, nettyRequest, result);
                    }
                });

                /*
                    查找Play插件(即繼承了PlayPlugin抽象類的類)中是否有該request的實現方法,若存在,則用Play插件的實現
                    Play自帶的插件實現有:
                        CorePlugin實現了/@kill和/@status路徑的訪問流程
                        DBPlugin實現了/@db路徑的訪問流程
                        Evolutions實現了/@evolutions路徑的訪問流程
                    這裏將插件的訪問與其餘訪問分開最主要緣由應該是插件訪問不須要啓動Play,而其餘命令必須先調用Play.start來啓動
                */
                boolean raw = Play.pluginCollection.rawInvocation(request, response);
                if (raw) {
                    copyResponse(ctx, request, response, nettyRequest);
                } else {
                    /*
                        Invoker.invoke就是向Invoker類中的ScheduledThreadPoolExecutor提交任務並執行
                        使用ScheduledThreadPoolExecutor是由於能夠定時,便於執行異步任務
                        Invoker類能夠理解爲是各個不一樣容器任務轉向Play任務的一個轉換類,他下面有2個靜態內部類,分別爲InvocationContext和Invocation
                        InvocationContext內存放了當前request映射的controller類的annotation
                        Invocation則爲Play任務的基類,他的實現類有:
                            Job:Play的異步任務
                            NettyInvocation:netty請求的實現類
                            ServletInvocation:Servlet容器中的實現類
                            WebSocketInvocation:websocket的實現類
                    */
                    Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, messageEvent));
                }

            } catch (Exception ex) {
                //處理服務器錯誤
                serve500(ex, ctx, nettyRequest);
            }
        }

        // 若是msg是websocket,則進行websocket接收
        if (msg instanceof WebSocketFrame) {
            WebSocketFrame frame = (WebSocketFrame) msg;
            websocketFrameReceived(ctx, frame);
        }

        if (Logger.isTraceEnabled()) {
            Logger.trace("messageReceived: end");
        }
    }

能夠很清楚的發現PlayHandler處理請求就是依靠了各個不一樣的Invocation實現來完成,那咱們就來看看Invocation他是如何一步步處理請求的。

Invocation

Invocation是一個抽象類,繼承了Runnable接口,它的run方法是這個類的核心。

public void run() {
        //waitInQueue是一個監視器,監視了這個invocation在隊列中等待的時間
        if (waitInQueue != null) {
            waitInQueue.stop();
        }
        try {
            //預初始化,是對語言文件的清空
            preInit();
            //初始化
            if (init()) {
                //運行執行前插件任務
                before();
                //處理request
                execute();
                //運行執行後插件任務
                after();
                //成功執行後的處理
                onSuccess();
            }
        } catch (Suspend e) {
            //Suspend類讓請求暫停
            suspend(e);
            after();
        } catch (Throwable e) {
            onException(e);
        } finally {
            _finally();
        }
    }

這裏面的preInit()、before()、after()是沒有實現類的,用的就是Invocation類中的方法。咱們先來看看Invocation類中對這些方法的實現,而後再來看看他的實現類對這些方法的修改

  • init
public boolean init() {
        //設置當前線程的classloader
        Thread.currentThread().setContextClassLoader(Play.classloader);
        //檢查是否爲dev模式,是則檢查文件是否修改,以進行熱加載
        Play.detectChanges();
        //若Play未啓動,啓動
        if (!Play.started) {
            if (Play.mode == Mode.PROD) {
                throw new UnexpectedException("Application is not started");
            }
            Play.start();
        }
        //InvocationContext中添加當前映射方法的annotation
        InvocationContext.current.set(getInvocationContext());
        return true;
    }

Play.start能夠先理解爲啓動Play服務,具體展開將在以後mvc章節詳解

  • execute

execute在Invocation類中是一個抽象方法,須要實現類完成本身的實現

  • onSuccess
public void onSuccess() throws Exception {
        Play.pluginCollection.onInvocationSuccess();
    }

執行play插件中的執行成功方法

  • onException
public void onException(Throwable e) {
        Play.pluginCollection.onInvocationException(e);
        if (e instanceof PlayException) {
            throw (PlayException) e;
        }
        throw new UnexpectedException(e);
    }

執行play插件中的執行異常方法,而後拋出異常

  • suspend
public void suspend(Suspend suspendRequest) {
        if (suspendRequest.task != null) {
            WaitForTasksCompletion.waitFor(suspendRequest.task, this);
        } else {
            Invoker.invoke(this, suspendRequest.timeout);
        }
    }

若暫停的請求有任務,那麼調用WaitForTasksCompletion.waitFor進行等待,直到任務完成再繼續運行請求
若沒有任務,那就將請求經過ScheduledThreadPoolExecutor.schedule延時一段時間後執行

  • _finally
public void _finally() {
        Play.pluginCollection.invocationFinally();
        InvocationContext.current.remove();
    }

執行play插件中的invocationFinally方法

NettyInvocation

NettyInvocation是netty請求使用的實現類,咱們來依次看看他對init,execute,onSuccess等方法的實現上相比Invocation改變了什麼

  • run
@Override
    public void run() {
        try {
            if (Logger.isTraceEnabled()) {
                Logger.trace("run: begin");
            }
            super.run();
        } catch (Exception e) {
            serve500(e, ctx, nettyRequest);
        }
        if (Logger.isTraceEnabled()) {
            Logger.trace("run: end");
        }
    }

NettyInvocation的run方法和基類惟一的差異就是包了一層try catch來處理服務器錯誤,這裏有一點要注意,能夠發現,NettyInvocation的run有一層try catch來處理500錯誤,PlayHandle中的messageReceived也有一層try catch來處理500錯誤,個人理解是前者是用來處理業務流程中的錯誤的,後者是爲了防止意外錯誤引起整個服務器掛掉因此又套了一層作保險

  • init
@Override
    public boolean init() {
        //設置當前線程的classloader
        Thread.currentThread().setContextClassLoader(Play.classloader);
        if (Logger.isTraceEnabled()) {
            Logger.trace("init: begin");
        }
        //設置當前線程的request和response
        Request.current.set(request);
        Response.current.set(response);
        try {
            //若是是dev模式檢查路徑更新
            if (Play.mode == Play.Mode.DEV) {
                Router.detectChanges(Play.ctxPath);
            }
            //若是是prod模式且靜態路徑緩存有當前請求信息,則從緩存中獲取
            if (Play.mode == Play.Mode.PROD && staticPathsCache.containsKey(request.domain + " " + request.method + " " + request.path)) {
                RenderStatic rs = null;
                synchronized (staticPathsCache) {
                    rs = staticPathsCache.get(request.domain + " " + request.method + " " + request.path);
                }
                //使用靜態資源
                serveStatic(rs, ctx, request, response, nettyRequest, event);
                if (Logger.isTraceEnabled()) {
                    Logger.trace("init: end false");
                }
                return false;
            }
            //檢查是否爲靜態資源
            Router.routeOnlyStatic(request);
            super.init();
        } catch (NotFound nf) {
            //返回404
            serve404(nf, ctx, request, nettyRequest);
            if (Logger.isTraceEnabled()) {
                Logger.trace("init: end false");
            }
            return false;
        } catch (RenderStatic rs) {
            //使用靜態資源
            if (Play.mode == Play.Mode.PROD) {
                synchronized (staticPathsCache) {
                    staticPathsCache.put(request.domain + " " + request.method + " " + request.path, rs);
                }
            }
            serveStatic(rs, ctx, request, response, nettyRequest, this.event);
            if (Logger.isTraceEnabled()) {
                Logger.trace("init: end false");
            }
            return false;
        }

        if (Logger.isTraceEnabled()) {
            Logger.trace("init: end true");
        }
        return true;
    }

能夠看出,NettyInvocation的初始化就是在基類初始化以前判斷request的路徑,處理靜態資源以及處理路徑不存在的資源,這裏也就說明了訪問路由設置中的靜態資源是不須要啓動play服務的,是否意味着能夠經過play搭建一個靜態資源服務器?

  • execute
@Override
    public void execute() throws Exception {
        //檢查鏈接是否中斷
        if (!ctx.getChannel().isConnected()) {
            try {
                ctx.getChannel().close();
            } catch (Throwable e) {
                // Ignore
            }
            return;
        }
        //渲染以前檢查長度
        saveExceededSizeError(nettyRequest, request, response);
        //運行
        ActionInvoker.invoke(request, response);
    }

ActionInvoker.invoke是play mvc的執行入口方法,這個在以後會進行詳解

  • success
@Override
    public void onSuccess() throws Exception {
        super.onSuccess();
        if (response.chunked) {
            closeChunked(request, response, ctx, nettyRequest);
        } else {
            copyResponse(ctx, request, response, nettyRequest);
        }
        if (Logger.isTraceEnabled()) {
            Logger.trace("execute: end");
        }
    }

成功以後就判斷是不是chunked類型的request,如果則關閉區塊流並返回response,若不是則返回正常的response

Job

Job是Play任務的基類,用來處理異步任務,Job雖然繼承了Invocation,但他並不會加入至Play的主線程池執行,Job有本身單獨的線程池進行處理。由於Job可能須要一個返回值,因此它同時繼承了Callable接口來提供返回值。
既然job能提供返回值,那真正起做用的方法就是call()而不是run(),咱們來看一下他的call方法

public V call() {
        Monitor monitor = null;
        try {
            if (init()) {
                before();
                V result = null;

                try {
                    lastException = null;
                    lastRun = System.currentTimeMillis();
                    monitor = MonitorFactory.start(getClass().getName()+".doJob()");
                    //執行任務並返回結果
                    result = doJobWithResult();
                    monitor.stop();
                    monitor = null;
                    wasError = false;
                } catch (PlayException e) {
                    throw e;
                } catch (Exception e) {
                    StackTraceElement element = PlayException.getInterestingStrackTraceElement(e);
                    if (element != null) {
                        throw new JavaExecutionException(Play.classes.getApplicationClass(element.getClassName()), element.getLineNumber(), e);
                    }
                    throw e;
                }
                after();
                return result;
            }
        } catch (Throwable e) {
            onException(e);
        } finally {
            if(monitor != null) {
                monitor.stop();
            }
            _finally();
        }
        return null;
    }

能夠看出,job的call方法和Invocation的run方法其實差很少,中間執行job的代碼其實就是execute方法的實現,可是有一點不一樣的是,job方法完成後不會調用onSuccess()方法。
這裏只是提一下Job與Invocation類的關係,job任務的處理放在以後的插件篇再談

WebSocketInvocation

WebSocketInvocation是websocket請求的實現類,咱們來具體看下他對Invocation的方法作了怎麼樣的複寫

  • init
@Override
    public boolean init() {
        Http.Request.current.set(request);
        Http.Inbound.current.set(inbound);
        Http.Outbound.current.set(outbound);
        return super.init();
    }

沒什麼大的變更,就是在當前線程下添加了出入站的通道信息

  • execute
@Override
    public void execute() throws Exception {
        WebSocketInvoker.invoke(request, inbound, outbound);
    }

execute方法就是將request請求,出入站通道傳入WebSocketInvoker.invoke進行處理,WebSocketInvoker.invoke的代碼以下,能夠看出WebSocketInvoker.invoke方法也就是調用了ActionInvoker.invoke來對請求進行處理。

public static void invoke(Http.Request request, Http.Inbound inbound, Http.Outbound outbound) {
        try {
            if (Play.mode == Play.Mode.DEV) {
                WebSocketController.class.getDeclaredField("inbound").set(null, Http.Inbound.current());
                WebSocketController.class.getDeclaredField("outbound").set(null, Http.Outbound.current());
                WebSocketController.class.getDeclaredField("params").set(null, Scope.Params.current());
                WebSocketController.class.getDeclaredField("request").set(null, Http.Request.current());
                WebSocketController.class.getDeclaredField("session").set(null, Scope.Session.current());
                WebSocketController.class.getDeclaredField("validation").set(null, Validation.current());
            }
            //websocket是沒有response的
            ActionInvoker.invoke(request, null);
        }catch (PlayException e) {
            throw e;
        } catch (Exception e) {
            throw new UnexpectedException(e);
        }
    }
  • onSuccess
@Override
    public void onSuccess() throws Exception {
        outbound.close();
        super.onSuccess();
    }

websocket只有當鏈接關閉時纔會觸發onSuccess方法,因此onSuccess相比Invocation也就多了一個關閉出站通道的操做

ServletInvocation

ServletInvocation是當使用Servlet容器時的實現類,也就是將程序用war打包後放在Servlet容器後運行的類,ServletInvocation不直接基礎於Invocation,並且繼承於DirectInvocation,DirectInvocation繼承了Invocation,DirectInvocation類添加了一個Suspend字段,用來處理線程暫停或等待操做。
由於在Servlet規範中請求線程的管理交由Servlet容器處理,因此ServletInvocation不是使用Invoker的ScheduledThreadPoolExecutor來執行的,那麼在配置文件中設置play.pool數量對於使用Servlet容器的程序是無效的。
既然ServletInvocation不是使用Invoker中的線程池運行的,那他是從什麼地方初始化這個類並運行的呢,這點將在本篇的ServletWrapper中再詳解

咱們來看看ServletInvocation對Invocation中的方法進行了怎樣的覆寫

  • init
@Override
    public boolean init() {
        try {
            return super.init();
        } catch (NotFound e) {
            serve404(httpServletRequest, httpServletResponse, e);
            return false;
        } catch (RenderStatic r) {
            try {
                serveStatic(httpServletResponse, httpServletRequest, r);
            } catch (IOException e) {
                throw new UnexpectedException(e);
            }
            return false;
        }
    }

很簡單的初始化,和NettyInvocation相比就是靜態資源的緩存交由Servlet容器處理

  • run
@Override
    public void run() {
        try {
            super.run();
        } catch (Exception e) {
            serve500(e, httpServletRequest, httpServletResponse);
            return;
        }
    }

與NettyInvocation同樣

  • execute
@Override
    public void execute() throws Exception {
        ActionInvoker.invoke(request, response);
        copyResponse(request, response, httpServletRequest, httpServletResponse);
    }

ServletInvocation在execute結束後就將結果返回,這裏不知道爲何不和NettyInvocation統一一下,都在execute階段返回結果或者都在onSuccess階段返回結果

使用netty的流程咱們已經理清楚了,簡單來講就是講netty的請求處理後交由ActionInvoker.invoke來執行,ActionInvoker.invoke也是以後要研究的重點。

ServletWrapper

這篇的一開始咱們以Server類爲入口闡述了使用java命令直接運行時的運行過程,那麼若是程序用war打包後放在Servlet容器中是如何運行的呢?
咱們能夠打開play程序包下resources/war的web.xml來看看默認的web.xml中是怎麼配置的。

<listener>
      <listener-class>play.server.ServletWrapper</listener-class>
  </listener>
  
  <servlet>
    <servlet-name>play</servlet-name>
    <servlet-class>play.server.ServletWrapper</servlet-class>    
  </servlet>

能夠看出監聽器和servlet入口都是ServletWrapper,那咱們從程序的建立開始一點點剖析play的運行過程

contextInitialized

contextInitialized是在啓動時自動加載,主要完成一些初始化的工做,代碼以下

public void contextInitialized(ServletContextEvent e) {
        //standalonePlayServer表示這是不是一個獨立服務器
        Play.standalonePlayServer = false;

        ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        //這是程序根目錄
        String appDir = e.getServletContext().getRealPath("/WEB-INF/application");
        File root = new File(appDir);

        //play id在web.xml中配置
        final String playId = System.getProperty("play.id", e.getServletContext().getInitParameter("play.id"));
        if (StringUtils.isEmpty(playId)) {
            throw new UnexpectedException("Please define a play.id parameter in your web.xml file. Without that parameter, play! cannot start your application. Please add a context-param into the WEB-INF/web.xml file.");
        }

        Play.frameworkPath = root.getParentFile();
        //使用預編譯的文件
        Play.usePrecompiled = true;
        //初始化,初始化過程當中會將運行模式強制改成prod
        Play.init(root, playId);
        //檢查配置文件中模式是否是dev,是dev提示自動切換爲prod
        Play.Mode mode = Play.Mode.valueOf(Play.configuration.getProperty("application.mode", "DEV").toUpperCase());
        if (mode.isDev()) {
            Logger.info("Forcing PROD mode because deploying as a war file.");
        }

        // Servlet 2.4手動加載路徑
        // Servlet 2.4 does not allow you to get the context path from the servletcontext...
        if (isGreaterThan(e.getServletContext(), 2, 4)) {
            loadRouter(e.getServletContext().getContextPath());
        }

        Thread.currentThread().setContextClassLoader(oldClassLoader);
    }

初始化過程有2個地方須要注意

  1. 無論配置文件中使用的是什麼模式,放在Servlet容器中會統一改成prod
  2. 默認使用預編譯模式加載

service

service是servlet處理request的方法,咱們先來看一下他的實現代碼

@Override
    protected void service(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
        //路由還沒初始化時初始化路由
        if (!routerInitializedWithContext) {
            loadRouter(httpServletRequest.getContextPath());
        }

        if (Logger.isTraceEnabled()) {
            Logger.trace("ServletWrapper>service " + httpServletRequest.getRequestURI());
        }

        Request request = null;
        try {
            Response response = new Response();
            response.out = new ByteArrayOutputStream();
            Response.current.set(response);
            //httpServletRequest轉爲play的request
            request = parseRequest(httpServletRequest);

            if (Logger.isTraceEnabled()) {
                Logger.trace("ServletWrapper>service, request: " + request);
            }
            //檢查插件中是否有實現
            boolean raw = Play.pluginCollection.rawInvocation(request, response);
            if (raw) {
                copyResponse(Request.current(), Response.current(), httpServletRequest, httpServletResponse);
            } else {
                //這裏不是Invoker.invoke()方法,是invokeInThread
                Invoker.invokeInThread(new ServletInvocation(request, response, httpServletRequest, httpServletResponse));
            }
        } catch (NotFound e) {
            //處理404
            if (Logger.isTraceEnabled()) {
                Logger.trace("ServletWrapper>service, NotFound: " + e);
            }
            serve404(httpServletRequest, httpServletResponse, e);
            return;
        } catch (RenderStatic e) {
            //處理靜態資源
            if (Logger.isTraceEnabled()) {
                Logger.trace("ServletWrapper>service, RenderStatic: " + e);
            }
            serveStatic(httpServletResponse, httpServletRequest, e);
            return;
        } catch(URISyntaxException e) {
            //處理404
            serve404(httpServletRequest, httpServletResponse, new NotFound(e.toString()));
            return;
        } catch (Throwable e) {
            throw new ServletException(e);
        } finally {
            //由於servlet的線程會複用,因此要手動刪除當前線程內的值
            Request.current.remove();
            Response.current.remove();
            Scope.Session.current.remove();
            Scope.Params.current.remove();
            Scope.Flash.current.remove();
            Scope.RenderArgs.current.remove();
            Scope.RouteArgs.current.remove();
            CachedBoundActionMethodArgs.clear();
        }
    }

看的出和PlayHandle其實就是一個處理方式,先轉爲play的request再扔到Invoker類中進行處理,那麼咱們來看看Invoker.invokeInThread的具體實現過程

public static void invokeInThread(DirectInvocation invocation) {
        boolean retry = true;
        while (retry) {
            invocation.run();
            if (invocation.retry == null) {
                retry = false;
            } else {
                try {
                    if (invocation.retry.task != null) {
                        invocation.retry.task.get();
                    } else {
                        Thread.sleep(invocation.retry.timeout);
                    }
                } catch (Exception e) {
                    throw new UnexpectedException(e);
                }
                retry = true;
            }
        }
    }

invokeInThread就是在當前線程下運行invocation,若是遇到暫停或者等待任務完成就循環直到完成。
要注意的一點是,Play 1.2.6未完成servlet模式下的websocket實現,因此若是要用websocket請用netty模式

總結

至此,play的2種正常運行的啓動邏輯已經分析完畢,play的啓動過程仍是在與不一樣的服務器運行容器打交道,如何將轉化後的請求交由ActionInvoker來處理。下一篇,咱們先不看ActionInvoker的具體實現,先看看play對配置文件及路由的分析處理過程。

相關文章
相關標籤/搜索