轻松学模块化编程(二)
十一、重构
重构 1 是指以对功能没有影响的方式改变代码。这只是为了让代码更容易理解,或者为将来增加一些功能做准备。例如,有时您重构代码以使其更容易测试。许多 ide 都提供了执行常见重构的菜单和快捷键。
我们将讨论两类重构,面向对象的和函数式的,对应于两种不同的编程风格。
面向对象的重构
以下操作是 OOP 中常见的重构:
-
更改方法或类名(重命名)
-
将方法从一个类移动到另一个类(委托)
-
将字段从一个类移动到另一个类
-
在方法中添加或删除参数
-
使用类中的一组方法和字段创建新类
-
将局部变量更改为类字段
-
用常量(static final)替换一组文字(字符串或数字)
-
用枚举替换一些常量或文字
-
将一个类从匿名类移动到顶级类
-
重命名字段
功能重构
以下动作是 FP 中常见的重构:
-
重命名函数
-
将一个函数包装在另一个函数中并调用它
-
在函数被调用的任何地方内联它
-
将公共代码提取到一个函数中(与前面的相反)
-
重命名函数参数
-
添加或移除参数
您可能会注意到两个列表之间的一些相似之处。重构的原则是通用的。
重构示例
与许多 ide 一样,NetBeans 支持重构。您可以通过选择一些代码,右键单击,并选择 Refactor 菜单来尝试一下。这里有一些重构代码的例子。
重命名方法
之前:
1 public static void main(String...args) {
2 animateDead();
3 }
4 public static void animateDead() {}
之后:
1 public static void main(String...args) {
2 doCoolThing();
3 }
4 public static void doCoolThing() {}
将方法从一个类移动到另一个类(委托)
之前:
1 public static void main(String...args) {
2 animateDead();
3 }
4 public static void animateDead() {}
之后:
1 public class Animator() {
2 public void animateDead() {}
3 }
4 public static void main(String...args) {
5 new Animator().animateDead();
6 }
用常量(Static Final)替换一组文字(字符串或数字)
之前:
1 public static void main(String...args) {
2 animateDead(123);
3 System.out.println(123);
4 }
5 public static void animateDead(int n) {}
之后:
1 public static final int NUM = 123;
2 public static void main(String...args) {
3 animateDead(NUM);
4 System.out.println(NUM);
5 }
6 public static void animateDead(int n) {}
重命名函数
之前:
1 function castaNastySpell() { /* cast a spell here */ }
之后:
1 function castSpell() { /* cast a spell here */ }
将一个函数包装在另一个函数中并调用它
之前:
1 castSpell('my cool spell');
之后:
1 (function(spell) { castSpell(spell) })('my cool spell');
在函数被调用的任何地方内联它
当一个函数太简单或者只在一个地方使用时,这可能在重构过程中完成。
之前:
1 function castSpell(spell) { alert('You cast ' + spell); }
2 castSpell('crucio');
3 castSpell('expelliarmus');
之后:
1 alert('You cast ' + 'crucio');
2 alert('You cast ' + 'expelliarmus');
将公共代码提取到一个函数中(与前面的相反)
之前:
1 alert('You cast crucio');
2 alert('You cast expelliarmus');
之后:
1 function castSpell(spell) { alert('You cast ' + spell); }
2 castSpell('crucio');
3 castSpell('expelliarmus');
Footnotes 1
是的,重构是一个词!
十二、实用工具
每种编程语言都内置了许多非常有用的类、函数或对象(有时称为实用程序)。java.util包包含许多日常编程有用的类。同样,JavaScript 和其他语言带有许多用于执行常见任务的内置对象。我将讲述其中的一些。
日期和时间
永远不要将日期值存储为文本。太容易搞砸了(图 12-1 )。
图 12-1
我可以用绳子来存储数据吗?
Java 日期-时间
Java 8 在java.time包中引入了一个新的改进的日期时间应用程序接口(API ),它比以前的 API 更安全、更易读、更全面。
例如,创建日期如下所示:
1 LocalDate date = LocalDate.of(2014, Month.MARCH, 2);
还有一个LocalDateTim e 类来表示日期和时间,LocalTime只表示时间,ZonedDateTime表示带时区的时间。
在 Java 8 之前,只有两个内置类来帮助处理日期:Date和Calendar。尽可能避免这些。
-
Date实际上表示日期和时间。 -
Calendar用于操作日期。
在 Java 7 和更低版本中,您必须执行以下操作来为日期添加五天:
1 Calendar cal = Calendar.getInstance();
2 cal.setTime(date);
3 cal.add(5, Calendar.DAY);
而在更高版本的 Java 中,您可以执行以下操作:
1 LocalDate.now().plusDays(5)
绝妙的约会
Groovy 有许多内置特性,使得日期更容易处理。例如,数字可用于加减天数,如下所示:
1 def date = new Date() + 5; //adds 5 days
Groovy 也有时间类别 1 用于操作日期和时间。这可以让你加减任意长度的时间。例如:
1 import groovy.time.TimeCategory
2 now = new Date()
3 println now
4 use(TimeCategory) {
5 nextWeekPlusTenHours = now + 1.week + 10.hours - 30.seconds
6 }
7 println nextWeekPlusTenHours
一个Category是一个类,可以用来给其他现有的类添加功能。在这种情况下,TimeCategory向Integer类添加了一堆方法。
Categories
这是 Groovy 中可用的众多元编程技术之一。要创建一个类别,您需要创建一组静态方法,这些方法对特定类型的一个参数进行操作(例如,Integer)。当使用类别时,该类型似乎具有那些方法。调用方法的对象用作参数。请看一下 TimeCategory 的文档,其中有一个实际例子。
JavaScript 日期
JavaScript 也有内置的Date 2 对象。
您可以用几种方法创建一个Date对象的实例(这些方法都创建相同的日期):
1 Date.parse('June 13, 2014')
2 new Date('2014-06-13')
3 new Date(2014, 5, 13)
请注意,如果您遵循国际标准(yyyy-MM-dd),将采用 UTC 时区;否则,它会假设您需要一个本地时间。
和通常的 JavaScript 一样,浏览器都有稍微不同的规则,所以你必须小心。
永远不要用
getYear!在 Java 和 JavaScript 中,Date对象的getYear方法并不像你想的那样,应该避免。由于历史原因,getYear实际上并不返回年份(如 2014)。你应该在 JavaScript 中使用getFullYear(),在 Java 中使用LocalDate或LocalDateTime。
Java 日期格式
DateFormat虽然在java.text里面,但是和java.util.Date是齐头并进的。
SimpleDateFormat对于将日期格式化成您想要的任何格式都很有用。例如:
1 SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
2 Date date = new Date();
3 System.out.println(sdf.format(date));
这将按照美国标准格式化日期:月/日/年。
Java 8 引入了java.time.format.DateTimeFormatter来使用新的日期和时间类进行格式化或解析。每个java.time类,比如LocalDate,都有一个格式方法和一个静态解析方法,这两个方法都采用了DateTimeFormatter.的一个实例
例如:
1 LocalDate date = LocalDate.now();
2 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");
3 String text = date.format(formatter);
4 LocalDate parsedDate = LocalDate.parse(text, formatter);
关于它的更多信息,请参见 SimpleDateFormat3的文档。有关更多信息,请参见 datetime formatter4文档。
货币
在 Java 中,如果您的代码必须处理多个国家的货币,那么java.util. Currency非常有用。它提供了以下方法:
-
getInstance(Locale): Static method to get an instance of Currency based on Locale -
getInstance(String): Static method to get an instance of Currency based on a currency code -
getSymbol():当前区域设置的货币符号 -
getSymbol(Locale):给定地区的货币符号 -
static getAvailableCurrencies():返回可用货币的集合
例如:
1 String pound = Currency.getInstance(Locale.UK).getSymbol(); // GBP
2 String dollar = Currency.getInstance("USD").getSymbol(); // $
时区
在 Java 8 和更高版本中,时区由java.time.ZoneId类表示。有两种类型的ZoneId,固定偏移量和地理区域。这是为了补偿夏令时等可能非常复杂的做法。
您可以通过多种方式获得一个ZoneId的实例,包括以下两种:
1 ZoneId mountainTime = ZoneId.of("America/Denver");
2 ZoneId myZone = ZoneId.systemDefault();
要打印出所有可用的 id,使用getAvailableZoneIds(),如下所示:
1 System.out.println(ZoneId.getAvailableZoneIds());
写一个这样做的程序并运行它。例如,在 groovyConsole 中,编写以下代码并执行:
import java.time.*
println(ZoneId.getAvailableZoneIds())
扫描仪
Scanner可用于解析文件或用户输入。它使用给定的模式将输入分成标记,默认模式是空白(“空白”是指空格、制表符或任何在文本中不可见的东西)。
例如,使用以下代码从用户处读取两个数字:
1 System.out.println("Please type two numbers");
2 Scanner sc = new Scanner(System.in);
3 int num1 = sc.nextInt();
4 int num2 = sc.nextInt();
写一个这样做的程序,然后试一试。因为这需要输入,所以不能用 groovyConsole 来完成。使用 NetBeans 构建一个 Java 应用程序,或者使用命令行上的groovy运行一个 Groovy 脚本。
http://docs.groovy-lang.org/latest/html/api/groovy/time/TimeCategory.html
2
3
https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html
4
https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
十三、构建器
构建过程通常是编译项目的源文件,运行测试,并生产一个或多个成品。
在一些公司中,整个团队的唯一工作就是维护和更新构建过程。在任何规模的项目中,拥有一个好的自动化构建工具是有帮助的。
还有许多其他的构建工具,但我们只打算介绍三种:
-
ant
-
maven【2】
-
等级3T3〕
蚂蚁
Ant 是第一个真正流行的 Java 项目构建器。它是基于 XML 的,要求您用 XML 创建可以由 Ant 执行的任务。Ant 构建文件通常被命名为build.xml,并且有一个<project>根元素。
一个任务是分工。任务依赖于其他任务。例如,jar任务通常依赖于compile任务。Maven 虽然扔掉了任务概念,但是在 Gradle 中又被使用了。
Ant 的批评者抱怨说,它使用 XML(一种声明性的、非常冗长的格式),做简单的事情需要大量的工作。然而,多年来它一直是一个非常受欢迎的工具,并且仍然在许多地方使用。
专家
Maven 是一个基于 XML 的声明式项目管理器。Maven 用于构建 Java 项目,但还能做更多的事情。Maven 也是一组标准,允许 Java/JVM 开发人员轻松地定义依赖项并将其集成到大型项目中。Maven 在某种程度上取代了 Ant,但也可以与它和其他构建工具集成。
Maven 主要是对 Java 项目倾向于依赖的大量开源库的一种反应。它有一个内置的依赖性管理标准(管理开源库的相互依赖性)。
虽然 Maven 是一个 Apache 开源项目,但可以说 Maven 的核心是由 Maven 背后的公司 Sonatype 运营的开源库库 Maven Central 。还有许多其他的库也遵循 Maven 标准,比如 JFrog 的 jCenter、、 4 ,所以您并不局限于 Maven Central。
Note
Ivy 5 是一个类似的构建工具,但与 Ant 的关系更密切。
许多构建工具,如 Ivy 和 Gradle,都是基于 Maven 的概念构建的。
使用 Maven
定义 Maven 项目的主要文件是 POM (项目对象模型)。POM 文件是用 XML 编写的,包含所有的依赖项、插件、属性和特定于当前项目的配置数据。POM 文件通常由以下内容组成:
-
基本属性(
artifactId、groupId、name、version、packaging) -
属国
-
外挂程式
每个主流的基于 Java 的 IDE(Eclipse、NetBeans 和 IntelliJ IDEA)都有一个 Maven 插件,它们非常有用。您可以使用 Maven 插件来创建项目、添加依赖项和编辑 POM 文件。
开始一个新项目
使用archetype:generate命令创建一个新的配置文件(pom.xml)和项目文件夹有一个简单的方法。
mvn archetype:generate
这将列出您可以创建的所有不同类型的项目。选择一个代表你想要的项目类型的数字(现在有数千个选项),然后回答一些问题,比如你的项目的名称。完成该过程后,运行以下命令来构建项目:
1 mvn package
如果您想使用任何额外的第三方库,您必须编辑 POM 以包含每个依赖项。幸运的是,大多数 ide 都很容易向 POM 添加依赖项。
* Maven:完整参考* 6 如果你想了解更多,可以在线获得。
生存期
Maven 使用声明式风格(不像 Ant 使用更强制性的方法)。这意味着您不是列出要采取的步骤,而是描述在构建的某些阶段应该发生什么。Maven 中的阶段是内置的,如下所示(并按此顺序执行):
-
validate:验证项目是否正确以及所有必要的信息是否可用 -
compile:编译项目的源代码 -
test:使用合适的单元测试框架测试编译后的源代码 -
package:获取编译后的代码,并将其打包成可分发的格式,比如 JAR -
integration-test:如果需要的话,将包处理并部署到一个可以运行集成测试的环境中 -
verify:运行任何检查以验证包是有效的并且符合质量标准 -
install:将包安装到本地存储库中,作为本地其他项目的依赖项使用 -
deploy:在集成或发布环境中,将最终的包复制到远程存储库中,以便与其他开发人员和项目共享有更多的阶段, 7 但是你不需要知道所有的阶段,直到你在做更复杂的构建。
执行代码
然而,有时你只是需要更多的控制你的构建。在 Maven 中,可以执行 Groovy 代码、Ant 构建文件和 Scala 代码,甚至可以用 Groovy 编写自己的插件。
例如,您可以通过以下方式将 Groovy 代码放入 POM 文件中:
1 <plugin>
2 <groupId>org.codehaus.groovy.maven</groupId>
3 <artifactId>gmaven-plugin</artifactId>
4 <executions>
5 <execution>
6 <id>groovy-magic</id>
7 <phase>prepare-package</phase>
8 <goals>
9 <goal>execute</goal>
10 </goals>
11 <configuration>
12 <source>
13 def depFile =
14 new File(project.build.outputDirectory, 'deps.txt')
15 project.dependencies.each {
16 depFile.write(
17 "${it.groupId}:${it.artifactId}:${it.version}")
18 }
19 ant.copy(todir: project.build.outputDirectory ) {
20 fileset(dir: project.build.sourceDirectory)
21 }
22 </source>
23 </configuration>
24 </execution>
25 </executions>
26 </plugin>
前面的代码将把项目的每个依赖项写到文件deps.txt中。然后它会将所有的源文件复制到project.build.outputDirectory(通常是target/classes)。
参见 The Maven Cookbook 中的 2 、 3 和 4 章节。 8
格拉德尔
Gradle 是一个自动化的构建工具,带有 Groovy 原生 DSL(领域特定语言),用于定义项目构建。它还有一个 Kotlin 原生 DSL,是 Android 项目的官方构建工具。
在撰写本文时,Gradle 网站是这样描述 Gradle 的:
从移动应用到微服务,从小型创业公司到大型企业,Gradle 帮助团队更快地构建、自动化和交付更好的软件。
——格雷尔。org9
Gradle 入门
要轻松入门,可以使用 Gradle 内置的 init 插件。安装 Gradle 后,运行以下命令:
gradle init
出现提示时,您可以选择带有 Junit 4 测试和 Groovy 构建脚本的 Java 应用程序(键入 2,enter,3,enter,1,enter,1,enter)。这将创建一个build.gradle文件、一个settings.gradle文件、gradlew和gradlew.bat(允许你从任何系统运行 Gradle,甚至不需要安装它)以及一些基本代码和一个测试。
项目和任务
每个 Gradle 构建由一个或多个项目组成,每个项目由一个或多个任务组成。
Gradle 构建的核心是build.gradle文件,称为构建脚本。任务可以通过写task,然后写一个任务名,最后写一个闭包来定义。例如:
1 task upper doLast {
2 String someString = 'test'
3 println "Original: $someString"
4 println "Uppercase: " + someString.toUpperCase()
5 }
就像在 Ant 中一样,一个任务可以依赖于其他任务,这意味着它们必须在该任务之前运行。在您的任务定义中,您可以使用任意数量的任务名称将dependsOn指定为List。例如:
1 task buildApp(dependsOn: [clean, jar]) {
2 // define task here
3 }
任务可以包含任何 Groovy 代码(或者如果使用 Kotlin DSL,可以包含 Kotlin 代码),但是您可以利用一些现有的 Gradle 插件来快速生成可靠且快速的构建。
插件
Gradle core 几乎没有内置。它有强大的插件,这使得它非常灵活。插件可以执行以下一项或多项操作:
-
向项目添加任务(例如,编译、测试)。
-
使用有用的默认值预先配置添加的任务。
-
将依赖项配置添加到项目中。
-
通过扩展向现有类型添加新的属性和方法。
我们将专注于构建基于 Java 的项目,所以我们将使用java插件;然而,Gradle 并不局限于 Java 项目。
例如,在您的build.gradle文件的开头,您应该会看到类似以下代码的内容:
1 plugins {
2 id 'java'
3 id 'application'
4 }
这将启用java插件和application插件。
Gradle 使用标准的项目组织惯例。例如,它期望在src/main/java下找到您的生产 Java 源代码,在src/test/java下找到您的测试 Java 源代码。这也是 Maven 所期待的风格。
属国
每个 Java 项目都倾向于依赖许多开源项目来构建。Gradle 建立在它之前的概念之上,因此您可以使用一个简单的 DSL 轻松定义您的依赖关系,如下例所示:
1 plugins { id 'java' }
2
3 sourceCompatibility = 1.11
4
5 repositories {
6 mavenLocal()
7 mavenCentral()
8 }
9
10 dependencies {
11 implementation 'com.google.guava:guava:23.0'
12 implementation 'io.reactivex.rxjava2:rxjava:2.2.10'
13 testImplementation group: 'junit', name: 'junit', version: '4.12+'
14 testImplementation "org.mockito:mockito-core:1.9.5"
15 }
这个构建脚本使用sourceCompatibility来定义Java 11的 Java 源代码版本(在编译过程中使用)。接下来,它告诉 Maven 首先使用本地存储库(mavenLocal),然后使用 Maven Central。
在dependencies块中,这个构建脚本为implementation范围和testImplementation范围定义了两个依赖项。testImplementation范围内的罐子仅用于测试,不会包含在任何最终产品中。
JUnit 行显示了定义依赖关系的更详细的样式。这里的“+”表示该版本或更高版本。
从头到尾做
您可以使用doFirst和doLast来指定应该在任务之前和之后运行的代码。例如,让我们从前面的任务开始,添加一些额外的代码:
1 task buildApp(dependsOn: [clean, jar]) {
2 doFirst { println "starting to build" }
3 doLast { println "done building" }
4 }
这将在任务运行之前打印出“开始构建”,在任务完成之后打印出“完成构建”。在命令提示符下键入gradle buildApp来运行该任务。您应该会看到类似如下的输出:
> Task :buildApp
starting to build
done building
BUILD SUCCESSFUL in 983ms
4 actionable tasks: 4 executed
您的 jar 文件现在将位于项目的build/libs/目录中。
Gradle 有一个庞大的在线用户指南,可在 gradle 上获得。org 。 10
2
3
4
https://bintray.com/bintray/jcenter
5
6
7
https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference
8
http://books.sonatype.com/mcookbook/reference/index.html
9
10
https://docs.gradle.org/current/userguide/userguide.html
十四、测试
测试是软件创建过程中非常重要的一部分。没有自动化测试,错误很容易潜入软件。
事实上,有些人甚至认为应该在编写代码之前先编写测试。这被称为 TDD (测试驱动开发)。
有多种测试框架和工具可以帮助你测试你的代码。这本书将涵盖其中一些工具,JUnit 和 Spock。
测试类型
以下是您可以编写的不同类型的测试:
-
单元测试:对单个 API 调用或一些孤立的代码或组件进行的测试
-
集成测试:将两个或多个组件集成在一起的大型系统的测试
-
验收测试:符合业务需求的高级测试
-
兼容性:确保所有的东西都能协同工作
-
功能性:确保工作正常
-
黑盒:在不知道/不考虑代码中发生了什么的情况下进行的测试
-
白盒:考虑到代码内部而编写的测试
-
灰盒:黑白盒混合测试
-
回归:在发现一个 bug 后创建一个测试,以确保 bug 不再出现
-
烟雾:测试中使用的大量数据样本
-
负载/压力/性能:系统如何处理负载(例如,一个网站的大量流量)
基于许多因素,您编写的测试的类型和数量会有所不同。一段代码越简单,需要的测试就越少。例如,getter 或 setter 本身不需要测试。
单元测试
JUnit 1 是一个编写可重复测试的简单框架。
典型的 JUnit 测试由用@Test注释标注的多个方法组成。
在每个 JUnit 测试类的顶部,您应该包括以下导入:
1 import static org.junit.jupiter.api.Assertions.*;
2 import org.junit.jupiter.api.Test;
3 import org.junit.jupiter.api.BeforeEach;
4 import org.junit.jupiter.api.AfterEach;
使用@BeforeEach注释每次测试前运行的初始化方法,使用@AfterEach注释每次测试后运行的分解方法。这些方法应该确保每个测试都是独立的。
每个测试方法应该测试一件事,方法名应该反映测试的目的。例如:
1 @Test
2 public void mapSizeIsCorrect() {
3
Map<String,String> map = new HashMap<>();
4 map.put("Smaug", "deadly");
5 map.put("Norbert", "cute");
6 assertEquals(2, map.size());
7 }
assertEquals方法的第一个参数是预期值,第二个参数是要测试的实际值。当两个值不相等时,这将抛出一个异常,测试将失败。失败的测试意味着代码没有满足我们的期望。在这一点上,软件应该被认为是不正确的,并且在测试成功(没有失败)之前不要再进一步。
哈姆克雷斯特
您可以使用 Hamcrest 核心匹配器创建更具可读性的测试。在 JUnit 中,您必须单独导入 Hamcrest 2 匹配器。您可以按如下方式导入它们:
1 import static org.hamcrest.CoreMatchers.equalTo;
2 import static org.hamcrest.CoreMatchers.is;
3 import static org.hamcrest.MatcherAssert.assertThat;
下面是一个使用 Hamcrest 匹配器的示例:
1 @Test
2 public void sizeIs10() {
3 assertThat(map.size(), is(2));
4 }
这个测试将断言 map 的大小是 2。还有许多其他匹配器,你甚至可以建立自己的。
假设
通常,在测试之外有一些超出你控制的变量,但是你的测试假设这些变量是真实的。当一个假设失败时,它不一定意味着你的测试失败。为此,JUnit 添加了Assumptions,您可以像这样导入它:
1 import static org.junit.jupiter.api.Assumptions.*;
您可以在测试断言之前验证假设。例如,如果将以下内容放在测试的开头,则该测试的其余部分只能在使用“/”作为文件分隔符的系统上运行(即,不是 Windows):
1 assumeTrue(File.separatorChar, is('/'));
当假设失败时,测试要么被标记为通过,要么被忽略,这取决于 JUnit 的版本。 3
斯波克
Spock 是 Java、Groovy 或任何其他 JVM 语言的测试框架。它充分利用了 Groovy 的优势,并且内置了对象模仿。斯波克网站 4 这样描述斯波克:
- 让它脱颖而出的是其优美且极具表现力的规范语言。由于它的 JUnit runner,Spock 与大多数 ide、构建工具和持续集成服务器兼容。斯波克的灵感来自于 JUnit、RSpec、jMock、Mockito、Groovy、Scala、Vulcans 和其他迷人的生命形式。
史巴克基础
Spock 中的测试类被称为规范。Spock 中规范的基本结构是一个扩展spock.lang.Specification的类,有多个测试方法(可能有描述性的字符串名)。类名应该以Spec结尾,例如,一个关于Vampire类的规范可以被命名为VampireSpec。
Spock 处理测试代码,并允许您使用基于 Groovy 的语法来指定测试。Spock 测试应该放在src/test/groovy目录下。
每个测试都由带有标签的代码块组成,比如when、then和where。学习斯波克最好的方法是用例子。
首先,将 Spock 作为一个依赖项添加到您的项目中。例如,在 Gradle 构建文件中,放置以下内容:
1 dependencies {
2 testImplementation "org.spockframework:spock-core:1.3-groovy-2.5"
3 }
简单的测试
让我们从重新创建一个简单的测试开始:
1 def "toString yields the String representation"() {
2 def array = ['a', 'b', 'c'] as String[]
3 when:
4 def arrayWrapper = new ArrayWrapper<String>(array)
5 then:
6 arrayWrapper.toString() == '[a, b, c]'
7 }
如图所示,断言只是 Groovy 条件表达式。在then:之后的每一行都将被测试 Groovy 的真实性。如果==表达式返回false,测试将失败,Spock 将给出详细的打印输出来解释失败的原因。
在没有任何when子句的情况下,可以用expect子句代替then;例如:
1 def "empty list size is zero"() {
2 expect: [].size() == 0
3 }
嘲弄的
模仿是指你使用一个工具来扩展一个接口或类,这个接口或类可以在测试中模仿那个类或接口的行为来帮助测试你的代码。在 JUnit 测试中,你需要使用一个库,像 Mockito 、5来模拟其他不属于测试的类。
在 Spock 中模仿接口或类是非常容易的。 6 简单使用Mock方法,如下例所示(其中Subscriber为接口):
1 class APublisher extends Specification {
2 def publisher = new Publisher()
3 def subscriber = Mock(Subscriber)
现在subscriber是被嘲讽的对象。您可以简单地使用重载的>>操作符来实现方法,如下所示。每当调用receive时,下面的例子抛出一个Exception:
1 def "can cope with misbehaving subscribers"() {
2 subscriber.receive(_) >> { throw new Exception() }
3
4 when:
5 publisher.send("event")
6 publisher.send("event")
7
8 then:
9 2 * subscriber.receive("event")
10 }
预期的行为可以通过使用一个数字或范围乘以(*)方法调用来描述,如下所示(它期望receive方法应该被调用两次)。
下划线(_)被视为通配符(很像 Scala)。
数据列表或表格
Spock 允许您使用数据列表或表格来更简单地在一个测试中测试多个测试用例。
例如:
1 def "subscribers receive published events at least once"(){
2 when: publisher.send(event)
3 then: (1.._) * subscriber.receive(event)
4 where: event << ["started", "paused", "stopped"]
5 }
重载的<<操作符用于为event变量提供一个列表。虽然这里是一个列表,但是任何Iterable的东西都可以使用。这具有为列表中的每个值运行测试的效果。
Ranges
这里的范围1.._表示“一次或多次”。你也可以用_..3来表示“三次或更少”的次数。
也可以使用表格格式的数据。例如,下面的测试有一个包含两列(名称和长度)的表:
1 def "length of NASA mission names"() {
2 expect:
3 name.size() == length
4
5 where:
6 name | length
7 "Mercury" | 7
8 "Gemini" | 6
9 "Apollo" | 6
10 }
在这种情况下,两列(name和length)用于替换expect块中的相应变量。可以使用任意数量的列。
您还可以添加@Unroll注释,以便每个表行产生一个单独的测试输出。然后,您可以使用#来引用列。例如,将先前测试的签名更改为以下内容:
1 @Unroll
2 def "length of NASA mission name, #name"() {
预期异常
使用then块中的thrown方法来期待抛出的Exception。
1 def "peek on empty stack throws"() {
2 when: stack.peek()
3 then: thrown(EmptyStackException)
4 }
您也可以通过简单地将抛出的Exception分配给thrown()来捕获它。例如:
1 def "peek on empty stack throws"() {
2 when: stack.peek()
3 then:
4 Exception e = thrown()
5 e.toString().contains("EmptyStackException")
6 }
在编写了一些测试之后,用 Gradle 或 Maven 运行测试。对于 Gradle run ./gradlew test (results go under build/reports/tests/test/)。对于 Maven 运行mvn test (results are under build/surefire-reports/).
其他测试框架
不幸的是,我们没有时间讨论许多其他的测试框架。有些用于实现 web 应用程序的自动化浏览器测试,如 Geb 7 和 Selenium。88
其他的像黄瓜一样启用 BDD(行为驱动开发)。黄瓜使测试能够用接近简单的英语来编写。例如,测试本书前面的一些代码的场景:
Scenario: Longest Dragon name
Given a list of Dragons
When the longest Dragon name is found
Then the name is "Norbert"
摘要
在这一章中,你已经学习了测试是软件开发的一个非常重要的部分,以及你应该编写的测试类型。您已经学习了如何使用 JUnit 和 Spock 来运行测试,并且学习了其他用于运行集成测试或 BDD 的测试框架。
Footnotes 12
http://hamcrest.org/JavaHamcrest/
3
http://junit.sourceforge.net/doc/ReleaseNotes4.4.html
4
https://code.google.com/p/spock/
5
6
也可以模仿类,但是需要包含 bytebuddy JAR 作为依赖:test runtime“net . byte buddy:byte-buddy:1 . 10 . 1”。
7
8
9
十五、输入/输出
很多时候,在开发应用程序时,您需要读取或写入文件或网络连接。这些东西通常简称为输入输出或 I/O。
在这一章中,我们将介绍 Java 和 JVM 中一些可用于 I/O 的实用程序。
文件
在 Java 中,java.io.File类用来表示文件和目录。例如:
1 File file = new File("path/file.txt");
2 File dir = new File("path/"); //directory
Java 7 增加了几个新的类和接口,用于在java.nio包下操作文件和文件系统。这种新的应用程序接口(API)允许开发人员访问许多以前无法从 Java API 获得的低级操作系统操作,例如WatchService和创建链接的能力(在 Linux/Unix 操作系统中)。
路径用于更一致地表示文件或目录路径。
1 Path path = Paths.get("/path/file");
这是以下内容的简写:
1 Path path = FileSystems.getDefault().getPath("/path/file");
下面的列表定义了 API 的一些最重要的类和接口:
-
这个类专门由静态方法组成,这些方法操作文件、目录或其他类型的文件。
-
这个类专门由静态方法组成,这些方法通过转换路径字符串或 URI 来返回路径。
-
WatchService:查看各种文件系统事件的界面,如创建、删除、修改等。
读取文件
要用 Java 读取文本文件,可以使用BufferedReader。例如:
1 public void readWithTry() {
2 Charset utf = StandardCharsets.UTF_8;
3 try (BufferedReader reader = Files.newBufferedReader (path, utf)) {
4 for (String line = br.readLine(); line!=null; line = br.readLine())
5 System.out.println(line);
6 } catch (IOException e) {
7 e.printStackTrace();
8 }
9 }
Java 的自动资源管理特性使得处理资源(比如文件)变得更加容易。在 Java 7 之前,程序员需要显式关闭所有打开的流,导致一些非常冗长的代码。通过使用前面的try语句,BufferedReader将自动关闭。
然而,在 Groovy 中,这可以减少到一行(省去异常处理),如下所示:
1 println path.toFile().text
Groovy 中的File类中添加了一个getText()方法,该方法只读取整个文件。一个getBytes()方法也可用于从文件中读取字节。
写文件
写文件类似于读文件。对于写入文本文件,应该使用PrintWriter。它包括以下方法(以及其他方法):
-
print(Object):直接调用toString()打印给定对象 -
println(Object):打印给定的对象,然后换行 -
println():打印换行符序列 -
printf(String format, Object...args):使用给定输入打印格式化字符串
例如,您可以使用PrintWriter轻松写入文件,如下所示:
1 public void printWithTry() {
2 try (FileOutputStream fos = new FileOutputStream("books.txt");
3 PrintWriter pw = new PrintWriter(fos)) {
4 pw.println("Modern Java");
5 } catch (IOException e) {
6 // log the exception
7 }
8 }
还有其他方式输出到文件,比如DataOutputStream,例如:
1 public void writeWithTry() {
2 try (FileOutputStream fos = new FileOutputStream("books.txt");
3 DataOutputStream dos = new DataOutputStream(fos)) {
4 dos.writeUTF("Modern Java");
5 } catch (IOException e) {
6 // log the exception
7 }
8 }
允许应用程序将原始 Java 数据类型写入输出流。然后,您可以使用DataInputStream将数据读回。如果你只是处理文本,你可以用PrintWriter和BufferedReader来代替。
在 Groovy 中,可以更容易地写入文件,如下所示:
1 new File("books.txt").text = "Modern Java"
2 new File("binary.txt").bytes = "Modern Java".bytes
Groovy 在File类中添加了一个setText方法和一个setBytes,这使得这个语法可以工作。
下载文件
尽管在实践中您可能从来没有这样做过,但是用代码下载一个网页/文件是相当简单的。
下面的 Java 代码在给定的 URL 上打开一个 HTTP 连接(本例中是 https://www.google.com ),将数据读入一个字节数组,并打印出结果文本。
1 URL url = new URL("https://www.google.com");
2 InputStream input = (InputStream) url.getContent();
3 ByteArrayOutputStream out = new ByteArrayOutputStream();
4 int n = 0;
5 byte[] arr = new byte[1024];
6
7 while (-1 != (n = input.read(arr)))
8 out.write(arr, 0, n);
9
10 System.out.println(new String(out.toByteArray()));
然而,在 Groovy 中,这也可以减少到一行(忽略异常)。
1 println "https://www.google.com".toURL().text
在 Groovy 中的String类中添加了一个toURL()方法,在URL类中添加了一个getText()方法。
摘要
阅读完本章后,您应该了解如何
-
探索 Java 中的文件系统
-
从文件中读取
-
写入文件
-
下载互联网
十六、版本控制
一旦人们开始他们的编程生涯,他们就会受到版本控制系统的沉重打击(VCS)。
版本控制软件用于跟踪、管理和保护对文件的更改。这是现代软件开发项目中非常重要的一部分。
这本书将涵盖两个流行的(但还有更多):
-
SVN(颠覆)
-
去(去)
每个 VCS 都有以下基本动作:
-
Add -
Commit -
Revert -
Remove -
Branch -
Merge
ide 有处理版本控制系统的插件,通常有对流行系统的内置支持,比如 SVN 和 Git。
破坏
SVN 1 这是一个巨大的飞跃。除了其他好处之外,它还允许将层次结构中的任何目录从系统中签出并使用。SVN 需要一个服务器来存储代码的历史、标签和分支。然后,程序员使用 SVN 客户端提交代码更改。
要开始在命令行上使用 SVN,您将签出一个项目,然后提交文件,例如:
1 svn checkout http://example.com/svn/trunk
2 svn add file
3 svn commit
饭桶
Git 2 是一个分布式版本控制系统。这意味着源代码的每个副本都包含代码的整个历史。然而,与大多数其他系统不同,它以一种非常紧凑、高效和安全的方式存储历史——每次提交都与一个哈希(一个紧凑但唯一的值,通过单向算法从较大的值中生成)相关联。它最初是由 Linux 的创造者(Linus Torvalds)创建的,非常受欢迎。
要开始在新项目中使用 Git,只需运行以下命令:
1 git init
创建一个名为README的文件,然后提交它,如下所示:
1 git add README
2 git commit -m "this is my comment"
安装 Git。转到
github.com 3 克隆一个库,例如git clone https://github.com/adamldavis/learning-groovy.git 。现在创建您自己的 GitHub 帐户,创建一个新的存储库,克隆它,并按照前面的说明向其中添加一个新文件。最后,使用git push将更改推送到 GitHub。
当您在本地 git 存储库中设置了一个远程主机时(就像您从 GitHub 克隆了一个存储库时),您可以向它推送更改,也可以从中提取更改。例如,git push命令将您的提交推送到远程主机,而git pull命令从主机获取更改(其他开发人员可能已经放在那里)。
其他有用的命令:
-
git log:显示所有提交,最近的先显示 -
git status:显示您的 git 库的当前状态 -
git show:给定一个提交散列,显示该提交的所有更改 -
git checkout:给定一个分支名称,加载该分支 -
git merge:将两个或多个开发历史结合在一起 -
git branch:可用于列出、创建或删除分支 -
git tag:可用于列出、创建或删除标签 -
给你有用的文档。像
git help <command>一样使用时给出帮助的具体命令
水银的
Mercurial 4 早于 Git,但与它非常相似。它被用于 Google Code 和 Bitbucket 上的很多项目。T55
安装 Mercurial。转到 Bitbucket 并使用 Mercurial 克隆一个存储库,例如,
hg clone https://bitbucket.org/adamldavis/dollar 。
https://subversion.apache.org/
2
3
4
5
十七、互联网
(由 xkcd 提供:Interblag)
现在,几乎所有的软件项目都是基于互联网的,要么是网络应用程序(生成 HTML 并通过 Firefox 等浏览器显示的程序),要么是网络服务(通过浏览器中的 JavaScript 或移动应用程序连接的程序,如 Android 和 iOS 设备上的程序)。
本章致力于学习与 web 应用程序和 web 服务相关的概念和一些代码。
网络 101
网络是一只复杂的野兽。你需要知道的是:
-
服务器:提供网页和其他内容的计算机
-
客户端:个人使用的接收网页的电脑
-
请求:客户端发送给服务器的数据
-
响应:请求后发送回客户端的数据
-
HTML:用来定义网页的语言
-
CSS :“级联样式表”;定义网页的样式
-
JavaScript:一种在网页中使用并在客户端执行的编程语言,尽管它也可以在服务器端使用
我的第一个网络应用
你应该为你的第一个 web 应用做一些非常基本的东西。这样,您将更好地理解许多 web 框架的“幕后”发生了什么。一个 web 框架是一组用于构建 web 应用程序的相关工具和库。
创建一个名为App.java的文件,并将以下代码复制到其中:
1 import java.io.IOException;
2 import java.io.OutputStream;
3 import java.net.InetSocketAddress;
4 import com.sun.net.httpserver.*;
5
6 public class App {
7
8 static class MyHandler implements HttpHandler {
9 public void handle(HttpExchange t) throws IOException {
10 String response = "<html> Hello Inter-webs! </html>";
11 t.sendResponseHeaders(200, response.length());
12 OutputStream os = t.getResponseBody();
13 os.write(response.getBytes());
14 os.close();
15 }
16 }
17
18 public static void main(String[] args) throws Exception {
19 HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
20 server.createContext("/", new MyHandler());
21 server.setExecutor(null); // creates a default executor
22 server.start();
23 System.out.println("Server running at http://localhost:8000");
24 }
25
26 }
所有这些只是创建一个HttpServer来监听端口 8000 上的连接,并用一条消息来响应。
运行这段代码(javac App.java && java App)后,打开你的网络浏览器,指向http://localhost:8000/(它应该显示“Hello Inter-webs!”).按 Ctrl+C 停止应用程序。
localhost是指你所在的电脑,:8000是指端口 8000。
恭喜你!您刚刚制作了一个 web 应用程序!现在还没有放到网上,而且极其简单,但是这是一个很好的开始。
Port?
URL(统一资源定位符):用于在任何网络或机器上定位资源的唯一名称。有时以“http”开头;有时它包括一个端口。
HTTP 超文本传输协议(HTTP Hypertext Transfer Protocol):用于网络通信的典型协议。
HTTPS(安全 HTTP) :类似于 HTTP,但使用非对称密钥对所有数据进行编码,因此除了预定的接收者之外,任何设备都无法读取数据。
端口:计算机间通信时必须指定的一个数字(HTTP 默认端口为 80)。
圣杯
Grails 是一个 Groovy 的 web 框架,它遵循了 Ruby on Rails 的例子(因此有了 Grails )。这是一个自以为是的 web 框架,带有命令行工具,可以非常快速地完成工作。Grails 使用约定优于配置来减少配置开销。这可以大大减少开始新项目或添加额外功能所需的工作量。
Grails 牢牢地活在 Java 生态系统中,并建立在 Spring Boot 和 Hibernate 等技术之上。Grails 还包括一个对象关系映射(ORM)框架,它将对象映射到数据库表,称为 GORM,,并有一个大的插件集合。
快速概述
这个概述基于 Grails 4.0.0,但是对于所有版本的 Grails,3.0 和更高版本,基础知识应该是相同的。安装 Grails 后, 1 您可以通过在命令行上运行以下命令来创建一个应用程序:
1 $ mkdir g4
2 $ cd g4
3 $ grails create-app --inplace
然后,您可以运行诸如create-domain-class和generate-all这样的命令来创建您的应用程序。运行grails help来查看可用命令的完整列表。我们将在本章后面更全面地介绍这些命令。
Grails 应用程序有一个非常特殊的项目结构。下面是该结构的大多数的简单分解:
-
grails-app:特定于 Grails 的文件夹。-
conf:配置文件,如 application.yml、logback.groovy。 -
controllers:具有索引/创建/编辑/删除或任何其他方法的控制器。 -
domain:领域模型;表示持久数据的类。 -
消息包,用于支持多种语言(英语、西班牙语等)。).
-
init:包含您的Application.groovy和Bootstrap.groovy文件,它们在应用程序启动时初始化应用程序。 -
services:后端服务,您的后端或“业务”逻辑就在其中。 -
你可以很容易地定义你自己的标签,在你的 GSP 文件中使用。
-
views:MVC 的观点;通常,这些是 GSP 文件(基于 HTML 并嵌入 Groovy 代码)。
-
-
assets-
stylesheets: CSS。 -
images:您的网络应用程序使用的图像。 -
javascripts:你的 JavaScript 文件。
-
-
不适合其他地方的通用代码。
-
main/groovy: Groovy 代码。 -
常规测试。
-
-
gradle:包含 Gradle 包装罐。
要创建新的域(模型)类,使用create-domain-class命令。运行以下命令(在项目的根目录下):
1 $ grails create-domain-class example.Comment
2 $ grails create-domain-class example.User
3 $ grails create-domain-class example.Post
为您的域类包含一个包是一个好主意(比如example.Post))。该命令创建域类和相关的 Spock 规范。将User和Comment更改如下:
1 class User { String name }
2 class Comment { String text }
Grails 中的域类也定义了它到数据库的映射。例如,编辑表示博客文章的域类,如下所示(假设已经创建了User和Comment):
1 class Post {
2 String text
3 int rating
4 Date created = new Date()
5 User createdBy
6
7 static hasMany = [comments: Comment]
8
9 static constraints = {
10 text(size:10..500)
11 }
12 }
静态的hasMany字段是表示数据库中一对多关系的映射——这意味着一个Post可以有多个Comments。Grails 在后台使用 Hibernate 为所有的域类和关系创建表。默认情况下,每个表都有一个自动分配的主键字段id。
要让 Grails 在定义域类之后自动创建控制器和视图(以及测试),请运行以下命令:
1 $ grails generate-all example.User
2 $ grails generate-all example.Comment
3 $ grails generate-all example.Post
如果现有文件存在,Grails 会询问您是否要覆盖它们。因此,使用该命令时要小心。
当您想要测试您的应用程序时,只需运行以下命令:
1 $ grails run-app
它最终应该输出以下内容:
Grails 应用程序运行于环境:开发中的http://localhost:8080。
接下来,打开浏览器并转到该 URL。基于默认生成的视图,您应该会看到以下内容,其中包含控制器列表、应用程序状态、工件和已安装插件列表:
如果您跟随链接到各自的控制器,您可以创建用户,然后创建帖子,然后在这些帖子上创建评论。
外挂程式
Grails 4.0 系统现在包括 190 多个插件。要列出所有插件,只需执行以下命令:
1 $ grails list-plugins
当你选择了一个你想要使用的插件,执行下面的命令来查看更多关于这个插件的信息(包括插件名称):
1 $ grails plugin-info [NAME]
这将告诉您如何将插件添加到项目中。编辑您的build.gradle文件,并在那里添加依赖项。
这只是对 Grails 的一个简要概述。已经有很多关于 Grails 以及如何使用它的书籍。关于使用 Grails 的更多信息,请访问
grails.org 。22
云
以下云提供商支持 Grails:
-
cloudfoundry【3】
-
亚马逊4
-
希罗库【5】
然而,这并不在本书的讨论范围之内,但是我们将很快讨论 Heroku。
Heroku 是首批云平台之一,自 2007 年 6 月开始开发。开始时,它只支持 Ruby,但后来它增加了对 Java、Scala、Groovy、Node.js、Clojure 和 Python 的支持。Heroku 支持多层账户,包括一个免费账户。
Heroku 依靠git将变化推送到你的服务器。例如,要使用 CLI 在 Heroku 中创建应用程序,请执行以下操作:
1 $ heroku create
2 $ git push heroku master
您的应用程序将启动并运行,Heroku 将识别您可以找到它的 URL。
去 Heroku 上启动一个 Grails 应用程序吧!
其余的
REST 代表具象状态转移。 6 它是在一篇博士论文中设计的,并作为新的 web 服务标准而广受欢迎。许多开发人员称赞它是比 SOAP 好得多的标准(我不打算描述 SOAP)。
在 REST 的最基本层面,每个 CRUD (创建、读取、更新、删除)操作都被映射到一个 HTTP 方法。例如:
-
创建 :
POST -
读作 :
GET -
更新 :
PUT -
删除 :
DELETE
假设传输机制是 HTTP,但是消息内容可以是任何类型,通常是 XML 或 JSON。
JSR 社区已经设计了用于构建 RESTful Java web 服务的 JAX-RS API,而 Groovy 和 Scala 都有一些对 XML 和 JSON 的内置支持以及各种构建 web 服务的方式。Spring Boot 7 和春天 MVC 对休息也有很大的支持。
使用 Maven 原型
您可以使用 Maven 创建一个简单的 Java REST (JAX-RS)应用程序,如下所示:
1 mvn archetype:generate
等东西下载完再选“tomcat-maven-archetype”(键入tomcat-maven按回车键,再键入“1”);进入;回车)。你需要输入一个groupId和artifactId。
创建应用程序后,您可以通过键入以下命令来启动它:
1 mvn tomcat:run
使用 Grails JSON 视图
Grails 有一个将视图呈现为 JSON 的插件。首先运行 plugin-info,看看如何将它包含在您的构建中:
1 $ grails plugin-info views-json
将它添加到 Grails 项目的构建依赖项之后,您可以使用 Groovy DSL 来定义如何在 JSON 中呈现您的响应。更多信息参见文档 8 。
总的来说,JSON views 允许您在带有.gson扩展名的grails-app/views目录下定义视图,这些视图可以使用 DSL 来生成 JSON,而不是使用生成 HTML 的.gsp文件。例如,这在编写生成 JSON 的 web 服务时非常有用。
使用以下内容创建一个名为grails-app/views/hello.gson的文件:
json.message {
hello "world"
}
这将产生作为 JSON 的{"message":{ "hello":"world"}}。
摘要
恭喜你!你现在明白互联网了。是的,它是一系列的管子。特德·史蒂文斯(见下文)是对的!
Footnotes 1他们想通过互联网传递大量信息。再说一次,互联网不是你随便扔东西的地方。它不是一辆大卡车。这是一系列的管子。如果你不明白,这些管道可以被填满,如果它们被填满,当你把你的信息放进去,它会排队,它会被任何人延迟,把大量的材料放进那个管道,大量的材料。
——老西奥多·泰德·富尔顿·史蒂文斯,美国阿拉斯加州参议员,1968 年 12 月 24 日至 2009 年 1 月 3 日
https://grails.org/download.html
2
3
4
5
6
www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
7
8
http://views.grails.org/latest/
十八、Swing 图形
Swing 是用于构建跨平台 GUI(图形用户界面)的 Java API。
如果你想写一个图形程序(例如,一个电脑游戏),你必须使用 Swing,或者 JavaFX,或者类似的东西。
在 Java 中有许多其他的图形库,但是 Swing 是内置的。
你好窗户
图形最基本的概念是把东西放到屏幕上。
在 Swing 中最简单的方法是使用JWindow,例如:
1 import javax.swing.*; import java.awt.Graphics;
2
3 public class HelloWindow extends JWindow {
4
5 public HelloWindow() {
6 setSize(500, 500); //width, height
7 setAlwaysOnTop(true);
8 setVisible(true);
9 }
10
11 @Override
12 public void paint(Graphics g) {
13 g.setFont(g.getFont().deriveFont(20f));
14 g.drawString("Hello Window", 10, 20); //x,y
15 }
16
17 public static void main(String[] args) {
18 new HelloWindow();
19 }
20
21 }
运行这段代码将在屏幕的左上方创建一个窗口,上面印有“Hello Window”字样。它看起来应该如图 18-1 所示。
图 18-1
你好窗口
在构造器中,会发生以下情况:
-
窗口的宽度和高度都设置为 500 像素。
-
用
setAlwaysOnTop方法将窗口设置为总是显示(在所有其他窗口之上)。 -
最后,调用
setVisible(true)使窗口可见。
每次在屏幕上绘制窗口时,都会调用paint方法。这种方法只完成以下工作:
-
将字体大小设置为 20
-
在坐标 x=10,y=20 处绘制字符串“
Hello World”(坐标始终以像素为单位)
你可能会注意到“窗口”没有你习惯的任何边缘、标题、菜单或最小化/最大化图标(要关闭它,你需要按下 Ctrl+C )。要得到这些东西,你用一个JFrame。这里有一个非常简单的例子:
1 import javax.swing.*;
2
3 public class HelloFrame extends JFrame {
4
5 public HelloFrame() {
6 super("Hello");
7 setSize(500, 500); //width, height
8 setAlwaysOnTop(true);
9 setVisible(true);
10 setDefaultCloseOperation(EXIT_ON_CLOSE);
11 }
12
13 public static void main(String[] args) {
14 new HelloFrame();
15 }
16
17 }
运行这段代码创建一个 500×500 的“带框架的窗口”,名称为“Hello”(图 18-2 ),关闭窗口将退出应用程序。
图 18-2
有 JFrame 的窗户
按我的按钮
按钮是用户与程序交互的方式之一。当按钮被按下时,你可以使用一个ActionListener,例如:
JOptionPane的showMessageDialog方法类似于 JavaScript 的alert方法,显示一个弹出窗口。
1 button.addActionListener(e -> JOptionPane.showMessageDialog(this, "Hello!"));
这使用了一个 Java lambda 表达式,因为 ActionListener 有一个抽象方法,因此是一个函数接口,正如我们前面所学的。
Groovy 语法略有不同(它只需要一个{ and })。
1 button.addActionListener({e -> JOptionPane.showMessageDialog(this, "Hello!")})
Swing 有很多以单词Listener结尾的接口,比如
-
KeyListener -
MouseListener -
WindowListener
监听器模式与观察者设计模式非常相似。
假浏览器
让我们做一个网络浏览器吧!
让我们从添加必要的导入开始:
1 import java.awt.*;
2 import java.awt.event.*;
3 import java.io.*;
4 import java.net.*;
5 import javax.swing.*;
然后,让我们继续为该类创建字段和构造器,如下所示:
1 public class Browser extends JFrame {
2
3 JTextField urlField = new JTextField();
4 JEditorPane viewer = new JEditorPane();
5 JScrollPane pane = new JScrollPane();
6
7 public Browser() {
8 super("Browser");
9 setSize(800,600);
10 setAlwaysOnTop(true);
11 setDefaultCloseOperation(EXIT_ON_CLOSE);
12 init();
13 }
JTextField将用于输入网址。JEditorPane用于显示 HTML,JScrollPane允许页面滚动。
接下来,我们定义了init()方法来把所有东西放在一起。
1 private void init() {
2 viewer.setContentType("text/html");
3 pane.setViewportView(viewer);
4 JPanel panel = new JPanel();
5 panel.setLayout(new BorderLayout(2,2));
6 panel.add(pane, BorderLayout.CENTER);
7 panel.add(urlField, BorderLayout.NORTH);
8 setContentPane(panel);
9 urlField.addKeyListener(new KeyAdapter() {
10 @Override
11 public void keyReleased(KeyEvent e) {
12 handleKeyPress(e);
13 }
14 });
15 }
viewer被设置为JScrollPane的视口视图,因此可以滚动。
JPanel是用BorderLayout创建的。这允许我们将urlField安排在滚动窗格的顶部,就像在真正的浏览器中一样。KeyListener用于在urlField内按下一个键时调用handleKeyPress。
接下来,我们填写handleKeyPress方法。
1 private void handleKeyPress(KeyEvent e) {
2 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
3 try {
4 viewer.setPage(new URL(urlField.getText()));
5 } catch (MalformedURLException ex) {
6 ex.printStackTrace();
7 } catch (IOException ex) {
8 ex.printStackTrace();
9 }
10 }
11 }
每当按下 Enter 键时,该方法简单地将页面JEditorPane设置为来自urlField的 URL。
最后,我们定义主方法。
1 public static void main(String[] args) {
2 new Browser().setVisible(true);
3 }
从第十七章开始运行你的应用。打开你的假浏览器,指向http://localhost:8000/处的 app。它应该看起来像图 18-3 。
图 18-3
运行假浏览器
狮身鹫首的怪兽
Griffon 1 是一个受 Grails 启发的桌面应用平台。它是用 Java 编写的,所以可以从纯 Java 中使用,但是使用 Groovy 增加了额外的功能。
首先安装懒骨头 2 和 Gradle。您可以使用以下命令安装它们:
$ curl -s http://get.sdkman.io | bash
$ sdk install lazybones
$ sdk install gradle
接下来编辑 lazybones 配置文件以添加griffon-lazybones-templates存储库。编辑$USER_HOME/.lazybones/config.groovy并输入以下内容:
bintrayRepositories = [
"griffon/griffon-lazybones-templates",
"pledbrook/lazybones-templates"
]
要开始新的项目类型,请使用以下命令:
$ lazybones create griffon-swing-groovy griffon-example
这将使用 Groovy 和 Swing 创建一个名为griffon-example的项目。对每个提示填写适当的响应(它将要求您提供包、版本、类名和其他值)。使用lazybones list命令查看其他类型的项目是可能的。
Griffon 使用 MVC 设计模式和 Groovy DSL 使构建 Swing 应用程序变得更加容易。
高级图形
虽然远远超出了本书的范围,但有几个库可以用于 2D 或 3D 图形。以下是其中的一些:
爪哇 2D
-
Java FX3
-
jfree chart【4】
-
小 2d【5】
-
jmagick【6】
Java 3D
-
jog【7】
-
jmmonkey yengene【8】
二维 JavaScript
-
D3 . js9
-
高图【10】
JavaScript 3D
- 三. js 11
图形词汇表
-
组件:Java 图形 API 中定义的任何图形元素。
-
双缓冲:图形中使用的一种技术,其中元素在被发送到计算机屏幕之前被绘制在内存中。这避免了闪烁。
-
框架:在 Swing 中,框架(JFrame)用来表示我们通常所说的 GUI 中的“窗口”。
-
GUI :图形用户界面。
-
布局:Swing 在面板或其他组件中排列组件时使用的策略。
-
菜单:有两种菜单:windows 内置菜单(JMenu)和弹出菜单(JPopupMenu)。
-
菜单项:在 Swing 中,
JMenuItem表示菜单中的一行,可以有与之相关联的动作。 -
面板:在 Swing 中,
JPanel用来包含其他组件。 -
像素:可绘制的屏幕最小单位。典型的屏幕有数百万像素排列在一个网格中。
-
窗口:屏幕的矩形部分。在 Swing 中,Window 对象没有边框,所以它可以用于一个闪屏图像。
摘要
您刚刚学到了以下内容:
-
用 Java 和 Groovy 创建跨平台 GUI
-
如何让网页浏览器比 IE 差
-
一些可用的图形库
2
https://github.com/pledbrook/lazybones
3
4
5
6
https://github.com/techblue/jmagick
7
http://download.java.net/media/jogl/www/
8
9
10
11