Java 日志与单元测试:从基础到应用
1. 日志框架概述
在开发应用程序时,日志记录是一项重要的调试和监控手段。目前有两种主流的 Java 日志 API,它们在日志级别设置、测试、格式化、输出等方面有诸多相似之处,但也存在一些差异。
1.1 日志级别设置
日志级别是日志记录器实例的属性之一。如果层次结构中较低级别的日志记录器没有设置级别,它将采用其父日志记录器的级别。
1.2 日志测试
若日志代码要保留在应用程序中,应尽量减少对性能的影响。这两种日志 API 都使初始测试变得非常快速,以确定是否启用了日志记录。判断日志是否开启以及特定日志事件是否应发生(例如,基于日志级别)的测试,旨在尽量减少对应用程序的影响。
1.3 日志格式化
两种日志 API 都可以配置生成日志的格式。它们使用不同的类来处理此行为,但结果相同。两者都支持常见的格式,如纯文本文件,也都可以将日志输出为 XML 文档。SDK 日志记录器的默认输出格式是 XML 文档,而 log4j 的默认格式是文本文件。每个 API 都有各种格式类,形成一个层次结构,开发人员可以对其进行扩展。
以下是两种平台的格式化器列表:
| 格式化器 | API | 描述 |
| — | — | — |
| SimpleFormatter | SDK | 以人类可读的格式打印 LogRecord 的简要摘要。摘要通常为一到两行。 |
| XMLFormatter | SDK | 将 LogRecord 格式化为标准 XML 格式。DTD 规范在 Java 日志 API 规范中提供。 |
| DateLayout | log4j | 处理所有与日期相关的选项和格式化工作。 |
| HTMLLayout | log4j | 以 HTML 表格形式输出事件。 |
| PatternLayout | log4j | 允许你完全自定义日志显示的各个方面。 |
| SimpleLayout | log4j | 由日志语句的级别、“ - ”和日志消息本身组成。 |
| XMLLayout | log4j | 根据 log4j 提供的 DTD 输出 XML 文档的一部分。 |
1.4 日志输出
两种 API 都支持多种输出目的地。例如,你可以将记录日志到控制台、文件或套接字。在 log4j 中,这些输出类称为附加器(appenders),在 SDK 中称为处理程序(handlers)。API 将这些输出对象视为多播,这意味着你可以为特定的日志记录器注册任意数量的输出对象。例如,你可能希望将日志消息同时发送到控制台和文件。你可以为这两种类型的输出注册处理程序,API 将处理其余的工作。
2. SDK 日志记录
从 SDK 1.4 开始,Sun 在 SDK 中包含了一个日志记录类和其他支持类。这个类基于最佳日志记录实践,其中许多实践来自 log4j 包本身。主要的日志记录类是
java.util.logging
。它定义了日志记录器的基本特征,并为插入格式化器、输出处理程序和前面讨论的其他行为提供了钩子。
2.1 eMotherEarth 应用程序的日志记录设置
在 eMotherEarth 应用程序中,首先要确定日志记录的特征。为这个应用程序创建两个日志记录层次结构:
- 第一个日志记录器适用于整个应用程序,与顶级包名
com.nealford.art.logging.emotherearth
相关联。这是所有其他包的基础包,它将捕获整个应用程序的所有日志消息。
- 第二个日志记录器仅与边界类相关联。我们希望单独记录所有数据库访问,包括返回的记录数量和添加顺序的信息。第二个日志记录器与包
com.nealford.art.logging.emotherearth.boundary
相关联。
2.2 欢迎控制器中的日志记录设置
欢迎控制器是应用程序中第一个执行的控制器,因此在这里设置第一个日志记录器。以下是相关代码:
public class Welcome extends HttpServlet {
private static Logger logger = Logger.getLogger(
Welcome.class.getPackage().getName().substring(0,
Welcome.class.getPackage().getName().lastIndexOf('.')));
private void setupLogger() {
Handler outputHandler = null;
try {
outputHandler = new FileHandler("/tmp/basic_log.xml");
} catch (Exception x) {
logger.severe("Create handler: "+ x.getMessage());
}
if (outputHandler != null)
logger.addHandler(outputHandler);
String logLevel = getServletContext().
getInitParameter("logLevel");
if (logLevel != null)
logger.setLevel(Level.parse(logLevel.toUpperCase()));
else
logger.setLevel(Level.SEVERE);
}
}
日志记录器的声明相当复杂,但它用于捕获当前包之上的包名。该声明是静态的,使日志记录器在整个类中可用。通过
getLogger()
方法检索日志记录器,参数是标识日志记录器名称的字符串。在这种情况下,我们获取当前包的名称并去掉最后一个术语,将顶级包名作为日志记录器名称。当我们在这个应用程序中创建其他日志记录器时,它们将继承
setupLogger()
方法中创建的特征(如级别和处理程序)。
setupLogger()
方法创建一个与文件关联的输出处理程序。它还根据 web 配置文件中的设置设置日志级别。这样,你可以在不重新编译应用程序的情况下设置日志级别。
2.3 日志记录的使用示例
以下是使用日志记录器输出消息的代码示例:
public void init() throws ServletException {
setupLogger();
logger.entering(this.getClass().getName(), "init()");
String driverClass =
getServletContext().getInitParameter("driverClass");
String password =
getServletContext().getInitParameter("password");
String dbUrl =
getServletContext().getInitParameter("dbUrl");
String user =
getServletContext().getInitParameter("user");
DBPool dbPool =
createConnectionPool(driverClass, password, dbUrl,
user);
getServletContext().setAttribute("dbPool", dbPool);
logger.exiting(this.getClass().getName(), "init()");
}
SDK 日志记录器有方便的方法用于记录进入和退出方法,这是常见的日志记录任务。这些方法映射到
Level.FINER
日志级别。
2.4 其他日志记录器的设置
设置继承这些特征的类的其他日志记录器很容易。例如,目录控制器有一个简单的声明,创建一个具有继承属性的日志记录器:
public class Catalog extends HttpServlet {
static Logger logger =Logger.getLogger(Catalog.class.getName());
}
这些日志记录器的输出将出现在欢迎 servlet 中指定的 XML 文档中。
2.5 边界类的日志记录
另一个为这个应用程序建立的日志记录器与边界类相关联。我们希望在单独的日志文件中记录数据库访问信息。为此,在
ProductDb
类中创建一个新的命名日志记录器,这是访问的第一个边界类:
public class ProductDb {
private static Logger logger = Logger.getLogger(
ProductDb.class.getPackage().getName());
}
与之前一样,我们在第一个访问的类中为这个日志记录器分配属性,每个子类从日志记录器层次结构中继承这些属性。这组边界日志记录器的独特属性是一个具有不同名称的日志文件,其中只包含数据库信息。
以下是边界类的
getProduct()
方法,它记录进入、退出和对日志文件读者感兴趣的信息:
public Product getProduct(int id) {
logger.entering(this.getClass().getName(), "getProduct");
Iterator it = getProductList().iterator();
while (it.hasNext()) {
Product p = (Product) it.next();
if (p.getId() == id) {
logger.info("Found product: " + p);
return p;
}
}
logger.info("Product for id[" + id + "] not found");
return null;
}
日志记录方法
logger.log()
是默认的日志记录方法,它包括一个接受日志级别、字符串和对象的重载版本。为了减少额外的输入,
Logger
类还包括一些已经为你设置好级别的方法,你只需要提供描述即可。例如,
logger.info()
就是一个在
INFO
级别记录消息的便利方法。
3. log4j 日志记录
SDK 中包含的日志 API 的一个缺点是它出现得较晚(在 1.4 SDK 中)。如果你使用的是早期版本的 SDK(例如,因为你的应用程序服务器尚未更新到最新版本),你无法使用它。但是,你可以使用 log4j。它是一个开源项目,已经存在很长时间,代码库经过了优化和完善。实际上,它对 SDK 中的日志 API 设计产生了影响。
3.1 eMotherEarth 应用程序的 log4j 日志记录设置
log4j 支持前面提到的所有功能。虽然类名不同,但它具有与 SDK 日志记录器相同的功能(在某些情况下更多)。使用 log4j 的示例在源代码存档中名为
art_emotherearth_log4j
。
日志记录器的设置方式与之前相同,使用相同的层次结构。创建顶级日志记录器的代码出现在欢迎控制器中:
public class Welcome extends HttpServlet {
static Logger logger = Logger.getLogger(
Welcome.class.getPackage().getName().substring(0,
Welcome.class.getPackage().getName().lastIndexOf('.')));
private void setupLogger() {
Appender outputAppender = null;
try {
outputAppender = new FileAppender(new XMLLayout(),
"/temp/log4j_basic.xml");
} catch (IOException x) {
logger.error("Appender error", x);
}
logger.addAppender(outputAppender);
String logLevel = getServletContext().
getInitParameter("logLevel");
if (logLevel != null)
logger.setLevel(Level.toLevel(logLevel));
else
logger.setLevel(Level.FATAL);
}
}
与 SDK 日志记录器的主要区别在于创建附加器的分层语法以及将字符串表示的级别转换为
Level
表示的技术。SDK 使用静态
parse()
方法进行转换,而 log4j 使用静态
toLevel()
方法。这两种方法没有优劣之分,只是实现相同结果的两种方式。
3.2 log4j 中的日志进入和退出记录
在 SDK 中存在
entering()
和
exiting()
方法,而在 log4j 中,你必须调用
info()
方法作为替代。以下是使用 log4j 记录进入和退出的示例:
private void forwardToView(HttpServletRequest request,
HttpServletResponse response) throws
ServletException, IOException {
logger.info(this.getClass().getName()+"fowardToView enter");
RequestDispatcher dispatcher = request.getRequestDispatcher(
"/CatalogView.jsp");
dispatcher.forward(request, response);
logger.info(this.getClass().getName()+"forwardToView exit");
}
3.3 log4j 的 XML 输出格式
两种 API 生成的 XML 格式也有所不同。log4j 不会尝试生成完整的 XML 文档,而是生成适合嵌入到更大 XML 文档中的 XML 片段。以下是为示例应用程序创建的日志文件的一部分:
<log4j:event
logger="com.nealford.art.logging.emotherearth"
timestamp="1046666191595"
level="INFO"
thread="HttpProcessor[8080][0]">
<log4j:message><![CDATA[
com.nealford.art.logging.emotherearth.controller.Welcome:
init() entry]]></log4j:message>
</log4j:event>
<log4j:event
logger="com.nealford.art.logging.emotherearth"
timestamp="1046666191635"
level="INFO"
thread="HttpProcessor[8080][0]">
<log4j:message><![CDATA[
com.nealford.art.logging.emotherearth.controller.Welcome:
init() exit]]></log4j:message>
</log4j:event>
这种 XML 格式比 SDK 提供的格式更“原始”。然而,log4j 为格式化器提供了许多自定义功能。开发人员可以轻松地对内置的
XMLFormatter
进行子类化,并生成他们喜欢的任何 XML 格式。
3.4 log4j 的配置优势
log4j 有广泛的配置选项。一个重要的方面是它易于安装到 Web 应用程序中。log4j 的文档包含了大量关于如何为 Web 应用程序最佳配置它的信息,包括资源文件的放置位置以及如何通过配置文档控制输出和其他属性。在这方面,log4j 优于 SDK。
4. 选择日志框架
两种框架都提供了日志记录的重要方面,并且都是最先进的日志记录包。它们有许多相似之处,虽然有些类的名称不同,但基本功能相同。以下是选择适合你的框架的标准:
-
选择 SDK 日志框架的情况
:
- 你不想使用第三方框架。
- 你使用的是 SDK 1.4 或更高版本。
- 你不需要复杂的配置信息来设置属性。
-
log4j 更合适的情况
:
- 你无法使用最新的 SDK。
- 你需要更多地控制日志条目的格式。
- 你需要大量控制配置信息。
如果你必须使用早期版本的 SDK,log4j 显然是更好的选择。与 SDK 日志功能相比,log4j 有更长的产品周期。然而,日志记录已经被广泛理解,因此两个包的稳定性都不应受到质疑。实际上,SDK 日志包很可能大量基于 log4j,这也解释了它们之间的许多相似之处。如果你在应用程序中需要日志记录,选择任何一个框架都不会出错。
5. 框架中的日志记录
日志记录通常与框架无关,这意味着它对应用程序的设计或实现没有影响。没有一个框架对日志记录有敌意,日志记录通常超出了框架管理的范围。
一些框架已经包含了 log4j,它在 Java 世界中几乎被普遍使用。Tapestry 和 WebWork 都已经包含了 log4j,并且有特定的配置参数允许你进行设置。
6. 单元测试概述
单元测试是近年来备受关注的关键最佳实践之一。单元测试是指对类的方法的原子功能进行测试。它与功能测试在范围上有所不同。理想的单元测试会检查方法行为的一个小方面。单元测试通常规模较小且具有内聚性。一个方法生成多个测试方法,每个测试方法测试一个行为片段是很常见的。
7. 单元测试的必要性
7.1 作为最佳实践
在开发过程中进行测试的代码质量更高,就像定期进行体育锻炼一样,我们都知道应该这样做,但往往难以付诸实践。
7.2 支持激进的开发计划
开发速度已经加快,出现了“互联网时间”这个特殊术语。与十年前典型的客户端/服务器应用程序的开发进度相比,如今的 Web 应用程序开发进度要快得多。管理人员发现,上市时间是一个重要特征,有时甚至比其他特征更重要。因此,近年来时间进度已经压缩。
8. 敏捷开发与单元测试
8.1 敏捷开发方法的兴起
项目时间压缩等因素导致了更敏捷的开发方法的发展。在过去十年中,开发人员研究了传统的重量级方法,发现它们对于“互联网时间”项目来说存在不足。许多过去的方法过于注重文档和前期设计(现在被称为“BDUF”项目——“Big Design Up Front”)。虽然有些项目在前期进行大量设计会更健康,但这样做所需的时间是一种奢侈。
传统开发面临的另一个问题是需求变化的速度。在整个项目生命周期中,需求保持稳定且明确的项目非常罕见。这导致了敏捷开发方法的出现,如极限编程(Extreme Programming)、Scrum(以橄榄球争球为非正式会议的模型命名)和 Crystal 方法等。这些方法的共同特点是喜欢变化而不是回避变化。在这些方法中,通常会将项目迭代的大部分设计和需求收集工作推迟到实际编码时进行。这种策略允许项目随着需求的变化而发展,并在开发周期的早期产生高质量的代码,有助于缓解上市时间的压力。
8.2 单元测试在敏捷开发中的作用
如果你使用敏捷方法,你必须能够快速响应生产代码中未预料到的设计更改。换句话说,你必须毫不留情地重构现有代码,而不用担心破坏正常工作的代码。单元测试是一种工具,它让你有信心更改工作代码而不会意外破坏某些功能。一旦你为一段代码拥有一套单元测试,你就可以将它们作为回归测试运行。回归测试是指再次运行已经通过的旧测试,以确保没有因后续开发而意外破坏任何东西。典型的敏捷项目每晚都会运行整个测试套件,以确保当天所做的更改没有破坏代码。
在许多敏捷方法中,单元测试在被测试的代码编写之前编写(称为测试优先编码或测试驱动开发)。单元测试成为需求收集的最后一步。如果你知道如何测试某个东西,你必须理解它的工作原理,因此你更有准备为它编写代码。
许多经理和开发人员认为花时间构建测试会减慢项目进度。但实际上,当测试做得正确时,情况恰恰相反。如果你随意编写测试,你将没有一套合理覆盖项目范围的测试。如果你一直编写测试,你就可以自由地对代码的设计和实现进行重大更改,而不用担心造成不良的副作用。即使出现了问题,当晚的回归测试也会暴露问题,并允许你在影响系统其他部分之前修复它。
综上所述,无论是日志记录还是单元测试,都是开发过程中不可或缺的重要环节。合理选择日志框架并正确运用单元测试方法,能够有效提升开发效率和代码质量。
9. JUnit 测试框架
JUnit 是最著名的测试框架之一,它为单元测试提供了强大的支持。JUnit 的核心思想是通过编写测试用例来验证代码的正确性。以下是使用 JUnit 进行单元测试的基本步骤:
9.1 引入 JUnit 依赖
首先,你需要在项目中引入 JUnit 的依赖。如果你使用的是 Maven 项目,可以在
pom.xml
中添加以下依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
9.2 编写测试类和测试方法
接下来,创建一个测试类,并在其中编写测试方法。测试方法需要使用
@Test
注解进行标记。以下是一个简单的示例:
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CalculatorTest {
@Test
public void testAddition() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result);
}
}
class Calculator {
public int add(int a, int b) {
return a + b;
}
}
在这个示例中,
CalculatorTest
是测试类,
testAddition
是测试方法。在
testAddition
方法中,我们创建了一个
Calculator
对象,并调用其
add
方法进行加法运算。然后使用
assertEquals
方法来验证计算结果是否符合预期。
9.3 运行测试
你可以使用 IDE 提供的测试运行器来运行测试类。在 IntelliJ IDEA 中,你可以右键点击测试类,选择 “Run ‘CalculatorTest’” 来运行测试。如果所有测试用例都通过,你将看到测试通过的提示信息。
9.4 JUnit 的其他注解
除了
@Test
注解外,JUnit 还提供了其他一些有用的注解:
-
@Before
:在每个测试方法执行之前执行,通常用于初始化测试数据。
-
@After
:在每个测试方法执行之后执行,通常用于清理测试数据。
-
@BeforeClass
:在所有测试方法执行之前执行一次,必须是静态方法。
-
@AfterClass
:在所有测试方法执行之后执行一次,必须是静态方法。
以下是使用这些注解的示例:
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class AnotherTest {
@BeforeClass
public static void setUpClass() {
// 初始化类级别的资源
}
@AfterClass
public static void tearDownClass() {
// 清理类级别的资源
}
@Before
public void setUp() {
// 初始化每个测试方法的资源
}
@After
public void tearDown() {
// 清理每个测试方法的资源
}
@Test
public void testSomething() {
// 测试逻辑
}
}
10. JWebUnit 测试框架
JWebUnit 是一个用于测试 Web 应用程序的框架。它提供了一组简单的 API 来模拟用户在浏览器中的操作,如点击链接、提交表单等。
10.1 引入 JWebUnit 依赖
如果你使用的是 Maven 项目,可以在
pom.xml
中添加以下依赖:
<dependency>
<groupId>net.sourceforge.jwebunit</groupId>
<artifactId>jwebunit-core</artifactId>
<version>3.0</version>
</dependency>
10.2 编写 JWebUnit 测试用例
以下是一个简单的 JWebUnit 测试用例示例:
import net.sourceforge.jwebunit.junit.WebTester;
import org.junit.Before;
import org.junit.Test;
public class MyWebAppTest {
private WebTester tester;
@Before
public void setUp() {
tester = new WebTester();
tester.setBaseUrl("http://example.com");
}
@Test
public void testLogin() {
tester.beginAt("/login");
tester.setTextField("username", "testuser");
tester.setTextField("password", "testpassword");
tester.clickButton("login");
tester.assertTitleEquals("Welcome Page");
}
}
在这个示例中,我们首先在
setUp
方法中初始化
WebTester
对象,并设置基础 URL。然后在
testLogin
方法中,我们模拟用户访问登录页面,输入用户名和密码,点击登录按钮,并验证登录成功后页面的标题是否符合预期。
10.3 JWebUnit 的常用方法
-
beginAt(String url):访问指定的 URL。 -
setTextField(String fieldName, String value):在指定的文本框中输入值。 -
clickButton(String buttonName):点击指定名称的按钮。 -
assertTitleEquals(String expectedTitle):验证页面的标题是否等于预期值。
11. 总结
11.1 日志记录总结
日志记录是开发应用程序时不可或缺的一部分。SDK 日志框架和 log4j 框架都提供了强大的日志记录功能。SDK 日志框架适合使用 SDK 1.4 或更高版本且不需要复杂配置的开发者;而 log4j 框架则更适合无法使用最新 SDK、需要更多日志格式控制和大量配置信息的开发者。
11.2 单元测试总结
单元测试是提高代码质量和开发效率的重要手段。JUnit 框架为普通 Java 代码的单元测试提供了便利,而 JWebUnit 框架则专门用于测试 Web 应用程序。通过使用这些测试框架,开发者可以在开发过程中及时发现和修复问题,确保代码的正确性和稳定性。
11.3 整体建议
在开发过程中,建议开发者合理使用日志记录和单元测试。在编写代码时,使用日志记录来记录程序的运行状态和错误信息,方便调试和排查问题;同时,编写单元测试来验证代码的功能是否符合预期,提高代码的可维护性和可扩展性。通过结合使用日志记录和单元测试,开发者可以更加高效地开发出高质量的应用程序。
以下是一个简单的流程图,展示了开发过程中日志记录和单元测试的使用流程:
graph LR
A[编写代码] --> B[添加日志记录]
B --> C[编写单元测试]
C --> D[运行单元测试]
D --> E{测试通过?}
E -- 是 --> F[部署应用程序]
E -- 否 --> G[修复代码]
G --> B
F --> H[监控日志]
H --> I{发现问题?}
I -- 是 --> G
I -- 否 --> J[持续运行]
通过这个流程图,我们可以看到日志记录和单元测试在整个开发过程中的重要作用。它们相互配合,帮助开发者及时发现和解决问题,确保应用程序的稳定运行。
超级会员免费看

被折叠的 条评论
为什么被折叠?



