输入在图形用户界面 (GUI) 中的处理方式与我们在控制台用户界面和服务器中的处理方式不同。在那些其他系统中,我们看到了一个单一的输入循环,它读取用户键入的命令或客户端发送的消息,解析它们,并决定如何将它们引导到程序的不同模块。
如果 GUI 电子邮件客户端是这样编写的,它可能看起来像这样(在伪代码中):
while (true) {
read mouse click
if (clicked on Check Mail button) doRefreshInbox();
else if (clicked on Compose button) doStartNewEmailMessage();
else if (clicked on an email in the inbox) doOpenEmail(…);
…
}
但是在GUI中,我们不直接写这种方法,因为它不是模块化的。GUI 由各种库组件(按钮、滚动条、文本框、菜单)组合在一起,这些组件需要自包含并处理自己的输入。例如,以下是创建按钮的方法:
JButton playButton = new JButton(“Play”);
在屏幕上看起来像这样:
玩
此按钮处理自己的鼠标和键盘输入。图形用户界面库的深处是一个输入循环,它从鼠标和键盘读取数据,并将这些输入事件传递给 GUI 的适当组件。
为了使您的程序在单击按钮时执行某些操作,您需要为它附加一个侦听器:
playButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
playSound();
}
});
这段代码创建了一个实现接口的匿名类ActionListener的实例,它只有一个方法,actionPerformed. 当这个ActionListener实例被赋予按钮时addActionListener(),按钮承诺actionPerformed每次用户按下按钮时都会调用它的方法。
GUI 事件处理是侦听器模式的一个实例,也称为发布-订阅。在侦听器模式中:
事件源生成(或发布)离散事件流,这些事件对应于源中的状态转换。
一个或多个侦听器注册对事件流的兴趣(订阅),提供一个在新事件发生时调用的函数。
在这个例子中:
是JButton事件源;
它的事件是按钮按下;
侦听器是匿名ActionListener实例
事件发生时调用的函数是actionPerformed
事件通常包含附加信息,这些信息可能被捆绑到事件对象中(如ActionEvent这里)或作为参数传递给侦听器函数。
当事件发生时,事件源通过调用它们的侦听器方法将其分发给所有订阅的侦听器。
因此,通过图形用户界面的控制流程如下:
一个顶级事件循环从鼠标和键盘读取输入。在 Java 和大多数图形用户界面工具包中,这个循环实际上是对你隐藏的。它隐藏在工具包中,通常在工具包创建的单独线程上运行,并且侦听器似乎被神奇地调用了。
每个侦听器都做自己的事情(这可能涉及例如修改视图树中的对象),然后立即返回到事件循环。
最后一部分——侦听器尽快返回事件循环——非常重要,因为它保留了用户界面的响应性。
Listener 模式不仅仅用于按钮按下。每个 GUI 对象都会生成事件,通常是低级输入事件的某种组合的结果。例如:
JButton按下时发送一个动作事件(无论是通过鼠标还是键盘)
JList当被选元素改变时发送一个选择事件(无论是通过鼠标还是键盘)
JTextField当其中的文本因任何原因发生更改时发送更改事件
回调
我们在上一节看到的actionPerformed监听器函数是一个通用设计模式的例子,一个打回来. 回调是客户端提供给模块以供模块调用的函数。这与正常的控制流形成对比,其中客户端执行所有调用:调用模块提供的函数。通过回调,客户端提供了一段代码供实现者调用。
这是思考这个想法的一个类比。正常的功能调用就像拿起电话并调用服务,就像打电话给您的银行以查看您的帐户余额。您向银行运营商提供查询您帐户所需的信息,他们通过电话向您回读帐户余额,然后您挂断了电话。你是客户,银行是你调用的模块。
有时银行给出答案的速度很慢。你被搁置了,你等到他们为你找出答案。这就像一个函数调用,它在准备好返回之前一直阻塞,我们在讨论套接字和消息传递时看到了这一点。
但有时这项任务可能需要很长时间,以至于银行不想让你搁置。然后银行会要求你回拨电话号码,他们会承诺给你回电并给出答案。这类似于提供回调函数。
侦听器模式中使用的回调类型不是对一次性请求(例如您的帐户余额)的回答。这更像是银行承诺提供的常规服务,根据需要使用您的回拨号码与您联系。Listener 模式的一个更好的类比是帐户欺诈保护,当您的帐户发生可疑交易时,银行会通过电话给您打电话。
Web 服务器中的路由处理
让我们看另一个使用回调进行输入处理的系统示例:Web 服务器。Web 服务器通常将其服务的网站划分为多个部分,称为路由。例如,服务器web.mit.edu可能具有以下路由:
/education(可通过 URL 访问http://web.mit.edu/education)
/research
/campus-life
等等。
Web 服务器的控制流是一个输入循环,等待来自 Web 浏览器的传入连接。当新连接到达时,服务器读取并解析请求(使用HTTP 有线协议)。然后将请求路由到为与请求匹配的路由注册的处理程序。通常只有请求的前缀必须与路由匹配,因此请求http://web.mit.edu/community/topic/arts.html将被路由到处理程序,/community除非注册了更具体的(更长的前缀)路由。
然后处理程序负责构建对请求的响应。
Oracle 的 Java 实现包括一个 Web 服务器,HttpServer. 您可以像这样创建一个新的HttpServer:
// port on which the server will listen for incoming connections
final int port = 8081;
// incoming connections may be queued until the server is ready to handle them;
// 0 means use the default system value for the length of this queue
final int useDefaultBacklog = 0;
// make the server
HttpServer server = HttpServer.create(new InetSocketAddress(port), useDefaultBacklog);
然后使用添加路由createContext:
server.createContext(“/song/”, new HttpHandler() {
public void handle(HttpExchange exchange) throws IOException {
…
}
});
在这里,我们正在创建一个实现HttpHandler接口的匿名实例,它是一个具有一个方法的接口,handle(). 这handle()是回调函数。每当传入请求以/song/前缀开头时,服务器都会调用它。回调的参数是一个HttpExchange对象,它为观察者方法提供有关请求的信息,以及用于生成返回到 Web 浏览器的响应的 mutator 方法。
回调函数的主体(如上所示…)应检查请求并生成响应。作为如何检查请求的示例,如果传入请求是http://localhost:8081/song/cello.wav,则:
String path = exchange.getRequestURI().getPath();
返回"/song/cello.wav",然后处理程序可以对其进行解析以提取"cello.wav"它应该播放的文件名:
// remove /song/ from the start of the path to get the filename
String soundFilename = path.substring(exchange.getHttpContext().getPath().length());
作为生成响应的示例,处理程序应首先说明它返回的响应类型(HTML、纯文本、其他):
exchange.getResponseHeaders().add(“Content-Type”, “text/html; charset=utf-8”);
final int statusOK = 200;
final int responseLengthNotKnownYet = 0;
exchange.sendResponseHeaders(statusOK, responseLengthNotKnownYet);
然后使用交换对象的输出流写入响应:
PrintWriter out =
new PrintWriter(new OutputStreamWriter(exchange.getResponseBody(),
StandardCharsets.UTF_8),
true );
out.println("playing " + soundFilename);
exchange.close();
再一次,这HttpHandler是一个回调的例子:我们作为客户端传递给模块的一段代码,让HttpServer模块在事件发生时调用,在这种情况下是与路由匹配的请求的到达。
在系统中使用回调不可避免地会迫使程序员考虑并发性,因为控制流不再受您的控制。您的回调可能随时被调用,并且在某些系统中,它可能是从与客户端最初来自的不同线程调用的。
一旦创建了 GUI 对象,Java 的图形用户界面库就会自动创建单个线程。这个事件处理线程不同于程序的主线程。它运行从鼠标和键盘读取的事件循环,并调用侦听器回调。
同样,HttpServer该类创建一个新线程来侦听传入连接并解析它们的 HTTP 请求,然后调用路由处理程序回调。
回调通过允许客户端在事件发生时提供自己的代码来运行,从而使代码更易于更改,因此不必事先将行为硬编码到实现中。
编写一个巨大的输入循环来处理大型系统中的每一个可能的事件既不安全也不容易理解:那段代码变成了一个无所不知的全控制庞然大物。回调允许系统中的每个模块对自己的事件负责。



