Java Web App体系结构
我曾經(jīng)利用Servlet,JSP,JAX-RS,Spring框架,Play框架,帶有Facelets的JSF和一些Spark框架。 以我的拙見(jiàn),所有這些解決方案都遠(yuǎn)非面向?qū)ο蠛蛢?yōu)雅的。 它們都充滿了靜態(tài)方法,無(wú)法測(cè)試的數(shù)據(jù)結(jié)構(gòu)和骯臟的駭客。 因此,大約一個(gè)月前,我決定創(chuàng)建自己的Java Web框架。 我將一些基本原則納入其基礎(chǔ):1)沒(méi)有NULL,2)沒(méi)有公共靜態(tài)方法,3)沒(méi)有可變的類以及4)沒(méi)有類的轉(zhuǎn)換,反射和instanceof運(yùn)算符。 這四個(gè)基本原則應(yīng)保證干凈的代碼和透明的體系結(jié)構(gòu)。 這就是Takes框架的誕生方式。 讓我們看看創(chuàng)建了什么以及它如何工作。
教父的制作(1972),弗朗西斯·福特·科波拉
簡(jiǎn)而言之,Java Web體系結(jié)構(gòu)
簡(jiǎn)單來(lái)說(shuō),這就是我理解Web應(yīng)用程序體系結(jié)構(gòu)及其組件的方式。
首先,要?jiǎng)?chuàng)建Web服務(wù)器,我們應(yīng)該創(chuàng)建一個(gè)新的網(wǎng)絡(luò)套接字 ,該套接字在某個(gè)TCP端口上接受連接。 通常是80,但是我將使用8080進(jìn)行測(cè)試。 這是通過(guò)Java使用ServerSocket類完成的:
import java.net.ServerSocket; public class Foo {public static void main(final String... args) throws Exception {final ServerSocket server = new ServerSocket(8080);while (true);} }這足以啟動(dòng)Web服務(wù)器。 現(xiàn)在,套接字已準(zhǔn)備就緒并且正在偵聽(tīng)端口8080。當(dāng)有人在其瀏覽器中打開(kāi)http://localhost:8080時(shí),將建立連接,瀏覽器將永遠(yuǎn)旋轉(zhuǎn)其等待輪。 編譯此代碼段,然后嘗試。 我們只是構(gòu)建了一個(gè)簡(jiǎn)單的Web服務(wù)器,而沒(méi)有使用任何框架。 我們尚未對(duì)傳入的連接做任何事情,但是我們也不拒絕它們。 所有這些都在該server對(duì)象內(nèi)對(duì)齊。 它是在后臺(tái)線程中完成的。 這就是為什么我們需要將while(true)放在后面。 沒(méi)有這種無(wú)休止的暫停,該應(yīng)用程序?qū)⒘⒓赐瓿善鋱?zhí)行,并且服務(wù)器套接字將關(guān)閉。
下一步是接受傳入的連接。 在Java中,這是通過(guò)對(duì)accept()方法的阻塞調(diào)用來(lái)完成的:
final Socket socket = server.accept();該方法正在阻塞其線程,并等待新的連接到達(dá)。 一旦發(fā)生這種情況,它將返回Socket的實(shí)例。 為了接受下一個(gè)連接,我們應(yīng)該再次調(diào)用accept() 。 因此,基本上,我們的Web服務(wù)器應(yīng)該像這樣工作:
public class Foo {public static void main(final String... args) throws Exception {final ServerSocket server = new ServerSocket(8080);while (true) {final Socket socket = server.accept();// 1. Read HTTP request from the socket// 2. Prepare an HTTP response// 3. Send HTTP response to the socket// 4. Close the socket}} }這是一個(gè)無(wú)休止的循環(huán),接受一個(gè)新的連接,理解它,創(chuàng)建一個(gè)響應(yīng),返回響應(yīng),然后再次接受一個(gè)新的連接。 HTTP協(xié)議是無(wú)狀態(tài)的,這意味著服務(wù)器不應(yīng)記住任何先前連接中發(fā)生的情況。 它關(guān)心的只是此特定連接中的傳入HTTP請(qǐng)求。
HTTP請(qǐng)求來(lái)自套接字的輸入流,看起來(lái)像是多行文本塊。 如果讀取套接字的輸入流,將看到以下內(nèi)容:
final BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()) ); while (true) {final String line = reader.readLine();if (line.isEmpty()) {break;}System.out.println(line); }您將看到如下內(nèi)容:
GET / HTTP/1.1 Host: localhost:8080 Connection: keep-alive Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8,ru;q=0.6,uk;q=0.4客戶端(例如Google Chrome瀏覽器)將此文本傳遞到已建立的連接中。 它連接到localhost端口8080,一旦連接就緒,它將立即將文本發(fā)送到其中,然后等待響應(yīng)。
我們的工作是使用在請(qǐng)求中獲得的信息來(lái)創(chuàng)建HTTP響應(yīng)。 如果我們的服務(wù)器非常原始,那么我們基本上可以忽略請(qǐng)求中的所有信息,而只需返回“ Hello,world!”。 到所有請(qǐng)求(為簡(jiǎn)單起見(jiàn),我使用IOUtils ):
import java.net.Socket; import java.net.ServerSocket; import org.apache.commons.io.IOUtils; public class Foo {public static void main(final String... args) throws Exception {final ServerSocket server = new ServerSocket(8080);while (true) {try (final Socket socket = server.accept()) {IOUtils.copy(IOUtils.toInputStream("HTTP/1.1 200 OK\r\n\r\nHello, world!"),socket.getOutputStream());}}} }而已。 服務(wù)器已準(zhǔn)備就緒。 嘗試編譯并運(yùn)行它。 將瀏覽器指向http:// localhost:8080 ,您將看到Hello, world! :
$ javac -cp commons-io.jar Foo.java $ java -cp commons-io.jar:. Foo & $ curl http://localhost:8080 -v * Rebuilt URL to: http://localhost:8080/ * Connected to localhost (::1) port 8080 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.37.1 > Host: localhost:8080 > Accept: */* > < HTTP/1.1 200 OK * no chunk, no close, no size. Assume close to signal end < * Closing connection 0 Hello, world!這就是構(gòu)建Web服務(wù)器所需的全部。 現(xiàn)在讓我們討論如何使其面向?qū)ο蠛涂山M合。 讓我們嘗試看看Takes框架是如何構(gòu)建的。
路由/調(diào)度
最重要的步驟是確定誰(shuí)負(fù)責(zé)構(gòu)建HTTP響應(yīng)。 每個(gè)HTTP請(qǐng)求都具有1)查詢,2)方法和3)許多標(biāo)頭。 使用這三個(gè)參數(shù),我們需要實(shí)例化一個(gè)將為我們構(gòu)建響應(yīng)的對(duì)象。 在大多數(shù)Web框架中,此過(guò)程稱為請(qǐng)求分派或路由。 這是我們?cè)赥akes中的做法:
final Take take = takes.route(request); final Response response = take.act();基本上有兩個(gè)步驟。 第一個(gè)是創(chuàng)建的一個(gè)實(shí)例Take從takes ,而第二個(gè)是創(chuàng)建的實(shí)例Response從take 。 為什么這樣做呢? 主要是為了分開(kāi)職責(zé)。 實(shí)例Takes負(fù)責(zé)調(diào)度請(qǐng)求和實(shí)例右Take ,和實(shí)例Take負(fù)責(zé)創(chuàng)建響應(yīng)。
要在Takes中創(chuàng)建一個(gè)簡(jiǎn)單的應(yīng)用程序,您應(yīng)該創(chuàng)建兩個(gè)類。 首先,執(zhí)行Takes :
import org.takes.Request; import org.takes.Take; import org.takes.Takes; public final class TsFoo implements Takes {@Overridepublic Take route(final Request request) {return new TkFoo();} }我們分別為Takes和Take使用這些Ts和Tk前綴。 您應(yīng)該創(chuàng)建的第二個(gè)類是Take的實(shí)現(xiàn):
import org.takes.Take; import org.takes.Response; import org.takes.rs.RsText; public final class TkFoo implements Take {@Overridepublic Response act() {return new RsText("Hello, world!");} }現(xiàn)在是時(shí)候啟動(dòng)服務(wù)器了:
import org.takes.http.Exit; import org.takes.http.FtBasic; public class Foo {public static void main(final String... args) throws Exception {new FtBasic(new TsFoo(), 8080).start(Exit.NEVER);} }該FtBasic類執(zhí)行與上述完全相同的套接字操作。 它在端口8080上啟動(dòng)服務(wù)器套接字,并通過(guò)我們提供給其構(gòu)造函數(shù)的TsFoo實(shí)例調(diào)度所有傳入的連接。 它以無(wú)休止的周期進(jìn)行此分派,每秒檢查一次是否應(yīng)該使用Exit實(shí)例停止。 顯然, Exit.NEVER始終不會(huì)回答“請(qǐng)別停下來(lái)”。
HTTP請(qǐng)求
現(xiàn)在,讓我們看看到達(dá)TsFoo的HTTP請(qǐng)求中TsFoo什么以及我們可以從中獲得什么。 這是在Takes中定義Request接口的方式:
public interface Request {Iterable<String> head() throws IOException;InputStream body() throws IOException; }該請(qǐng)求分為兩部分:頭部和身體。 根據(jù)RFC 2616中的 HTTP規(guī)范,頭部包含開(kāi)始于正文的空行之前的所有行。 框架中有許多有用的裝飾器用于Request 。 例如, RqMethod將幫助您從標(biāo)題的第一行獲取方法名稱:
final String method = new RqMethod(request).method();RqHref將幫助提取查詢部分并進(jìn)行解析。 例如,這是請(qǐng)求:
GET /user?id=123 HTTP/1.1 Host: www.example.com此代碼將提取123 :
final int id = Integer.parseInt(new RqHref(request).href().param("id").get(0) );RqPrint可以將整個(gè)請(qǐng)求或其主體打印為String :
final String body = new RqPrint(request).printBody();這里的想法是使Request接口保持簡(jiǎn)單,并向其裝飾器提供此請(qǐng)求解析功能。 這種方法有助于框架使類保持較小且具有凝聚力。 每個(gè)裝飾器都非常小巧,堅(jiān)固,只能做一件事。 所有這些裝飾器都在org.takes.rq包中。 您可能已經(jīng)知道, Rq前綴代表Request 。
第一個(gè)Real Web App
讓我們創(chuàng)建第一個(gè)真正的Web應(yīng)用程序,它將做一些有用的事情。 我建議從Entry類開(kāi)始,這是Java從命令行啟動(dòng)應(yīng)用程序所必需的:
import org.takes.http.Exit; import org.takes.http.FtCLI; public final class Entry {public static void main(final String... args) throws Exception {new FtCLI(new TsApp(), args).start(Exit.NEVER);} }此類僅包含一個(gè)main()靜態(tài)方法,當(dāng)應(yīng)用程序從命令行啟動(dòng)時(shí),JVM將調(diào)用該方法。 如您所見(jiàn),它實(shí)例化FtCLI ,為它提供類TsApp和命令行參數(shù)的實(shí)例。 我們稍后將創(chuàng)建TsApp類。 FtCLI (轉(zhuǎn)換為“帶有命令行界面的前端”)創(chuàng)建相同F(xiàn)tBasic的實(shí)例,將其包裝到一些有用的修飾符中,并根據(jù)命令行參數(shù)進(jìn)行配置。 例如,-- --port=8080將轉(zhuǎn)換為8080端口號(hào),并作為FtBasic構(gòu)造函數(shù)的第二個(gè)參數(shù)傳遞。
該Web應(yīng)用程序本身稱為TsApp并擴(kuò)展了TsWrap :
import org.takes.Take; import org.takes.Takes; import org.takes.facets.fork.FkRegex; import org.takes.facets.fork.TsFork; import org.takes.ts.TsWrap; import org.takes.ts.TsClasspath; final class TsApp extends TsWrap {TsApp() {super(TsApp.make());}private static Takes make() {return new TsFork(new FkRegex("/robots.txt", ""),new FkRegex("/css/.*", new TsClasspath()),new FkRegex("/", new TkIndex()));} }我們將在稍后討論此TsFork課程。
如果您使用的是Maven,則應(yīng)以pom.xml開(kāi)頭:
<?xml version="1.0"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>foo</groupId><artifactId>foo</artifactId><version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>org.takes</groupId><artifactId>takes</artifactId><version>0.9</version> <!-- check the latest in Maven Central --></dependency></dependencies><build><finalName>foo</finalName><plugins><plugin><artifactId>maven-dependency-plugin</artifactId><executions><execution><goals><goal>copy-dependencies</goal></goals><configuration><outputDirectory>${project.build.directory}/deps</outputDirectory></configuration></execution></executions></plugin></plugins></build> </project>運(yùn)行mvn clean package應(yīng)該在target目錄中構(gòu)建foo.jar文件,并在target/deps構(gòu)建所有JAR依賴項(xiàng)的集合。 現(xiàn)在,您可以從命令行運(yùn)行該應(yīng)用程序:
$ mvn clean package $ java -Dfile.encoding=UTF-8 -cp ./target/foo.jar:./target/deps/* foo.Entry --port=8080該應(yīng)用程序已準(zhǔn)備就緒,您可以將其部署到Heroku。 只需在存儲(chǔ)庫(kù)的根目錄中創(chuàng)建一個(gè)Procfile文件,然后將存儲(chǔ)庫(kù)推送到Heroku。 這是Procfile樣子:
web: java -Dfile.encoding=UTF-8 -cp target/foo.jar:target/deps/* foo.Entry --port=${PORT}叉車
這個(gè)TsFork類似乎是框架的核心元素之一。 它有助于路線傳入的HTTP請(qǐng)求到右收 。 它的邏輯非常簡(jiǎn)單,并且里面只有幾行代碼。 它封裝了“ forks”的集合,它們是Fork<Take>接口的實(shí)例:
public interface Fork<T> {Iterator<T> route(Request req) throws IOException; }它唯一的route()方法要么返回一個(gè)空的迭代器,要么返回一個(gè)帶有單個(gè)Take的迭代器。 TsFork遍歷所有fork,調(diào)用它們的route()方法,直到其中一個(gè)返回take 。 一旦出現(xiàn)這種情況, TsFork返回此取給調(diào)用者,這是FtBasic 。
現(xiàn)在讓我們自己創(chuàng)建一個(gè)簡(jiǎn)單的fork。 例如,當(dāng)請(qǐng)求/status URL時(shí),我們想顯示應(yīng)用程序的/status 。 這是代碼:
final class TsApp extends TsWrap {private static Takes make() {return new TsFork(new Fork.AtTake() {@Overridepublic Iterator<Take> route(Request req) {final Collection<Take> takes = new ArrayList<>(1);if (new RqHref(req).href().path().equals("/status")) {takes.add(new TkStatus());}return takes.iterator();}});} }我相信這里的邏輯很明確。 我們要么返回一個(gè)空的迭代器,要么返回一個(gè)內(nèi)部帶有TkStatus實(shí)例的迭代器。 如果返回一個(gè)空的迭代器,則TsFork將嘗試在集合中找到另一個(gè)實(shí)際上獲取Take實(shí)例的fork,以產(chǎn)生Response 。 順便說(shuō)一句,如果未找到任何內(nèi)容,并且所有派生都返回空的迭代器,則TsFork將引發(fā)“找不到頁(yè)面”異常。
這種精確的邏輯由一個(gè)名為FkRegex即用即用的叉子FkRegex ,它嘗試將請(qǐng)求URI路徑與提供的正則表達(dá)式進(jìn)行匹配:
final class TsApp extends TsWrap {private static Takes make() {return new TsFork(new FkRegex("/status", new TkStatus()));} }我們可以組成TsFork類的多層結(jié)構(gòu)。 例如:
final class TsApp extends TsWrap {private static Takes make() {return new TsFork(new FkRegex("/status",new TsFork(new FkParams("f", "json", new TkStatusJSON()),new FkParams("f", "xml", new TkStatusXML()))));} }同樣,我認(rèn)為這很明顯。 實(shí)例FkRegex會(huì)問(wèn)的一個(gè)封裝實(shí)例TsFork返回一個(gè)take,它會(huì)嘗試從一個(gè)獲取它FkParams封裝。 如果HTTP查詢?yōu)?status?f=xml ,則將返回TkStatusXML的實(shí)例。
HTTP響應(yīng)
現(xiàn)在讓我們討論HTTP響應(yīng)的結(jié)構(gòu)及其面向?qū)ο蟮某橄驲esponse 。 界面外觀如下:
public interface Response {Iterable<String> head() throws IOException;InputStream body() throws IOException; }看起來(lái)非常類似于Request ,不是嗎? 好吧,它是相同的,主要是因?yàn)镠TTP請(qǐng)求和響應(yīng)的結(jié)構(gòu)幾乎相同。 唯一的區(qū)別是第一行。
有很多有用的裝飾器,可以幫助您建立響應(yīng)。 它們是可組合的 ,這使它們非常方便。 例如,如果要構(gòu)建一個(gè)包含HTML頁(yè)面的響應(yīng),則可以這樣編寫它們:
final class TkIndex implements Take {@Overridepublic Response act() {return new RsWithStatus(new RsWithType(new RsWithBody("<html>Hello, world!</html>"),"text/html"),200);} }在此示例中,裝飾器RsWithBody創(chuàng)建一個(gè)帶有主體但根本沒(méi)有標(biāo)題的響應(yīng)。 然后, RsWithType添加標(biāo)題Content-Type: text/html 。 然后, RsWithStatus確保響應(yīng)的第一行包含HTTP/1.1 200 OK 。
您可以創(chuàng)建自己的裝飾器,以重用現(xiàn)有的裝飾器。 看看RsPage在RsPage是如何完成的。
模板如何?
如我們所見(jiàn),返回簡(jiǎn)單的“ Hello,world”頁(yè)面不是什么大問(wèn)題。 但是,諸如HTML頁(yè)面,XML文檔,JSON數(shù)據(jù)集等更復(fù)雜的輸出呢? 有一些方便的Response裝飾器可以實(shí)現(xiàn)所有功能。 讓我們從簡(jiǎn)單的模板引擎Velocity開(kāi)始。 好吧,這不是那么簡(jiǎn)單。 它相當(dāng)強(qiáng)大,但是我建議僅在簡(jiǎn)單情況下使用它。 下面是它的工作原理:
final class TkIndex implements Take {@Overridepublic Response act() {return new RsVelocity("Hello, ${name}").with("name", "Jeffrey");} }RsVelocity構(gòu)造函數(shù)接受必須為Velocity模板的單個(gè)參數(shù)。 然后,調(diào)用with()方法,將數(shù)據(jù)注入Velocity上下文中。 當(dāng)需要呈現(xiàn)HTTP響應(yīng)時(shí), RsVelocity將根據(jù)配置的上下文“評(píng)估”模板。 同樣,我建議您僅對(duì)簡(jiǎn)單輸出使用此模板方法。
對(duì)于更復(fù)雜HTML文檔,我建議您將XML / XSLT與Xembly結(jié)合使用。 我在之前的幾篇文章中對(duì)此想法進(jìn)行了解釋: 瀏覽器和RESTful API 中的XML + XSLT,以及同一URL中的網(wǎng)站 。 它簡(jiǎn)單而強(qiáng)大-Java生成XML輸出,而XSLT處理器將其轉(zhuǎn)換為HTML文檔。 這就是我們將表示形式與數(shù)據(jù)分開(kāi)的方式。 就MVC而言,XSL樣式表是“視圖”, TkIndex是“控制器”。
我將很快寫另一篇關(guān)于Xembly和XSL模板的文章。
同時(shí),我們將在Takes中為JSF / Facelets和JSP渲染創(chuàng)建裝飾器。 如果您有興趣提供幫助,請(qǐng)分叉框架并提交請(qǐng)求。
持久性呢?
現(xiàn)在,出現(xiàn)的一個(gè)問(wèn)題是如何處理持久性實(shí)體,例如數(shù)據(jù)庫(kù),內(nèi)存結(jié)構(gòu),網(wǎng)絡(luò)連接等。我的建議是在Entry類內(nèi)部對(duì)其進(jìn)行初始化,并將其作為參數(shù)傳遞給TsApp構(gòu)造函數(shù)。 然后, TsApp將它們傳遞到構(gòu)造函數(shù)的定制需要 。
例如,我們有一個(gè)PostgreSQL數(shù)據(jù)庫(kù),其中包含一些需要渲染的表數(shù)據(jù)。 這是在Entry類中初始化與它的連接的方式(我使用的是BoneCP連接池):
public final class Entry {public static void main(final String... args) throws Exception {new FtCLI(new TsApp(Entry.postgres()), args).start(Exit.NEVER);}private static Source postgres() {final BoneCPDataSource src = new BoneCPDataSource();src.setDriverClass("org.postgresql.Driver");src.setJdbcUrl("jdbc:postgresql://localhost/db");src.setUser("root");src.setPassword("super-secret-password");return src;} }現(xiàn)在, TsApp的構(gòu)造TsApp必須接受類型為java.sql.Source的單個(gè)參數(shù):
final class TsApp extends TsWrap {TsApp(final Source source) {super(TsApp.make(source));}private static Takes make(final Source source) {return new TsFork(new FkRegex("/", new TkIndex(source)));} }TkIndex類還接受Source類的單個(gè)參數(shù)。 我相信您知道如何在TkIndex中使用它來(lái)獲取SQL表數(shù)據(jù)并將其轉(zhuǎn)換為HTML。 這里的要點(diǎn)是,在實(shí)例化依賴項(xiàng)時(shí),必須將其注入到應(yīng)用程序中(類TsApp的實(shí)例)。 這是一種純凈的依賴注入機(jī)制,它絕對(duì)沒(méi)有容器。 在“依賴注入容器是代碼污染者”中閱讀有關(guān)它的更多信息。
單元測(cè)試
由于每個(gè)類都是不可變的,并且所有依賴項(xiàng)僅通過(guò)構(gòu)造函數(shù)注入,因此單元測(cè)試非常容易。 假設(shè)我們要測(cè)試TkStatus ,它應(yīng)該返回HTML響應(yīng)(我正在使用JUnit 4和Hamcrest ):
import org.junit.Test; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; public final class TkIndexTest {@Testpublic void returnsHtmlPage() throws Exception {MatcherAssert.assertThat(new RsPrint(new TkStatus().act()).printBody(),Matchers.equalsTo("<html>Hello, world!</html>"));} }此外,我們可以開(kāi)始在測(cè)試HTTP服務(wù)器的整個(gè)應(yīng)用程序或任何個(gè)人起飛 ,并通過(guò)一個(gè)真實(shí)的TCP套接字測(cè)試它的行為; 例如(我正在使用jcabi-http發(fā)出HTTP請(qǐng)求并檢查輸出):
public final class TkIndexTest {@Testpublic void returnsHtmlPage() throws Exception {new FtRemote(new TsFixed(new TkIndex())).exec(new FtRemote.Script() {@Overridepublic void exec(final URI home) throws IOException {new JdkRequest(home).fetch().as(RestResponse.class).assertStatus(HttpURLConnection.HTTP_OK).assertBody(Matchers.containsString("Hello, world!"));}});} }FtRemote在隨機(jī)的TCP端口啟動(dòng)測(cè)試Web服務(wù)器,并在提供的FtRemote.Script實(shí)例上調(diào)用exec()方法。 此方法的第一個(gè)參數(shù)是剛啟動(dòng)的Web服務(wù)器主頁(yè)的URI。
Takes框架的體系結(jié)構(gòu)非常模塊化且可組合。 任何個(gè)體取可以進(jìn)行測(cè)試作為一個(gè)獨(dú)立的部件,絕對(duì)獨(dú)立于框架和其他需要 。
為什么叫名字?
這就是我經(jīng)常聽(tīng)到的問(wèn)題。 這個(gè)想法很簡(jiǎn)單,它起源于電影業(yè)。 當(dāng)影片制成,劇組芽許多需要以捕捉現(xiàn)實(shí),把它放在電影。 每次捕獲稱為一次獲取 。
換句話說(shuō), 拍攝就像現(xiàn)實(shí)的快照。
同樣適用于此框架。 Take每個(gè)實(shí)例在某個(gè)特定時(shí)刻代表一個(gè)現(xiàn)實(shí)。 然后,將該現(xiàn)實(shí)以Response的形式發(fā)送給用戶。
翻譯自: https://www.javacodegeeks.com/2015/04/java-web-app-architecture-in-takes-framework.html
總結(jié)
以上是生活随笔為你收集整理的Java Web App体系结构的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 小郡肝是什么 小郡肝的简介
- 下一篇: java自动gc_具有Java 7中自动