app访问java web
我曾经利用Servlet,JSP,JAX-RS,Spring框架,Play框架,带有Facelets的JSF和一些Spark框架。 以我的拙见,所有这些解决方案都远非面向对象和优雅的。 它们都充满了静态方法,不可测试的数据结构和肮脏的骇客。 因此,大约一个月前,我决定创建自己的Java Web框架。 我将一些基本原则纳入其基础:1)没有NULL,2)没有公共静态方法,3)没有可变的类以及4)没有类的转换,反射和instanceof运算符。 这四个基本原则应保证干净的代码和透明的体系结构。 这就是Takes框架的诞生方式。 让我们看看创建了什么以及它如何工作。
教父的制作(1972),弗朗西斯·福特·科波拉
简而言之,Java Web体系结构
简单来说,这就是我理解Web应用程序体系结构及其组件的方式。
首先,要创建Web服务器,我们应该创建一个新的网络套接字 ,该套接字在某个TCP端口上接受连接。 通常是80,但是我将使用8080进行测试。 这是在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);
}
}
这足以启动Web服务器。 现在,套接字已准备就绪,正在侦听8080端口。当有人在其浏览器中打开http://localhost:8080时,将建立连接,浏览器将永远旋转其等待轮。 编译此代码段,然后尝试。 我们只是构建了一个简单的Web服务器,而没有使用任何框架。 我们尚未对传入的连接做任何事情,但是我们也不拒绝它们。 所有这些都在该server对象内对齐。 它是在后台线程中完成的。 这就是为什么我们需要将while(true)放在后面。 没有这种无休止的暂停,应用程序将立即完成执行,服务器套接字将关闭。
下一步是接受传入的连接。 在Java中,这是通过对accept()方法的阻塞调用来完成的:
final Socket socket = server.accept();
该方法正在阻塞其线程,并等待新的连接到达。 一旦发生这种情况,它将返回Socket的实例。 为了接受下一个连接,我们应该再次调用accept() 。 因此,基本上,我们的Web服务器应该像这样工作:
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
}
}
}
这是一个无休止的循环,接受一个新的连接,理解它,创建一个响应,返回响应,然后再次接受一个新的连接。 HTTP协议是无状态的,这意味着服务器不应记住任何先前连接中发生的情况。 它关心的只是此特定连接中的传入HTTP请求。
HTTP请求来自套接字的输入流,看起来像多行文本块。 如果读取套接字的输入流,将看到以下内容:
final BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
while (true) {
final String line = reader.readLine();
if (line.isEmpty()) {
break;
}
System.out.println(line);
}
您将看到如下内容:
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,一旦连接就绪,它将立即将文本发送到其中,然后等待响应。
我们的工作是使用在请求中获得的信息来创建HTTP响应。 如果我们的服务器非常原始,那么我们基本上可以忽略请求中的所有信息,而只需返回“ Hello,world!”。 到所有请求(为简单起见,我使用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()
);
}
}
}
}
而已。 服务器已准备就绪。 尝试编译并运行它。 将浏览器指向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!
这就是构建Web服务器所需的全部。 现在让我们讨论如何使其面向对象和可组合。 让我们尝试看看Takes框架是如何构建的。
路由/调度
最重要的步骤是确定谁负责构建HTTP响应。 每个HTTP请求都有1)查询,2)方法和3)多个标头。 使用这三个参数,我们需要实例化一个将为我们建立响应的对象。 在大多数Web框架中,此过程称为请求分派或路由。 这是我们在Takes中的做法:
final Take take = takes.route(request);
final Response response = take.act();
基本上有两个步骤。 第一个是创建的一个实例Take从takes ,而第二个是创建的实例Response从take 。 为什么这样做呢? 主要是为了分开职责。 实例Takes负责调度请求和实例右Take ,和实例Take负责创建响应。
要在Takes中创建一个简单的应用程序,您应该创建两个类。 首先,执行Takes :
import org.takes.Request;
import org.takes.Take;
import org.takes.Takes;
public final class TsFoo implements Takes {
@Override
public Take route(final Request request) {
return new TkFoo();
}
}
我们分别为Takes和Take使用这些Ts和Tk前缀。 您应该创建的第二个类是Take的实现:
import org.takes.Take;
import org.takes.Response;
import org.takes.rs.RsText;
public final class TkFoo implements Take {
@Override
public Response act() {
return new RsText("Hello, world!");
}
}
现在是时候启动服务器了:
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类执行与上述完全相同的套接字操作。 它在端口8080上启动服务器套接字,并通过我们提供给其构造函数的TsFoo实例调度所有传入的连接。 它以无休止的周期进行此调度,每秒检查一次是否应该使用Exit实例停止。 显然, Exit.NEVER永远不会回答“请别停下来”。
HTTP请求
现在,让我们看看到达TsFoo的HTTP请求中TsFoo什么以及我们可以从中获得什么。 这是在Takes中定义Request接口的方式:
public interface Request {
Iterable<String> head() throws IOException;
InputStream body() throws IOException;
}
该请求分为两部分:头部和身体。 根据RFC 2616中的 HTTP规范,头部包含开始于正文的空行之前的所有行。 框架中有许多有用的装饰器用于Request 。 例如, RqMethod将帮助您从标题的第一行获取方法名称:
final String method = new RqMethod(request).method();
RqHref将帮助提取查询部分并进行解析。 例如,这是请求:
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可以将整个请求或其主体打印为String :
final String body = new RqPrint(request).printBody();
这里的想法是使Request接口保持简单,并向其装饰器提供此请求解析功能。 这种方法有助于框架使类保持较小且具有凝聚力。 每个装饰器都非常小巧,坚固,只能做一件事。 所有这些装饰器都在org.takes.rq包中。 您可能已经知道, Rq前缀代表Request 。
第一个Real Web App
让我们创建第一个真正的Web应用程序,它将做一些有用的事情。 我建议从Entry类开始,这是Java从命令行启动应用程序所必需的:
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);
}
}
此类仅包含一个main()静态方法,当应用程序从命令行启动时,JVM将调用该方法。 如您所见,它将实例化FtCLI ,为其提供类TsApp和命令行参数的实例。 我们稍后将创建TsApp类。 FtCLI (转换为“带有命令行界面的前端”)创建相同FtBasic的实例,将其包装到一些有用的修饰符中,并根据命令行参数进行配置。 例如,-- --port=8080将转换为8080端口号,并作为FtBasic构造函数的第二个参数传递。
该Web应用程序本身称为TsApp并扩展了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,则应使用pom.xml开头:
<?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>
运行mvn clean package应该在target目录中构建foo.jar文件,并在target/deps构建所有JAR依赖项的集合。 现在,您可以从命令行运行该应用程序:
$ mvn clean package
$ java -Dfile.encoding=UTF-8 -cp ./target/foo.jar:./target/deps/* foo.Entry --port=8080
该应用程序已准备就绪,您可以将其部署到Heroku。 只需在存储库的根目录中创建一个Procfile文件,然后将存储库推送到Heroku。 这是Procfile外观:
web: java -Dfile.encoding=UTF-8 -cp target/foo.jar:target/deps/* foo.Entry --port=${PORT}
叉车
这个TsFork类似乎是框架的核心元素之一。 它有助于路线传入的HTTP请求到右收 。 它的逻辑非常简单,里面只有几行代码。 它封装了“ forks”的集合,它们是Fork<Take>接口的实例:
public interface Fork<T> {
Iterator<T> route(Request req) throws IOException;
}
它唯一的route()方法要么返回一个空的迭代器,要么返回一个具有单个Take的迭代器。 TsFork遍历所有fork,调用它们的route()方法,直到其中一个返回take 。 一旦出现这种情况, TsFork返回此取给调用者,这是FtBasic 。
现在让我们自己创建一个简单的fork。 例如,我们要在请求/status URL时显示应用程序的/status 。 这是代码:
final class TsApp extends TsWrap {
private static Takes make() {
return new TsFork(
new Fork.AtTake() {
@Override
public 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();
}
}
);
}
}
我相信这里的逻辑很明确。 我们要么返回一个空的迭代器,要么返回一个内部带有TkStatus实例的迭代器。 如果返回一个空的迭代器, TsFork将尝试在集合中找到另一个实际上获取Take实例的fork,以产生Response 。 顺便说一句,如果未找到任何内容,并且所有派生都返回空的迭代器,则TsFork将抛出“找不到页面”异常。
这种确切的逻辑由一个名为FkRegex即用即用的叉子FkRegex ,它尝试将请求URI路径与提供的正则表达式进行匹配:
final class TsApp extends TsWrap {
private static Takes make() {
return new TsFork(
new FkRegex("/status", new TkStatus())
);
}
}
我们可以组成TsFork类的多层结构。 例如:
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())
)
)
);
}
}
同样,我认为这很明显。 实例FkRegex会问的一个封装实例TsFork返回一个take,它会尝试从一个获取它FkParams封装。 如果HTTP查询为/status?f=xml ,则将返回TkStatusXML的实例。
HTTP响应
现在让我们讨论HTTP响应的结构及其面向对象的抽象Response 。 界面外观如下:
public interface Response {
Iterable<String> head() throws IOException;
InputStream body() throws IOException;
}
看起来非常类似于Request ,不是吗? 好吧,它是相同的,主要是因为HTTP请求和响应的结构几乎相同。 唯一的区别是第一行。
有很多有用的装饰器,可以帮助您建立响应。 它们是可组合的 ,这使它们非常方便。 例如,如果要构建一个包含HTML页面的响应,则可以这样编写它们:
final class TkIndex implements Take {
@Override
public Response act() {
return new RsWithStatus(
new RsWithType(
new RsWithBody("<html>Hello, world!</html>"),
"text/html"
),
200
);
}
}
在此示例中,装饰器RsWithBody创建一个带有主体但没有头的响应。 然后, RsWithType添加标题Content-Type: text/html 。 然后, RsWithStatus确保响应的第一行包含HTTP/1.1 200 OK 。
您可以创建自己的装饰器,以重用现有的装饰器。 看看RsPage在RsPage是如何完成的。
模板如何?
如我们所见,返回简单的“ Hello,world”页面不是什么大问题。 但是,诸如HTML页面,XML文档,JSON数据集等更复杂的输出呢? 有一些方便的Response装饰器可以实现所有这些功能。 让我们从简单的模板引擎Velocity开始。 好吧,这不是那么简单。 它相当强大,但是我建议仅在简单情况下使用它。 下面是它的工作原理:
final class TkIndex implements Take {
@Override
public Response act() {
return new RsVelocity("Hello, ${name}")
.with("name", "Jeffrey");
}
}
RsVelocity构造函数接受必须为Velocity模板的单个参数。 然后,调用with()方法,将数据注入Velocity上下文中。 当需要呈现HTTP响应时, RsVelocity将根据配置的上下文“评估”模板。 同样,我建议您仅对简单输出使用这种模板方法。
对于更复杂HTML文档,我建议您将XML / XSLT与Xembly结合使用。 我在之前的几篇文章中对此想法进行了解释: 浏览器和RESTful API 中的XML + XSLT,以及相同URL中的网站 。 它简单而强大-Java生成XML输出,而XSLT处理器将其转换为HTML文档。 这就是我们将表示形式与数据分开的方式。 就MVC而言,XSL样式表是一个“视图”,而TkIndex是一个“控制器”。
我将很快写另一篇关于Xembly和XSL模板的文章。
同时,我们将在Takes中为JSF / Facelets和JSP渲染创建装饰器。 如果您有兴趣提供帮助,请分叉框架并提交拉取请求。
持久性呢?
现在,出现的一个问题是如何处理持久性实体,例如数据库,内存结构,网络连接等。我的建议是在Entry类内部对其进行初始化,并将其作为参数传递给TsApp构造函数。 然后, TsApp将它们传递到构造函数的定制需要 。
例如,我们有一个PostgreSQL数据库,其中包含一些需要渲染的表数据。 这是在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;
}
}
现在, TsApp的构造TsApp必须接受类型为java.sql.Source的单个参数:
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类的单个参数。 我相信您知道如何在TkIndex中使用它来获取SQL表数据并将其转换为HTML。 这里的要点是,必须在实例化时将依赖项注入到应用程序中(类TsApp的实例)。 这是一种纯净的依赖注入机制,它绝对没有容器。 在“依赖注入容器是代码污染者”中阅读有关它的更多信息。
单元测试
由于每个类都是不可变的,并且所有依赖项仅通过构造函数注入,因此单元测试非常容易。 假设我们要测试TkStatus ,它应该返回HTML响应(我正在使用JUnit 4和Hamcrest ):
import org.junit.Test;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
public final class TkIndexTest {
@Test
public void returnsHtmlPage() throws Exception {
MatcherAssert.assertThat(
new RsPrint(
new TkStatus().act()
).printBody(),
Matchers.equalsTo("<html>Hello, world!</html>")
);
}
}
此外,我们可以开始在测试HTTP服务器的整个应用程序或任何个人起飞 ,并通过一个真实的TCP套接字测试它的行为; 例如(我正在使用jcabi-http发出HTTP请求并检查输出):
public final class TkIndexTest {
@Test
public void returnsHtmlPage() throws Exception {
new FtRemote(new TsFixed(new TkIndex())).exec(
new FtRemote.Script() {
@Override
public void exec(final URI home) throws IOException {
new JdkRequest(home)
.fetch()
.as(RestResponse.class)
.assertStatus(HttpURLConnection.HTTP_OK)
.assertBody(Matchers.containsString("Hello, world!"));
}
}
);
}
}
FtRemote在随机的TCP端口启动测试Web服务器,并在提供的FtRemote.Script实例上调用exec()方法。 此方法的第一个参数是刚启动的Web服务器主页的URI。
Takes框架的体系结构非常模块化且可组合。 任何个体取可以进行测试作为一个独立的部件,绝对独立于框架和其他需要 。
为什么叫名字?
这就是我经常听到的问题。 这个想法很简单,它起源于电影业。 当影片制成,剧组芽许多需要以捕捉现实,把它放在电影。 每次捕获称为一次获取 。
换句话说, 拍摄就像现实的快照。
同样适用于此框架。 Take每个实例在某个特定时刻代表一个现实。 然后,将该现实以Response的形式发送给用户。
翻译自: https://www.javacodegeeks.com/2015/04/java-web-app-architecture-in-takes-framework.html
app访问java web