1. 简介
假设我们要开发一个网站,顾客可以在上面购买课程和电子书。购买后,客户通常还希望提供 PDF 格式的电子发票。作为网站的所有者,你可以选择用 MS Word 手动创建这些发票。或者,用更现代化的方式,与一个基于 Web 的产品 MyFancyPDFInvoices 集成。MyFancyPDFInvoices 提供了 REST API,你可以用一些购买数据参数调用它,然后就可以得到一个格式良好的 PDF。
比如,我们要给 user_id
为 freddieFox
用户开一张的 amount
为 50
的 PDF 发票,对该 REST 服务一个简化的调用应该这样的:
POST /invoices?user_id=freddieFox&amount=50
作为这次 REST 调用的结果,我们会得到返回的一个 JSON 对象,其中包含生成的 PDF 的 URL,然后我们就可以下载并与客户共享。
{
"id": 1,
"user_id": "freddieFox",
"amount": 50,
"pdf_url": "https://cdn.myfancypdfinvoices.com/invoice-freddieFox-1.pdf"
}
目标
我们的任务是用纯 Java、一个嵌入式 Tomcat 和一个小型的、自己编写的依赖注入框架创建这个 MyFancyPDFInvoices REST 服务。但是,实际 PDF 生成是“假的”。通过这种方式,你可以学到很多关于用纯 Java 创建实际有用的 Web 应用的知识,而不必过于担心具体的细节,比如 PDF 生成是如何工作的。
理论说得够多了。你已经打开 IDE 了吗?我们开始吧!
2. 项目设置
在本节,我们将为 MyFancyPDFInvoices REST 服务创建项目骨架。
更重要的是,我们将学习用 Maven 创建任何 Java 项目的方法。Maven 是 Java 生态圈中最流行的两个构建工具之一(另一个是 Gradle),而这就是我们要用它的原因。
用 Maven 设置 Java web 应用程序
由于在你的机器上安装 Maven 本课程的前提条件之一,因此请转到你喜欢的任何文件夹,然后为项目创建一个文件夹myfancypdfinvoices
。
在这个文件夹中再创建一个名为 pom.xml
的空文件,这样最终的目录结构看起来就像这样(Windows 上):
c:\dev\myfancypdfinvoices
c:\dev\myfancypdfinvoices\pom.xml
然后,打开 pom.xml
文件,添加如下内容:
<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>com.lovoinfo</groupId>
<artifactId>myfancypdfinvoices</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>14</maven.compiler.source>
<maven.compiler.target>14</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Still Empty For Now -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
</project>
我们把它分解一下。
<groupId>com.marcobehler</groupId>
<artifactId>myfancypdfinvoices</artifactId>
<version>1.0-SNAPSHOT</version>
<groupId>
中填写域名,这是自定义的,可以写上你自己的域名,或者就编造一个域名。因此,这里我用的是com.lovoinfo
,你可以相应地选择你自己的 groupId。<artifactId>
中填写项目的实际名称。因此这里选的是myfancypdfinvoices
。- 将
<version>
保留为1.0-SNAPSHOT
,这是 Maven 的默认值。
<packaging>jar</packaging>
可以省略这个标记,因为如果缺少它,Maven 会自动暗示它。但是,如果你要写,请确保将其设置为jar
,这意味着在你用 Maven 构建项目之后,会得到一个漂亮的小 .jar
文件作为输出。
历史上,Web 应用程序应该设置为 <packaging>war</packaging>
。打包后我们会得到一个 WAR 文件,然后将它放到一个 Servlet 容器(比如 Tomcat)中来执行我们的 Web 应用程序。
而现在,Spring Boot 等框架会预先配置 Maven 生成所谓普通 jar 文件。它们之所以称为普通,是因为这些 .jar
文件包含 Tomcat 和 所有其他第三方库,所有这些都在一个文件中。你可以像这样运行它们:
java -jar yourjarfile.jar
本教程中我们用一样的做法,这样你就可以理解像这样打包应用程序并非 Spring Boot 固有的,你可以用任何 Maven 下国母来实现这一点。
<maven.compiler.source>14</maven.compiler.source>
<maven.compiler.target>14</maven.compiler.target>
我的机器上装的是 Java 14,因此 pom.xml 文件中的编译器标记部分显示的就是 14
。请确保将这个版本号替换为你自己已安装的Java 的版本号。
<dependencies>
<!-- Still Empty For Now -->
</dependencies>
你的项目目前还没有用到任何第三方外部库,因此 <dependencies>
还是空的。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
虽然不是严格要求,不过最好是明确指定你的项目要使用的 maven-compiler-plugin
版本,因为较早的编译器插件版本不能与较新的Java版本(如11或14)一起正常工作。
根据您的Maven安装附带的插件版本,这可能会导致不一致的构建错误。
maven-compiler-plugin:3.8.1
或更高版本能正确支持较新的 Java 版本。
检查点:用 Maven 设置一个 Java web 应用程序
要确定是否做对了,请转到你的新目录,并执行命令 mvn validate
。
cd c:\dev\myfancypdfinvoices
mvn validate
你应该看到类似这样的输出:
C:\dev\myfancypdfinvoices>mvn validate
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------< com.lovoinfo:myfancypdfinvoices >-----------------
[INFO] Building myfancypdfinvoices 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.203 s
[INFO] Finished at: 2020-06-19T11:05:16+01:00
[INFO] ------------------------------------------------------------------------
现在,你可以开始让你的 Web 应用程序交付第一个 HTML 页面了。
3. 渲染 HTML 页面
这里我们不会立即开始为 MyFancyPDFInvoes 应用程序编写 REST 服务。
下面我们首先进行一些小步骤,显示一个简单的 hello-world HTML 页面。
要做到这一点,或者更确切地说,在 Java 语言中几乎任何与 Web 相关的事情,都是通过 Servlet API 实现。
用 HttpServlet 显示网页
要开始使用 Servlet,我们需要两件东西。
- 一个 Servlet 容器,用来运行 Servlet,并且让 Servlet 在你打开浏览器,转到 http://localhost:8080 时马上可用。Java 中流行的 Servlet 容器有 Tomcat 或者 Jetty。
- HttpServlet,包含显示 HTML 页面的代码。
过去我们必须把 Tomcat 下载到自己的电脑上,把应用程序的 .war
文件复制到 Tomcat 目录,然后启动 Tomcat。但是请记住,现在我们是在把应用程序打包成一个 .jar
文件,也就是说 Tomcat 会放在 .jar
文件内,我们必须嵌入它。
好在有一个可嵌入的 Tomcat 版本,下一步我们就把它添加到项目中。
嵌入 Tomcat
要嵌入 Tomcat,请打开 pom.xml
文件,把下面的代码块添加进去,这样就可以把最新稳定的 Tomcat 9.0.36 版添加到项目中。确切的次要版本并不重要,不过一定要选择 9.x 甚至 8.x,不要太旧了。
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.36</version>
</dependency>
</dependencies>
tomcat-embed-core
库就是我们开始用 Java 编写 Web 应用程序所需要的。
现在,创建Maven期望正常工作的以下目录,并确保将com/marcobehler目录替换为您的*pom.xml*
文件中的*groupId*
目录。 默认做法是将您的包结构命名为您的groupId。
## Windows
cd c:\dev\myfancypdfinvoices
mkdir src\main\java\com\marcobehler
## Linux
cd /home/marco/myfancypdfinvoices
mkdir -p src/main/java/com/marcobehler
然后,最后是在新创建的目录中创建第一个 HttpServlet
的时候了,它会打印出一个非常简单的 HTML 页面。
package com.marcobehler;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyFirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html; charset=UTF-8");
response.getWriter().print(
"<html>\n" +
"<body>\n" +
"<h1>Hello World</h1>\n" +
"<p>This is my very first, embedded Tomcat, HTML Page!</p>\n" +
"</body>\n" +
"</html>");
}
}
我们把这段代码分解一下。
response.setContentType("text/html; charset=UTF-8");
这是在设置 Content-Type
标头,让浏览器知道是在发送 text/html
;如果是发送 JSON,就应该是 application/json
;如果是发送 XML,就应该是 application/xml
。像 Spring MVC 这样的现代框架会自动把这事干了。
response.getWriter().print(
"<html>\n" +
"<body>\n" +
"<h1>Hello World</h1>\n" +
"<p>This is my very first, embedded Tomcat, HTML Page!</p>\n" +
"</body>\n" +
"</html>");
HTML 页面其实就是字符串而已。因此,生成 HTML 页面最简单的方式(但不是最容易维护的方式)就是通过拼接字符串来生成 HTML 页面,然后直接把字符串写到 HttpServletResponse 中,最后由 Servlet 容器发送到浏览器。
尽管像 Spring Security 这类框架也经常采用这种揉意大利面条的方式(参见示例),但现实中我们不一定会这样做。不过,对于我们的 Web 应用程序的发展来说,这是一个很好的开始。
启动 Tomcat 并注册 HttpServlet
我们的 servlet 已经准备好了,但是还需要配置和启动 Tomcat,并告诉它有关我们的 HttpServlet 的信息。为此,我们最好是创建一个 ApplicationLauncher 类,这个类带有一个简单的 main()
方法,如下所示:
package com.marcobehler;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
public class ApplicationLauncher {
public static void main(String[] args) throws LifecycleException {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.getConnector();
Context ctx = tomcat.addContext("", null);
Wrapper servlet = Tomcat.addServlet(ctx, "myFirstServlet", new MyFirstServlet());
servlet.setLoadOnStartup(1);
servlet.addMapping("/*");
tomcat.start();
}
}
我们把代码分解一下。
Tomcat tomcat = new Tomcat();
实例化一个新的 Tomcat 很简单,只需要调用 new Tomcat()
即可。不过这还不能运行 Tomcat,但允许我们对它进行配置。
tomcat.setPort(8080);
在 Java 生态圈中,在端口 8080 上启动 Servlet 容器是惯例了,这样我们就能打开浏览器,并转到 [http://localhost:8080](http://localhost:8080/)
。不过,放任何你喜欢的端口都是可以的。
tomcat.getConnector();
这一行看起来好像什么都不做,但是会引导 Tomcat 的 HTTP 引擎。不幸的是,这一行是必需的,而且其 API的副产品有点古怪。如果漏掉这一行的话,就无法连接到 [http://localhost:8080](http://localhost:8080/)
。
Context ctx = tomcat.addContext("", null);
第二个参数是 docBase
,这是对包含 Tomcat 可以交付的静态文件的目录的引用。因为我们没打算在这里放任何静态文件,所以指定 null
就可以了。
Wrapper servlet = Tomcat.addServlet(ctx, "myFirstServlet", new MyFirstServlet());
我们需要将我们的 Servlet 添加到 Tomcat。第二个参数(就是 Servlet的名称)实际上并不重要,只要不与另一个注册的 Servlet 冲突就可以了。
servlet.setLoadOnStartup(1);
启动 Tomcat 不会自动加载我们的 Servlet,而是在第一个 HTTP 请求时初始化 Servlet。如果想立即启动它,可以设置 loadOnStartup
为 1
。
servlet.addMapping("/*");
这行代码是告诉 Tomcat:你的 Servlet 应该对任何以 /
开头的请求作出反应,即所有到来的请求,无论是[http://localhost:8080/register](http://localhost:8080/register)
、[http://localhost:8080/login](http://localhost:8080/login)
、[http://localhost:8080/anyOtherUrl](http://localhost:8080/anyOtherUrl)
等等。
tomcat.start();
最后,我们需要启动 Tomcat。
检查点:HttpServlet 和 网页
现在有两种方式可以运行您的应用程序:
- 只需用 IDE 执行
ApplicationLauncher
类的 main 方法就可以了。 - 如果不用 IDE(不推荐),可以在项目中执行
mvn compile exec:java -Dexec.mainClass="com.marcobehler.myfancypdfinvoices.ApplicationLauncher"
命令(必须确保 mainClass 指向正确的包)。这条命令会编译我们的代码,然后指定的编译过的类。包括为所有第三方依赖设置正确的类路径。
好了,运行我们的应用程序。如果一切正常,那么打开浏览器,转到http://localhost:8080,应该就可以看到如下图所示的页面:
应用程序打包
下面试试把我们的应用打包成一个 .jar
文件,看看能否运行(这就是按 Spring Boot 方式运行应用程序)。转到项目目录,执行 mvn package
命令。
## Windows
cd c:\dev\myfancypdfinvoices
mvn package
应该能看到类似如下的输出:
## Windows
C:\dev\myfancypdfinvoices>mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------< com.marcobehler:myfancypdfinvoices >-----------------
[INFO] Building myfancypdfinvoices 1.0-SNAPSHOT
....
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myfancypdfinvoices ---
[INFO] Building jar: C:\dev\myfancypdfinvoices\target\myfancypdfinvoices-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.093 s
[INFO] Finished at: 2020-06-19T13:55:16+01:00
[INFO] ------------------------------------------------------------------------
结尾处有一行有点意思。
[INFO] Building jar: C:\dev\myfancypdfinvoices\target\myfancypdfinvoices-1.0-SNAPSHOT.jar
这一行是告诉我们:Maven 将我们的源代码打包,放到 target
目录中一个 .jar
文件中。我们试试执行一下这个 .jar
文件。
## Windows
C:\dev\myfancypdfinvoices>java -jar target\myfancypdfinvoices-1.0-SNAPSHOT.jar
应该会得到一个类似如下的错误消息:
## Windows
C:\dev\myfancypdfinvoices>java -jar target\myfancypdfinvoices-1.0-SNAPSHOT.jar
no main manifest attribute, in target\myfancypdfinvoices-1.0-SNAPSHOT.jar
这是一种令人费解的说法,即 Java 不知道在 .jar 文件中运行哪个类。它应该运行我们的 ApplicationLauncher
类,但为此,我们需要 Maven 创建一个清单(manifest)文件,并将其放入 .jar 文件中。下面我们就这么办吧。
用 Maven 添加一个 Manifest 文件
打开 pom.xml
文件,将如下插件添加到 <plugins>
部分。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.marcobehler.ApplicationLauncher</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
我们把它分解一下。
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
我们需要配置 maven-jar-plugin
,给 Maven 项目添加一个 manifest 文件。配置的时候,请注意要确保使用最新的版本(在本文写的时候是 3.2.0
)。
<configuration>
<archive>
<manifest>
<mainClass>com.marcobehler.ApplicationLauncher</mainClass>
</manifest>
</archive>
</configuration>
这里才是真正干活的地方。这是指示 Maven 创建一个 manifest.mf
文件,指向com.marcobehler.ApplicationLauncher
。请确保修改这里,让它匹配你自己的包结构!
完成这些操作后,回到命令行,重新打包,再运行项目。
## Windows
cd c:\dev\myfancypdfinvoices
mvn package
C:\dev\myfancypdfinvoices>java -jar target\myfancypdfinvoices-1.0-SNAPSHOT.jar
我们会得到一条错误消息,指向 NoClassDefFoundError
:
C:\dev\myfancypdfinvoices>java -jar target\myfancypdfinvoices-1.0-SNAPSHOT.jar
Error: Unable to initialize main class com.marcobehler.ApplicationLauncher
Caused by: java.lang.NoClassDefFoundError: javax/servlet/Servlet
这是啥玩意呢?不幸的是,Maven 有一个小小的不便之处,就是:在我们创建 .jar 文件时,默认情况下,Maven 只会把我们自己写的源代码放入 .jar 文件中,不会把项目所依赖的外部库放入!
通过查看 import 语句,我们可以看到 ApplicationLauncher 和 MyFirstServlet 都依赖于类 javax/servlet/Servlet
(还有更多),而由于 Java 找不到它们,所以我们会得到这个错误。
这意味着,我们需要让外部库放在 .jar 文件中,为此,我们需要另一个 Maven 插件:shade 插件 maven-shade-plugin
。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.marcobehler.ApplicationLauncher</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
我们把这段配置分解一下。
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
我们是在配置最新版的 maven-shade-plugin
。
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
一旦调用 mvn package
,shade 插件就会执行其 shade
目标,确保除了我们自己的源代码外,还将所有第三方库包含到 .jar 文件中。
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.marcobehler.ApplicationLauncher</mainClass>
</transformer>
</transformers>
</configuration>
这里只是在 manifest.mf 文件中设置 mainClass
,我们为什么会需要这一块呢?
这与 shade 插件的工作方式有关。Shade 插件实际上会把每个第三方库解压缩,并放入 .jar 文件。如果一个第三方库带有一个 manifest.mf
文件,那么就可能有 N个清单文件进入 .jar 文件,彼此覆盖。
如果我们在这里指定了主类,Shade 插件会确保最终的清单文件会指定正确的类。这也意味着你可以随时删除上一步中刚刚添加的 maven-jar-plugin
。
好了,设置已经足够了。下面我们看看是否能起作用。
检查点:执行得到的 .jar 文件
现在我们该知道执行的步骤:
## Windows
cd c:\dev\myfancypdfinvoices
mvn package
C:\dev\myfancypdfinvoices>java -jar target\myfancypdfinvoices-1.0-SNAPSHOT.jar
注意:运行 shade-plugin 时,它会在项目文件夹下创建一个临时文件
dependency-reduce.pom.xml
,而且不会删掉。不过,我们自己可以安全地忽略它或者删除它。
这次我们应该得到不同的输出:
C:\dev\myfancypdfinvoices>java -jar target\myfancypdfinvoices-1.0-SNAPSHOT.jar
Jun 19, 2020 2:18:14 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
Jun 19, 2020 2:18:15 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
Jun 19, 2020 2:18:15 PM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/9.0.36]
Jun 19, 2020 2:18:15 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
如果还是收到一条错误消息,请确保关闭在 IDE 中启动的正在运行的 Tomcat 实例。
这就是说,我们能打开浏览器,转到 [http://localhost:8080](http://localhost:8080/)
了。试试看!
恭喜!我们刚刚创建了第一个带有嵌入式 Tomcat 的可执行的 .jar 文件,这个文件可以提供 HTML 页面。这也不是那么难,对吧?
为什么要这么做?
要得到一个可运行的 .jar 文件,我们得写大量的样板代码。有趣的是,没有办法绕过这一点,也就是说,必须有人来干这种垃圾活。如果用惯了 Spring Boot,你可能会想:我一生从来就没有一次需要对外部库进行 shade。是的,你是对的。
不过,只是因为有了 Spring Boot Maven plugin
,这个插件会自动添加到通过initializr创建的所有 Spring Boot 项目中,尽力让你免受这种工作的折磨(不过它做 shade 略有不同)。它依然必须在背后完成。
如果我们用的不是 Spring Boot,而是另一个 Web 框架,我们就祈祷它有一个类似的插件,否则我们就得自己干这种垃圾活。
要点:希望你在现实生活中不必多遮遮掩掩,但自己做一次仍然是一个很好的练习。
接下来,我们打算让 Servlet 讲一些 JSON!
JSON 端点
写一个假 HTML 页面是一项不错的成就,不过下面我们继续为 MyFancyPDFInvoices 开发一些实际的业务功能:列出和创建发票的 JSON 端点。
为浏览器的 GET 请求创建一个 JSON 端点
下面我们从简单的端点开始,获取发票。最后,希望能调用 http://localhost:8080/invoices
,获取我们的系统有的所有发表的一个 JSON 列表。
这个 JSON 可能是如下这样的:
[
{
"id": 1,
"user_id": "freddieFox",
"amount": 50,
"pdf_url": "https://cdn.myfancypdfinvoices.com/invoice-freddieFox-1.pdf"
},
{
"id": 2,
"user_id": "americanAirlines",
"amount": 1000,
"pdf_url": "https://cdn.myfancypdfinvoices.com/invoice-americanAirlines-1.pdf"
}
]
在开始创建 JSON 之前,我们先采取更简单的步骤。请记住,目前我们有一个 Servlet(MyFirstServlet
),它监听每个传入的请求。
因此,我们需要对它进行重构,让它可以对传入的不同请求 URI(比如 /
或者 /invoices
),做出不同的响应。我们先把这个 Servlet 重命名为 MyFancyPdfInvoicesServlet
。
重构后的代码应该是这样子的:
package com.marcobehler;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyFancyPdfInvoicesServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getRequestURI().equalsIgnoreCase("/")) {
response.setContentType("text/html; charset=UTF-8");
response.getWriter().print(
"<html>\n" +
"<body>\n" +
"<h1>Hello World</h1>\n" +
"<p>This is my very first, embedded Tomcat, HTML Page!</p>\n" +
"</body>\n" +
"</html>");
}
else if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
response.setContentType("application/json; charset=UTF-8");
response.getWriter().print("[]");
}
}
}
我们把代码分解一下。
public class MyFancyPdfInvoicesServlet extends HttpServlet {
Servlet 的名称现在是 MyFancyPdfInvoicesServlet
。
if (request.getRequestURI().equalsIgnoreCase("/")) {
}
else if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
}
Servlet 现在现在检查传入的请求 URI,为 /
显示一个 HTML 页面,为 /invoices
返回 JSON。
else if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
response.setContentType("application/json; charset=UTF-8");
response.getWriter().print("[]");
}
通过将 HTTP 响应的 content-type 设置为 application/json
,让浏览器知道你实际上是在给它发送 JSON。
目前,发送 JSON 的最简单的方法是,将一个空 JSON 数组 []
作为一个简单字符串返回。在系统中开始有实际的发票之前,这是一个查看一切是否正常的很好的检查点。
检查点:为浏览器的 GET 请求创建一个 JSON 端点
重启我们的应用程序(即 ApplicationLauncher 的 main 方法),并转到 [http://localhost:8080/invoices](http://localhost:8080/invoices)
。
我们应该能在浏览器中看到空的 []
JSON 数组。
为浏览器的 POST 请求创建一个 JSON 端点
在用真发票替换这个空的伪造的 JSON 数组之前,我们需要让用户创建一个发票,也就是说我们需要创建一个新的 REST 端点。
为生成发票,目前我们只需要一个 user_id
和一个 amount
作为输入参数,然后我们就会得到一个带有生成的 PDF 的 id
和 pdf_url
的 JSON 对象:
{
"id": 1,
"user_id": "freddieFox",
"amount": 50,
"pdf_url": "https://cdn.myfancypdfinvoices.com/invoice-freddieFox-1.pdf"
}
最后,我们希望能用 user_id
和 amount
作为 POST 请求体的参数(即 user_id=5&amount=50
),对 [http://localhost:8080](http://localhost:8080/)
发出一次 POST 请求。这意味着几件事情:
MyFancyPdfInvoicesServlet
目前只能处理GET
请求,因此我们还需要让它能处理POST
请求。- Servlet 需要能读取
user_id
和amount
参数值。 - 我们还需要创建一个
invoice
对象,而这个对象的类我们还没写。 - 我们需要把这个发票转换成 JSON,并且为此我们还需要一个第三方库,因为 Java 没有提供任何方便的内置的 JSON 功能。
创建 Invoice 类
下面我们先从中间开始,编写 Invoice
类。目前,我们把它与其它类一起放在同一个包中(后面会改)。这个类有 4 个属性:
- 一个唯一的
id
。 - 发票所属的
userId
。 - 一个
amount
。 - 一个
pdfUrl
。请记住,在本课程中我们不打算实现真正的 PDF 生成,而是返回一个虚拟的 PDF URL。
package com.marcobehler;
import java.util.UUID;
public class Invoice {
private String id, userId, pdfUrl;
private Integer amount;
public Invoice() {
}
public Invoice(String userId, Integer amount, String pdfUrl) {
this.id = UUID.randomUUID().toString();
this.userId = userId;
this.pdfUrl = pdfUrl;
this.amount = amount;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPdfUrl() {
return pdfUrl;
}
public void setPdfUrl(String pdfUrl) {
this.pdfUrl = pdfUrl;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
}
我们把代码分解一下。
import java.util.UUID;
public class Invoice {
private String id, userId, pdfUrl;
这里是每张发票所需的四个字段。
private Integer amount;
public Invoice() {
}
public Invoice(String userId, Integer amount, String pdfUrl) {
this.id = UUID.randomUUID().toString();
this.userId = userId;
this.pdfUrl = pdfUrl;
除了默认的 Invoice()
构造器外,还有一个额外的辅助构造器。通过这个辅助构造器,我们只要传入一个 userId
、pdfUrl
和 amount
,它就会自动生成一个随机的 id
。
创建 InvoiceService 类
实际的发票创建将在 InvoiceService
类中发生。这个类我们也必须创建,它应该是这样子:
package com.marcobehler;
public class InvoiceService {
public Invoice create(String userId, Integer amount) {
// TODO 真实 pdf 创建,并将其存在网络服务器上
return new Invoice(userId, amount, "http://www.africau.edu/images/default/sample.pdf");
}
}
下面我们把这段代码分解一下。
public Invoice create(String userId, Integer amount) {
InvoiceService
带有一个方法 create()
,该方法有两个参数:userId
和 amount
。
return new Invoice(userId, amount, "http://www.africau.edu/images/default/sample.pdf");
如前所属,我们不是创建一个真的 PDF,而是值返回一个 invoice
对象,该对象总是有相同的虚拟 PDF。
第三方 JSON 库
OK,我们有了领域类 Invoice
,以及 InvoiceService
。现在,我们需要给我们的 Servlet JSON 的能力。为此,需要给项目添加一个第三方 JSON 库。
在 Java 生态圈中,执行 JSON 转换的最流行的选择之一叫做Jackson。
我们需要把如下依赖添加到 pom.xml 中,将最新的 Jackson 版本添加到项目(不要担心具体的版本,因为所有 Jackson 版本都行)。
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
这些就够了。现在是时候把所有内容都弄到我们的 Servlet 中了。
public class MyFancyPdfInvoicesServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
String userId = request.getParameter("user_id");
Integer amount = Integer.valueOf(request.getParameter("amount"));
Invoice invoice = new InvoiceService().create(userId, amount);
response.setContentType("application/json; charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(invoice);
response.getWriter().print(json);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
我们把这段代码分解一下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
除了之前的 doGet()
方法以外,我们还需要重载 doPost()
方法来处理 POST 请求。
String userId = request.getParameter("user_id");
Integer amount = Integer.valueOf(request.getParameter("amount"));
我们假设调用者(用户)始终是发送 user_id
和 amount
参数。然后,我们只是获取这些值,不过对 amount 来说,需要将字符串转换为整型。
这显然那不太理想。在生产中,我们实际上需要确保在这里放上一些验证(即 null 检查),否则以后会遇到一些讨厌的异常。目前我们暂时跳过验证,但在后面 Spring 化项目时候再添加进来。
Invoice invoice = new InvoiceService().create(userId, amount);
这里我们是在实例化一个新的 InvoiceService
,然后用它来创建一个新的 invoice 对象。
String json = new ObjectMapper().writeValueAsString(invoice);
response.getWriter().print(json);
最后,我们需要将 invoice 对象转换为 JSON 字符串。我们可以用 Jackson 特定的 ObjectMapper
对象搞定这件事。
检查点:POST 到 JSON 端点
有了上面的代码,我们就可以重启应用程序,针对新的 /invoices
端点执行一个 POST 请求。我们可以用像 Postman 这样的浏览器插件或者 Intellij 内置的 REST 客户端来做这事。
如果是用 Intellij 的 REST 客户端,我们会像下面这样执行一个请求:
POST http://localhost:8080/invoices?user_id=freddieFox&amount=50
Accept: application/json
###
然后就会给我们返回一个像下面这样的 JSON 对象:这是我们的一张发票!
{
"id": "f1cc98aa-9411-40f7-bab1-3857326e1651",
"userId": "freddieFox",
"pdfUrl": "http://www.africau.edu/images/default/sample.pdf",
"amount": 50
}
但是请稍等,有点不对劲。 JSON 响应中的 userId
和 pdfUrl
都是驼峰法则写法,我们希望它们是用下划线的写法。为此,我们需要配置一下 Jackson。
public class Invoice {
private String id;
@JsonProperty("user_id")
private String userId;
@JsonProperty("pdf_url")
private String pdfUrl;
private Integer amount;
Jackson 让我们可以用 @JsonProperty
注解来注解字段或者 getter。它的值定义了结果 JSON 字符串中字段的名称。
在给 Invoice
类添加了注解后,重启应用程序,再执行请求。现在我们应该能看到如下输出:
{
"id": "0afca669-cee2-4472-b7bb-37445c9af939",
"amount": 50,
"user_id": "freddieFox",
"pdf_url": "http://www.africau.edu/images/default/sample.pdf"
}
看起来好多了!
不错,我们可以创建发票,不过依然需要修正一下之前编写的 GET 端点。
重构应用程序
InvoiceService
类目前只包含一个创建新发票的方法。下面我们给它另一个方法,让它可以找到所有现有的发票。我们打算用一个简单的、内存中的列表来存储发票(而不是用数据库这种更复杂的东西)。
package com.marcobehler;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class InvoiceService {
List<Invoice> invoices = new CopyOnWriteArrayList<>(); // (1)
public List<Invoice> findAll() {
return invoices;
}
public Invoice create(String userId, Integer amount) {
// TODO 真实 pdf 创建,并将其存在网络服务器上
Invoice invoice = new Invoice(userId, amount, "http://www.africau.edu/images/default/sample.pdf");
invoices.add(invoice);
return invoice;
}
}
下面我们把代码分解一下。
List<Invoice> invoices = new CopyOnWriteArrayList<>(); // (1)
我们将所有发票存在一个线程安全的 List 中。CopyOnWriteArrayList
是线程安全的,而 ArrayList
不是。
public List<Invoice> findAll() {
return invoices;
}
findAll()
方法只是返回该列表。
public Invoice create(String userId, Integer amount) {
// TODO 真实 pdf 创建,并将其存在网络服务器上
Invoice invoice = new Invoice(userId, amount, "http://www.africau.edu/images/default/sample.pdf");
invoices.add(invoice);
return invoice;
}
在创建 invoice 时,现在还需要确保将新 invoice 添加到 invoices
列表中。
我们的 InvoiceService
看起来不错,不过请记住,在我们的 Servlet 中还是返回一个硬编码的空 JSON []
。我们需要对这个 Servlet 做些修改,通过调用 InvoiceService,在 Jackson 的 ObjectMapper 帮助下,将结果列表转换成 JSON。
同时,我们还要稍微重构一下我们的 Servlet,不要每次来一个请求就创建这些服务的新实例。
最终的 Servlet 代码会是这样子:
package com.marcobehler;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
public class MyFancyPdfInvoicesServlet extends HttpServlet {
private InvoiceService invoiceService = new InvoiceService();
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
String userId = request.getParameter("user_id");
Integer amount = Integer.valueOf(request.getParameter("amount"));
Invoice invoice = invoiceService.create(userId, amount);
response.setContentType("application/json; charset=UTF-8");
String json = objectMapper.writeValueAsString(invoice);
response.getWriter().print(json);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getRequestURI().equalsIgnoreCase("/")) {
response.setContentType("text/html; charset=UTF-8");
response.getWriter().print(
"<html>\n" +
"<body>\n" +
"<h1>Hello World</h1>\n" +
"<p>This is my very first, embedded Tomcat, HTML Page!</p>\n" +
"</body>\n" +
"</html>");
} else if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
response.setContentType("application/json; charset=UTF-8");
List<Invoice> invoices = invoiceService.findAll(); // (2)
response.getWriter().print(objectMapper.writeValueAsString(invoices)); // (3)
}
}
}
下面我们把代码分解一下。
private InvoiceService invoiceService = new InvoiceService();
private ObjectMapper objectMapper = new ObjectMapper();
我们的 Servlet 现在有两个新变量 invoiceService
和 objectMapper
,这样就不用在每次有人调用 doGet()
或者 doPost()
方法的时候都要实例化它们。一个实例就够了。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
String userId = request.getParameter("user_id");
Integer amount = Integer.valueOf(request.getParameter("amount"));
Invoice invoice = invoiceService.create(userId, amount);
response.setContentType("application/json; charset=UTF-8");
String json = objectMapper.writeValueAsString(invoice);
response.getWriter().print(json);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
这里是把 new InvoiceService
和 new ObjectMapper
用对新字段的引用替换。
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getRequestURI().equalsIgnoreCase("/")) {
response.setContentType("text/html; charset=UTF-8");
response.getWriter().print(
"<html>\n" +
"<body>\n" +
"<h1>Hello World</h1>\n" +
"<p>This is my very first, embedded Tomcat, HTML Page!</p>\n" +
"</body>\n" +
"</html>");
} else if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
response.setContentType("application/json; charset=UTF-8");
List<Invoice> invoices = invoiceService.findAll(); // (2)
response.getWriter().print(objectMapper.writeValueAsString(invoices)); // (3)
}
}
这里是不再硬编码 []
JSON 数组,而是调用 invoiceService
获取所有发票,并用 objectMapper
将其转换成 JSON。
检查点:完整的 REST 服务
重启应用程序,打开你喜欢的 REST 客户端,做下面的事情两遍,创建两个发票:
POST http://localhost:8080/invoices?user_id=freddieFox&amount=50
Accept: application/json
Cache-Control: no-cache
###
然后执行一个 GET,应该就可以返回两张发票。
GET http://localhost:8080/invoices
Accept: application/json
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 285
Date: Mon, 22 Jun 2020 09:10:22 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
{
"id": "92b7e9de-978a-453d-9b97-5f294d233104",
"amount": 50,
"user_id": "freddieFox",
"pdf_url": "http://www.africau.edu/images/default/sample.pdf"
},
{
"id": "1cc48f53-76d0-4c44-ae93-aa4b69db3537",
"amount": 50,
"user_id": "freddieFox",
"pdf_url": "http://www.africau.edu/images/default/sample.pdf"
}
]
我们可以用 REST 客户端创建更多发票,应该总是可以通过 GET /invoices
端口获取这些发票。
虽然 pdf_url
是硬编码的,但是我们可以看到新创建的发票都有随机生成的 invoice id。
恭喜!我们刚实现了一个大的里程碑!不过还没那么快。下面我们来整理一下我们的项目。
包重构
目前我们是把所有类都放到一个包中。对于我来说,主要的包是 com.marcobehler
,所以包结构看起来就是下面这样的:
组织应用程序的方式有很多种,每种方式都各有利弊。不过,对于一个新应用程序来说,一个好的开始是:
- 将领域类(比如
invoice
),放到model
子包中。 - 将服务类(比如
InvoiceService
),放到service
子包中。 - 将 Servlet 或者与 Web 相关的类,放到
web
子包中。 - 让根包变成 {groupId.appName},比如
com.marcobehler.myfancypdfinvoices
。 - 让应用程序启动器放在根包中。
同样,这不是一成不变的,并且可以有很多变化。不过现在,将这些类移到像如下这样的新的包结构中:
超棒,现在是时候向我们的项目添加一些依赖注入了!
穷人的依赖注入
下面我们放一些依赖注入到项目中。
创建全局 Application 类
在最后一步中,我们给 MyFancyPdfInvoicesServlet
加了两个新的变量:invoiceService
和 objectMapper
。
如果只有一个 HttpServlet,而且没有其他人需要访问这两个类,那么这没问题。
不过,如果还有其它的一些类需要在 Java 和 JSON 之间转换该怎么办?然后它们也需要一个 ObjectMapper。
而且,如果其它一些模块,比如 CLI 工具或者批处理作业,需要访问我们的 InvoiceService
,该怎么办?
然后,在 MyFancyPdfInvoicesServlet
类中用私有字段就不再管用,因为你最希望有一个中心位置,在那里你可以定位所有这些服务,然后以某种方式访问它们。
如果没有 Spring,这很可能导致我们去实现 Application 类模式,这是一种依赖管理的穷人版。
在 context
子包下创建如下的 Application
类:
package com.marcobehler.myfancypdfinvoices.context;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.marcobehler.myfancypdfinvoices.services.InvoiceService;
public class Application {
public static final InvoiceService invoiceService = new InvoiceService();
public static final ObjectMapper objectMapper = new ObjectMapper();
}
这个类包含将 invoiceService
和 objectMapper
的设置为 static final 字段,从而有效地将它们变成单例。
现在我们可以清理一下我们的 Servlet了:
package com.marcobehler.myfancypdfinvoices.web;
import com.marcobehler.myfancypdfinvoices.context.Application;
import com.marcobehler.myfancypdfinvoices.model.Invoice;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
public class MyFancyPdfInvoicesServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
String userId = request.getParameter("user_id");
Integer amount = Integer.valueOf(request.getParameter("amount"));
Invoice invoice = Application.invoiceService.create(userId, amount);
response.setContentType("application/json; charset=UTF-8");
String json = Application.objectMapper.writeValueAsString(invoice);
response.getWriter().print(json);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getRequestURI().equalsIgnoreCase("/")) {
response.setContentType("text/html; charset=UTF-8");
response.getWriter().print(
"<html>\n" +
"<body>\n" +
"<h1>Hello World</h1>\n" +
"<p>This is my very first, embedded Tomcat, HTML Page!</p>\n" +
"</body>\n" +
"</html>");
} else if (request.getRequestURI().equalsIgnoreCase("/invoices")) {
response.setContentType("application/json; charset=UTF-8");
List<Invoice> invoices = Application.invoiceService.findAll();
response.getWriter().print(Application.objectMapper.writeValueAsString(invoices));
}
}
}
我们来分解一下。
Invoice invoice = Application.invoiceService.create(userId, amount);
response.setContentType("application/json; charset=UTF-8");
String json = Application.objectMapper.writeValueAsString(invoice);
response.getWriter().print(json);
List<Invoice> invoices = Application.invoiceService.findAll();
response.getWriter().print(Application.objectMapper.writeValueAsString(invoices));
这里我们不再是在 Servlet 中创建这些服务,而是从 Application
访问它们。
主要的好处是,现在我们打算写的任何其它类也都可以访问相同的服务,也可以通过 Application
类。
检查点:Application 类
确保重新启动应用程序,并检查 POST 和 GET 端点是否仍然起作用!
添加 User 类
下面我们给 InvoiceService
增加点复杂度。目前该类的 create()
方法接受一个 userId
,并且会很乐意为该用户生成发票。
然而,这并不是现实世界中发生的事情。为什么呢?因为你需要确保用户确实存在!
所以,我们来给应用程序添加一个很简单的用户领域对象,这个对象只有两个字段:id
和 name
:
package com.marcobehler.myfancypdfinvoices.model;
public class User {
private String id;
private String name;
public User() {
}
public User(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这是个很简单的 POJO,带有 getter 和 setter 以及一个额外的构造器,这样我们就能更快地创建用户。
添加 UserService
光有用户是做不了什么的,还得有一个对应的 UserService
让我们可以比如找到用户。现在我们将让它总是返回一个用户,弄出来一个该服务的虚拟实现。在后面的课程中,会用真实的数据库访问替换这个虚拟实现。
package com.marcobehler.myfancypdfinvoices.services;
import com.marcobehler.myfancypdfinvoices.model.User;
import java.util.UUID;
public class UserService {
public User findById(String id) {
String randomName = UUID.randomUUID().toString();
// 总是能找到用户,每个用户有一个随机的名字。
return new User(id, randomName);
}
}
findById()
是一个简单的方法,总是实例化并返回一个新的 User 对象,这个对象对于任何给定的 id,都带有一个随机的名字。
添加验证检查
最后,将一些验证放到 InvoiceService 中:
package com.marcobehler.myfancypdfinvoices.services;
import com.marcobehler.myfancypdfinvoices.model.Invoice;
import com.marcobehler.myfancypdfinvoices.model.User;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class InvoiceService {
List<Invoice> invoices = new CopyOnWriteArrayList<>();
public List<Invoice> findAll() {
return invoices;
}
public Invoice create(String userId, Integer amount) {
User user = new UserService().findById(userId);
if (user == null) {
throw new IllegalStateException();
}
// TODO 真实 pdf 创建,并存到网络服务器上
Invoice invoice = new Invoice(userId, amount, "http://www.africau.edu/images/default/sample.pdf");
invoices.add(invoice);
return invoice;
}
}
下面我们把代码分拆讲解。
public Invoice create(String userId, Integer amount) {
User user = new UserService().findById(userId);
在创建发票之前,现在我们要构建一个新的 UserService,检测用户是否存在。
if (user == null) {
throw new IllegalStateException();
}
如果用户不存在,只需要抛出一个异常。没有错误消息的异常不是最优的,不过现在就这样好了,后面的课程中会用更合理的方法替换。
重构全局 Application 类
在 InvoiceService
内创建一个新的 UserService
实例没有多大意义,因为我们有一个全局的 Application
类会跟踪所有其它类。那么我们来做一些重构吧。
第一步很简单,我们需要把 UserService
添加到 Application
类中:
package com.marcobehler.myfancypdfinvoices.context;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.marcobehler.myfancypdfinvoices.services.InvoiceService;
import com.marcobehler.myfancypdfinvoices.services.UserService;
public class Application {
public static final InvoiceService invoiceService = new InvoiceService();
public static final ObjectMapper objectMapper = new ObjectMapper();
public static final UserService userService = new UserService();
}
这意味着我们也可以修改 InvoiceService 中相应的行,从 Application 类获取 UserService:
public Invoice create(String userId, Integer amount) {
User user = Application.userService.findById(userId);
if (user == null) {
throw new IllegalStateException();
}
不过,这还不是最佳的。为什么呢?
因为我们的 InvoiceService
仍然需要主动再次调用 Application,才能获取 UserService
的一个实例。
简而言之,它必须知道 UserService,必须知道从哪里可以获取 UserService。
如果 InvoiceService 不必操心这,只是用 UserService,不是更好吗?
这会是很棒的,我们可以通过用一些依赖注入,将我们小小的应用程序推向这个梦想。
依赖注入第一步
首先,我们来重构一下 InvoiceService,让任何构建新 InvoiceService 的人也需要传入一个有效的 UserService。
我们的 InvoiceService 看起来会是这样的:
package com.marcobehler.myfancypdfinvoices.services;
import com.marcobehler.myfancypdfinvoices.context.Application;
import com.marcobehler.myfancypdfinvoices.model.Invoice;
import com.marcobehler.myfancypdfinvoices.model.User;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class InvoiceService {
private final UserService userService;
private List<Invoice> invoices = new CopyOnWriteArrayList<>();
public InvoiceService(UserService userService) {
this.userService = userService;
}
public List<Invoice> findAll() {
return invoices;
}
public Invoice create(String userId, Integer amount) {
User user = userService.findById(userId);
if (user == null) {
throw new IllegalStateException();
}
Invoice invoice = new Invoice(userId, amount, "http://www.africau.edu/images/default/sample.pdf");
invoices.add(invoice);
return invoice;
}
}
我们把代码分解一下解释。
public class InvoiceService {
private final UserService userService;
我们给 InvoiceService 类添加了一个新的 final UserService 字段。
public InvoiceService(UserService userService) {
this.userService = userService;
}
从现在开始,每个试图构建一个 InvoiceService
的人都需要传入一个 UserService
。
public Invoice create(String userId, Integer amount) {
User user = userService.findById(userId);
if (user == null) {
throw new IllegalStateException();
然后,create()
方法只是用这个 UserService
。
好多了,对吧?现在 InvoiceService 不需要操心模板代码了,但是可以保证获得一个可以用的注入的 UserService。
现在我们需要修复一下 Application 类,否则没法通过编译:
package com.marcobehler.myfancypdfinvoices.context;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.marcobehler.myfancypdfinvoices.services.InvoiceService;
import com.marcobehler.myfancypdfinvoices.services.UserService;
public class Application {
public static final UserService userService = new UserService();
public static final InvoiceService invoiceService = new InvoiceService(userService);
public static final ObjectMapper objectMapper = new ObjectMapper();
}
我们把代码分解一下。
public static final UserService userService = new UserService();
确保 UserService 在 InvoiceService 之前创建。
public static final InvoiceService invoiceService = new InvoiceService(userService);
在创建 InvoiceService
时,将 userService
注入。
结果
好多了!现在不是主动从 Application 类获取依赖,而是被动地在 Application 类中装配到一起。
不过,想一下,如果 UserService 还有其它iylai,即它正常工作所需要的类,又会发生什么呢?仅仅是更多都相互依赖的服务吗?
那么将所有东西都装到一个 Application 类中,过段时间后 Application 类就会变得相当麻烦和笨重。
这正是 Spring 框架的用武之地:替换这个 Application 类。
在下一个模块中我们来看看 Spring 是如何做到的!
结尾部分
我们在本模块中学了相当多的东西。下面我们快速回顾一下。
在最开始,我们学习了如何使用嵌入式 Tomcat 和 Servlet API 编写简单的 Java web 应用程序。这包括 Shade Maven 项目,这样就可以构建一个可执行的 .jar 文件。
我们还学习了如何通过 servlet API 编写 HTML 和 JSON,包括使用 Jackson 第三方依赖以及用注解配置其行为。
最终我们学习了如何在 Java 中处理类之间的依赖关系,首先是主动地处理(Application类),然后是被动地处理(构造器注入)。
这意味着我们已经为练习做好了准备。
源代码和作业
模块源代码
本模块的源代码在仓库的 #1-04-di
中:
git clone https://github.com/marcobehler/myfancypdfinvoices.git
git checkout #1-04-di
作业
建议不跳过这一部分。做作业会帮助你练习在本模块中学到的概念。
- 在不理解看本模块的备注的情况下,创建一个新的 Maven 项目
mybank
。坚持一段时间后,你可以作弊看看课程备注。但是首先,看看你是否还记得 pom.xml 文件的内容。 - 这个银行项目应该提供一个 REST API,让你找到和创建交易(transaction)。一笔交易由一个
id
,一个amount
,一个timestamp
和 一个reference
字符串(比如”吃麦当劳外卖”)组成。API 应该返回 JSON,时间邮戳(timestamp)应该格式化为yyyy-MM-dd’T’HH:mm’Z'
。请使用嵌入式的 Tomcat、一个 Servlet 以及相应的服务来完成。注意:如果你用 Java 8+ 的 datetime 作为时间邮戳,你会需要一个额外的 Jackson 库。找出是哪一个! - 在不用任何第三方库的情况下,增强你的项目,这样你就能调用
java -jar -Dserver.port=8090 {jarname}.jar*
,Tomcat 在端口 8090 上启动,而不是 8080。如果为设置系统属性,默认就是 8080 端口。 - 创建项目,并用相应的 GET/POST 端口玩玩。如果能成功创建和找到交易,请继续下一个模块。
恭喜你!夸奖一下自己吧,因为这是一项繁重的工作!
作业的解决方案
在 mybank 仓库的 #1-di
分支上可以找到这些作业的源代码。
git clone https://github.com/marcobehler/mybank.git
git checkout #1-di